@vobs/cli 1.7.7 → 1.8.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/dist/banner.cjs +22 -3
- package/dist/banner.cjs.map +1 -1
- package/dist/banner.d.cts +6 -2
- package/dist/banner.d.ts +6 -2
- package/dist/banner.js +20 -3
- package/dist/banner.js.map +1 -1
- package/dist/cli.cjs +364 -24
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +10 -1
- package/dist/cli.d.ts +10 -1
- package/dist/cli.js +354 -15
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.cjs +2 -2
- package/dist/commands/add.cjs.map +1 -1
- package/dist/commands/add.js +2 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/build.cjs +2 -2
- package/dist/commands/build.cjs.map +1 -1
- package/dist/commands/build.js +2 -2
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/check.cjs +382 -0
- package/dist/commands/check.cjs.map +1 -0
- package/dist/commands/check.d.cts +34 -0
- package/dist/commands/check.d.ts +34 -0
- package/dist/commands/check.js +343 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/dev.cjs +2 -2
- package/dist/commands/dev.cjs.map +1 -1
- package/dist/commands/dev.js +2 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/dsh.cjs +4 -4
- package/dist/commands/dsh.cjs.map +1 -1
- package/dist/commands/dsh.js +4 -4
- package/dist/commands/dsh.js.map +1 -1
- package/dist/commands/generate.cjs +2 -2
- package/dist/commands/generate.cjs.map +1 -1
- package/dist/commands/generate.js +2 -2
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/index.cjs +36 -19
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.js +27 -10
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init.cjs +20 -3
- package/dist/commands/init.cjs.map +1 -1
- package/dist/commands/init.js +20 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/index.cjs +402 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +391 -17
- package/dist/index.js.map +1 -1
- package/dist/repl.cjs +34 -17
- package/dist/repl.cjs.map +1 -1
- package/dist/repl.js +25 -8
- package/dist/repl.js.map +1 -1
- package/dist/start.cjs +402 -28
- package/dist/start.cjs.map +1 -1
- package/dist/start.d.cts +8 -1
- package/dist/start.d.ts +8 -1
- package/dist/start.js +391 -17
- package/dist/start.js.map +1 -1
- package/dist/utils/logger.cjs +2 -2
- package/dist/utils/logger.cjs.map +1 -1
- package/dist/utils/logger.js +2 -2
- package/dist/utils/logger.js.map +1 -1
- package/dist/version.cjs +47 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.d.cts +3 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -2
- package/src/banner.ts +11 -5
- package/src/cli-process.test.ts +139 -0
- package/src/cli.ts +31 -3
- package/src/commands/check.test.ts +238 -0
- package/src/commands/check.ts +461 -0
- package/src/commands/dsh.ts +4 -2
- package/src/commands/init.ts +2 -1
- package/src/start.ts +66 -5
- package/src/utils/logger.ts +14 -2
- package/src/version.ts +27 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
var __VOBS_CJS_FILE_URL = require("url").pathToFileURL(__filename).href;
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
23
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
24
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
25
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
26
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
27
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
28
|
+
mod
|
|
29
|
+
));
|
|
30
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
31
|
+
|
|
32
|
+
// packages/cli/src/commands/check.ts
|
|
33
|
+
var check_exports = {};
|
|
34
|
+
__export(check_exports, {
|
|
35
|
+
REPORT_PATH: () => REPORT_PATH,
|
|
36
|
+
VOBS_C118: () => VOBS_C118,
|
|
37
|
+
VOBS_C210: () => VOBS_C210,
|
|
38
|
+
VOBS_C232: () => VOBS_C232,
|
|
39
|
+
analyzeSource: () => analyzeSource,
|
|
40
|
+
checkCommand: () => checkCommand
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(check_exports);
|
|
43
|
+
var import_promises = require("fs/promises");
|
|
44
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
45
|
+
var import_typescript = __toESM(require("typescript"), 1);
|
|
46
|
+
|
|
47
|
+
// packages/cli/src/utils/logger.ts
|
|
48
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
49
|
+
function timestamp() {
|
|
50
|
+
return import_picocolors.default.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}]`);
|
|
51
|
+
}
|
|
52
|
+
__name(timestamp, "timestamp");
|
|
53
|
+
function info(msg) {
|
|
54
|
+
console.error(`${timestamp()} ${import_picocolors.default.cyan("\u2139")} ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
__name(info, "info");
|
|
57
|
+
function success(msg) {
|
|
58
|
+
console.log(`${timestamp()} ${import_picocolors.default.green("\u2714")} ${msg}`);
|
|
59
|
+
}
|
|
60
|
+
__name(success, "success");
|
|
61
|
+
function warn(msg) {
|
|
62
|
+
console.error(`${timestamp()} ${import_picocolors.default.yellow("\u26A0")} ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
__name(warn, "warn");
|
|
65
|
+
function error(msg) {
|
|
66
|
+
console.error(`${timestamp()} ${import_picocolors.default.red("\u2716")} ${msg}`);
|
|
67
|
+
}
|
|
68
|
+
__name(error, "error");
|
|
69
|
+
function step(msg) {
|
|
70
|
+
console.log(`
|
|
71
|
+
${import_picocolors.default.bold(import_picocolors.default.cyan(msg))}`);
|
|
72
|
+
}
|
|
73
|
+
__name(step, "step");
|
|
74
|
+
var logger = { info, success, warn, error, step };
|
|
75
|
+
|
|
76
|
+
// packages/cli/src/commands/check.ts
|
|
77
|
+
var REPORT_PATH = ".vobs/check.json";
|
|
78
|
+
var TEST_FILE = /(?:^|\/)(?:[^/]*\.(?:test|spec)\.tsx?|__tests__\/)/u;
|
|
79
|
+
var VOBS_C210 = "VOBS_C210";
|
|
80
|
+
var VOBS_C232 = "VOBS_C232";
|
|
81
|
+
var VOBS_C118 = "VOBS_C118";
|
|
82
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "lib", "build", ".git", ".vobs", "coverage"]);
|
|
83
|
+
async function collectSources(root) {
|
|
84
|
+
let rootStat;
|
|
85
|
+
try {
|
|
86
|
+
rootStat = await (0, import_promises.stat)(root);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${root}`);
|
|
89
|
+
}
|
|
90
|
+
if (!rootStat.isDirectory()) {
|
|
91
|
+
throw new Error(`\u4E0D\u662F\u76EE\u5F55\uFF1A${root}\uFF08vobs check \u63A5\u53D7\u4E00\u4E2A\u76EE\u5F55\uFF09`);
|
|
92
|
+
}
|
|
93
|
+
const found = [];
|
|
94
|
+
const walk = /* @__PURE__ */ __name(async (dir) => {
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
|
|
98
|
+
} catch (error2) {
|
|
99
|
+
if (dir === root) {
|
|
100
|
+
throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55\uFF1A${root}\uFF08${error2 instanceof Error ? error2.message : String(error2)}\uFF09`);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
107
|
+
await walk(import_node_path.default.join(dir, entry.name));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!entry.isFile()) continue;
|
|
111
|
+
if (!/\.tsx?$/u.test(entry.name) || entry.name.endsWith(".d.ts")) continue;
|
|
112
|
+
found.push(import_node_path.default.join(dir, entry.name));
|
|
113
|
+
}
|
|
114
|
+
}, "walk");
|
|
115
|
+
await walk(root);
|
|
116
|
+
return found.sort();
|
|
117
|
+
}
|
|
118
|
+
__name(collectSources, "collectSources");
|
|
119
|
+
function returnsJsx(node) {
|
|
120
|
+
let seen = false;
|
|
121
|
+
const visit = /* @__PURE__ */ __name((child) => {
|
|
122
|
+
if (seen) return;
|
|
123
|
+
if (import_typescript.default.isJsxElement(child) || import_typescript.default.isJsxSelfClosingElement(child) || import_typescript.default.isJsxFragment(child)) {
|
|
124
|
+
seen = true;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (child !== node && import_typescript.default.isFunctionLike(child)) return;
|
|
128
|
+
import_typescript.default.forEachChild(child, visit);
|
|
129
|
+
}, "visit");
|
|
130
|
+
import_typescript.default.forEachChild(node, visit);
|
|
131
|
+
return seen;
|
|
132
|
+
}
|
|
133
|
+
__name(returnsJsx, "returnsJsx");
|
|
134
|
+
function signalNameOf(node) {
|
|
135
|
+
if (!import_typescript.default.isPropertyAccessExpression(node)) return void 0;
|
|
136
|
+
if (node.name.text !== "value") return void 0;
|
|
137
|
+
return node.expression.getText();
|
|
138
|
+
}
|
|
139
|
+
__name(signalNameOf, "signalNameOf");
|
|
140
|
+
function lineSnippet(source, node) {
|
|
141
|
+
const { line } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
142
|
+
return (source.text.split(/\r?\n/u)[line] ?? "").trim();
|
|
143
|
+
}
|
|
144
|
+
__name(lineSnippet, "lineSnippet");
|
|
145
|
+
function diagnosticAt(source, file, node, rest) {
|
|
146
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
147
|
+
return {
|
|
148
|
+
...rest,
|
|
149
|
+
file,
|
|
150
|
+
line: line + 1,
|
|
151
|
+
column: character + 1,
|
|
152
|
+
snippet: lineSnippet(source, node)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
__name(diagnosticAt, "diagnosticAt");
|
|
156
|
+
var isWriteOperator = /* @__PURE__ */ __name((kind) => kind === import_typescript.default.SyntaxKind.EqualsToken || kind === import_typescript.default.SyntaxKind.PlusEqualsToken || kind === import_typescript.default.SyntaxKind.MinusEqualsToken || kind === import_typescript.default.SyntaxKind.AsteriskEqualsToken || kind === import_typescript.default.SyntaxKind.SlashEqualsToken, "isWriteOperator");
|
|
157
|
+
function ruleEffectSelfSubscription(source, file) {
|
|
158
|
+
const found = [];
|
|
159
|
+
const inspectEffectBody = /* @__PURE__ */ __name((body) => {
|
|
160
|
+
const reads = /* @__PURE__ */ new Map();
|
|
161
|
+
const writes = /* @__PURE__ */ new Map();
|
|
162
|
+
const visit2 = /* @__PURE__ */ __name((node, insideUntrack) => {
|
|
163
|
+
if (node !== body && import_typescript.default.isFunctionLike(node)) return;
|
|
164
|
+
const nextUntracked = insideUntrack || import_typescript.default.isCallExpression(node) && import_typescript.default.isIdentifier(node.expression) && node.expression.text === "untrack";
|
|
165
|
+
if (import_typescript.default.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {
|
|
166
|
+
const name = signalNameOf(node.left);
|
|
167
|
+
if (name !== void 0 && !nextUntracked) writes.set(name, node);
|
|
168
|
+
} else if ((import_typescript.default.isPrefixUnaryExpression(node) || import_typescript.default.isPostfixUnaryExpression(node)) && (node.operator === import_typescript.default.SyntaxKind.PlusPlusToken || node.operator === import_typescript.default.SyntaxKind.MinusMinusToken)) {
|
|
169
|
+
const name = signalNameOf(node.operand);
|
|
170
|
+
if (name !== void 0) {
|
|
171
|
+
if (!nextUntracked) writes.set(name, node);
|
|
172
|
+
reads.set(name, node);
|
|
173
|
+
}
|
|
174
|
+
} else if (import_typescript.default.isCallExpression(node) && import_typescript.default.isPropertyAccessExpression(node.expression) && node.expression.name.text === "set") {
|
|
175
|
+
if (!nextUntracked) writes.set(node.expression.expression.getText(), node);
|
|
176
|
+
} else {
|
|
177
|
+
const name = signalNameOf(node);
|
|
178
|
+
if (name !== void 0) {
|
|
179
|
+
const parent = node.parent;
|
|
180
|
+
const isAssignmentTarget = import_typescript.default.isBinaryExpression(parent) && isWriteOperator(parent.operatorToken.kind) && parent.left === node;
|
|
181
|
+
if (!isAssignmentTarget) reads.set(name, node);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
import_typescript.default.forEachChild(node, (child) => visit2(child, nextUntracked));
|
|
185
|
+
}, "visit");
|
|
186
|
+
visit2(body, false);
|
|
187
|
+
for (const [name, writeNode] of writes) {
|
|
188
|
+
if (!reads.has(name)) continue;
|
|
189
|
+
found.push(diagnosticAt(source, file, writeNode, {
|
|
190
|
+
code: VOBS_C210,
|
|
191
|
+
severity: "error",
|
|
192
|
+
message: `effect \u5199\u5165\u4E86\u5B83\u81EA\u5DF1\u4F9D\u8D56\u7684\u4FE1\u53F7 "${name}" \u2014\u2014 \u8FD9\u6B21\u5199\u5165\u4F1A\u628A\u5B83\u91CD\u65B0\u8C03\u5EA6\uFF0C\u5F62\u6210\u81EA\u8BA2\u9605\u5FAA\u73AF`,
|
|
193
|
+
fix: `\u628A\u8FD9\u6B21\u5199\u5165\u5305\u8FDB untrack\uFF1Auntrack(() => { ${name}.value = next })\uFF1B\u5982\u679C\u8FD9\u4E2A effect \u672C\u6765\u5C31\u53EA\u8BE5\u505A\u526F\u4F5C\u7528\uFF0C\u68C0\u67E5\u662F\u4E0D\u662F\u8BEF\u8BFB\u4E86\u4E0D\u8BE5\u8BFB\u7684\u4FE1\u53F7\u3002`
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
}, "inspectEffectBody");
|
|
197
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
198
|
+
if (import_typescript.default.isCallExpression(node) && import_typescript.default.isIdentifier(node.expression) && (node.expression.text === "effect" || node.expression.text === "renderEffect")) {
|
|
199
|
+
const callback = node.arguments[0];
|
|
200
|
+
if (callback !== void 0 && (import_typescript.default.isArrowFunction(callback) || import_typescript.default.isFunctionExpression(callback))) {
|
|
201
|
+
inspectEffectBody(callback.body);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
import_typescript.default.forEachChild(node, visit);
|
|
205
|
+
}, "visit");
|
|
206
|
+
visit(source);
|
|
207
|
+
return found;
|
|
208
|
+
}
|
|
209
|
+
__name(ruleEffectSelfSubscription, "ruleEffectSelfSubscription");
|
|
210
|
+
function isKeyedListCall(node) {
|
|
211
|
+
if (!import_typescript.default.isCallExpression(node)) return false;
|
|
212
|
+
if (!import_typescript.default.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "map") return false;
|
|
213
|
+
const callback = node.arguments[0];
|
|
214
|
+
return callback !== void 0 && import_typescript.default.isFunctionLike(callback) && returnsJsx(callback);
|
|
215
|
+
}
|
|
216
|
+
__name(isKeyedListCall, "isKeyedListCall");
|
|
217
|
+
function ruleListInBranch(source, file) {
|
|
218
|
+
const found = [];
|
|
219
|
+
const message = "list \u5199\u5728\u4E09\u5143/&& \u7684\u5206\u652F\u91CC \u2014\u2014 \u4F1A\u8D70\u591A\u6001\u63D2\u5165\uFF0C\u5931\u53BB keyed \u590D\u7528";
|
|
220
|
+
const fix = "\u628A list \u63D0\u6210**\u76F4\u63A5\u7684**\u5B50\u8868\u8FBE\u5F0F\uFF1A\u5148\u5199\u6761\u4EF6\u5206\u652F\uFF0C\u518D\u5355\u72EC\u5199 {items.map(...)}\u3002\u4E09\u5143\u91CC\u540C\u65F6\u6709\u8282\u70B9\u4E0E list \u65F6\uFF0Clist \u90A3\u4E00\u652F\u4E0D\u4F1A\u7F16\u8BD1\u6210 insertList\u3002";
|
|
221
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
222
|
+
if (import_typescript.default.isConditionalExpression(node)) {
|
|
223
|
+
for (const branch of [node.whenTrue, node.whenFalse]) {
|
|
224
|
+
if (isKeyedListCall(branch)) {
|
|
225
|
+
found.push(diagnosticAt(source, file, branch, { code: VOBS_C232, severity: "warning", message, fix }));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} else if (import_typescript.default.isBinaryExpression(node) && node.operatorToken.kind === import_typescript.default.SyntaxKind.AmpersandAmpersandToken && isKeyedListCall(node.right)) {
|
|
229
|
+
found.push(diagnosticAt(source, file, node.right, { code: VOBS_C232, severity: "warning", message, fix }));
|
|
230
|
+
}
|
|
231
|
+
import_typescript.default.forEachChild(node, visit);
|
|
232
|
+
}, "visit");
|
|
233
|
+
visit(source);
|
|
234
|
+
return found;
|
|
235
|
+
}
|
|
236
|
+
__name(ruleListInBranch, "ruleListInBranch");
|
|
237
|
+
function containsSignalRead(node) {
|
|
238
|
+
let seen = false;
|
|
239
|
+
const visit = /* @__PURE__ */ __name((child) => {
|
|
240
|
+
if (seen) return;
|
|
241
|
+
if (import_typescript.default.isFunctionLike(child)) return;
|
|
242
|
+
if (signalNameOf(child) !== void 0) {
|
|
243
|
+
seen = true;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
import_typescript.default.forEachChild(child, visit);
|
|
247
|
+
}, "visit");
|
|
248
|
+
visit(node);
|
|
249
|
+
return seen;
|
|
250
|
+
}
|
|
251
|
+
__name(containsSignalRead, "containsSignalRead");
|
|
252
|
+
function ruleSignalCapturedInBody(source, file) {
|
|
253
|
+
const found = [];
|
|
254
|
+
const inspectComponent = /* @__PURE__ */ __name((fn) => {
|
|
255
|
+
const body = fn.body;
|
|
256
|
+
if (body === void 0 || !import_typescript.default.isBlock(body) || !returnsJsx(fn)) return;
|
|
257
|
+
const captured = [];
|
|
258
|
+
for (const statement of body.statements) {
|
|
259
|
+
if (!import_typescript.default.isVariableStatement(statement)) continue;
|
|
260
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
261
|
+
if (!import_typescript.default.isIdentifier(declaration.name) || declaration.initializer === void 0) continue;
|
|
262
|
+
if (containsSignalRead(declaration.initializer)) {
|
|
263
|
+
captured.push({ name: declaration.name.text, node: declaration });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (captured.length === 0) return;
|
|
268
|
+
const usedInJsx = /* @__PURE__ */ new Set();
|
|
269
|
+
const visit2 = /* @__PURE__ */ __name((node) => {
|
|
270
|
+
if (import_typescript.default.isJsxExpression(node) && node.expression !== void 0) {
|
|
271
|
+
const scan = /* @__PURE__ */ __name((child) => {
|
|
272
|
+
if (import_typescript.default.isIdentifier(child)) usedInJsx.add(child.text);
|
|
273
|
+
import_typescript.default.forEachChild(child, scan);
|
|
274
|
+
}, "scan");
|
|
275
|
+
scan(node.expression);
|
|
276
|
+
}
|
|
277
|
+
import_typescript.default.forEachChild(node, visit2);
|
|
278
|
+
}, "visit");
|
|
279
|
+
visit2(body);
|
|
280
|
+
for (const item of captured) {
|
|
281
|
+
if (!usedInJsx.has(item.name)) continue;
|
|
282
|
+
found.push(diagnosticAt(source, file, item.node, {
|
|
283
|
+
code: VOBS_C118,
|
|
284
|
+
severity: "warning",
|
|
285
|
+
message: `\u7EC4\u4EF6\u4F53\u91CC\u8BFB\u4E86\u4FE1\u53F7\u5E76\u5B58\u8FDB "${item.name}" \u2014\u2014 \u7EC4\u4EF6\u4F53\u53EA\u6267\u884C\u4E00\u6B21\uFF0C\u8FD9\u4E2A\u503C\u4E4B\u540E\u6C38\u8FDC\u4E0D\u4F1A\u66F4\u65B0`,
|
|
286
|
+
fix: `\u628A\u8BFB\u53D6\u653E\u8FDB\u52A8\u6001\u8868\u8FBE\u5F0F\uFF08\u76F4\u63A5\u5728 JSX \u91CC\u7528 .value\uFF09\uFF0C\u6216\u6539\u6210 memo \u6D3E\u751F\u503C\uFF1Aconst ${item.name} = memo(() => ...)\u3002`
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
}, "inspectComponent");
|
|
290
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
291
|
+
if (import_typescript.default.isFunctionDeclaration(node) || import_typescript.default.isFunctionExpression(node) || import_typescript.default.isArrowFunction(node)) {
|
|
292
|
+
inspectComponent(node);
|
|
293
|
+
}
|
|
294
|
+
import_typescript.default.forEachChild(node, visit);
|
|
295
|
+
}, "visit");
|
|
296
|
+
visit(source);
|
|
297
|
+
return found;
|
|
298
|
+
}
|
|
299
|
+
__name(ruleSignalCapturedInBody, "ruleSignalCapturedInBody");
|
|
300
|
+
var IGNORE_NEXT_LINE = /vobs-check-ignore-next-line/u;
|
|
301
|
+
function suppressedLines(text) {
|
|
302
|
+
const suppressed = /* @__PURE__ */ new Set();
|
|
303
|
+
const lines = text.split(/\r?\n/u);
|
|
304
|
+
for (let index = 0; index < lines.length; index++) {
|
|
305
|
+
if (IGNORE_NEXT_LINE.test(lines[index])) suppressed.add(index + 2);
|
|
306
|
+
}
|
|
307
|
+
return suppressed;
|
|
308
|
+
}
|
|
309
|
+
__name(suppressedLines, "suppressedLines");
|
|
310
|
+
function analyzeSource(text, file) {
|
|
311
|
+
const source = import_typescript.default.createSourceFile(file, text, import_typescript.default.ScriptTarget.Latest, true, import_typescript.default.ScriptKind.TSX);
|
|
312
|
+
const suppressed = suppressedLines(text);
|
|
313
|
+
return [
|
|
314
|
+
...ruleEffectSelfSubscription(source, file),
|
|
315
|
+
...ruleListInBranch(source, file),
|
|
316
|
+
...ruleSignalCapturedInBody(source, file)
|
|
317
|
+
].filter((item) => !suppressed.has(item.line)).sort((a, b) => a.line - b.line || a.column - b.column);
|
|
318
|
+
}
|
|
319
|
+
__name(analyzeSource, "analyzeSource");
|
|
320
|
+
async function checkCommand(options = {}) {
|
|
321
|
+
const root = import_node_path.default.resolve(options.dir ?? process.cwd());
|
|
322
|
+
let all;
|
|
323
|
+
try {
|
|
324
|
+
all = await collectSources(root);
|
|
325
|
+
} catch (error2) {
|
|
326
|
+
logger.error(error2 instanceof Error ? error2.message : String(error2));
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const files = options.includeTests === true ? all : all.filter((file) => !TEST_FILE.test(import_node_path.default.relative(root, file).split(import_node_path.default.sep).join("/")));
|
|
331
|
+
const skipped = all.length - files.length;
|
|
332
|
+
const diagnostics = [];
|
|
333
|
+
for (const absolute of files) {
|
|
334
|
+
const text = await (0, import_promises.readFile)(absolute, "utf8");
|
|
335
|
+
const relative = import_node_path.default.relative(root, absolute).split(import_node_path.default.sep).join("/");
|
|
336
|
+
diagnostics.push(...analyzeSource(text, relative));
|
|
337
|
+
}
|
|
338
|
+
const errors = diagnostics.filter((item) => item.severity === "error");
|
|
339
|
+
const report = { root, files: files.length, skippedTests: skipped, diagnostics };
|
|
340
|
+
if (options.write === true) {
|
|
341
|
+
const target = import_node_path.default.join(root, REPORT_PATH);
|
|
342
|
+
await (0, import_promises.mkdir)(import_node_path.default.dirname(target), { recursive: true });
|
|
343
|
+
await (0, import_promises.writeFile)(target, `${JSON.stringify(report, null, 2)}
|
|
344
|
+
`, "utf8");
|
|
345
|
+
logger.info(`\u62A5\u544A\u5199\u5165 ${REPORT_PATH}\uFF08\u5F00\u53D1\u53F0\u9762\u677F\u8BFB\u5B83\u663E\u793A\u300C\u9879\u76EE\u300D\u9875\uFF09`);
|
|
346
|
+
}
|
|
347
|
+
if (options.json === true) {
|
|
348
|
+
console.log(JSON.stringify(report, null, 2));
|
|
349
|
+
if (errors.length > 0) process.exitCode = 1;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (diagnostics.length === 0) {
|
|
353
|
+
logger.success(`\u68C0\u67E5\u901A\u8FC7 \u2014\u2014 ${files.length} \u4E2A\u6587\u4EF6${skipped > 0 ? `\uFF08\u8DF3\u8FC7 ${skipped} \u4E2A\u6D4B\u8BD5\u6587\u4EF6\uFF09` : ""}\uFF0C\u6CA1\u6709\u53D1\u73B0\u95EE\u9898`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
console.log("");
|
|
357
|
+
for (const item of diagnostics) {
|
|
358
|
+
const mark = item.severity === "error" ? "\u2716" : "!";
|
|
359
|
+
console.log(` ${mark} ${item.file}:${item.line}:${item.column} ${item.code}`);
|
|
360
|
+
console.log(` ${item.message}`);
|
|
361
|
+
if (item.snippet !== "") console.log(` ${item.snippet}`);
|
|
362
|
+
console.log(` \u2192 ${item.fix}`);
|
|
363
|
+
console.log("");
|
|
364
|
+
}
|
|
365
|
+
const warnings = diagnostics.length - errors.length;
|
|
366
|
+
console.log(
|
|
367
|
+
` ${errors.length} \u4E2A\u9519\u8BEF \xB7 ${warnings} \u4E2A\u8B66\u544A \xB7 \u5171\u68C0\u67E5 ${files.length} \u4E2A\u6587\u4EF6${skipped > 0 ? `\uFF08\u8DF3\u8FC7 ${skipped} \u4E2A\u6D4B\u8BD5\u6587\u4EF6\uFF0C\u7528 --include-tests \u7EB3\u5165\uFF09` : ""}
|
|
368
|
+
`
|
|
369
|
+
);
|
|
370
|
+
if (errors.length > 0) process.exitCode = 1;
|
|
371
|
+
}
|
|
372
|
+
__name(checkCommand, "checkCommand");
|
|
373
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
374
|
+
0 && (module.exports = {
|
|
375
|
+
REPORT_PATH,
|
|
376
|
+
VOBS_C118,
|
|
377
|
+
VOBS_C210,
|
|
378
|
+
VOBS_C232,
|
|
379
|
+
analyzeSource,
|
|
380
|
+
checkCommand
|
|
381
|
+
});
|
|
382
|
+
//# sourceMappingURL=check.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/commands/check.ts","../../src/utils/logger.ts"],"sourcesContent":["/**\n * `vobs check` —— 不跑应用就能给出的源码问题清单。\n *\n * 为什么需要它:vobs 里最容易犯的错是**不发声**的错(effect 自订阅、列表写进三元分支\n * 后失去 keyed 复用)。运行时护栏(`@vobs/vobs/dev`)能抓到,但那要求先把应用跑起来;\n * 静态检查让你(和 AI)在**写完之后、运行之前**就知道哪里不对。\n *\n * 产出与框架其余部分同一套词汇(code / severity / fix),字段直接对应\n * `@vobs/runtime/error` 的 `VobsError`,因此能直接喂给开发台面板或 AI。\n *\n * 规则的高精度是刻意的:宁可少报,也不要误报 —— 误报会让 AI 去改本来正确的代码。\n *\n * ## 已知限制\n *\n * 三条规则都是**启发式**的(靠名字与形状判断),所以会撞到合法代码:\n *\n * - `VOBS_C210`:把「同一个 `X.value` 的读写」当作信号自订阅。**分不清信号与恰好叫 `value`\n * 的普通字段** —— `entry.value = x`(DTO / ref / 配置对象)会被误报。收窄成「只认裸标识符」\n * 会连 `props.name.value` 这种真信号一起放过,所以保持现状 + 提供行内抑制。\n * - `VOBS_C232` / `VOBS_C118`:前者看的是「列表表达式是否写在分支里」,后者看「组件体里\n * 是否把信号读取存进了局部变量」。都只看形状,不做类型推断。\n *\n * 撞上误报时的正规做法是**行内抑制**(`// vobs-check-ignore-next-line`),\n * 而不是关掉整条规则或改写本来正确的代码。\n */\nimport { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport ts from 'typescript'\nimport { logger } from '../utils/logger.js'\n\nexport interface CheckOptions {\n readonly dir?: string\n readonly json?: boolean\n /** 把结果写到 `<root>/.vobs/check.json`(开发台面板读这个文件显示「项目」页)。 */\n readonly write?: boolean\n /** 把测试文件也纳入检查(默认跳过:fixture 里常有意为之的写法会淹没真问题)。 */\n readonly includeTests?: boolean\n}\n\n/** 检查报告的固定落点,相对被检查的根目录。 */\nexport const REPORT_PATH = '.vobs/check.json'\n\n/** 测试文件默认跳过。 */\nconst TEST_FILE = /(?:^|\\/)(?:[^/]*\\.(?:test|spec)\\.tsx?|__tests__\\/)/u\n\nexport interface CheckDiagnostic {\n /** 稳定错误码,AI 与文档按它检索。 */\n readonly code: string\n readonly severity: 'error' | 'warning'\n readonly message: string\n /** 该怎么改。只报错对 AI 没有价值。 */\n readonly fix: string\n /** 相对目标目录的路径。 */\n readonly file: string\n readonly line: number\n readonly column: number\n /** 出错那一行的原文,方便直接定位。 */\n readonly snippet: string\n}\n\n/** effect 写入了自己依赖的信号。 */\nexport const VOBS_C210 = 'VOBS_C210'\n/** 列表写在分支位置 —— 失去 keyed 复用。 */\nexport const VOBS_C232 = 'VOBS_C232'\n/** 在组件体里读信号并存进局部变量 —— 组件体只执行一次,之后永不更新。 */\nexport const VOBS_C118 = 'VOBS_C118'\n\nconst SKIP_DIRS = new Set(['node_modules', 'dist', 'lib', 'build', '.git', '.vobs', 'coverage'])\n\n/**\n * 收集目标下的 .ts/.tsx。\n *\n * **根目录**读不到时**必须抛错**,不能返回空数组:此前那个 `catch { return }` 把\n * 不存在的目录、以及\"误传一个文件进来\"都变成 `0 个文件` + exit 0 ——\n * `vobs check <不存在的目录>` 会打印 `✔ 检查通过 —— 0 个文件,没有发现问题`。\n * CI 里把路径写错就是**永久绿灯**,比报错更危险(一个检查工具在最该失败的场景下静默通过)。\n *\n * 子目录读不到仍然跳过(权限不足/竞态删除不该让整次检查失败)——那是有意的容错,\n * 与\"根目录不存在\"不是一回事。\n */\nasync function collectSources(root: string): Promise<string[]> {\n let rootStat\n try {\n rootStat = await stat(root)\n } catch {\n throw new Error(`路径不存在:${root}`)\n }\n if (!rootStat.isDirectory()) {\n throw new Error(`不是目录:${root}(vobs check 接受一个目录)`)\n }\n\n const found: string[] = []\n const walk = async (dir: string): Promise<void> => {\n let entries\n try {\n entries = await readdir(dir, { withFileTypes: true })\n } catch (error) {\n // 根目录在第一次 stat 之后消失(竞态)也必须暴露,而不是混进\"0 个文件\"\n if (dir === root) {\n throw new Error(`无法读取目录:${root}(${error instanceof Error ? error.message : String(error)})`)\n }\n return\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue\n await walk(path.join(dir, entry.name))\n continue\n }\n if (!entry.isFile()) continue\n if (!/\\.tsx?$/u.test(entry.name) || entry.name.endsWith('.d.ts')) continue\n found.push(path.join(dir, entry.name))\n }\n }\n await walk(root)\n return found.sort()\n}\n\n/* ------------------------------------------------------------------ 工具 */\n\n/** 该节点是否返回 JSX(函数体里出现 JSX 元素/片段)。 */\nfunction returnsJsx(node: ts.Node): boolean {\n let seen = false\n const visit = (child: ts.Node): void => {\n if (seen) return\n if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {\n seen = true\n return\n }\n // 不进入嵌套函数:内层组件返回 JSX 不代表外层是组件\n if (child !== node && (ts.isFunctionLike(child))) return\n ts.forEachChild(child, visit)\n }\n ts.forEachChild(node, visit)\n return seen\n}\n\n/**\n * `X.value` 形式的内存读取,返回接收者的文本(`count`、`props.name` 都算)。\n *\n * 用文本而不是标识符:信号经常是别人传进来的(`props.name.value`),\n * 只认裸标识符会把这些全漏掉。\n */\nfunction signalNameOf(node: ts.Node): string | undefined {\n if (!ts.isPropertyAccessExpression(node)) return undefined\n if (node.name.text !== 'value') return undefined\n return node.expression.getText()\n}\n\nfunction lineSnippet(source: ts.SourceFile, node: ts.Node): string {\n const { line } = source.getLineAndCharacterOfPosition(node.getStart(source))\n return (source.text.split(/\\r?\\n/u)[line] ?? '').trim()\n}\n\nfunction diagnosticAt(\n source: ts.SourceFile,\n file: string,\n node: ts.Node,\n rest: Omit<CheckDiagnostic, 'file' | 'line' | 'column' | 'snippet'>\n): CheckDiagnostic {\n const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source))\n return {\n ...rest,\n file,\n line: line + 1,\n column: character + 1,\n snippet: lineSnippet(source, node)\n }\n}\n\nconst isWriteOperator = (kind: ts.SyntaxKind): boolean =>\n kind === ts.SyntaxKind.EqualsToken\n || kind === ts.SyntaxKind.PlusEqualsToken\n || kind === ts.SyntaxKind.MinusEqualsToken\n || kind === ts.SyntaxKind.AsteriskEqualsToken\n || kind === ts.SyntaxKind.SlashEqualsToken\n\n/* ------------------------------------------------- 规则 A:effect 自订阅 */\n\nfunction ruleEffectSelfSubscription(source: ts.SourceFile, file: string): CheckDiagnostic[] {\n const found: CheckDiagnostic[] = []\n\n const inspectEffectBody = (body: ts.Node): void => {\n const reads = new Map<string, ts.Node>()\n const writes = new Map<string, ts.Node>()\n\n const visit = (node: ts.Node, insideUntrack: boolean): void => {\n // 嵌套函数有自己的订阅语义,不算在本次 effect 的读写里\n if (node !== body && ts.isFunctionLike(node)) return\n\n // untrack(...) 里的写入不建立订阅 → 正是正确写法\n const nextUntracked = insideUntrack\n || (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'untrack')\n\n if (ts.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {\n const name = signalNameOf(node.left)\n if (name !== undefined && !nextUntracked) writes.set(name, node)\n } else if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node))\n && (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken)) {\n // `count.value++` 同时是读和写 —— 这正是自订阅的经典形态\n const name = signalNameOf(node.operand)\n if (name !== undefined) {\n if (!nextUntracked) writes.set(name, node)\n reads.set(name, node)\n }\n } else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)\n && node.expression.name.text === 'set') {\n if (!nextUntracked) writes.set(node.expression.expression.getText(), node)\n } else {\n const name = signalNameOf(node)\n if (name !== undefined) {\n const parent = node.parent\n // 只有赋值类运算符的左侧才不是「读」;`count.value < 1` 的左侧仍然是读\n const isAssignmentTarget = ts.isBinaryExpression(parent)\n && isWriteOperator(parent.operatorToken.kind)\n && parent.left === node\n if (!isAssignmentTarget) reads.set(name, node)\n }\n }\n\n ts.forEachChild(node, child => visit(child, nextUntracked))\n }\n\n visit(body, false)\n\n for (const [name, writeNode] of writes) {\n if (!reads.has(name)) continue\n found.push(diagnosticAt(source, file, writeNode, {\n code: VOBS_C210,\n severity: 'error',\n message: `effect 写入了它自己依赖的信号 \"${name}\" —— 这次写入会把它重新调度,形成自订阅循环`,\n fix: `把这次写入包进 untrack:untrack(() => { ${name}.value = next });`\n + '如果这个 effect 本来就只该做副作用,检查是不是误读了不该读的信号。'\n }))\n }\n }\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)\n && (node.expression.text === 'effect' || node.expression.text === 'renderEffect')) {\n const callback = node.arguments[0]\n if (callback !== undefined && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {\n inspectEffectBody(callback.body)\n }\n }\n ts.forEachChild(node, visit)\n }\n visit(source)\n\n return found\n}\n\n/* -------------------------------------- 规则 B:列表写在分支位置(C232) */\n\nfunction isKeyedListCall(node: ts.Node): boolean {\n if (!ts.isCallExpression(node)) return false\n if (!ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== 'map') return false\n const callback = node.arguments[0]\n return callback !== undefined && ts.isFunctionLike(callback) && returnsJsx(callback)\n}\n\nfunction ruleListInBranch(source: ts.SourceFile, file: string): CheckDiagnostic[] {\n const found: CheckDiagnostic[] = []\n const message = 'list 写在三元/&& 的分支里 —— 会走多态插入,失去 keyed 复用'\n const fix = '把 list 提成**直接的**子表达式:先写条件分支,再单独写 {items.map(...)}。'\n + '三元里同时有节点与 list 时,list 那一支不会编译成 insertList。'\n\n const visit = (node: ts.Node): void => {\n if (ts.isConditionalExpression(node)) {\n for (const branch of [node.whenTrue, node.whenFalse]) {\n if (isKeyedListCall(branch)) {\n found.push(diagnosticAt(source, file, branch, { code: VOBS_C232, severity: 'warning', message, fix }))\n }\n }\n } else if (ts.isBinaryExpression(node)\n && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken\n && isKeyedListCall(node.right)) {\n found.push(diagnosticAt(source, file, node.right, { code: VOBS_C232, severity: 'warning', message, fix }))\n }\n ts.forEachChild(node, visit)\n }\n visit(source)\n\n return found\n}\n\n/* --------------------------------- 规则 C:组件体里读信号存局部变量(C118) */\n\nfunction containsSignalRead(node: ts.Node): boolean {\n let seen = false\n const visit = (child: ts.Node): void => {\n if (seen) return\n // 函数体内的读取是**延迟**的(事件处理器、定时器、回调),不算「组件体里读信号」。\n // 这条必须放在最前面:`const handler = () => { signal.value }` 里的读取完全正常。\n if (ts.isFunctionLike(child)) return\n if (signalNameOf(child) !== undefined) {\n seen = true\n return\n }\n ts.forEachChild(child, visit)\n }\n visit(node)\n return seen\n}\n\nfunction ruleSignalCapturedInBody(source: ts.SourceFile, file: string): CheckDiagnostic[] {\n const found: CheckDiagnostic[] = []\n\n const inspectComponent = (fn: ts.FunctionLikeDeclaration): void => {\n const body = fn.body\n if (body === undefined || !ts.isBlock(body) || !returnsJsx(fn)) return\n\n // 组件体里「读信号 + 存进 const」\n const captured: { name: string; node: ts.VariableDeclaration }[] = []\n for (const statement of body.statements) {\n if (!ts.isVariableStatement(statement)) continue\n for (const declaration of statement.declarationList.declarations) {\n if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue\n if (containsSignalRead(declaration.initializer)) {\n captured.push({ name: declaration.name.text, node: declaration })\n }\n }\n }\n if (captured.length === 0) return\n\n // 这些名字是否在 JSX 表达式里被用到\n const usedInJsx = new Set<string>()\n const visit = (node: ts.Node): void => {\n if (ts.isJsxExpression(node) && node.expression !== undefined) {\n const scan = (child: ts.Node): void => {\n if (ts.isIdentifier(child)) usedInJsx.add(child.text)\n ts.forEachChild(child, scan)\n }\n scan(node.expression)\n }\n ts.forEachChild(node, visit)\n }\n visit(body)\n\n for (const item of captured) {\n if (!usedInJsx.has(item.name)) continue\n found.push(diagnosticAt(source, file, item.node, {\n code: VOBS_C118,\n severity: 'warning',\n message: `组件体里读了信号并存进 \"${item.name}\" —— 组件体只执行一次,这个值之后永远不会更新`,\n fix: `把读取放进动态表达式(直接在 JSX 里用 .value),或改成 memo 派生值:`\n + `const ${item.name} = memo(() => ...)。`\n }))\n }\n }\n\n const visit = (node: ts.Node): void => {\n if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {\n inspectComponent(node)\n }\n ts.forEachChild(node, visit)\n }\n visit(source)\n\n return found\n}\n\n/* -------------------------------------------------------------- 入口 */\n\n/**\n * 行内抑制:`// vobs-check-ignore-next-line`。\n *\n * 这三条规则都是**启发式**的:它们靠名字与形状判断「这看起来像信号自订阅 / 像写在分支里的列表 /\n * 像在组件体里读信号」,而有些合法代码恰好长成那样。检查器没有逃生口的话,用户只能关掉整条\n * 规则(或者被 CI 挡住去做假的改写)—— 那比漏报更糟。\n *\n * 只支持「抑制下一行」:作用范围最小、读代码时一眼看得见,也没有整文件豁免那种一刀切。\n * 实测就撞到过一例:`ListEntry.value` 是普通字段,但写法与信号自订阅完全相同\n * (后来给它改了名,但用户代码里不会有这种运气)。\n */\nconst IGNORE_NEXT_LINE = /vobs-check-ignore-next-line/u\n\n/** 被行内注释抑制的行号集合(1-based,收集的是「注释的下一行」)。 */\nfunction suppressedLines(text: string): Set<number> {\n const suppressed = new Set<number>()\n const lines = text.split(/\\r?\\n/u)\n for (let index = 0; index < lines.length; index++) {\n if (IGNORE_NEXT_LINE.test(lines[index])) suppressed.add(index + 2)\n }\n return suppressed\n}\n\nexport function analyzeSource(text: string, file: string): CheckDiagnostic[] {\n const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)\n const suppressed = suppressedLines(text)\n return [\n ...ruleEffectSelfSubscription(source, file),\n ...ruleListInBranch(source, file),\n ...ruleSignalCapturedInBody(source, file)\n ]\n .filter(item => !suppressed.has(item.line))\n .sort((a, b) => a.line - b.line || a.column - b.column)\n}\n\nexport async function checkCommand(options: CheckOptions = {}): Promise<void> {\n const root = path.resolve(options.dir ?? process.cwd())\n let all: string[]\n try {\n all = await collectSources(root)\n } catch (error) {\n // 路径无效必须是**失败**,不能是\"0 个文件,检查通过\"(见 collectSources 注释)\n logger.error(error instanceof Error ? error.message : String(error))\n process.exitCode = 1\n return\n }\n const files = options.includeTests === true\n ? all\n : all.filter(file => !TEST_FILE.test(path.relative(root, file).split(path.sep).join('/')))\n const skipped = all.length - files.length\n\n const diagnostics: CheckDiagnostic[] = []\n for (const absolute of files) {\n const text = await readFile(absolute, 'utf8')\n const relative = path.relative(root, absolute).split(path.sep).join('/')\n diagnostics.push(...analyzeSource(text, relative))\n }\n\n const errors = diagnostics.filter(item => item.severity === 'error')\n const report = { root, files: files.length, skippedTests: skipped, diagnostics }\n\n if (options.write === true) {\n const target = path.join(root, REPORT_PATH)\n await mkdir(path.dirname(target), { recursive: true })\n await writeFile(target, `${JSON.stringify(report, null, 2)}\\n`, 'utf8')\n // --json 时 stdout 必须只剩 JSON,所以这行提示走 logger(stderr)\n logger.info(`报告写入 ${REPORT_PATH}(开发台面板读它显示「项目」页)`)\n }\n\n if (options.json === true) {\n console.log(JSON.stringify(report, null, 2))\n if (errors.length > 0) process.exitCode = 1\n return\n }\n\n if (diagnostics.length === 0) {\n logger.success(`检查通过 —— ${files.length} 个文件${skipped > 0 ? `(跳过 ${skipped} 个测试文件)` : ''},没有发现问题`)\n return\n }\n\n console.log('')\n for (const item of diagnostics) {\n const mark = item.severity === 'error' ? '✖' : '!'\n console.log(` ${mark} ${item.file}:${item.line}:${item.column} ${item.code}`)\n console.log(` ${item.message}`)\n if (item.snippet !== '') console.log(` ${item.snippet}`)\n console.log(` → ${item.fix}`)\n console.log('')\n }\n const warnings = diagnostics.length - errors.length\n console.log(\n ` ${errors.length} 个错误 · ${warnings} 个警告 · 共检查 ${files.length} 个文件`\n + `${skipped > 0 ? `(跳过 ${skipped} 个测试文件,用 --include-tests 纳入)` : ''}\\n`\n )\n\n if (errors.length > 0) process.exitCode = 1\n}\n","import pc from 'picocolors'\n\n/*\n * 诊断/日志一律走 **stderr**,stdout 只留给**数据**。\n *\n * 为什么这不是洁癖:`vobs check --json` 的输出是给 AI 与开发台面板消费的机器接口,\n * 而 `--json --write` 时 `logger.info('报告写入 …')` 会先往 stdout 打一行带时间戳的\n * `[14:24:23] ℹ …`,于是 `JSON.parse(stdout)` 直接失败(实测\n * `.artifacts/probe-audit-cli-check.mjs`:同一脚本里 `--json` 可 parse、`--json --write` 不可)。\n * `warn` 同理。\n *\n * 注意:`error` 本来就走 stderr;`success`/`step` 保持 stdout(它们是给人看的**主输出**,\n * 不是旁路日志),`check` 的成功行也仍是 stdout。\n */\nfunction timestamp(): string {\n return pc.dim(`[${new Date().toLocaleTimeString()}]`)\n}\n\nexport function info(msg: string): void {\n console.error(`${timestamp()} ${pc.cyan('ℹ')} ${msg}`)\n}\n\nexport function success(msg: string): void {\n console.log(`${timestamp()} ${pc.green('✔')} ${msg}`)\n}\n\nexport function warn(msg: string): void {\n console.error(`${timestamp()} ${pc.yellow('⚠')} ${msg}`)\n}\n\nexport function error(msg: string): void {\n console.error(`${timestamp()} ${pc.red('✖')} ${msg}`)\n}\n\nexport function step(msg: string): void {\n console.log(`\\n ${pc.bold(pc.cyan(msg))}`)\n}\n\nexport const logger = { info, success, warn, error, step }"],"mappings":"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","names":["pc","error","path","ts","visit"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface CheckOptions {
|
|
2
|
+
readonly dir?: string;
|
|
3
|
+
readonly json?: boolean;
|
|
4
|
+
/** 把结果写到 `<root>/.vobs/check.json`(开发台面板读这个文件显示「项目」页)。 */
|
|
5
|
+
readonly write?: boolean;
|
|
6
|
+
/** 把测试文件也纳入检查(默认跳过:fixture 里常有意为之的写法会淹没真问题)。 */
|
|
7
|
+
readonly includeTests?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** 检查报告的固定落点,相对被检查的根目录。 */
|
|
10
|
+
declare const REPORT_PATH = ".vobs/check.json";
|
|
11
|
+
interface CheckDiagnostic {
|
|
12
|
+
/** 稳定错误码,AI 与文档按它检索。 */
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly severity: 'error' | 'warning';
|
|
15
|
+
readonly message: string;
|
|
16
|
+
/** 该怎么改。只报错对 AI 没有价值。 */
|
|
17
|
+
readonly fix: string;
|
|
18
|
+
/** 相对目标目录的路径。 */
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly line: number;
|
|
21
|
+
readonly column: number;
|
|
22
|
+
/** 出错那一行的原文,方便直接定位。 */
|
|
23
|
+
readonly snippet: string;
|
|
24
|
+
}
|
|
25
|
+
/** effect 写入了自己依赖的信号。 */
|
|
26
|
+
declare const VOBS_C210 = "VOBS_C210";
|
|
27
|
+
/** 列表写在分支位置 —— 失去 keyed 复用。 */
|
|
28
|
+
declare const VOBS_C232 = "VOBS_C232";
|
|
29
|
+
/** 在组件体里读信号并存进局部变量 —— 组件体只执行一次,之后永不更新。 */
|
|
30
|
+
declare const VOBS_C118 = "VOBS_C118";
|
|
31
|
+
declare function analyzeSource(text: string, file: string): CheckDiagnostic[];
|
|
32
|
+
declare function checkCommand(options?: CheckOptions): Promise<void>;
|
|
33
|
+
|
|
34
|
+
export { type CheckDiagnostic, type CheckOptions, REPORT_PATH, VOBS_C118, VOBS_C210, VOBS_C232, analyzeSource, checkCommand };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface CheckOptions {
|
|
2
|
+
readonly dir?: string;
|
|
3
|
+
readonly json?: boolean;
|
|
4
|
+
/** 把结果写到 `<root>/.vobs/check.json`(开发台面板读这个文件显示「项目」页)。 */
|
|
5
|
+
readonly write?: boolean;
|
|
6
|
+
/** 把测试文件也纳入检查(默认跳过:fixture 里常有意为之的写法会淹没真问题)。 */
|
|
7
|
+
readonly includeTests?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** 检查报告的固定落点,相对被检查的根目录。 */
|
|
10
|
+
declare const REPORT_PATH = ".vobs/check.json";
|
|
11
|
+
interface CheckDiagnostic {
|
|
12
|
+
/** 稳定错误码,AI 与文档按它检索。 */
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly severity: 'error' | 'warning';
|
|
15
|
+
readonly message: string;
|
|
16
|
+
/** 该怎么改。只报错对 AI 没有价值。 */
|
|
17
|
+
readonly fix: string;
|
|
18
|
+
/** 相对目标目录的路径。 */
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly line: number;
|
|
21
|
+
readonly column: number;
|
|
22
|
+
/** 出错那一行的原文,方便直接定位。 */
|
|
23
|
+
readonly snippet: string;
|
|
24
|
+
}
|
|
25
|
+
/** effect 写入了自己依赖的信号。 */
|
|
26
|
+
declare const VOBS_C210 = "VOBS_C210";
|
|
27
|
+
/** 列表写在分支位置 —— 失去 keyed 复用。 */
|
|
28
|
+
declare const VOBS_C232 = "VOBS_C232";
|
|
29
|
+
/** 在组件体里读信号并存进局部变量 —— 组件体只执行一次,之后永不更新。 */
|
|
30
|
+
declare const VOBS_C118 = "VOBS_C118";
|
|
31
|
+
declare function analyzeSource(text: string, file: string): CheckDiagnostic[];
|
|
32
|
+
declare function checkCommand(options?: CheckOptions): Promise<void>;
|
|
33
|
+
|
|
34
|
+
export { type CheckDiagnostic, type CheckOptions, REPORT_PATH, VOBS_C118, VOBS_C210, VOBS_C232, analyzeSource, checkCommand };
|