@lingfan-engine/stories-build 0.3.4 → 0.3.6

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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/emit/emit.ts +75 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingfan-engine/stories-build",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "description": "Stories 创作链编译包:把 Stories.src 的多目录原生 TS 源编译成 Resources/Stories/** + project.json,并产出 cell 注册生成物",
6
6
  "license": "Apache-2.0",
@@ -28,8 +28,8 @@
28
28
  ],
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "@lingfan-engine/core": "^0.3.4",
32
- "@lingfan-engine/editor": "^0.3.4",
31
+ "@lingfan-engine/core": "^0.3.6",
32
+ "@lingfan-engine/editor": "^0.3.6",
33
33
  "esbuild": "^0.28.2",
34
34
  "tsx": "^4.23.15",
35
35
  "typescript": "~6.0.3"
package/src/emit/emit.ts CHANGED
@@ -6,9 +6,12 @@ import {
6
6
  writeFileSync,
7
7
  } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
- import { STORIES_DIR, type SerializedProject } from "@lingfan-engine/core";
9
+ import {
10
+ compareOrdinal,
11
+ STORIES_DIR,
12
+ type SerializedProject,
13
+ } from "@lingfan-engine/core";
10
14
  import { renderFunRegister, type CellScan } from "../cell/index.ts";
11
- import { walkFiles } from "../shared/index.ts";
12
15
 
13
16
  /** 写盘所需的位置与内容(全部由编排层算好,本模块只做差量判定与落盘) */
