@vobs/cli 1.7.8 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/banner.cjs +22 -3
  2. package/dist/banner.cjs.map +1 -1
  3. package/dist/banner.d.cts +6 -2
  4. package/dist/banner.d.ts +6 -2
  5. package/dist/banner.js +20 -3
  6. package/dist/banner.js.map +1 -1
  7. package/dist/cli.cjs +361 -21
  8. package/dist/cli.cjs.map +1 -1
  9. package/dist/cli.d.cts +10 -1
  10. package/dist/cli.d.ts +10 -1
  11. package/dist/cli.js +351 -12
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/add.cjs +2 -2
  14. package/dist/commands/add.cjs.map +1 -1
  15. package/dist/commands/add.js +2 -2
  16. package/dist/commands/add.js.map +1 -1
  17. package/dist/commands/build.cjs +2 -2
  18. package/dist/commands/build.cjs.map +1 -1
  19. package/dist/commands/build.js +2 -2
  20. package/dist/commands/build.js.map +1 -1
  21. package/dist/commands/check.cjs +382 -0
  22. package/dist/commands/check.cjs.map +1 -0
  23. package/dist/commands/check.d.cts +34 -0
  24. package/dist/commands/check.d.ts +34 -0
  25. package/dist/commands/check.js +343 -0
  26. package/dist/commands/check.js.map +1 -0
  27. package/dist/commands/dev.cjs +2 -2
  28. package/dist/commands/dev.cjs.map +1 -1
  29. package/dist/commands/dev.js +2 -2
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/commands/dsh.cjs +2 -2
  32. package/dist/commands/dsh.cjs.map +1 -1
  33. package/dist/commands/dsh.js +2 -2
  34. package/dist/commands/dsh.js.map +1 -1
  35. package/dist/commands/generate.cjs +2 -2
  36. package/dist/commands/generate.cjs.map +1 -1
  37. package/dist/commands/generate.js +2 -2
  38. package/dist/commands/generate.js.map +1 -1
  39. package/dist/commands/index.cjs +34 -17
  40. package/dist/commands/index.cjs.map +1 -1
  41. package/dist/commands/index.js +25 -8
  42. package/dist/commands/index.js.map +1 -1
  43. package/dist/commands/init.cjs +20 -3
  44. package/dist/commands/init.cjs.map +1 -1
  45. package/dist/commands/init.js +20 -3
  46. package/dist/commands/init.js.map +1 -1
  47. package/dist/index.cjs +399 -25
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.js +388 -14
  50. package/dist/index.js.map +1 -1
  51. package/dist/repl.cjs +34 -17
  52. package/dist/repl.cjs.map +1 -1
  53. package/dist/repl.js +25 -8
  54. package/dist/repl.js.map +1 -1
  55. package/dist/start.cjs +399 -25
  56. package/dist/start.cjs.map +1 -1
  57. package/dist/start.d.cts +8 -1
  58. package/dist/start.d.ts +8 -1
  59. package/dist/start.js +388 -14
  60. package/dist/start.js.map +1 -1
  61. package/dist/utils/logger.cjs +2 -2
  62. package/dist/utils/logger.cjs.map +1 -1
  63. package/dist/utils/logger.js +2 -2
  64. package/dist/utils/logger.js.map +1 -1
  65. package/dist/version.cjs +47 -0
  66. package/dist/version.cjs.map +1 -0
  67. package/dist/version.d.cts +3 -0
  68. package/dist/version.d.ts +3 -0
  69. package/dist/version.js +23 -0
  70. package/dist/version.js.map +1 -0
  71. package/package.json +3 -2
  72. package/src/banner.ts +11 -5
  73. package/src/cli-process.test.ts +139 -0
  74. package/src/cli.ts +30 -2
  75. package/src/commands/check.test.ts +238 -0
  76. package/src/commands/check.ts +461 -0
  77. package/src/commands/init.ts +2 -1
  78. package/src/start.ts +66 -5
  79. package/src/utils/logger.ts +14 -2
  80. package/src/version.ts +27 -0
package/dist/index.cjs CHANGED
@@ -69,7 +69,7 @@ function timestamp() {
69
69
  }
70
70
  __name(timestamp, "timestamp");
71
71
  function info(msg) {
72
- console.log(`${timestamp()} ${import_picocolors.default.cyan("\u2139")} ${msg}`);
72
+ console.error(`${timestamp()} ${import_picocolors.default.cyan("\u2139")} ${msg}`);
73
73
  }
74
74
  __name(info, "info");
