@lingfan-engine/stories-build 0.2.0 → 0.3.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/package.json +4 -3
- package/src/authoring.d.ts +31 -0
- package/src/cell/imports.ts +21 -7
- package/src/cell/index.ts +3 -2
- package/src/cell/scan-cells.ts +10 -1
- package/src/cell/scan-file.ts +11 -1
- package/src/cell/types.ts +10 -0
- package/src/cli/cli.ts +49 -4
- package/src/compiler/cell-scan.ts +12 -2
- package/src/compiler/compiler-contract.ts +12 -0
- package/src/compiler/story-compiler/assemble/mount-uis.ts +43 -0
- package/src/compiler/story-compiler/assemble/session.ts +30 -0
- package/src/compiler/story-compiler/graph/build.ts +85 -13
- package/src/compiler/story-compiler/graph/core.ts +66 -2
- package/src/compiler/story-compiler/graph/fold.ts +3 -0
- package/src/compiler/story-compiler/graph/index.ts +12 -1
- package/src/compiler/story-compiler/graph/scan/alias.ts +100 -0
- package/src/compiler/story-compiler/graph/scan/imports.ts +53 -6
- package/src/compiler/story-compiler/graph/scan/module-scan.ts +5 -0
- package/src/compiler/story-compiler/graph/scan/ui-import.ts +136 -0
- package/src/compiler/story-compiler/graph/scan/ui-specifier.ts +202 -0
- package/src/compiler/story-compiler/graph/state-root.ts +1 -1
- package/src/compiler/story-compiler/graph.ts +1 -0
- package/src/compiler/story-compiler/index.ts +1 -0
- package/src/compiler/story-compiler/translate/expr/words/arg.ts +5 -0
- package/src/compiler/story-compiler.ts +11 -0
- package/src/config/story-config.ts +6 -2
- package/src/const-eval/const-eval-pass.ts +18 -3
- package/src/discovery/source-discovery.ts +8 -3
- package/src/emit/emit.ts +11 -1
- package/src/host-plugins/index.ts +11 -0
- package/src/host-plugins/virtual-modules.ts +138 -0
- package/src/index.ts +20 -0
- package/src/pipeline/gates.ts +73 -2
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/merge.ts +132 -0
- package/src/pipeline/orchestrate.ts +101 -126
- package/src/pipeline/tsconfig-paths.ts +196 -0
- package/src/pipeline/watch.ts +4 -3
- package/src/shared/generated-dir.ts +31 -0
- package/src/shared/index.ts +9 -5
- package/src/shared/limits.ts +17 -0
- package/src/shared/texts.ts +41 -0
- package/src/story-list/index.ts +2 -0
- package/src/story-list/story-registry.ts +55 -0
- package/src/typegen/font-render.ts +1 -1
- package/src/typegen/index.ts +1 -1
- package/src/typegen/render.ts +1 -1
- package/src/typegen/script-render.ts +1 -1
- package/src/typegen/state-enum-ast.ts +1 -1
- package/src/typegen/state-render.ts +34 -8
- package/src/ui/index.ts +9 -0
- package/src/ui/render-register.ts +114 -0
- package/src/ui/scan-imports.ts +314 -0
- package/src/ui/ui-registry.ts +71 -0
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, posix } from "node:path";
|
|
2
2
|
import {
|
|
3
|
+
RESOURCES_DIR,
|
|
3
4
|
STORIES_DIR,
|
|
4
5
|
type Story,
|
|
5
6
|
type StoryColumn,
|
|
6
7
|
} from "@lingfan-engine/core";
|
|
7
8
|
import { TS_SOURCE_MANIFEST_KEY } from "@lingfan-engine/editor";
|
|
8
9
|
import {
|
|
10
|
+
DEFAULT_GENERATED_DIR,
|
|
9
11
|
defaultStoriesBuildTexts,
|
|
10
12
|
StoryBuildError,
|
|
11
13
|
type StoriesBuildTexts,
|
|
12
14
|
} from "../shared/index.ts";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
assertKnownGuards,
|
|
17
|
+
assertKnownOps,
|
|
18
|
+
assertKnownUis,
|
|
19
|
+
assertUiImports,
|
|
20
|
+
} from "./gates.ts";
|
|
14
21
|
import { scanCellSources } from "../compiler/index.ts";
|
|
15
22
|
import { runConstEvalPass } from "../const-eval/index.ts";
|
|
16
23
|
import { assertRoundTrip, compileStory } from "../emit/index.ts";
|
|
17
24
|
import {
|
|
18
25
|
loadDefaultCompiler,
|
|
19
|
-
type CompiledStoryEntry,
|
|
20
26
|
type StoriesCompiler,
|
|
21
27
|
} from "../compiler/index.ts";
|
|
22
28
|
import { emitArtifacts } from "../emit/index.ts";
|
|
@@ -29,7 +35,6 @@ import {
|
|
|
29
35
|
evalConfigModule,
|
|
30
36
|
mergeDefineCallsInto,
|
|
31
37
|
runStateSchemaPass,
|
|
32
|
-
type LocatedDefineCall,
|
|
33
38
|
} from "../const-eval/index.ts";
|
|
34
39
|
import { discoverSources } from "../discovery/index.ts";
|
|
35
40
|
import {
|
|
@@ -37,6 +42,13 @@ import {
|
|
|
37
42
|
fontNameSet,
|
|
38
43
|
scanFontRegistry,
|
|
39
44
|
} from "../fonts/index.ts";
|
|
45
|
+
import {
|
|
46
|
+
UI_DIR_NAME,
|
|
47
|
+
renderUiRegister,
|
|
48
|
+
scanUiRegistry,
|
|
49
|
+
scanUiTreeImports,
|
|
50
|
+
uiNameSet,
|
|
51
|
+
} from "../ui/index.ts";
|
|
40
52
|
import {
|
|
41
53
|
collectDeclaredNames,
|
|
42
54
|
collectEnumTypeSlotsForBuild,
|
|
@@ -47,13 +59,23 @@ import {
|
|
|
47
59
|
} from "../typegen/index.ts";
|
|
48
60
|
import { walkFiles } from "../shared/index.ts";
|
|
49
61
|
import { DEFAULT_EVAL_TIMEOUT_MS } from "../const-eval/index.ts";
|
|
62
|
+
import { loadPathsConfig } from "./tsconfig-paths.ts";
|
|
63
|
+
import {
|
|
64
|
+
mergeDefines,
|
|
65
|
+
moduleSlotFiles,
|
|
66
|
+
raiseFormAConflict,
|
|
67
|
+
readCompiledEntry,
|
|
68
|
+
} from "./merge.ts";
|
|
69
|
+
import { createAliasResolver } from "../compiler/story-compiler/graph/scan/alias.ts";
|
|
70
|
+
import { createUiChannel } from "../compiler/story-compiler/graph/scan/ui-specifier.ts";
|
|
71
|
+
import { UI_REGISTER_BASENAME } from "../compiler/story-compiler/graph/core.ts";
|
|
50
72
|
|
|
51
73
|
/** 构建产物的目录名与生成物文件名(默认值 = 参考宿主的现状目录名) */
|
|
52
|
-
export const DEFAULT_SOURCES_DIR = "Stories.src";
|
|
53
|
-
/**
|
|
54
|
-
export
|
|
55
|
-
/**
|
|
56
|
-
export
|
|
74
|
+
export const DEFAULT_SOURCES_DIR = "src/Stories.src";
|
|
75
|
+
/** 资源目录名(相对工程根):权威声明在引擎契约的 `RESOURCES_DIR`(此处别名转发) */
|
|
76
|
+
export { RESOURCES_DIR as DEFAULT_RESOURCES_DIR } from "@lingfan-engine/core";
|
|
77
|
+
/** 生成物目录名:权威声明在 `shared/generated-dir.ts`(此处只转发,保持包出口不变) */
|
|
78
|
+
export { DEFAULT_GENERATED_DIR } from "../shared/index.ts";
|
|
57
79
|
|
|
58
80
|
/**
|
|
59
81
|
* 构建的可配置项:只列目录名这类工程布局事实(把同一工程挪到别的目录名只改这里)。
|
|
@@ -64,7 +86,7 @@ export interface StoryBuildOptions {
|
|
|
64
86
|
readonly sourcesDir?: string;
|
|
65
87
|
/** 资源目录名(相对工程根;清单与 `Stories/**` 都住在它下面) */
|
|
66
88
|
readonly resourcesDir?: string;
|
|
67
|
-
/**
|
|
89
|
+
/** 生成物目录名(相对源根的父目录;`fun_register.g.ts` 落在这里) */
|
|
68
90
|
readonly generatedDir?: string;
|
|
69
91
|
/** 编译器注入(过渡期/测试用);缺省 = 装载 `compiler` 域的默认实现 */
|
|
70
92
|
readonly compiler?: StoriesCompiler;
|
|
@@ -84,6 +106,8 @@ const STATE_TYPEGEN_FILE = "state.gen.d.ts";
|
|
|
84
106
|
const SCRIPT_TYPEGEN_FILE = "script.gen.d.ts";
|
|
85
107
|
/** font 字面量类型面文件名(字体名注册表的键=值收窄面,内容随注册表) */
|
|
86
108
|
const FONT_TYPEGEN_FILE = "font.gen.d.ts";
|
|
109
|
+
/** UI 注册表生成物文件名(宿主「名字→组件」表 + 故事源具名句柄 + 类型面,内容随注册表) */
|
|
110
|
+
const UI_REGISTER_FILE = `${UI_REGISTER_BASENAME}.ts`;
|
|
87
111
|
|
|
88
112
|
/**
|
|
89
113
|
* 编译 `root/Stories.src/` 下的 TS 源 → `root/Resources/`(纯编排,fs 全在参数 root 之下;
|
|
@@ -106,7 +130,7 @@ export async function buildStories(
|
|
|
106
130
|
options: StoryBuildOptions = {},
|
|
107
131
|
): Promise<BuildReport> {
|
|
108
132
|
const sourcesDirName = options.sourcesDir ?? DEFAULT_SOURCES_DIR;
|
|
109
|
-
const resourcesDirName = options.resourcesDir ??
|
|
133
|
+
const resourcesDirName = options.resourcesDir ?? RESOURCES_DIR;
|
|
110
134
|
const generatedDirName = options.generatedDir ?? DEFAULT_GENERATED_DIR;
|
|
111
135
|
// 文案与求值时限:入口处一次性合并/取缺省,下游各段只收完整对象/数值(纯透传)
|
|
112
136
|
const texts: StoriesBuildTexts = {
|
|
@@ -115,10 +139,42 @@ export async function buildStories(
|
|
|
115
139
|
};
|
|
116
140
|
const evalTimeoutMs = options.evalTimeoutMs ?? DEFAULT_EVAL_TIMEOUT_MS;
|
|
117
141
|
|
|
142
|
+
// 生成物目录相对工程根(默认布局 = `src/gen`):源根名与其父目录在这里一次算清,
|
|
143
|
+
// 下游只收成品字符串。`posix.dirname("Stories.src")` = ".",故源根在工程根时
|
|
144
|
+
// 生成物仍落在 `gen/`——旧布局的路径同样由这套算法得出,不留分支。
|
|
145
|
+
const generatedRelRoot = posix.join(
|
|
146
|
+
posix.dirname(sourcesDirName),
|
|
147
|
+
generatedDirName,
|
|
148
|
+
);
|
|
149
|
+
// cell 搬运 import 说明符时要从生成物目录回指源根:两个目录都由工程根算出,
|
|
150
|
+
// 这里一次性折成相对路径(默认布局 = `"../Stories.src"`)传给扫描段。
|
|
151
|
+
const cellRootPrefix = posix.relative(generatedRelRoot, sourcesDirName);
|
|
152
|
+
|
|
118
153
|
const sourcesDir = join(root, sourcesDirName);
|
|
119
154
|
const resourcesDir = join(root, resourcesDirName);
|
|
120
155
|
const storiesDir = join(resourcesDir, STORIES_DIR);
|
|
121
156
|
|
|
157
|
+
// 作者写的别名来自工程自己的 `tsconfig.json`:一次构建读一次,后续各段共用同一份形状。
|
|
158
|
+
// 缺失或无 `paths` ⇒ `null`,退化为纯相对路径(宿主零配置也能跑)。
|
|
159
|
+
const pathsConfig = loadPathsConfig(root);
|
|
160
|
+
|
|
161
|
+
// UI 根与源根是兄弟目录:同一套「谁在哪」的推导只在这里做一次,
|
|
162
|
+
// 扫描段与闸门文案都从这两个值出发。
|
|
163
|
+
const uiDirName = UI_DIR_NAME;
|
|
164
|
+
const uiRelRoot = posix.join(posix.dirname(sourcesDirName), uiDirName);
|
|
165
|
+
const uiRoot = join(root, uiRelRoot);
|
|
166
|
+
// 生成物里一律写相对路径(不写别名):别名是创作期便利,一旦进生成物就把宿主
|
|
167
|
+
// 绑死在某个 tsconfig 上。默认布局下 `src/gen` 回指 `src/ui` 得 `"../ui"`。
|
|
168
|
+
const uiRelFromGenerated = posix.relative(generatedRelRoot, uiRelRoot);
|
|
169
|
+
// 注册表只读文件系统、不依赖编译,故先于通道扫描:通道要用它回答「这条具名
|
|
170
|
+
// 导入指向哪个 UI 文件」,晚一步就查不到。
|
|
171
|
+
const uiEntries = scanUiRegistry(uiRoot);
|
|
172
|
+
|
|
173
|
+
// 别名解析器与 UI 通道都在这里一次造好:UI 文件住在源根的兄弟目录 `src/ui/`,
|
|
174
|
+
// 它的沙箱比模块导入宽一级,两个消费者(cell 搬运、故事编译)共用同一份判定。
|
|
175
|
+
const alias = createAliasResolver(pathsConfig, sourcesDir);
|
|
176
|
+
const uiChannel = createUiChannel(sourcesDir, alias, uiEntries);
|
|
177
|
+
|
|
122
178
|
// —— config.ts 单次求值:named exports 四导出(folders/start/vars/state)+ 形态 B
|
|
123
179
|
// define 累积一并取出;布局校验(parseFolders/parseStart)、define 合并冲突与
|
|
124
180
|
// 形态 A/B 互斥全部 fail-closed(读取统一走本通道,图谱层不再解析 config)——
|
|
@@ -140,7 +196,13 @@ export async function buildStories(
|
|
|
140
196
|
// —— cell 声明扫描(构建期 AST 扫描):源树(排除生成物目录,仅 .ts;机制不动)。
|
|
141
197
|
// 源树同时是模块层 define 收集与枚举槽位回收的输入(只读复用,不重扫)——
|
|
142
198
|
const sourceTree = walkFiles(sourcesDir);
|
|
143
|
-
const cellScan = scanCellSources(
|
|
199
|
+
const cellScan = scanCellSources(
|
|
200
|
+
sourceTree,
|
|
201
|
+
generatedDirName,
|
|
202
|
+
cellRootPrefix,
|
|
203
|
+
// UI 句柄导入由 UI 通道处置:它不进生成物、也不受源根沙箱约束。
|
|
204
|
+
(_fromRel, spec) => uiChannel.classify(_fromRel, spec).kind !== "not-ui",
|
|
205
|
+
);
|
|
144
206
|
|
|
145
207
|
// —— 编译期求值:含 node_modules 值导入的文件整体求值,导出常量值喂给
|
|
146
208
|
// 编译器内联;求值诊断与双求值确定性闸门 fail-closed,一次报全部 ——
|
|
@@ -151,6 +213,7 @@ export async function buildStories(
|
|
|
151
213
|
sourcesDirName,
|
|
152
214
|
texts,
|
|
153
215
|
evalTimeoutMs,
|
|
216
|
+
pathsConfig,
|
|
154
217
|
);
|
|
155
218
|
|
|
156
219
|
// —— 模块层构建期 define 收集与汇入:源模块顶层 define 语句(函数体外)AST 折叠,
|
|
@@ -163,14 +226,7 @@ export async function buildStories(
|
|
|
163
226
|
generatedDirName,
|
|
164
227
|
});
|
|
165
228
|
if (evaluated.state !== undefined && moduleDefineCalls.length > 0) {
|
|
166
|
-
|
|
167
|
-
[
|
|
168
|
-
`${sourcesDirName}/${CONFIG_FILE}: 形态 A(state 导出)与模块层构建期 define 互斥——形态 A/B 二选一:`,
|
|
169
|
-
...moduleDefineCalls.map(
|
|
170
|
-
(call) => `- ${call.origin}${defineLabel(call)}`,
|
|
171
|
-
),
|
|
172
|
-
].join("\n"),
|
|
173
|
-
);
|
|
229
|
+
raiseFormAConflict(sourcesDirName, CONFIG_FILE, moduleDefineCalls);
|
|
174
230
|
}
|
|
175
231
|
const moduleDefineConflicts = mergeDefineCallsInto(
|
|
176
232
|
evaluated.defineTree,
|
|
@@ -207,6 +263,7 @@ export async function buildStories(
|
|
|
207
263
|
const fontNames = fontNameSet(fontEntries);
|
|
208
264
|
|
|
209
265
|
// —— story-compiler:故事/模块清单一次性编译(诊断非空 = fail-closed,一次报全部)——
|
|
266
|
+
// 别名解析器在上面已造好,这里直接复用(读配置与展开候选都在 `tsconfig-paths.ts`)。
|
|
210
267
|
const compiler = options.compiler ?? (await loadDefaultCompiler());
|
|
211
268
|
const compiled = await compiler.compileStories({
|
|
212
269
|
root,
|
|
@@ -217,6 +274,8 @@ export async function buildStories(
|
|
|
217
274
|
constValues,
|
|
218
275
|
fontNames,
|
|
219
276
|
characters: moduleCharacters,
|
|
277
|
+
alias,
|
|
278
|
+
uiRegistry: uiEntries,
|
|
220
279
|
texts,
|
|
221
280
|
});
|
|
222
281
|
if (compiled.diags.length > 0) {
|
|
@@ -271,6 +330,7 @@ export async function buildStories(
|
|
|
271
330
|
const gateOptions = {
|
|
272
331
|
resourcesDir: resourcesDirName,
|
|
273
332
|
sourcesDir: sourcesDirName,
|
|
333
|
+
uiDirName,
|
|
274
334
|
texts,
|
|
275
335
|
};
|
|
276
336
|
// —— op 闸门与守卫名闸门(两条 fail-early 判定,各自带定位一次报全部)——
|
|
@@ -280,6 +340,12 @@ export async function buildStories(
|
|
|
280
340
|
new Set(cellScan.guards.map((guard) => guard.name)),
|
|
281
341
|
gateOptions,
|
|
282
342
|
);
|
|
343
|
+
// —— 列级 uis 闸门:产物里声明的 UI 名必须在源里真有实现文件 ——
|
|
344
|
+
// 与导入期同一条落位规则(`ui/<名>/<名>.<扩展名>`),此处只核存在性。
|
|
345
|
+
assertKnownUis(merged, uiNameSet(uiEntries), gateOptions);
|
|
346
|
+
// —— UI import 面闸门:`src/ui/**` 的越界依赖在构建期一次报全 ——
|
|
347
|
+
// UI 不进编译链,宿主打包器又拦不住反向依赖,这里是唯一的观测点。
|
|
348
|
+
assertUiImports(scanUiTreeImports(uiRoot, uiEntries), gateOptions);
|
|
283
349
|
|
|
284
350
|
// —— 编译与往返自检(config.start → project.json entry 由托管键更新落地)——
|
|
285
351
|
const serialized = compileStory(merged, markedManifest);
|
|
@@ -308,7 +374,8 @@ export async function buildStories(
|
|
|
308
374
|
configSource: sourceTree.get(CONFIG_FILE) ?? "",
|
|
309
375
|
moduleFiles: moduleSlotFiles(sourceTree, moduleDefineCalls),
|
|
310
376
|
}),
|
|
311
|
-
|
|
377
|
+
sourceRootDir: sourcesDirName,
|
|
378
|
+
generatedDir: generatedRelRoot,
|
|
312
379
|
});
|
|
313
380
|
|
|
314
381
|
// —— script 命名空间 ambient 声明(免导入直呼):内容恒定、与工程状态无关,
|
|
@@ -319,30 +386,38 @@ export async function buildStories(
|
|
|
319
386
|
// 空注册表恒产出封闭空对象,未注册名在 TS 面不可见(改拼即 TS2551)——
|
|
320
387
|
const fontTypegenContent = renderFontTypegen(fontEntries);
|
|
321
388
|
|
|
389
|
+
// —— UI 注册表生成物:三段导出(宿主 uis 表 / 故事源具名句柄 / UiNameRegistry)。
|
|
390
|
+
// 恒产出:没有 UI 时 `uis` 是空表、句柄段整段省略,宿主与类型面都还有形状可引 ——
|
|
391
|
+
const uiRegisterContent = renderUiRegister(uiEntries, uiRelFromGenerated);
|
|
392
|
+
|
|
322
393
|
// —— 差量写盘 + 生成物(逻辑路径,相对资源根)——
|
|
323
394
|
const { added, updated, removed } = emitArtifacts({
|
|
324
395
|
root,
|
|
325
396
|
resourcesDir,
|
|
326
397
|
storiesDir,
|
|
327
|
-
generatedRel: `${
|
|
398
|
+
generatedRel: `${generatedRelRoot}/${GENERATED_FILE}`,
|
|
328
399
|
serialized,
|
|
329
400
|
cellScan,
|
|
330
401
|
typegen: {
|
|
331
|
-
rel: `${
|
|
402
|
+
rel: `${generatedRelRoot}/${TYPEGEN_FILE}`,
|
|
332
403
|
content: typegenContent,
|
|
333
404
|
},
|
|
334
405
|
stateGen: {
|
|
335
|
-
rel: `${
|
|
406
|
+
rel: `${generatedRelRoot}/${STATE_TYPEGEN_FILE}`,
|
|
336
407
|
content: stateTypegenContent,
|
|
337
408
|
},
|
|
338
409
|
scriptGen: {
|
|
339
|
-
rel: `${
|
|
410
|
+
rel: `${generatedRelRoot}/${SCRIPT_TYPEGEN_FILE}`,
|
|
340
411
|
content: scriptTypegenContent,
|
|
341
412
|
},
|
|
342
413
|
fontGen: {
|
|
343
|
-
rel: `${
|
|
414
|
+
rel: `${generatedRelRoot}/${FONT_TYPEGEN_FILE}`,
|
|
344
415
|
content: fontTypegenContent,
|
|
345
416
|
},
|
|
417
|
+
uiGen: {
|
|
418
|
+
rel: `${generatedRelRoot}/${UI_REGISTER_FILE}`,
|
|
419
|
+
content: uiRegisterContent,
|
|
420
|
+
},
|
|
346
421
|
});
|
|
347
422
|
|
|
348
423
|
return {
|
|
@@ -357,103 +432,3 @@ export async function buildStories(
|
|
|
357
432
|
warnings: drainBuildWarnings(),
|
|
358
433
|
};
|
|
359
434
|
}
|
|
360
|
-
|
|
361
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
362
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** 互斥文案里的一条调用签名(path 形带键名;spec 形统一签名) */
|
|
366
|
-
function defineLabel(call: LocatedDefineCall): string {
|
|
367
|
-
return call.call.kind === "path"
|
|
368
|
-
? `define("${call.call.path}")`
|
|
369
|
-
: "define({…})";
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/** 枚举槽位回收的模块源文(按调用落点去重排序;树里缺文本的文件跳过) */
|
|
373
|
-
function moduleSlotFiles(
|
|
374
|
-
sourceTree: ReadonlyMap<string, string>,
|
|
375
|
-
calls: readonly { rel: string }[],
|
|
376
|
-
): Map<string, string> {
|
|
377
|
-
const files = new Map<string, string>();
|
|
378
|
-
for (const rel of [...new Set(calls.map((call) => call.rel))].sort()) {
|
|
379
|
-
const text = sourceTree.get(rel);
|
|
380
|
-
if (text !== undefined) files.set(rel, text);
|
|
381
|
-
}
|
|
382
|
-
return files;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* 编译条目防御读取:取条目列(columnId 必须在产物列集中)、回填产物 sourcePath、
|
|
387
|
-
* 复核 story 骨架(columns 列集 + id/entry 字符串)。更深的合法性(列 id 安全段、
|
|
388
|
-
* 重复、entry 存在性等)交给 serializeProject/assembleProject 既有校验兜底。
|
|
389
|
-
*/
|
|
390
|
-
function readCompiledEntry(
|
|
391
|
-
entry: CompiledStoryEntry,
|
|
392
|
-
sourcesDirName: string,
|
|
393
|
-
): { story: Story; column: StoryColumn } {
|
|
394
|
-
const sourceLabel = `${sourcesDirName}/${entry.sourceRelPath}`;
|
|
395
|
-
if (!isRecord(entry.story) || !Array.isArray(entry.story.columns)) {
|
|
396
|
-
throw new StoryBuildError(
|
|
397
|
-
`${sourceLabel}: 编译器条目 story 缺少列集(compileStories 契约破坏)`,
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
const column = entry.story.columns.find(
|
|
401
|
-
(item) => isRecord(item) && item.id === entry.columnId,
|
|
402
|
-
);
|
|
403
|
-
if (column === undefined) {
|
|
404
|
-
throw new StoryBuildError(
|
|
405
|
-
`${sourceLabel}: 编译器条目 columnId "${entry.columnId}" 不在产物列集中(compileStories 契约破坏)`,
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
if (
|
|
409
|
-
typeof entry.story.id !== "string" ||
|
|
410
|
-
entry.story.id === "" ||
|
|
411
|
-
typeof entry.story.entry !== "string" ||
|
|
412
|
-
entry.story.entry === ""
|
|
413
|
-
) {
|
|
414
|
-
throw new StoryBuildError(
|
|
415
|
-
`${sourceLabel}: 编译器条目 story 缺少 id/entry(compileStories 契约破坏)`,
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
const story = entry.story as unknown as Story;
|
|
419
|
-
const typedColumn = column as unknown as StoryColumn;
|
|
420
|
-
typedColumn.sourcePath = `${STORIES_DIR}/${entry.sourceRelPath.replace(/\.ts$/, ".json")}`;
|
|
421
|
-
return { story, column: typedColumn };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* 跨来源的 defines 合并:同键同值收敛,同键异值 = 冲突 fail-closed(一次报全部)。
|
|
426
|
-
* 来源 = config 状态(config 先行:形态 A state 或形态 B define 合并树)+ 各故事文件;
|
|
427
|
-
* config 与故事同键异值同样冲突。
|
|
428
|
-
*/
|
|
429
|
-
function mergeDefines(
|
|
430
|
-
bySource: ReadonlyMap<string, { story: Story; columns: StoryColumn[] }>,
|
|
431
|
-
sourcesDirName: string,
|
|
432
|
-
schemaDefines: Record<string, unknown> = {},
|
|
433
|
-
): Record<string, unknown> {
|
|
434
|
-
const merged: Record<string, unknown> = {};
|
|
435
|
-
const conflicts: string[] = [];
|
|
436
|
-
const put = (key: string, value: unknown, source: string): void => {
|
|
437
|
-
if (key in merged && merged[key] !== value) {
|
|
438
|
-
conflicts.push(
|
|
439
|
-
`- defines["${key}"] 在 ${sourcesDirName}/${source} 与先前的声明不一致(${JSON.stringify(merged[key])} ≠ ${JSON.stringify(value)})`,
|
|
440
|
-
);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
merged[key] = value;
|
|
444
|
-
};
|
|
445
|
-
for (const [key, value] of Object.entries(schemaDefines)) {
|
|
446
|
-
put(key, value, "config.ts(state/define)");
|
|
447
|
-
}
|
|
448
|
-
for (const [source, { story }] of bySource) {
|
|
449
|
-
for (const [key, value] of Object.entries(story.defines ?? {})) {
|
|
450
|
-
put(key, value, source);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
if (conflicts.length > 0) {
|
|
454
|
-
throw new StoryBuildError(
|
|
455
|
-
`跨故事文件的 defines 冲突(工程级 defines 只有一份,请统一到工程清单或消除同名异值):\n${conflicts.join("\n")}`,
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
return merged;
|
|
459
|
-
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 工程 tsconfig 的 `compilerOptions.paths` 读取与展开(构建期,Node 侧)。
|
|
6
|
+
*
|
|
7
|
+
* 别名不是本引擎的规定:作者在工程 `tsconfig.json` 里写什么(`@/*`、`@ui/*`、`~/*`
|
|
8
|
+
* 任意形态),stories-build 就认什么。本文件只做两件事——把工程配置读成结构化形状
|
|
9
|
+
* (`loadPathsConfig`),把一条说明符展开成候选绝对路径(`expandAlias`)。
|
|
10
|
+
*
|
|
11
|
+
* 为什么不借 tsc 的 `matchPatternOrExact` / `tryParsePatterns`:两个函数不在
|
|
12
|
+
* `typescript.d.ts` 的公开面,TypeScript 升级后可能改名或消失(运行期还在,编译期不可见)。
|
|
13
|
+
* 通配匹配本身就是「前缀 + 后缀 + 最长前缀胜出」,自己实现才能长期稳定。读配置仍走公开
|
|
14
|
+
* API(`readConfigFile` + `parseJsonConfigFileContent`)。
|
|
15
|
+
*
|
|
16
|
+
* 注意事项:
|
|
17
|
+
* - 解析顺序不在这里定。调用方按「相对路径 → 引擎包 → 别名 → 不支持外部包」判定,
|
|
18
|
+
* 本文件只回答「这条说明符能展开成哪些绝对路径」。
|
|
19
|
+
* - 不遍历工程目录:解析时把 host 的 `readDirectory` 换成返回空数组,耗时与工程体量无关。
|
|
20
|
+
* - 只有 `paths` 才算别名来源,不做「没配 paths 就按约定猜前缀」的推断。
|
|
21
|
+
* - 目标模板里的 `*` 按命中段替换;`baseDir` 取**声明 paths 的那个 tsconfig 所在目录**
|
|
22
|
+
* (子级覆盖 ⇒ 子目录,父级声明 ⇒ 父目录);显式 `baseUrl` 优先按 baseUrl。
|
|
23
|
+
* - `extends` 只跟随相对路径与绝对路径形态;包名形态的 extends 不追(退化为工程根目录),
|
|
24
|
+
* 这类工程很少见且没有可靠的本地解析入口。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** 工程根 tsconfig 文件名(只读这一个文件,不向上/向下找) */
|
|
28
|
+
const TSCONFIG_FILE = "tsconfig.json";
|
|
29
|
+
|
|
30
|
+
/** 一条 `paths` 条目:通配键拆成前缀 + 后缀,精确键只按字符串全等命中 */
|
|
31
|
+
export interface PathsPattern {
|
|
32
|
+
readonly prefix: string;
|
|
33
|
+
readonly suffix: string;
|
|
34
|
+
/** 目标模板(可含 `*`,按声明序) */
|
|
35
|
+
readonly targets: readonly string[];
|
|
36
|
+
/** 键里带 `*` ⇒ 通配模式;否则是精确键 */
|
|
37
|
+
readonly wildcard: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 工程 `paths` 的结构化形状:`baseDir` 是目标模板的基准目录(绝对路径) */
|
|
41
|
+
export interface PathsConfig {
|
|
42
|
+
readonly patterns: readonly PathsPattern[];
|
|
43
|
+
readonly baseDir: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 读 `root/tsconfig.json` 的 `compilerOptions.paths`。
|
|
48
|
+
* 文件缺失、读不动或没有 `paths` ⇒ `null`(构建退化为纯相对路径,宿主零配置也能跑)。
|
|
49
|
+
*/
|
|
50
|
+
export function loadPathsConfig(root: string): PathsConfig | null {
|
|
51
|
+
const configPath = resolve(root, TSCONFIG_FILE);
|
|
52
|
+
if (!ts.sys.fileExists(configPath)) return null;
|
|
53
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
54
|
+
if (read.error !== undefined) return null;
|
|
55
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
56
|
+
read.config,
|
|
57
|
+
// 只取配置语义,不看工程里有哪些文件:readDirectory 返回空 = 不遍历目录
|
|
58
|
+
{ ...ts.sys, readDirectory: () => [] },
|
|
59
|
+
dirname(configPath),
|
|
60
|
+
undefined,
|
|
61
|
+
configPath,
|
|
62
|
+
);
|
|
63
|
+
const paths = parsed.options.paths;
|
|
64
|
+
if (paths === undefined) return null;
|
|
65
|
+
return {
|
|
66
|
+
patterns: Object.entries(paths).map(([key, targets]) => ({
|
|
67
|
+
...splitPattern(key),
|
|
68
|
+
targets: [...targets],
|
|
69
|
+
})),
|
|
70
|
+
baseDir:
|
|
71
|
+
parsed.options.baseUrl ?? declaringDir(configPath) ?? dirname(configPath),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 按最长前缀把一条说明符展开成候选绝对路径(按声明序、去重后返回)。
|
|
77
|
+
* 未命中任何模式 ⇒ 空数组;不探测文件是否存在(存在性由调用方在自己的文件树上判)。
|
|
78
|
+
*/
|
|
79
|
+
export function expandAlias(
|
|
80
|
+
config: PathsConfig,
|
|
81
|
+
specifier: string,
|
|
82
|
+
): readonly string[] {
|
|
83
|
+
const hit = matchPattern(config.patterns, specifier);
|
|
84
|
+
if (hit === undefined) return [];
|
|
85
|
+
const expanded: string[] = [];
|
|
86
|
+
for (const target of hit.pattern.targets) {
|
|
87
|
+
const filled = hit.pattern.wildcard
|
|
88
|
+
? replaceStar(target, hit.middle)
|
|
89
|
+
: target;
|
|
90
|
+
const absolute = resolve(config.baseDir, filled);
|
|
91
|
+
if (!expanded.includes(absolute)) expanded.push(absolute);
|
|
92
|
+
}
|
|
93
|
+
return expanded;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 通配键拆前缀/后缀(第一个 `*` 切分,与 tsc 同一套通配语义) */
|
|
97
|
+
function splitPattern(key: string): Omit<PathsPattern, "targets"> {
|
|
98
|
+
const star = key.indexOf("*");
|
|
99
|
+
if (star === -1) return { prefix: key, suffix: "", wildcard: false };
|
|
100
|
+
return {
|
|
101
|
+
prefix: key.slice(0, star),
|
|
102
|
+
suffix: key.slice(star + 1),
|
|
103
|
+
wildcard: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 匹配:精确键(无 `*`)先按字符串全等命中;否则在命中的通配模式里取前缀最长的那个。
|
|
109
|
+
* 前缀等长的多个模式按声明序取首个——tsconfig 里同前缀重复声明本身没有意义。
|
|
110
|
+
*/
|
|
111
|
+
function matchPattern(
|
|
112
|
+
patterns: readonly PathsPattern[],
|
|
113
|
+
specifier: string,
|
|
114
|
+
): { pattern: PathsPattern; middle: string } | undefined {
|
|
115
|
+
let exact: PathsPattern | undefined;
|
|
116
|
+
let best: { pattern: PathsPattern; middle: string } | undefined;
|
|
117
|
+
for (const pattern of patterns) {
|
|
118
|
+
if (!pattern.wildcard) {
|
|
119
|
+
if (pattern.prefix === specifier) exact = pattern;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const fixed = pattern.prefix.length + pattern.suffix.length;
|
|
123
|
+
if (specifier.length < fixed) continue;
|
|
124
|
+
if (!specifier.startsWith(pattern.prefix)) continue;
|
|
125
|
+
if (!specifier.endsWith(pattern.suffix)) continue;
|
|
126
|
+
if (
|
|
127
|
+
best !== undefined &&
|
|
128
|
+
pattern.prefix.length <= best.pattern.prefix.length
|
|
129
|
+
)
|
|
130
|
+
continue;
|
|
131
|
+
best = {
|
|
132
|
+
pattern,
|
|
133
|
+
middle: specifier.slice(
|
|
134
|
+
pattern.prefix.length,
|
|
135
|
+
specifier.length - pattern.suffix.length,
|
|
136
|
+
),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (exact !== undefined) return { pattern: exact, middle: "" };
|
|
140
|
+
return best;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 目标模板里的 `*` 全部替换成命中段(tsc 语义:模板里的每个 `*` 都填同一个值) */
|
|
144
|
+
function replaceStar(target: string, middle: string): string {
|
|
145
|
+
return target.includes("*") ? target.split("*").join(middle) : target;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 声明 `compilerOptions.paths` 的那个 tsconfig 所在目录:先看自身,再沿 `extends`
|
|
150
|
+
* 反向遍历(数组形态下后者覆盖前者)。找不到 ⇒ `null`(调用方退化为工程根目录)。
|
|
151
|
+
*/
|
|
152
|
+
function declaringDir(
|
|
153
|
+
configPath: string,
|
|
154
|
+
seen = new Set<string>(),
|
|
155
|
+
): string | null {
|
|
156
|
+
if (seen.has(configPath)) return null;
|
|
157
|
+
seen.add(configPath);
|
|
158
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
159
|
+
if (read.error !== undefined) return null;
|
|
160
|
+
const json = read.config as
|
|
161
|
+
{ compilerOptions?: { paths?: unknown }; extends?: unknown } | undefined;
|
|
162
|
+
if (json?.compilerOptions?.paths !== undefined) return dirname(configPath);
|
|
163
|
+
const parents = extendsList(json?.extends);
|
|
164
|
+
for (let index = parents.length - 1; index >= 0; index -= 1) {
|
|
165
|
+
const parentPath = resolveExtends(parents[index]!, dirname(configPath));
|
|
166
|
+
if (parentPath === null) continue;
|
|
167
|
+
const found = declaringDir(parentPath, seen);
|
|
168
|
+
if (found !== null) return found;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `extends` 的取值形态:单条字符串或字符串数组(非字符串项忽略) */
|
|
174
|
+
function extendsList(value: unknown): string[] {
|
|
175
|
+
if (typeof value === "string") return [value];
|
|
176
|
+
if (!Array.isArray(value)) return [];
|
|
177
|
+
return value.filter((item): item is string => typeof item === "string");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 相对/绝对路径形态的 extends 解析成实际文件名;包名形态 ⇒ null(不追) */
|
|
181
|
+
function resolveExtends(specifier: string, fromDir: string): string | null {
|
|
182
|
+
const looksLocal =
|
|
183
|
+
specifier.startsWith("./") ||
|
|
184
|
+
specifier.startsWith("../") ||
|
|
185
|
+
isAbsolute(specifier);
|
|
186
|
+
if (!looksLocal) return null;
|
|
187
|
+
const base = resolve(fromDir, specifier);
|
|
188
|
+
for (const candidate of [
|
|
189
|
+
base,
|
|
190
|
+
`${base}.json`,
|
|
191
|
+
resolve(base, TSCONFIG_FILE),
|
|
192
|
+
]) {
|
|
193
|
+
if (ts.sys.fileExists(candidate)) return candidate;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
package/src/pipeline/watch.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { watch } from "node:fs";
|
|
10
10
|
import { join, resolve } from "node:path";
|
|
11
|
+
import { RESOURCES_DIR } from "@lingfan-engine/core";
|
|
11
12
|
import { DEFAULT_SOURCES_DIR, buildStories } from "./index.ts";
|
|
12
13
|
import type { StoryBuildOptions } from "./orchestrate.ts";
|
|
13
|
-
import { walkFiles } from "../shared/index.ts";
|
|
14
|
+
import { GEN_PREFIX, walkFiles } from "../shared/index.ts";
|
|
14
15
|
|
|
15
16
|
/** 相对导入抽取(单双引号皆可;type 导入同算依赖——保守向) */
|
|
16
17
|
const REL_IMPORT_RE = /(?:^|[\s;])from\s+["'](\.{1,2}\/[^"']+)["']/g;
|
|
@@ -108,7 +109,7 @@ export interface WatchRoundReport {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/** 源树与字体目录的监听目标(相对工程根) */
|
|
111
|
-
const WATCHED_FONT_DIR = join(
|
|
112
|
+
const WATCHED_FONT_DIR = join(RESOURCES_DIR, "Fonts");
|
|
112
113
|
|
|
113
114
|
/**
|
|
114
115
|
* 常驻增量监听入口:首轮全量构建落基准 → `fs.watch` 监听源目录(与字体目录,
|
|
@@ -208,7 +209,7 @@ function readSourceTexts(
|
|
|
208
209
|
const texts = new Map<string, string>();
|
|
209
210
|
for (const [rel, text] of walkFiles(sourcesDir)) {
|
|
210
211
|
const posix = rel.replace(/\\/g, "/");
|
|
211
|
-
if (posix.startsWith(
|
|
212
|
+
if (posix.startsWith(GEN_PREFIX) || !posix.endsWith(".ts")) continue;
|
|
212
213
|
texts.set(`${sourcesDirName}/${posix}`, text);
|
|
213
214
|
}
|
|
214
215
|
return texts;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成物目录名的唯一声明。
|
|
3
|
+
*
|
|
4
|
+
* 这个概念此前有两份独立声明——`pipeline/orchestrate.ts` 的 `DEFAULT_GENERATED_DIR`
|
|
5
|
+
* 与 `compiler/.../graph/core.ts` 的 `GEN_PREFIX`(同一目录的带斜杠写法),另有六处
|
|
6
|
+
* 现场手拼 `"gen"` / `"gen/"`。判定分散在发现域、编译器域、监听域与 UI 门禁域四处,
|
|
7
|
+
* 但判的是同一个目录,故下沉到不依赖任何业务域的 `shared/`,各处引用同一份。
|
|
8
|
+
*
|
|
9
|
+
* 为什么不能就近放:编译器域不能依赖 `pipeline/`(`pipeline/` 依赖 `compiler/`,
|
|
10
|
+
* 反向成环),而 `config/`、`discovery/`、`ui/` 又不依赖 `compiler/`——四个方向都要
|
|
11
|
+
* 用,只有 `shared/` 站得住。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 生成物目录名:相对**源根的父目录**(默认布局下 = `src/gen`),不是相对源根。
|
|
16
|
+
*
|
|
17
|
+
* 为什么放在源根之外:生成物是「要被编译进前端工程的模块」,故事源是「只归
|
|
18
|
+
* stories-build 的创作源」。同住 `src/` 之下后,前端工程里没有任何路径指向
|
|
19
|
+
* `Stories.src/`,故事源因此结构上不可能被打包器收进最终 JS(不必靠「碰巧没人
|
|
20
|
+
* import」)。
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_GENERATED_DIR = "gen";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 生成物目录的**路径前缀**形式(`gen/`):`rel.startsWith(GEN_PREFIX)` 判「在此目录之下」。
|
|
26
|
+
*
|
|
27
|
+
* 带不带尾斜杠是两件事,别混用——`DEFAULT_GENERATED_DIR` 匹配目录名本身
|
|
28
|
+
* (`entry === DEFAULT_GENERATED_DIR`,用于「整目录被列为源目录」这种判定),
|
|
29
|
+
* `GEN_PREFIX` 只匹配其下的条目(`gen/x.ts` 是生成物,`gen_backup/x.ts` 不是)。
|
|
30
|
+
*/
|
|
31
|
+
export const GEN_PREFIX = `${DEFAULT_GENERATED_DIR}/`;
|
package/src/shared/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 通用出口:失败类型、文件遍历、目录布局名、追溯上限与集中文案表。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `walkFiles` 被发现、写盘、编译器三处共同依赖,`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 这几样跨域用得多——`StoryBuildError` 被十来个文件当统一失败口径,
|
|
5
|
+
* `walkFiles` 被发现、写盘、编译器三处共同依赖,`DEFAULT_GENERATED_DIR` /
|
|
6
|
+
* `GEN_PREFIX` 被发现、编译器、监听、UI 门禁四域共用(判的是同一个生成物目录),
|
|
7
|
+
* `MAX_ALIAS_HOPS` 被编译器与类型生成两域共用(同一条类型别名链的两端),
|
|
8
|
+
* `StoriesBuildTexts` 是构建期全部可注入文案的唯一出处(缺省实现 = 现场字符串)——
|
|
9
|
+
* 放进任何业务域都会让别的域为了一个工具反向依赖它,故单独成域。
|
|
8
10
|
*/
|
|
9
11
|
export { StoryBuildError } from "./errors.ts";
|
|
12
|
+
export { DEFAULT_GENERATED_DIR, GEN_PREFIX } from "./generated-dir.ts";
|
|
13
|
+
export { MAX_ALIAS_HOPS } from "./limits.ts";
|
|
10
14
|
export { walkFiles } from "./walk-files.ts";
|
|
11
15
|
export { defaultStoriesBuildTexts, type StoriesBuildTexts } from "./texts.ts";
|