14
17
  export interface EmitOptions {
@@ -30,14 +33,22 @@ export interface EmitOptions {
30
33
  readonly uiGen: { readonly rel: string; readonly content: string };
31
34
  }
32
35
 
36
+ /** 工具状态目录名(相对工程根):工具的私有状态不属于资源契约,与 `Resources/` 分开放 */
37
+ const STATE_DIR = ".lingfan";
38
+ /** 产物账文件名(住在工具状态目录里) */
39
+ const LEDGER_FILE = "stories-build.json";
40
+ /** 本模块会主动删除的产物命名空间(`Stories/**`)的前缀 */
41
+ const STORIES_PREFIX = `${STORIES_DIR}/`;
42
+
33
43
  /**
34
44
  * 差量写盘 + 生成物更新:
35
- * - `Stories/**` 由源全量管理——内容不同的产物才写(新增/更新分开计),不在产物集内的
36
- * 既有列文件 = 陈旧,删除;
45
+ * - `Stories/**` 的产物内容不同才写(新增/更新分开计)。陈旧判定收窄为「出现在上一轮
46
+ * 产物账里、且不在本轮产物集里」——只有本工具自己写过的路径才可删,作者手写的
47
+ * `.story` / JSON 列文件从不出现在账里,因此构建不会碰它们;
37
48
  * - 生成物(`<sourcesDir>/<generatedDir>/fun_register.g.ts`、`stories.gen.d.ts`、
38
49
  * `state.gen.d.ts`、`script.gen.d.ts`、`font.gen.d.ts`、`ui_register.g.ts`)同规则:零守卫时若存在则删除,否则内容不同才写。它们的逻辑路径与
39
50
  * `Stories/**` 不同命名空间,报告里以 `Stories.src/` 开头即生成物。
40
- * 返回值里的路径都是逻辑路径(相对工程根),供 CLI 直接打印。
51
+ * 返回值里的路径都是逻辑路径(相对资源根),供 CLI 直接打印。
41
52
  */
42
53
  export function emitArtifacts(options: EmitOptions): {
43
54
  added: string[];
@@ -47,7 +58,6 @@ export function emitArtifacts(options: EmitOptions): {
47
58
  const {
48
59
  root,
49
60
  resourcesDir,
50
- storiesDir,
51
61
  generatedRel,
52
62
  serialized,
53
63
  cellScan,
@@ -57,7 +67,6 @@ export function emitArtifacts(options: EmitOptions): {
57
67
  fontGen,
58
68
  uiGen,
59
69
  } = options;
60
- const existing = walkFiles(storiesDir);
61
70
  const added: string[] = [];
62
71
  const updated: string[] = [];
63
72
  const removed: string[] = [];
@@ -69,11 +78,20 @@ export function emitArtifacts(options: EmitOptions): {
69
78
  writeFileSync(target, text);
70
79
  (exists ? updated : added).push(rel);
71
80
  }
72
- for (const rel of existing.keys()) {
73
- const logical = `${STORIES_DIR}/${rel}`;
74
- if (serialized.files.has(logical)) continue;
75
- rmSync(join(storiesDir, rel), { force: true });
76
- removed.push(logical);
81
+ // 清理只在「上一轮账里有、本轮产物集里没有」时发生:账是删除的唯一依据。手写文件
82
+ // 从不出现在账里,因此构建不会删掉作者的 `.story` / 手写 JSON 列文件。
83
+ // 账缺失(首次运行 / 刚升级)时一律不删——旧版本没有账,无法区分「上轮我写的」与
84
+ // 「作者手写的」,宁可不删。
85
+ const previousProducts = readLedger(root);
86
+ if (previousProducts !== undefined) {
87
+ for (const logical of previousProducts) {
88
+ if (!logical.startsWith(STORIES_PREFIX)) continue;
89
+ if (serialized.files.has(logical)) continue;
90
+ const target = join(resourcesDir, logical);
91
+ if (!existsSync(target)) continue;
92
+ rmSync(target, { force: true });
93
+ removed.push(logical);
94
+ }
77
95
  }
78
96
 
79
97
  const genTarget = join(root, generatedRel);
@@ -133,9 +151,54 @@ export function emitArtifacts(options: EmitOptions): {
133
151
  added,
134
152
  updated,
135
153
  );
154
+ // 账 = 本轮写进资源根的 `Stories/**` 产物集:下一轮据此判定哪些既有文件是「我的」。
155
+ writeLedger(root, serialized.files.keys());
136
156
  return { added, updated, removed };
137
157
  }
138
158
 
159
+ /** 账在磁盘上的形态:只有一个字段,数组按码元序,便于人工 diff 与版本控制。 */
160
+ interface ProductsLedger {
161
+ readonly products: readonly string[];
162
+ }
163
+
164
+ /** 逻辑路径 → 落盘绝对路径的账文件位置(工具状态目录,不在资源根内)。 */
165
+ function ledgerPath(root: string): string {
166
+ return join(root, STATE_DIR, LEDGER_FILE);
167
+ }
168
+
169
+ /**
170
+ * 读上一轮产物账;返回 `undefined` 表示「没有可用的账」。
171
+ *
172
+ * 缺失、不可解析、结构不符都按同一个结果处理:调用方据此跳过全部清理。旧版本没有账,
173
+ * 无法区分「上轮本工具写的」与「作者手写的」,误删的代价远大于留一个陈旧文件。
174
+ */
175
+ function readLedger(root: string): readonly string[] | undefined {
176
+ const target = ledgerPath(root);
177
+ if (!existsSync(target)) return undefined;
178
+ let parsed: unknown;
179
+ try {
180
+ parsed = JSON.parse(readFileSync(target, "utf8"));
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ if (typeof parsed !== "object" || parsed === null) return undefined;
185
+ const products = (parsed as { products?: unknown }).products;
186
+ if (!Array.isArray(products)) return undefined;
187
+ return products.filter((item): item is string => typeof item === "string");
188
+ }
189
+
190
+ /** 把本轮产物集整体写成账:只取 `Stories/**`、按码元序;内容相同则不碰文件(幂等)。 */
191
+ function writeLedger(root: string, products: Iterable<string>): void {
192
+ const sorted = [...products].filter((rel) => rel.startsWith(STORIES_PREFIX));
193
+ sorted.sort(compareOrdinal);
194
+ const ledger: ProductsLedger = { products: sorted };
195
+ const text = `${JSON.stringify(ledger, null, 2)}\n`;
196
+ const target = ledgerPath(root);
197
+ if (existsSync(target) && readFileSync(target, "utf8") === text) return;
198
+ mkdirSync(dirname(target), { recursive: true });
199
+ writeFileSync(target, text);
200
+ }
201
+
139
202
  /** 恒产出生成物的差量落盘:存在即内容比较,不同才写(新增/更新分开计)。 */
140
203
  function writeConstantArtifact(
141
204
  target: string,