75
75
  function success(msg) {
@@ -77,7 +77,7 @@ function success(msg) {
77
77
  }
78
78
  __name(success, "success");
79
79
  function warn(msg) {
80
- console.log(`${timestamp()} ${import_picocolors.default.yellow("\u26A0")} ${msg}`);
80
+ console.error(`${timestamp()} ${import_picocolors.default.yellow("\u26A0")} ${msg}`);
81
81
  }
82
82
  __name(warn, "warn");
83
83
  function error(msg) {
@@ -181,6 +181,23 @@ function addPackage(targetDir, pm, pkg, dev) {
181
181
  }
182
182
  __name(addPackage, "addPackage");
183
183
 
184
+ // packages/cli/src/version.ts
185
+ var import_node_fs3 = require("fs");
186
+ var cached;
187
+ function cliVersion() {
188
+ if (cached !== void 0) return cached;
189
+ try {
190
+ const manifest = JSON.parse(
191
+ (0, import_node_fs3.readFileSync)(new URL("../package.json", __VOBS_CJS_FILE_URL), "utf8")
192
+ );
193
+ cached = manifest.version ?? "0.0.0";
194
+ } catch {
195
+ cached = "0.0.0";
196
+ }
197
+ return cached;
198
+ }
199
+ __name(cliVersion, "cliVersion");
200
+
184
201
  // packages/cli/src/commands/init.ts
185
202
  async function initCommand(options) {
186
203
  const projectName = options.name || await p.text({
@@ -226,7 +243,7 @@ async function initCommand(options) {
226
243
  name: projectName,
227
244
  packageName: projectName.toLowerCase().replace(/\s+/g, "-"),
228
245
  typescript: useTypescript,
229
- vobsVersion: "1.0.0"
246
+ vobsVersion: cliVersion()
230
247
  });
231
248
  logger.success("Project scaffolded");
232
249
  logger.step("Installing dependencies...");
@@ -378,7 +395,7 @@ async function addCommand(options) {
378
395
  __name(addCommand, "addCommand");
379
396
 
380
397
  // packages/cli/src/commands/dsh.ts
381
- var import_node_fs3 = require("fs");
398
+ var import_node_fs4 = require("fs");
382
399
  var import_promises = require("fs/promises");
383
400
  var import_node_child_process3 = require("child_process");
384
401
  var import_node_http = require("http");
@@ -389,7 +406,7 @@ var PLATFORM_MODULES = ["react", "react-dom"];
389
406
  var vobsVersion = /* @__PURE__ */ __name(() => {
390
407
  try {
391
408
  const manifest = JSON.parse(
392
- (0, import_node_fs3.readFileSync)(new URL("../../package.json", __VOBS_CJS_FILE_URL), "utf8")
409
+ (0, import_node_fs4.readFileSync)(new URL("../../package.json", __VOBS_CJS_FILE_URL), "utf8")
393
410
  );
394
411
  return manifest.version ?? "1.7.5";
395
412
  } catch {
@@ -431,7 +448,7 @@ async function dshInitCommand(target, options) {
431
448
  "dsh-plugin"
432
449
  );
433
450
  ensureDir(targetDir);
434
- (0, import_node_fs3.writeFileSync)(
451
+ (0, import_node_fs4.writeFileSync)(
435
452
  (0, import_node_path5.resolve)(targetDir, ".gitignore"),
436
453
  ["node_modules/", "*.log", "*.tsbuildinfo", "", "# lib/ \u662F\u6784\u5EFA\u4EA7\u7269\uFF0C\u5FC5\u987B\u63D0\u4EA4\uFF1ADSH \u5B89\u88C5\u65F6\u4E0D\u4F1A\u6784\u5EFA\u4F60\u7684\u5305\u3002", ""].join("\n"),
437
454
  "utf8"
@@ -458,7 +475,7 @@ __name(dshInitCommand, "dshInitCommand");
458
475
  async function dshBuildCommand() {
459
476
  const projectDir = process.cwd();
460
477
  const configFile = (0, import_node_path5.resolve)(projectDir, "vite.config.ts");
461
- if (!(0, import_node_fs3.existsSync)(configFile)) {
478
+ if (!(0, import_node_fs4.existsSync)(configFile)) {
462
479
  logger.error("\u5F53\u524D\u76EE\u5F55\u6CA1\u6709 vite.config.ts\uFF0C\u8BF7\u5728\u63D2\u4EF6\u9879\u76EE\u6839\u76EE\u5F55\u8FD0\u884C\u3002");
463
480
  process.exitCode = 1;
464
481
  return;
@@ -481,7 +498,7 @@ async function dshBuildCommand() {
481
498
  logger.info("\u6784\u5EFA Host \u4EA7\u7269 \u2026");
482
499
  const hostTarget = (0, import_node_path5.resolve)(projectDir, hostEntry.replace(/^\.\//u, ""));
483
500
  const hostEntrySource = (0, import_node_path5.resolve)(projectDir, "src", "host", "index.ts");
484
- if (!(0, import_node_fs3.existsSync)(hostEntrySource)) {
501
+ if (!(0, import_node_fs4.existsSync)(hostEntrySource)) {
485
502
  logger.warn(`\u6CA1\u6709\u627E\u5230 ${hostEntrySource}\uFF0C\u8DF3\u8FC7 Host \u6784\u5EFA\u3002`);
486
503
  return;
487
504
  }
@@ -594,7 +611,7 @@ async function dshDevCommand(options) {
594
611
  packageRoot = (0, import_node_path5.dirname)((0, import_node_path5.dirname)(projectRequire.resolve("@vobs/dsh")));
595
612
  }
596
613
  const candidate = (0, import_node_path5.join)(packageRoot, "dist", "preview.js");
597
- previewFile = (0, import_node_fs3.existsSync)(candidate) ? candidate : void 0;
614
+ previewFile = (0, import_node_fs4.existsSync)(candidate) ? candidate : void 0;
598
615
  } catch {
599
616
  previewFile = void 0;
600
617
  }
@@ -666,7 +683,7 @@ data: 1
666
683
  return;
667
684
  }
668
685
  if (url === "/client.js") {
669
- if (!(0, import_node_fs3.existsSync)(clientFile)) {
686
+ if (!(0, import_node_fs4.existsSync)(clientFile)) {
670
687
  response.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
671
688
  response.end(`\u8FD8\u6CA1\u6709\u4EA7\u7269\uFF1A${clientEntry}
672
689
  ${buildError ?? ""}`);
@@ -687,7 +704,7 @@ ${buildError ?? ""}`);
687
704
  __name(dshDevCommand, "dshDevCommand");
688
705
  function readManifest(dir) {
689
706
  try {
690
- return JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path5.resolve)(dir, "package.json"), "utf8"));
707
+ return JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path5.resolve)(dir, "package.json"), "utf8"));
691
708
  } catch {
692
709
  return void 0;
693
710
  }
@@ -710,10 +727,10 @@ function collectIssues(dir) {
710
727
  result.errors.push("\u7F3A\u5C11 dsh.bundle.patch \u2014\u2014 DSH \u4E0D\u4F1A\u628A\u8FD9\u4E2A\u5305\u5F53\u4F5C\u7EC4\u5408\u5305\uFF08bundle\uFF09");
711
728
  } else {
712
729
  const patchPath = (0, import_node_path5.resolve)(dir, bundle.patch.replace(/^\.\//u, ""));
713
- if (!(0, import_node_fs3.existsSync)(patchPath)) {
730
+ if (!(0, import_node_fs4.existsSync)(patchPath)) {
714
731
  result.errors.push(`dsh.bundle.patch \u6307\u5411\u7684 ${bundle.patch} \u4E0D\u5B58\u5728`);
715
732
  } else {
716
- const text5 = (0, import_node_fs3.readFileSync)(patchPath, "utf8");
733
+ const text5 = (0, import_node_fs4.readFileSync)(patchPath, "utf8");
717
734
  if (!text5.includes("insert:")) result.errors.push("cordis.patch.yml \u91CC\u6CA1\u6709 insert: \u6BB5\u843D\uFF0C\u63D2\u4EF6\u884C\u4E0D\u4F1A\u8FDB\u7EC4\u5408\u6811");
718
735
  else if (name !== "" && !text5.includes(name)) {
719
736
  result.warnings.push(`cordis.patch.yml \u91CC\u6CA1\u6709\u51FA\u73B0\u5305\u540D ${name}\uFF0C\u786E\u8BA4 insert \u7684 name \u5199\u5BF9\u4E86`);
@@ -727,7 +744,7 @@ function collectIssues(dir) {
727
744
  const hostEntry = exportsField["."];
728
745
  const clientEntry = exportsField["./client"];
729
746
  if (typeof hostEntry !== "string") result.errors.push('exports["."] \u672A\u58F0\u660E \u2014\u2014 DSH \u52A0\u8F7D\u4E0D\u5230 Host \u4FA7\u63D2\u4EF6');
730
- else if (!(0, import_node_fs3.existsSync)((0, import_node_path5.resolve)(dir, hostEntry.replace(/^\.\//u, "")))) {
747
+ else if (!(0, import_node_fs4.existsSync)((0, import_node_path5.resolve)(dir, hostEntry.replace(/^\.\//u, "")))) {
731
748
  result.errors.push(`exports["."] \u6307\u5411\u7684 ${hostEntry} \u4E0D\u5B58\u5728\uFF0C\u5148\u6784\u5EFA`);
732
749
  } else result.passed.push("Host \u5165\u53E3\u5B58\u5728");
733
750
  if (typeof clientEntry !== "string") result.errors.push('exports["./client"] \u672A\u58F0\u660E \u2014\u2014 \u9875\u9762\u62FF\u4E0D\u5230\u5BA2\u6237\u7AEF bundle');
@@ -745,10 +762,10 @@ function collectIssues(dir) {
745
762
  } else result.passed.push("\u6CA1\u6709 workspace: \u534F\u8BAE\u4F9D\u8D56");
746
763
  if (typeof clientEntry === "string") {
747
764
  const clientPath = (0, import_node_path5.resolve)(dir, clientEntry.replace(/^\.\//u, ""));
748
- if (!(0, import_node_fs3.existsSync)(clientPath)) {
765
+ if (!(0, import_node_fs4.existsSync)(clientPath)) {
749
766
  result.errors.push(`\u5BA2\u6237\u7AEF\u4EA7\u7269 ${clientEntry} \u4E0D\u5B58\u5728\uFF0C\u5148\u8FD0\u884C\u6784\u5EFA`);
750
767
  } else {
751
- const code = (0, import_node_fs3.readFileSync)(clientPath, "utf8");
768
+ const code = (0, import_node_fs4.readFileSync)(clientPath, "utf8");
752
769
  const loads = (code.match(/__ModuleLoader__\.load\(/gu) ?? []).length;
753
770
  if (loads !== 1) result.errors.push(`\u5BA2\u6237\u7AEF\u4EA7\u7269\u91CC __ModuleLoader__.load \u51FA\u73B0 ${loads} \u6B21\uFF0C\u5E94\u4E3A 1 \u6B21\uFF08\u5148\u6784\u5EFA\uFF09`);
754
771
  else result.passed.push("\u4EA7\u7269\u5F62\u6001\u662F DSH \u5BA2\u6237\u7AEF\u6A21\u5757\u534F\u8BAE");
@@ -799,7 +816,7 @@ function buildInstallSpec(options) {
799
816
  if (options.spec !== void 0) return { spec: options.spec };
800
817
  if (options.from !== void 0) {
801
818
  const target = (0, import_node_path5.resolve)(options.from);
802
- if (!(0, import_node_fs3.existsSync)(target)) return { error: `--from \u6307\u5411\u7684\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${target}` };
819
+ if (!(0, import_node_fs4.existsSync)(target)) return { error: `--from \u6307\u5411\u7684\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${target}` };
803
820
  return { spec: target };
804
821
  }
805
822
  if (options.repo !== void 0) {
@@ -906,8 +923,309 @@ async function dshCommand(action, target, options) {
906
923
  }
907
924
  __name(dshCommand, "dshCommand");
908
925
 
926
+ // packages/cli/src/commands/check.ts
927
+ var import_promises2 = require("fs/promises");
928
+ var import_node_path6 = __toESM(require("path"), 1);
929
+ var import_typescript = __toESM(require("typescript"), 1);
930
+ var REPORT_PATH = ".vobs/check.json";
931
+ var TEST_FILE = /(?:^|\/)(?:[^/]*\.(?:test|spec)\.tsx?|__tests__\/)/u;
932
+ var VOBS_C210 = "VOBS_C210";
933
+ var VOBS_C232 = "VOBS_C232";
934
+ var VOBS_C118 = "VOBS_C118";
935
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "lib", "build", ".git", ".vobs", "coverage"]);
936
+ async function collectSources(root) {
937
+ let rootStat;
938
+ try {
939
+ rootStat = await (0, import_promises2.stat)(root);
940
+ } catch {
941
+ throw new Error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${root}`);
942
+ }
943
+ if (!rootStat.isDirectory()) {
944
+ throw new Error(`\u4E0D\u662F\u76EE\u5F55\uFF1A${root}\uFF08vobs check \u63A5\u53D7\u4E00\u4E2A\u76EE\u5F55\uFF09`);
945
+ }
946
+ const found = [];
947
+ const walk = /* @__PURE__ */ __name(async (dir) => {
948
+ let entries;
949
+ try {
950
+ entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
951
+ } catch (error2) {
952
+ if (dir === root) {
953
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55\uFF1A${root}\uFF08${error2 instanceof Error ? error2.message : String(error2)}\uFF09`);
954
+ }
955
+ return;
956
+ }
957
+ for (const entry of entries) {
958
+ if (entry.isDirectory()) {
959
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
960
+ await walk(import_node_path6.default.join(dir, entry.name));
961
+ continue;
962
+ }
963
+ if (!entry.isFile()) continue;
964
+ if (!/\.tsx?$/u.test(entry.name) || entry.name.endsWith(".d.ts")) continue;
965
+ found.push(import_node_path6.default.join(dir, entry.name));
966
+ }
967
+ }, "walk");
968
+ await walk(root);
969
+ return found.sort();
970
+ }
971
+ __name(collectSources, "collectSources");
972
+ function returnsJsx(node) {
973
+ let seen = false;
974
+ const visit = /* @__PURE__ */ __name((child) => {
975
+ if (seen) return;
976
+ if (import_typescript.default.isJsxElement(child) || import_typescript.default.isJsxSelfClosingElement(child) || import_typescript.default.isJsxFragment(child)) {
977
+ seen = true;
978
+ return;
979
+ }
980
+ if (child !== node && import_typescript.default.isFunctionLike(child)) return;
981
+ import_typescript.default.forEachChild(child, visit);
982
+ }, "visit");
983
+ import_typescript.default.forEachChild(node, visit);
984
+ return seen;
985
+ }
986
+ __name(returnsJsx, "returnsJsx");
987
+ function signalNameOf(node) {
988
+ if (!import_typescript.default.isPropertyAccessExpression(node)) return void 0;
989
+ if (node.name.text !== "value") return void 0;
990
+ return node.expression.getText();
991
+ }
992
+ __name(signalNameOf, "signalNameOf");
993
+ function lineSnippet(source, node) {
994
+ const { line } = source.getLineAndCharacterOfPosition(node.getStart(source));
995
+ return (source.text.split(/\r?\n/u)[line] ?? "").trim();
996
+ }
997
+ __name(lineSnippet, "lineSnippet");
998
+ function diagnosticAt(source, file, node, rest) {
999
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
1000
+ return {
1001
+ ...rest,
1002
+ file,
1003
+ line: line + 1,
1004
+ column: character + 1,
1005
+ snippet: lineSnippet(source, node)
1006
+ };
1007
+ }
1008
+ __name(diagnosticAt, "diagnosticAt");
1009
+ 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");
1010
+ function ruleEffectSelfSubscription(source, file) {
1011
+ const found = [];
1012
+ const inspectEffectBody = /* @__PURE__ */ __name((body) => {
1013
+ const reads = /* @__PURE__ */ new Map();
1014
+ const writes = /* @__PURE__ */ new Map();
1015
+ const visit2 = /* @__PURE__ */ __name((node, insideUntrack) => {
1016
+ if (node !== body && import_typescript.default.isFunctionLike(node)) return;
1017
+ const nextUntracked = insideUntrack || import_typescript.default.isCallExpression(node) && import_typescript.default.isIdentifier(node.expression) && node.expression.text === "untrack";
1018
+ if (import_typescript.default.isBinaryExpression(node) && isWriteOperator(node.operatorToken.kind)) {
1019
+ const name = signalNameOf(node.left);
1020
+ if (name !== void 0 && !nextUntracked) writes.set(name, node);
1021
+ } 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)) {
1022
+ const name = signalNameOf(node.operand);
1023
+ if (name !== void 0) {
1024
+ if (!nextUntracked) writes.set(name, node);
1025
+ reads.set(name, node);
1026
+ }
1027
+ } else if (import_typescript.default.isCallExpression(node) && import_typescript.default.isPropertyAccessExpression(node.expression) && node.expression.name.text === "set") {
1028
+ if (!nextUntracked) writes.set(node.expression.expression.getText(), node);
1029
+ } else {
1030
+ const name = signalNameOf(node);
1031
+ if (name !== void 0) {
1032
+ const parent = node.parent;
1033
+ const isAssignmentTarget = import_typescript.default.isBinaryExpression(parent) && isWriteOperator(parent.operatorToken.kind) && parent.left === node;
1034
+ if (!isAssignmentTarget) reads.set(name, node);
1035
+ }
1036
+ }
1037
+ import_typescript.default.forEachChild(node, (child) => visit2(child, nextUntracked));
1038
+ }, "visit");
1039
+ visit2(body, false);
1040
+ for (const [name, writeNode] of writes) {
1041
+ if (!reads.has(name)) continue;
1042
+ found.push(diagnosticAt(source, file, writeNode, {
1043
+ code: VOBS_C210,
1044
+ severity: "error",
1045
+ 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`,
1046
+ 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`
1047
+ }));
1048
+ }
1049
+ }, "inspectEffectBody");
1050
+ const visit = /* @__PURE__ */ __name((node) => {
1051
+ if (import_typescript.default.isCallExpression(node) && import_typescript.default.isIdentifier(node.expression) && (node.expression.text === "effect" || node.expression.text === "renderEffect")) {
1052
+ const callback = node.arguments[0];
1053
+ if (callback !== void 0 && (import_typescript.default.isArrowFunction(callback) || import_typescript.default.isFunctionExpression(callback))) {
1054
+ inspectEffectBody(callback.body);
1055
+ }
1056
+ }
1057
+ import_typescript.default.forEachChild(node, visit);
1058
+ }, "visit");
1059
+ visit(source);
1060
+ return found;
1061
+ }
1062
+ __name(ruleEffectSelfSubscription, "ruleEffectSelfSubscription");
1063
+ function isKeyedListCall(node) {
1064
+ if (!import_typescript.default.isCallExpression(node)) return false;
1065
+ if (!import_typescript.default.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "map") return false;
1066
+ const callback = node.arguments[0];
1067
+ return callback !== void 0 && import_typescript.default.isFunctionLike(callback) && returnsJsx(callback);
1068
+ }
1069
+ __name(isKeyedListCall, "isKeyedListCall");
1070
+ function ruleListInBranch(source, file) {
1071
+ const found = [];
1072
+ 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";
1073
+ 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";
1074
+ const visit = /* @__PURE__ */ __name((node) => {
1075
+ if (import_typescript.default.isConditionalExpression(node)) {
1076
+ for (const branch of [node.whenTrue, node.whenFalse]) {
1077
+ if (isKeyedListCall(branch)) {
1078
+ found.push(diagnosticAt(source, file, branch, { code: VOBS_C232, severity: "warning", message, fix }));
1079
+ }
1080
+ }
1081
+ } else if (import_typescript.default.isBinaryExpression(node) && node.operatorToken.kind === import_typescript.default.SyntaxKind.AmpersandAmpersandToken && isKeyedListCall(node.right)) {
1082
+ found.push(diagnosticAt(source, file, node.right, { code: VOBS_C232, severity: "warning", message, fix }));
1083
+ }
1084
+ import_typescript.default.forEachChild(node, visit);
1085
+ }, "visit");
1086
+ visit(source);
1087
+ return found;
1088
+ }
1089
+ __name(ruleListInBranch, "ruleListInBranch");
1090
+ function containsSignalRead(node) {
1091
+ let seen = false;
1092
+ const visit = /* @__PURE__ */ __name((child) => {
1093
+ if (seen) return;
1094
+ if (import_typescript.default.isFunctionLike(child)) return;
1095
+ if (signalNameOf(child) !== void 0) {
1096
+ seen = true;
1097
+ return;
1098
+ }
1099
+ import_typescript.default.forEachChild(child, visit);
1100
+ }, "visit");
1101
+ visit(node);
1102
+ return seen;
1103
+ }
1104
+ __name(containsSignalRead, "containsSignalRead");
1105
+ function ruleSignalCapturedInBody(source, file) {
1106
+ const found = [];
1107
+ const inspectComponent = /* @__PURE__ */ __name((fn) => {
1108
+ const body = fn.body;
1109
+ if (body === void 0 || !import_typescript.default.isBlock(body) || !returnsJsx(fn)) return;
1110
+ const captured = [];
1111
+ for (const statement of body.statements) {
1112
+ if (!import_typescript.default.isVariableStatement(statement)) continue;
1113
+ for (const declaration of statement.declarationList.declarations) {
1114
+ if (!import_typescript.default.isIdentifier(declaration.name) || declaration.initializer === void 0) continue;
1115
+ if (containsSignalRead(declaration.initializer)) {
1116
+ captured.push({ name: declaration.name.text, node: declaration });
1117
+ }
1118
+ }
1119
+ }
1120
+ if (captured.length === 0) return;
1121
+ const usedInJsx = /* @__PURE__ */ new Set();
1122
+ const visit2 = /* @__PURE__ */ __name((node) => {
1123
+ if (import_typescript.default.isJsxExpression(node) && node.expression !== void 0) {
1124
+ const scan = /* @__PURE__ */ __name((child) => {
1125
+ if (import_typescript.default.isIdentifier(child)) usedInJsx.add(child.text);
1126
+ import_typescript.default.forEachChild(child, scan);
1127
+ }, "scan");
1128
+ scan(node.expression);
1129
+ }
1130
+ import_typescript.default.forEachChild(node, visit2);
1131
+ }, "visit");
1132
+ visit2(body);
1133
+ for (const item of captured) {
1134
+ if (!usedInJsx.has(item.name)) continue;
1135
+ found.push(diagnosticAt(source, file, item.node, {
1136
+ code: VOBS_C118,
1137
+ severity: "warning",
1138
+ 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`,
1139
+ 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`
1140
+ }));
1141
+ }
1142
+ }, "inspectComponent");
1143
+ const visit = /* @__PURE__ */ __name((node) => {
1144
+ if (import_typescript.default.isFunctionDeclaration(node) || import_typescript.default.isFunctionExpression(node) || import_typescript.default.isArrowFunction(node)) {
1145
+ inspectComponent(node);
1146
+ }
1147
+ import_typescript.default.forEachChild(node, visit);
1148
+ }, "visit");
1149
+ visit(source);
1150
+ return found;
1151
+ }
1152
+ __name(ruleSignalCapturedInBody, "ruleSignalCapturedInBody");
1153
+ var IGNORE_NEXT_LINE = /vobs-check-ignore-next-line/u;
1154
+ function suppressedLines(text5) {
1155
+ const suppressed = /* @__PURE__ */ new Set();
1156
+ const lines = text5.split(/\r?\n/u);
1157
+ for (let index = 0; index < lines.length; index++) {
1158
+ if (IGNORE_NEXT_LINE.test(lines[index])) suppressed.add(index + 2);
1159
+ }
1160
+ return suppressed;
1161
+ }
1162
+ __name(suppressedLines, "suppressedLines");
1163
+ function analyzeSource(text5, file) {
1164
+ const source = import_typescript.default.createSourceFile(file, text5, import_typescript.default.ScriptTarget.Latest, true, import_typescript.default.ScriptKind.TSX);
1165
+ const suppressed = suppressedLines(text5);
1166
+ return [
1167
+ ...ruleEffectSelfSubscription(source, file),
1168
+ ...ruleListInBranch(source, file),
1169
+ ...ruleSignalCapturedInBody(source, file)
1170
+ ].filter((item) => !suppressed.has(item.line)).sort((a, b) => a.line - b.line || a.column - b.column);
1171
+ }
1172
+ __name(analyzeSource, "analyzeSource");
1173
+ async function checkCommand(options = {}) {
1174
+ const root = import_node_path6.default.resolve(options.dir ?? process.cwd());
1175
+ let all;
1176
+ try {
1177
+ all = await collectSources(root);
1178
+ } catch (error2) {
1179
+ logger.error(error2 instanceof Error ? error2.message : String(error2));
1180
+ process.exitCode = 1;
1181
+ return;
1182
+ }
1183
+ const files = options.includeTests === true ? all : all.filter((file) => !TEST_FILE.test(import_node_path6.default.relative(root, file).split(import_node_path6.default.sep).join("/")));
1184
+ const skipped = all.length - files.length;
1185
+ const diagnostics = [];
1186
+ for (const absolute of files) {
1187
+ const text5 = await (0, import_promises2.readFile)(absolute, "utf8");
1188
+ const relative2 = import_node_path6.default.relative(root, absolute).split(import_node_path6.default.sep).join("/");
1189
+ diagnostics.push(...analyzeSource(text5, relative2));
1190
+ }
1191
+ const errors = diagnostics.filter((item) => item.severity === "error");
1192
+ const report = { root, files: files.length, skippedTests: skipped, diagnostics };
1193
+ if (options.write === true) {
1194
+ const target = import_node_path6.default.join(root, REPORT_PATH);
1195
+ await (0, import_promises2.mkdir)(import_node_path6.default.dirname(target), { recursive: true });
1196
+ await (0, import_promises2.writeFile)(target, `${JSON.stringify(report, null, 2)}
1197
+ `, "utf8");
1198
+ logger.info(`\u62A5\u544A\u5199\u5165 ${REPORT_PATH}\uFF08\u5F00\u53D1\u53F0\u9762\u677F\u8BFB\u5B83\u663E\u793A\u300C\u9879\u76EE\u300D\u9875\uFF09`);
1199
+ }
1200
+ if (options.json === true) {
1201
+ console.log(JSON.stringify(report, null, 2));
1202
+ if (errors.length > 0) process.exitCode = 1;
1203
+ return;
1204
+ }
1205
+ if (diagnostics.length === 0) {
1206
+ 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`);
1207
+ return;
1208
+ }
1209
+ console.log("");
1210
+ for (const item of diagnostics) {
1211
+ const mark = item.severity === "error" ? "\u2716" : "!";
1212
+ console.log(` ${mark} ${item.file}:${item.line}:${item.column} ${item.code}`);
1213
+ console.log(` ${item.message}`);
1214
+ if (item.snippet !== "") console.log(` ${item.snippet}`);
1215
+ console.log(` \u2192 ${item.fix}`);
1216
+ console.log("");
1217
+ }
1218
+ const warnings = diagnostics.length - errors.length;
1219
+ console.log(
1220
+ ` ${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` : ""}
1221
+ `
1222
+ );
1223
+ if (errors.length > 0) process.exitCode = 1;
1224
+ }
1225
+ __name(checkCommand, "checkCommand");
1226
+
909
1227
  // packages/cli/src/banner.ts
910
- var BANNER_LINE1 = "\u{1D5E9}\u{1D5E2}\u{1D5D5}\u{1D5E6} \u{1D5D6}\u{1D5DF}\u{1D5DC} \u{1D603}\u{1D7ED}.\u{1D7EC}";
1228
+ var BANNER_LINE1 = "\u{1D5E9}\u{1D5E2}\u{1D5D5}\u{1D5E6} \u{1D5D6}\u{1D5DF}\u{1D5DC}";
911
1229
  var BANNER_TAGLINE = "Signals First \xB7 Zero Re-renders \xB7 Ultra Lightweight";
912
1230
  var INDENT = " ";
913
1231
  var DIM = "\x1B[2m";
@@ -923,12 +1241,12 @@ function showLogo() {
923
1241
  }
924
1242
  __name(showLogo, "showLogo");
925
1243
  function showBanner() {
926
- console.log(`${INDENT}${BANNER_LINE1}`);
1244
+ console.log(`${INDENT}${BANNER_LINE1} v${cliVersion()}`);
927
1245
  }
928
1246
  __name(showBanner, "showBanner");
929
1247
  function showDevBanner() {
930
1248
  console.log();
931
- console.log(`${INDENT}${BANNER_LINE1}`);
1249
+ console.log(`${INDENT}${BANNER_LINE1} v${cliVersion()}`);
932
1250
  console.log();
933
1251
  }
934
1252
  __name(showDevBanner, "showDevBanner");
@@ -982,6 +1300,14 @@ function createCLI() {
982
1300
  dev: options.dev
983
1301
  });
984
1302
  });
1303
+ cli.command("check [dir]", "\u9759\u6001\u68C0\u67E5\u6E90\u7801\uFF1Aeffect \u81EA\u8BA2\u9605 / \u5217\u8868\u5199\u8FDB\u5206\u652F / \u7EC4\u4EF6\u4F53\u91CC\u8BFB\u4FE1\u53F7\uFF08\u884C\u5185\u6291\u5236\uFF1A\u4E0A\u4E00\u884C\u5199 // vobs-check-ignore-next-line\uFF09").option("--json", "\u4EE5 JSON \u8F93\u51FA\uFF08\u7ED9 AI \u4E0E\u5DE5\u5177\u6D88\u8D39\uFF09").option("--write", `\u628A\u7ED3\u679C\u5199\u5230 ${".vobs/check.json"}\uFF08\u5F00\u53D1\u53F0\u9762\u677F\u8BFB\u5B83\uFF09`).option("--include-tests", "\u628A\u6D4B\u8BD5\u6587\u4EF6\u4E5F\u7EB3\u5165\u68C0\u67E5\uFF08\u9ED8\u8BA4\u8DF3\u8FC7\uFF09").action(async (dir, options) => {
1304
+ await checkCommand({
1305
+ dir,
1306
+ json: options.json === true,
1307
+ write: options.write === true,
1308
+ includeTests: options.includeTests === true
1309
+ });
1310
+ });
985
1311
  cli.command("dsh [action] [target]", "DSH \u63D2\u4EF6\u5DE5\u5177\u94FE\uFF08init / dev / build / check / install\uFF09").option("--dir <dir>", "init\uFF1A\u76EE\u6807\u76EE\u5F55").option("--pm <pm>", "init\uFF1A\u5305\u7BA1\u7406\u5668\uFF08pnpm/npm\uFF09").option("--port <port>", "dev\uFF1A\u9884\u89C8\u7AEF\u53E3", { default: 5199 }).option("--profile <name>", "install\uFF1ADSH profile \u540D", { default: "desktop" }).option("--spec <spec>", "install\uFF1A\u5B8C\u6574\u5B89\u88C5 spec").option("--repo <owner/name>", "install\uFF1AGitHub owner/name").option("--tag <ref>", "install\uFF1AGit tag / \u5206\u652F / commit").option("--subpath <path>", "install\uFF1A\u4ED3\u5E93\u5185\u5B50\u76EE\u5F55").option("--from <path>", "install\uFF1A\u4ECE\u672C\u5730\u76EE\u5F55\u5B89\u88C5").option("--dry", "install\uFF1A\u53EA\u6253\u5370\u547D\u4EE4\uFF0C\u4E0D\u6267\u884C").action(async (action, target, options) => {
986
1312
  await dshCommand(action, target, {
987
1313
  dir: options.dir,
@@ -1003,18 +1329,30 @@ function createCLI() {
1003
1329
  showBanner();
1004
1330
  });
1005
1331
  cli.help();
1006
- cli.version("1.0.0");
1332
+ cli.version(cliVersion());
1007
1333
  return cli;
1008
1334
  }
1009
1335
  __name(createCLI, "createCLI");
1336
+ var KNOWN_SUBCOMMANDS = [
1337
+ "init",
1338
+ "create",
1339
+ "dev",
1340
+ "build",
1341
+ "generate",
1342
+ "g",
1343
+ "add",
1344
+ "check",
1345
+ "dsh",
1346
+ "logo"
1347
+ ];
1010
1348
 
1011
1349
  // packages/cli/src/repl.ts
1012
- var import_promises2 = require("readline/promises");
1350
+ var import_promises3 = require("readline/promises");
1013
1351
  var import_node_process = require("process");
1014
1352
  var import_picocolors2 = __toESM(require("picocolors"), 1);
1015
1353
  async function startRepl() {
1016
1354
  showLogo();
1017
- const rl = (0, import_promises2.createInterface)({
1355
+ const rl = (0, import_promises3.createInterface)({
1018
1356
  input: import_node_process.stdin,
1019
1357
  output: import_node_process.stdout,
1020
1358
  prompt: ` ${import_picocolors2.default.magenta("\u276F")} `,
@@ -1125,16 +1463,52 @@ function splitArgs(line) {
1125
1463
  __name(splitArgs, "splitArgs");
1126
1464
 
1127
1465
  // packages/cli/src/start.ts
1466
+ var META_FLAGS = /* @__PURE__ */ new Set(["--help", "-h", "--version", "-V"]);
1467
+ function isFlag(token) {
1468
+ return token.startsWith("-") && token !== "-";
1469
+ }
1470
+ __name(isFlag, "isFlag");
1128
1471
  async function start(args) {
1129
1472
  if (args.length === 0 || args[0] === "cli") {
1130
1473
  await startRepl();
1131
1474
  return;
1132
1475
  }
1133
- if (args.includes("--version") || args.includes("-V")) {
1476
+ const first = args[0];
1477
+ const hasSubcommand = KNOWN_SUBCOMMANDS.includes(first);
1478
+ if (!hasSubcommand && (args.includes("--version") || args.includes("-V"))) {
1134
1479
  showBanner();
1135
1480
  return;
1136
1481
  }
1137
- createCLI().parse();
1482
+ if (!hasSubcommand && !isFlag(first)) {
1483
+ process.stderr.write(`\u2716 \u672A\u77E5\u547D\u4EE4\uFF1A${first}
1484
+
1485
+ `);
1486
+ try {
1487
+ createCLI().parse(["node", "vobs", "--help"]);
1488
+ } catch {
1489
+ }
1490
+ process.exitCode = 1;
1491
+ return;
1492
+ }
1493
+ if (!hasSubcommand && isFlag(first) && !META_FLAGS.has(first)) {
1494
+ process.stderr.write(`\u2716 \u672A\u77E5\u9009\u9879\uFF1A${first}
1495
+
1496
+ `);
1497
+ try {
1498
+ createCLI().parse(["node", "vobs", "--help"]);
1499
+ } catch {
1500
+ }
1501
+ process.exitCode = 1;
1502
+ return;
1503
+ }
1504
+ try {
1505
+ createCLI().parse();
1506
+ } catch (error2) {
1507
+ const message = error2 instanceof Error ? error2.message : String(error2);
1508
+ process.stderr.write(`\u2716 ${message}
1509
+ `);
1510
+ process.exitCode = 1;
1511
+ }
1138
1512
  }
1139
1513
  __name(start, "start");
1140
1514
  // Annotate the CommonJS export names for ESM import in node: