openxiangda 1.0.162 → 1.0.164

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 (33) hide show
  1. package/README.md +6 -2
  2. package/lib/change-baseline.js +88 -2
  3. package/lib/cli.js +565 -73
  4. package/lib/js-code-build.js +120 -8
  5. package/lib/publish-lease.js +13 -1
  6. package/lib/release-mainline.js +545 -0
  7. package/lib/runtime-lineage.js +19 -12
  8. package/lib/sdd.js +137 -0
  9. package/lib/workspace-bootstrap.js +36 -7
  10. package/lib/workspace-init.js +12 -3
  11. package/lib/worktree-owner.js +13 -0
  12. package/openxiangda-skills/SKILL.md +9 -3
  13. package/openxiangda-skills/references/automation-v3.md +1 -1
  14. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +1 -1
  15. package/openxiangda-skills/references/workflow-v3.md +1 -1
  16. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +8 -2
  17. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -2
  18. package/package.json +3 -1
  19. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +2 -1
  20. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -2
  21. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +2 -1
  22. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -3
  23. package/templates/openxiangda-react-spa/AGENTS.md +8 -6
  24. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +172 -20
  25. package/templates/openxiangda-react-spa/scripts/deploy.mjs +19 -39
  26. package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +4 -5
  27. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +3 -1
  28. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -1
  29. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +3 -1
  30. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -1
  31. package/templates/sy-lowcode-app-workspace/AGENTS.md +4 -4
  32. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +172 -20
  33. package/templates/sy-lowcode-app-workspace/scripts/guard-publish.mjs +4 -5
@@ -19,13 +19,29 @@ import path from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { build } from "vite";
21
21
 
22
- const rootDir = fileURLToPath(new URL("..", import.meta.url));
23
22
  const scriptFile = fileURLToPath(import.meta.url);
23
+ const defaultRootDir = fileURLToPath(new URL("..", import.meta.url));
24
+ const rootDir = path.resolve(process.env.OPENXIANGDA_WORKSPACE_ROOT || defaultRootDir);
24
25
  const args = process.argv.slice(2);
25
26
  const require = createRequire(import.meta.url);
26
- const CACHE_VERSION = 2;
27
- const CACHE_FILE = path.join(rootDir, ".openxiangda", "build-cache.json");
27
+ const ts = require("typescript");
28
+ const CACHE_VERSION = 3;
29
+ const builderFingerprint = crypto
30
+ .createHash("sha256")
31
+ .update(readFileSync(scriptFile))
32
+ .digest("hex");
33
+ const workspaceBuilderFile = path.join(rootDir, "scripts", "build-js-code.mjs");
34
+ const workspaceBuilderMatches =
35
+ existsSync(workspaceBuilderFile) &&
36
+ crypto.createHash("sha256").update(readFileSync(workspaceBuilderFile)).digest("hex") ===
37
+ builderFingerprint;
38
+ const cacheFileName =
39
+ path.resolve(defaultRootDir) !== rootDir && !workspaceBuilderMatches
40
+ ? "build-cache.cli-v3.json"
41
+ : "build-cache.json";
42
+ const CACHE_FILE = path.join(rootDir, ".openxiangda", cacheFileName);
28
43
  const CACHE_LOCK_FILE = `${CACHE_FILE}.lock`;
44
+ const TSCONFIG_FILE = path.join(rootDir, "tsconfig.js-code-nodes.json");
29
45
  const forceBuild = args.includes("--force") || args.includes("--no-cache");
30
46
 
31
47
  const sourceKinds = {
@@ -84,11 +100,24 @@ function assertScriptCode(scriptCode) {
84
100
  function listScriptCodes(kind) {
85
101
  if (!existsSync(kind.sourceRoot)) return [];
86
102
  return readdirSync(kind.sourceRoot, { withFileTypes: true })
87
- .filter((entry) => entry.isDirectory() && existsSync(path.join(kind.sourceRoot, entry.name, "index.ts")))
103
+ .filter(
104
+ (entry) =>
105
+ entry.isDirectory() &&
106
+ ["index.ts", "index.tsx"].some((name) =>
107
+ existsSync(path.join(kind.sourceRoot, entry.name, name)),
108
+ ),
109
+ )
88
110
  .map((entry) => entry.name)
89
111
  .sort();
90
112
  }
91
113
 
114
+ function targetEntryFile(target) {
115
+ const base = path.join(target.kind.sourceRoot, target.scriptCode);
116
+ return ["index.ts", "index.tsx"]
117
+ .map((name) => path.join(base, name))
118
+ .find((file) => existsSync(file)) || path.join(base, "index.ts");
119
+ }
120
+
92
121
  function parseScriptSpec(rawSpec, defaultKind) {
93
122
  const prefixed = String(rawSpec).match(/^(js-code-nodes|automations|functions)[:/](.+)$/);
94
123
  const requestedKind = prefixed?.[1] || defaultKind?.name;
@@ -101,7 +130,9 @@ function parseScriptSpec(rawSpec, defaultKind) {
101
130
  if (requestedKind) return { kind: sourceKinds[requestedKind], scriptCode };
102
131
 
103
132
  const matches = Object.values(sourceKinds).filter((kind) =>
104
- existsSync(path.join(kind.sourceRoot, scriptCode, "index.ts")),
133
+ ["index.ts", "index.tsx"].some((name) =>
134
+ existsSync(path.join(kind.sourceRoot, scriptCode, name)),
135
+ ),
105
136
  );
106
137
  if (matches.length > 1) {
107
138
  console.warn(
@@ -111,7 +142,7 @@ function parseScriptSpec(rawSpec, defaultKind) {
111
142
  return { kind: matches[0] || sourceKinds["js-code-nodes"], scriptCode };
112
143
  }
113
144
 
114
- function resolveBuildTargets() {
145
+ function resolveBuildSelection() {
115
146
  const sourceArgs = readArgs("source");
116
147
  const sourceNames = [...new Set(sourceArgs)];
117
148
  if (sourceNames.length > 1) throw new Error("--source may only select one source kind");
@@ -128,7 +159,10 @@ function resolveBuildTargets() {
128
159
  : (selectedKind ? [selectedKind] : Object.values(sourceKinds)).flatMap((kind) =>
129
160
  listScriptCodes(kind).map((scriptCode) => ({ kind, scriptCode })),
130
161
  );
131
- return [...new Map(targets.map((target) => [`${target.kind.name}/${target.scriptCode}`, target])).values()];
162
+ return {
163
+ scoped: specs.length > 0,
164
+ targets: [...new Map(targets.map((target) => [`${target.kind.name}/${target.scriptCode}`, target])).values()],
165
+ };
132
166
  }
133
167
 
134
168
  function packageVersion(name) {
@@ -144,6 +178,7 @@ const toolFingerprint = crypto
144
178
  .update(
145
179
  JSON.stringify({
146
180
  cacheVersion: CACHE_VERSION,
181
+ builder: builderFingerprint,
147
182
  node: process.version,
148
183
  platform: process.platform,
149
184
  arch: process.arch,
@@ -157,14 +192,25 @@ const toolFingerprint = crypto
157
192
  .digest("hex");
158
193
 
159
194
  function emptyCache() {
160
- return { version: CACHE_VERSION, toolFingerprint, typecheck: null, targets: {} };
195
+ return {
196
+ version: CACHE_VERSION,
197
+ toolFingerprint,
198
+ typecheck: null,
199
+ typechecks: {},
200
+ targets: {},
201
+ };
161
202
  }
162
203
 
163
204
  function readCache(file = CACHE_FILE) {
164
205
  try {
165
206
  const cache = JSON.parse(readFileSync(file, "utf8"));
166
207
  if (cache.version !== CACHE_VERSION) return emptyCache();
167
- return { ...emptyCache(), ...cache, targets: cache.targets || {} };
208
+ return {
209
+ ...emptyCache(),
210
+ ...cache,
211
+ typechecks: cache.typechecks || {},
212
+ targets: cache.targets || {},
213
+ };
168
214
  } catch {
169
215
  return emptyCache();
170
216
  }
@@ -231,7 +277,7 @@ function typecheckInputHash() {
231
277
  }
232
278
 
233
279
  function targetInputHash(target, dependencies) {
234
- const entry = relativeWorkspaceFile(path.join(target.kind.sourceRoot, target.scriptCode, "index.ts"));
280
+ const entry = relativeWorkspaceFile(targetEntryFile(target));
235
281
  return hashFileSet(
236
282
  [...configInputFiles(), entry, ...(dependencies || [])].filter(Boolean),
237
283
  `target:${target.kind.name}/${target.scriptCode}`,
@@ -258,11 +304,94 @@ function isTargetCacheHit(cache, target) {
258
304
  );
259
305
  }
260
306
 
261
- function typecheckScripts(cache) {
307
+ function formatTypeScriptDiagnostics(diagnostics) {
308
+ return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
309
+ getCanonicalFileName: (file) => file,
310
+ getCurrentDirectory: () => rootDir,
311
+ getNewLine: () => "\n",
312
+ });
313
+ }
314
+
315
+ function scopedTypecheckKey(targets) {
316
+ return targets
317
+ .map((target) => `${target.kind.name}/${target.scriptCode}`)
318
+ .sort()
319
+ .join(",");
320
+ }
321
+
322
+ function scopedAmbientDeclarationFiles() {
323
+ return walkFiles(path.join(rootDir, "src"))
324
+ .filter((file) => file.endsWith(".d.ts"))
325
+ .map(relativeWorkspaceFile)
326
+ .filter(Boolean);
327
+ }
328
+
329
+ function createScopedTypecheckProgram(targets) {
330
+ const loaded = ts.readConfigFile(TSCONFIG_FILE, ts.sys.readFile);
331
+ if (loaded.error) {
332
+ process.stderr.write(formatTypeScriptDiagnostics([loaded.error]));
333
+ throw new Error("JS_CODE scoped TypeScript config validation failed");
334
+ }
335
+ const files = [
336
+ ...targets.map((target) =>
337
+ relativeWorkspaceFile(targetEntryFile(target)),
338
+ ),
339
+ ...scopedAmbientDeclarationFiles(),
340
+ ].filter(Boolean);
341
+ const parsed = ts.parseJsonConfigFileContent(
342
+ { ...loaded.config, files, include: [] },
343
+ ts.sys,
344
+ rootDir,
345
+ { noEmit: true },
346
+ TSCONFIG_FILE,
347
+ );
348
+ if (parsed.errors.length > 0) {
349
+ process.stderr.write(formatTypeScriptDiagnostics(parsed.errors));
350
+ throw new Error("JS_CODE scoped TypeScript config validation failed");
351
+ }
352
+ return ts.createProgram({
353
+ rootNames: parsed.fileNames,
354
+ options: parsed.options,
355
+ projectReferences: parsed.projectReferences,
356
+ });
357
+ }
358
+
359
+ function scopedTypecheckInputHash(program, cacheKey) {
360
+ const sourceFiles = program
361
+ .getSourceFiles()
362
+ .map((sourceFile) => relativeWorkspaceFile(sourceFile.fileName))
363
+ .filter(Boolean);
364
+ return hashFileSet([...configInputFiles(), ...sourceFiles], `typecheck:${cacheKey}`);
365
+ }
366
+
367
+ function typecheckSelectedScripts(cache, targets) {
368
+ const cacheKey = scopedTypecheckKey(targets);
369
+ const program = createScopedTypecheckProgram(targets);
370
+ const inputHash = scopedTypecheckInputHash(program, cacheKey);
371
+ if (
372
+ !forceBuild &&
373
+ cache.toolFingerprint === toolFingerprint &&
374
+ cache.typechecks?.[cacheKey]?.inputHash === inputHash
375
+ ) {
376
+ console.log(`[build-js-code] scoped TypeScript validation cache hit (${targets.length} selected)`);
377
+ return { cacheHit: true, cacheKey, inputHash, mode: "scoped" };
378
+ }
379
+
380
+ console.log(`[build-js-code] validating TypeScript for ${targets.length} selected target(s)...`);
381
+ const diagnostics = ts.getPreEmitDiagnostics(program);
382
+ if (diagnostics.length > 0) {
383
+ process.stderr.write(formatTypeScriptDiagnostics(diagnostics));
384
+ throw new Error("JS_CODE scoped TypeScript validation failed");
385
+ }
386
+ console.log("[build-js-code] scoped TypeScript validation passed");
387
+ return { cacheHit: false, cacheKey, inputHash, mode: "scoped" };
388
+ }
389
+
390
+ function typecheckAllScripts(cache) {
262
391
  const inputHash = typecheckInputHash();
263
392
  if (!forceBuild && cache.toolFingerprint === toolFingerprint && cache.typecheck?.inputHash === inputHash) {
264
393
  console.log("[build-js-code] TypeScript validation cache hit");
265
- return { cacheHit: true, inputHash };
394
+ return { cacheHit: true, inputHash, mode: "full" };
266
395
  }
267
396
 
268
397
  console.log("[build-js-code] validating TypeScript once for this batch...");
@@ -275,7 +404,13 @@ function typecheckScripts(cache) {
275
404
  console.log("[build-js-code] TypeScript validation passed");
276
405
  // pnpm may materialize/update a lockfile on the first invocation. Record the
277
406
  // post-validation inputs so the next process can reuse this successful tsc.
278
- return { cacheHit: false, inputHash: typecheckInputHash() };
407
+ return { cacheHit: false, inputHash: typecheckInputHash(), mode: "full" };
408
+ }
409
+
410
+ function typecheckScripts(cache, targets, scoped) {
411
+ return scoped
412
+ ? typecheckSelectedScripts(cache, targets)
413
+ : typecheckAllScripts(cache);
279
414
  }
280
415
 
281
416
  function collectBuildDependencies(buildResult, entry) {
@@ -296,7 +431,7 @@ function collectBuildDependencies(buildResult, entry) {
296
431
 
297
432
  async function buildScript(target) {
298
433
  const { kind, scriptCode } = target;
299
- const entry = path.join(kind.sourceRoot, scriptCode, "index.ts");
434
+ const entry = targetEntryFile(target);
300
435
  if (!existsSync(entry)) throw new Error(`${kind.label} script not found: ${entry}`);
301
436
  const outDir = path.join(kind.outputRoot, scriptCode);
302
437
  const external = Array.from(
@@ -361,14 +496,31 @@ function saveCache(typecheck, updatedTargets) {
361
496
  if (typecheck.cacheHit && updatedTargets.size === 0) return;
362
497
  const lockFd = acquireCacheLock();
363
498
  try {
364
- const latest = readCache();
499
+ const stored = readCache();
500
+ // A scoped build updates only part of the cache. Never carry metadata from
501
+ // a different compiler/builder fingerprint into the new cache, otherwise
502
+ // untouched targets could become false hits after this process stamps the
503
+ // cache with the current fingerprint.
504
+ const latest = stored.toolFingerprint === toolFingerprint
505
+ ? stored
506
+ : emptyCache();
507
+ const validatedAt = new Date().toISOString();
508
+ const mergedTypechecks = { ...latest.typechecks };
509
+ if (!typecheck.cacheHit && typecheck.mode === "scoped") {
510
+ mergedTypechecks[typecheck.cacheKey] = {
511
+ inputHash: typecheck.inputHash,
512
+ validatedAt,
513
+ };
514
+ }
365
515
  const merged = {
366
516
  ...latest,
367
517
  version: CACHE_VERSION,
368
518
  toolFingerprint,
369
- typecheck: typecheck.cacheHit
370
- ? latest.typecheck
371
- : { inputHash: typecheck.inputHash, validatedAt: new Date().toISOString() },
519
+ typecheck:
520
+ !typecheck.cacheHit && typecheck.mode === "full"
521
+ ? { inputHash: typecheck.inputHash, validatedAt }
522
+ : latest.typecheck,
523
+ typechecks: mergedTypechecks,
372
524
  targets: { ...latest.targets, ...Object.fromEntries(updatedTargets) },
373
525
  };
374
526
  const tempFile = `${CACHE_FILE}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
@@ -398,7 +550,7 @@ function saveCache(typecheck, updatedTargets) {
398
550
  }
399
551
  }
400
552
 
401
- const targets = resolveBuildTargets();
553
+ const { scoped, targets } = resolveBuildSelection();
402
554
  if (targets.length === 0) {
403
555
  console.log(
404
556
  "no JS_CODE scripts found under src/js-code-nodes/<scriptCode>/index.ts, src/automations/<scriptCode>/index.ts, or src/functions/<functionCode>/index.ts",
@@ -407,7 +559,7 @@ if (targets.length === 0) {
407
559
  }
408
560
 
409
561
  const cache = readCache();
410
- const typecheck = typecheckScripts(cache);
562
+ const typecheck = typecheckScripts(cache, targets, scoped);
411
563
  const updatedTargets = new Map();
412
564
  let builtCount = 0;
413
565
  let cachedCount = 0;
@@ -7,17 +7,16 @@
7
7
  if (!process.env.OPENXIANGDA_PROFILE) {
8
8
  const lines = [
9
9
  '',
10
- '❌ 错误:请使用 openxiangda workspace publish --profile <name>',
10
+ '❌ 错误:请使用带 change 与精确 scope 的 openxiangda workspace publish',
11
11
  '',
12
12
  '此 npm script(publish:all / publish:oss / register / publish:changed / openxiangda:publish 等)',
13
13
  '是工作区内部实现,直接调用会缺少 token / profile / appType / OSS 密钥的注入,',
14
14
  '可能发到错误的环境,或彻底发不出去。',
15
15
  '',
16
16
  '正确入口:',
17
- ' openxiangda workspace publish --profile <name> # 全量',
18
- ' openxiangda workspace publish --profile <name> --changed # 增量(推荐)',
19
- ' openxiangda workspace publish --profile <name> --page X # 单页',
20
- ' openxiangda workspace publish --profile <name> --form Y # 单表',
17
+ ' openxiangda workspace publish --change <change> --profile <name> --only pages/X,forms/Y --dry-run',
18
+ ' openxiangda workspace publish --change <change> --profile <name> --only pages/X,forms/Y',
19
+ ' 发布后 merge/fast-forward 并 push 冻结 SHA,再运行 release integration-status 与 release end',
21
20
  '',
22
21
  '更多说明详见工作区根目录的 AGENTS.md。',
23
22
  '',