@lingfan-engine/stories-build 0.0.0-stage → 0.1.2
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/LICENSE +201 -0
- package/bin/stories-build.mjs +16 -0
- package/bin/stories-watch.mjs +16 -0
- package/package.json +50 -3
- package/src/authoring.d.ts +51 -0
- package/src/cell/errors.ts +12 -0
- package/src/cell/imports.ts +127 -0
- package/src/cell/index.ts +33 -0
- package/src/cell/render-register.ts +33 -0
- package/src/cell/scan-cells.ts +63 -0
- package/src/cell/scan-file.ts +108 -0
- package/src/cell/types.ts +27 -0
- package/src/cell/whitelist.ts +22 -0
- package/src/cli/cli.ts +99 -0
- package/src/cli/watch-cli.ts +42 -0
- package/src/compiler/cell-scan.ts +27 -0
- package/src/compiler/compiler-contract.ts +87 -0
- package/src/compiler/extension-load.ts +67 -0
- package/src/compiler/index.ts +22 -0
- package/src/compiler/story-compiler/assemble/functions.ts +202 -0
- package/src/compiler/story-compiler/assemble/index.ts +8 -0
- package/src/compiler/story-compiler/assemble/session.ts +207 -0
- package/src/compiler/story-compiler/assemble/types.ts +37 -0
- package/src/compiler/story-compiler/assemble.ts +5 -0
- package/src/compiler/story-compiler/graph/build.ts +208 -0
- package/src/compiler/story-compiler/graph/core.ts +102 -0
- package/src/compiler/story-compiler/graph/fold.ts +121 -0
- package/src/compiler/story-compiler/graph/index.ts +17 -0
- package/src/compiler/story-compiler/graph/scan/bindings.ts +277 -0
- package/src/compiler/story-compiler/graph/scan/imports.ts +170 -0
- package/src/compiler/story-compiler/graph/scan/index.ts +9 -0
- package/src/compiler/story-compiler/graph/scan/module-scan.ts +199 -0
- package/src/compiler/story-compiler/graph/state-root.ts +147 -0
- package/src/compiler/story-compiler/graph.ts +18 -0
- package/src/compiler/story-compiler/index.ts +34 -0
- package/src/compiler/story-compiler/translate/expr/assign.ts +204 -0
- package/src/compiler/story-compiler/translate/expr/engine-text.ts +16 -0
- package/src/compiler/story-compiler/translate/expr/expr.ts +174 -0
- package/src/compiler/story-compiler/translate/expr/index.ts +24 -0
- package/src/compiler/story-compiler/translate/expr/lower/index.ts +4 -0
- package/src/compiler/story-compiler/translate/expr/lower/lower.ts +266 -0
- package/src/compiler/story-compiler/translate/expr/lower/numeric.ts +56 -0
- package/src/compiler/story-compiler/translate/expr/probe.ts +23 -0
- package/src/compiler/story-compiler/translate/expr/state-path.ts +47 -0
- package/src/compiler/story-compiler/translate/expr/words/arg.ts +87 -0
- package/src/compiler/story-compiler/translate/expr/words/call.ts +125 -0
- package/src/compiler/story-compiler/translate/expr/words/callee.ts +43 -0
- package/src/compiler/story-compiler/translate/expr/words/command.ts +93 -0
- package/src/compiler/story-compiler/translate/expr/words/dispatch.ts +16 -0
- package/src/compiler/story-compiler/translate/expr/words/index.ts +8 -0
- package/src/compiler/story-compiler/translate/expr/words/static-for.ts +96 -0
- package/src/compiler/story-compiler/translate/font-check.ts +94 -0
- package/src/compiler/story-compiler/translate/index.ts +10 -0
- package/src/compiler/story-compiler/translate/shared.ts +132 -0
- package/src/compiler/story-compiler/translate/stmt/control/branch.ts +71 -0
- package/src/compiler/story-compiler/translate/stmt/control/index.ts +11 -0
- package/src/compiler/story-compiler/translate/stmt/control/iter.ts +72 -0
- package/src/compiler/story-compiler/translate/stmt/control/loop.ts +114 -0
- package/src/compiler/story-compiler/translate/stmt/control/switch.ts +75 -0
- package/src/compiler/story-compiler/translate/stmt/control-flow.ts +25 -0
- package/src/compiler/story-compiler/translate/stmt/index.ts +17 -0
- package/src/compiler/story-compiler/translate/stmt/statements.ts +145 -0
- package/src/compiler/story-compiler/translate.ts +15 -0
- package/src/compiler/story-compiler.ts +119 -0
- package/src/config/index.ts +15 -0
- package/src/config/manifest-read.ts +26 -0
- package/src/config/state-type.ts +85 -0
- package/src/config/story-config.ts +113 -0
- package/src/const-eval/config-eval.ts +181 -0
- package/src/const-eval/const-eval-pass.ts +133 -0
- package/src/const-eval/const-eval.ts +114 -0
- package/src/const-eval/const-expr.ts +358 -0
- package/src/const-eval/contracts.ts +31 -0
- package/src/const-eval/define-merge.ts +195 -0
- package/src/const-eval/diag/compare.ts +39 -0
- package/src/const-eval/diag/index.ts +5 -0
- package/src/const-eval/diag/locate.ts +45 -0
- package/src/const-eval/index.ts +44 -0
- package/src/const-eval/module-characters.ts +353 -0
- package/src/const-eval/module-define.ts +300 -0
- package/src/const-eval/round/evaluate.ts +149 -0
- package/src/const-eval/round/index.ts +4 -0
- package/src/const-eval/round/source.ts +134 -0
- package/src/const-eval/state-schema.ts +269 -0
- package/src/discovery/index.ts +7 -0
- package/src/discovery/source-discovery.ts +193 -0
- package/src/emit/compile-roundtrip.ts +45 -0
- package/src/emit/emit.ts +147 -0
- package/src/emit/index.ts +9 -0
- package/src/emit/report.ts +29 -0
- package/src/fonts/font-registry.ts +53 -0
- package/src/fonts/index.ts +7 -0
- package/src/index.ts +76 -0
- package/src/pipeline/gates.ts +107 -0
- package/src/pipeline/index.ts +18 -0
- package/src/pipeline/orchestrate.ts +459 -0
- package/src/pipeline/watch.ts +244 -0
- package/src/shared/errors.ts +2 -0
- package/src/shared/index.ts +11 -0
- package/src/shared/texts.ts +85 -0
- package/src/shared/walk-files.ts +18 -0
- package/src/typegen/collect.ts +107 -0
- package/src/typegen/font-render.ts +30 -0
- package/src/typegen/index.ts +18 -0
- package/src/typegen/render.ts +68 -0
- package/src/typegen/script-render.ts +21 -0
- package/src/typegen/state-enum-ast.ts +365 -0
- package/src/typegen/state-render.ts +242 -0
- package/src/typegen/types.ts +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编译期求值域:node_modules 值导入与状态初始声明的常量折叠。
|
|
3
|
+
*
|
|
4
|
+
* `const-eval` 起 worker 隔离求值(并注入 define/defineVars 构建期内建),
|
|
5
|
+
* `const-eval-pass` 按模块筛出待求值的导出喂进去,`config-eval` 用同一通道
|
|
6
|
+
* 单次求值 config.ts 四导出(folders/start/vars/state + define 累积),
|
|
7
|
+
* `const-expr`/`module-define` 用 AST 折叠模块层构建期 define(
|
|
8
|
+
* define 域),`module-characters` 用同一套 AST 折叠模块层构建期角色声明
|
|
9
|
+
* (全工程角色表),`define-merge` 把各源汇入同一棵合并树,`state-schema` 把形态
|
|
10
|
+
* A/B 的状态树走同一降维文法扁平化为 defines——各段是同一件事的分步,
|
|
11
|
+
* 分开就只剩互相调用的碎片。
|
|
12
|
+
*/
|
|
13
|
+
export {
|
|
14
|
+
evalConstModule,
|
|
15
|
+
DEFAULT_EVAL_TIMEOUT_MS,
|
|
16
|
+
type ConstEvalResult,
|
|
17
|
+
type DefineCall,
|
|
18
|
+
type DefineBad,
|
|
19
|
+
type JsonValue,
|
|
20
|
+
} from "./const-eval.ts";
|
|
21
|
+
export { formatConstDiagFile, runConstEvalPass } from "./const-eval-pass.ts";
|
|
22
|
+
export {
|
|
23
|
+
evalConstExpr,
|
|
24
|
+
type ConstExprContext,
|
|
25
|
+
type ConstExprResult,
|
|
26
|
+
} from "./const-expr.ts";
|
|
27
|
+
export {
|
|
28
|
+
collectModuleDefines,
|
|
29
|
+
type LocatedDefineCall,
|
|
30
|
+
} from "./module-define.ts";
|
|
31
|
+
export {
|
|
32
|
+
collectModuleCharacters,
|
|
33
|
+
type CharacterTable,
|
|
34
|
+
type CollectModuleCharactersInput,
|
|
35
|
+
type LocatedCharacterDecl,
|
|
36
|
+
} from "./module-characters.ts";
|
|
37
|
+
export { evalConfigModule, type ConfigEvalOutcome } from "./config-eval.ts";
|
|
38
|
+
export {
|
|
39
|
+
mergeDefineCalls,
|
|
40
|
+
mergeDefineCallsInto,
|
|
41
|
+
type DefineMergeResult,
|
|
42
|
+
type OriginatedDefineCall,
|
|
43
|
+
} from "./define-merge.ts";
|
|
44
|
+
export { runStateSchemaPass } from "./state-schema.ts";
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块层构建期角色声明收集器(编排层段):扫描 Stories.src 各 .ts 模块顶层的
|
|
3
|
+
* character(...) 表达式语句,产出全工程角色表;汇编层把它前置成每个故事
|
|
4
|
+
* start 列头的 character 命令(进场即有,跨章共用)。
|
|
5
|
+
*
|
|
6
|
+
* 收集规则(与模块层 define 收集器同一套):
|
|
7
|
+
* - 只收顶层表达式语句的 character 调用(列体内的 character 是运行期词,不在此碰);
|
|
8
|
+
* - config.ts 走自己的求值通道,根级文件的结构违规与生成物目录由既有诊断负责,
|
|
9
|
+
* 两者都跳过;
|
|
10
|
+
* - 实参文法 = `character(key, spec)`:key 必须字符串字面量;spec 走 const-expr
|
|
11
|
+
* 折叠(字符串/数字/布尔/null/数组/对象字面量/枚举成员引用),引用运行期值
|
|
12
|
+
* fail-closed 报错带声明位置;
|
|
13
|
+
* - spec 对象只认 CharacterDef 的六个可选样式字段(name/color/size/font/
|
|
14
|
+
* textColor/screen),全部收窄为字符串,未知字段拒绝;
|
|
15
|
+
* - 顶层声明遮蔽 character(同名 import/const/函数/enum)时 fail-closed——
|
|
16
|
+
* 构建期声明只识别全局内建,遮蔽即语义歧义。
|
|
17
|
+
*
|
|
18
|
+
* 冲突处理:跨文件同名角色且定义不等 = 构建期 fail-closed(报两处声明位置);
|
|
19
|
+
* 定义完全相同 = 幂等合并。
|
|
20
|
+
*
|
|
21
|
+
* fail-closed:一次报全部,格式与其余管线诊断同构(`- 源文件:行:列 原因`)。
|
|
22
|
+
*/
|
|
23
|
+
import * as ts from "typescript";
|
|
24
|
+
import { CONFIG_FILE } from "../config/index.ts";
|
|
25
|
+
import { StoryBuildError } from "../shared/index.ts";
|
|
26
|
+
import type { CharacterDef } from "@lingfan-engine/core";
|
|
27
|
+
import { type ConstExprContext, evalConstExpr } from "./const-expr.ts";
|
|
28
|
+
|
|
29
|
+
/** 一条模块层角色声明:求值产物 + 声明位置(跨文件冲突诊断要带两处位置) */
|
|
30
|
+
export interface LocatedCharacterDecl {
|
|
31
|
+
readonly key: string;
|
|
32
|
+
readonly def: CharacterDef;
|
|
33
|
+
/** `源文件相对路径:行:列`(行列为 1 基,与其余管线诊断同构) */
|
|
34
|
+
readonly origin: string;
|
|
35
|
+
readonly rel: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CollectModuleCharactersInput {
|
|
39
|
+
/** 源目录绝对路径:被 import 的枚举文件按它读盘折叠 */
|
|
40
|
+
readonly sourcesDir: string;
|
|
41
|
+
/** 源树(rel → 源文,walkFiles 结果);收集器只读,不改写 */
|
|
42
|
+
readonly files: ReadonlyMap<string, string>;
|
|
43
|
+
/** 生成物目录名(相对源根):gen 产物不参与收集 */
|
|
44
|
+
readonly generatedDirName: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 全工程角色表(key → 定义;同名冲突已在收集期判定处理) */
|
|
48
|
+
export type CharacterTable = Map<string, CharacterDef>;
|
|
49
|
+
|
|
50
|
+
/** CharacterDef 的可选样式字段(key 之外);spec 对象只认这些键 */
|
|
51
|
+
const CHARACTER_FIELDS = [
|
|
52
|
+
"name",
|
|
53
|
+
"color",
|
|
54
|
+
"size",
|
|
55
|
+
"font",
|
|
56
|
+
"textColor",
|
|
57
|
+
"screen",
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
export function collectModuleCharacters(
|
|
61
|
+
input: CollectModuleCharactersInput,
|
|
62
|
+
): CharacterTable {
|
|
63
|
+
const decls: LocatedCharacterDecl[] = [];
|
|
64
|
+
const diags: string[] = [];
|
|
65
|
+
for (const rel of [...input.files.keys()].sort()) {
|
|
66
|
+
if (rel === CONFIG_FILE) continue;
|
|
67
|
+
if (!rel.endsWith(".ts")) continue;
|
|
68
|
+
if (!rel.includes("/")) continue; // 根级结构违规由图谱层既有诊断负责
|
|
69
|
+
if (rel.startsWith(`${input.generatedDirName}/`)) continue;
|
|
70
|
+
const text = input.files.get(rel);
|
|
71
|
+
if (text !== undefined)
|
|
72
|
+
collectFromFile(input.sourcesDir, rel, text, decls, diags);
|
|
73
|
+
}
|
|
74
|
+
// 冲突判定:同名角色定义不等 = fail-closed;全等 = 幂等
|
|
75
|
+
const table: CharacterTable = new Map();
|
|
76
|
+
const origins = new Map<string, string>();
|
|
77
|
+
for (const decl of decls) {
|
|
78
|
+
const existing = table.get(decl.key);
|
|
79
|
+
if (existing === undefined) {
|
|
80
|
+
table.set(decl.key, decl.def);
|
|
81
|
+
origins.set(decl.key, decl.origin);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (JSON.stringify(existing) === JSON.stringify(decl.def)) continue; // 幂等
|
|
85
|
+
diags.push(
|
|
86
|
+
`${decl.origin}角色 "${decl.key}" 与 ${origins.get(decl.key)} 处的声明冲突(同名不同义):请统一两处定义,或改用列内 character 覆盖`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (diags.length > 0) {
|
|
90
|
+
throw new StoryBuildError(
|
|
91
|
+
`模块层构建期角色声明收集失败(${diags.length} 条):\n${diags.join("\n")}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return table;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function collectFromFile(
|
|
98
|
+
sourcesDir: string,
|
|
99
|
+
rel: string,
|
|
100
|
+
text: string,
|
|
101
|
+
decls: LocatedCharacterDecl[],
|
|
102
|
+
diags: string[],
|
|
103
|
+
): void {
|
|
104
|
+
const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true);
|
|
105
|
+
const ctx: ConstExprContext = {
|
|
106
|
+
sourcesDir,
|
|
107
|
+
rel,
|
|
108
|
+
imports: collectImportBindings(sf),
|
|
109
|
+
localEnums: collectLocalEnums(sf),
|
|
110
|
+
enumFiles: new Map(),
|
|
111
|
+
};
|
|
112
|
+
const shadows = collectTopLevelBindingNames(sf).has("character");
|
|
113
|
+
for (const stmt of sf.statements) {
|
|
114
|
+
if (!ts.isExpressionStatement(stmt)) continue;
|
|
115
|
+
const call = stmt.expression;
|
|
116
|
+
if (!isCharacterCall(call)) continue;
|
|
117
|
+
const origin = formatOrigin(rel, sf, call);
|
|
118
|
+
if (shadows) {
|
|
119
|
+
diags.push(
|
|
120
|
+
`${origin}character 被本文件顶层声明遮蔽——构建期角色声明只识别全局内建,请消除遮蔽`,
|
|
121
|
+
);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
collectCall(call, origin, ctx, decls, diags);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectCall(
|
|
129
|
+
call: ts.CallExpression,
|
|
130
|
+
origin: string,
|
|
131
|
+
ctx: ConstExprContext,
|
|
132
|
+
decls: LocatedCharacterDecl[],
|
|
133
|
+
diags: string[],
|
|
134
|
+
): void {
|
|
135
|
+
const [first, second] = call.arguments;
|
|
136
|
+
if (
|
|
137
|
+
first === undefined ||
|
|
138
|
+
second === undefined ||
|
|
139
|
+
call.arguments.length > 2
|
|
140
|
+
) {
|
|
141
|
+
diags.push(`${origin}character 调用接受 2 个实参:(key, 样式对象)`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const keyExpr = unwrapExpr(first);
|
|
145
|
+
if (!ts.isStringLiteral(keyExpr) || keyExpr.text === "") {
|
|
146
|
+
diags.push(`${origin}character 首参必须是非空字符串字面量 key`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const spec = evalConstExpr(second, ctx);
|
|
150
|
+
if (!spec.ok) {
|
|
151
|
+
diags.push(`${origin}character("${keyExpr.text}"):${spec.reason}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(spec.value)) {
|
|
155
|
+
diags.push(
|
|
156
|
+
`${origin}character("${keyExpr.text}"):第二参必须是普通对象字面量`,
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const def: CharacterDef = { key: keyExpr.text };
|
|
161
|
+
for (const [field, raw] of Object.entries(spec.value)) {
|
|
162
|
+
if (!(CHARACTER_FIELDS as readonly string[]).includes(field)) {
|
|
163
|
+
diags.push(
|
|
164
|
+
`${origin}character("${keyExpr.text}"):未知字段 "${field}"(只认 ${CHARACTER_FIELDS.join("/")})`,
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (typeof raw !== "string") {
|
|
169
|
+
diags.push(
|
|
170
|
+
`${origin}character("${keyExpr.text}"):字段 ${field} 必须是字符串`,
|
|
171
|
+
);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
def[field as keyof CharacterDef] = raw;
|
|
175
|
+
}
|
|
176
|
+
decls.push({ key: keyExpr.text, def, origin, rel: ctx.rel });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isCharacterCall(expr: ts.Expression): expr is ts.CallExpression {
|
|
180
|
+
return (
|
|
181
|
+
ts.isCallExpression(expr) &&
|
|
182
|
+
ts.isIdentifier(expr.expression) &&
|
|
183
|
+
expr.expression.text === "character"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function unwrapExpr(expr: ts.Expression): ts.Expression {
|
|
188
|
+
if (
|
|
189
|
+
ts.isParenthesizedExpression(expr) ||
|
|
190
|
+
ts.isAsExpression(expr) ||
|
|
191
|
+
ts.isTypeAssertionExpression(expr)
|
|
192
|
+
) {
|
|
193
|
+
return unwrapExpr(expr.expression);
|
|
194
|
+
}
|
|
195
|
+
return expr;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
199
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatOrigin(rel: string, sf: ts.SourceFile, node: ts.Node): string {
|
|
203
|
+
const pos = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
204
|
+
return `${rel}:${pos.line + 1}:${pos.character + 1} `;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 模块 import 登记(与 const-expr 的 ImportedEnum 同文法;值/type 导入同等可用) */
|
|
208
|
+
function collectImportBindings(
|
|
209
|
+
sf: ts.SourceFile,
|
|
210
|
+
): Map<string, { specifier: string; exportedName: string }> {
|
|
211
|
+
const imports = new Map<
|
|
212
|
+
string,
|
|
213
|
+
{ specifier: string; exportedName: string }
|
|
214
|
+
>();
|
|
215
|
+
for (const stmt of sf.statements) {
|
|
216
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
217
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
218
|
+
const clause = stmt.importClause;
|
|
219
|
+
if (clause === undefined) continue;
|
|
220
|
+
const specifier = stmt.moduleSpecifier.text;
|
|
221
|
+
if (clause.name !== undefined) {
|
|
222
|
+
imports.set(clause.name.text, { specifier, exportedName: "default" });
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
clause.namedBindings !== undefined &&
|
|
226
|
+
ts.isNamedImports(clause.namedBindings)
|
|
227
|
+
) {
|
|
228
|
+
for (const el of clause.namedBindings.elements) {
|
|
229
|
+
imports.set(el.name.text, {
|
|
230
|
+
specifier,
|
|
231
|
+
exportedName: el.propertyName?.text ?? el.name.text,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return imports;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 本文件 enum 声明的成员表(折叠规则与跨文件枚举一致;不可折叠成员不登记) */
|
|
240
|
+
function collectLocalEnums(
|
|
241
|
+
sf: ts.SourceFile,
|
|
242
|
+
): Map<string, ReadonlyMap<string, string | number>> {
|
|
243
|
+
const enums = new Map<string, ReadonlyMap<string, string | number>>();
|
|
244
|
+
for (const stmt of sf.statements) {
|
|
245
|
+
if (!ts.isEnumDeclaration(stmt)) continue;
|
|
246
|
+
const table = new Map<string, string | number>();
|
|
247
|
+
let auto = 0;
|
|
248
|
+
let sawAuto = false;
|
|
249
|
+
let foldable = true;
|
|
250
|
+
for (const member of stmt.members) {
|
|
251
|
+
if (!ts.isIdentifier(member.name)) {
|
|
252
|
+
foldable = false;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
const init = member.initializer;
|
|
256
|
+
if (init === undefined) {
|
|
257
|
+
if (sawAuto) {
|
|
258
|
+
table.set(member.name.text, auto);
|
|
259
|
+
auto += 1;
|
|
260
|
+
} else if (table.size === 0) {
|
|
261
|
+
sawAuto = true;
|
|
262
|
+
table.set(member.name.text, 0);
|
|
263
|
+
auto = 1;
|
|
264
|
+
} else {
|
|
265
|
+
foldable = false;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const value = foldEnumInitializer(init);
|
|
271
|
+
if (value === undefined) {
|
|
272
|
+
foldable = false;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (typeof value === "number") {
|
|
276
|
+
sawAuto = true;
|
|
277
|
+
auto = value + 1;
|
|
278
|
+
}
|
|
279
|
+
table.set(member.name.text, value);
|
|
280
|
+
}
|
|
281
|
+
if (foldable) enums.set(stmt.name.text, table);
|
|
282
|
+
}
|
|
283
|
+
return enums;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function foldEnumInitializer(init: ts.Expression): string | number | undefined {
|
|
287
|
+
const unwrapped = unwrapExpr(init);
|
|
288
|
+
if (ts.isStringLiteral(unwrapped)) return unwrapped.text;
|
|
289
|
+
if (ts.isNumericLiteral(unwrapped)) return Number(unwrapped.text);
|
|
290
|
+
if (
|
|
291
|
+
ts.isPrefixUnaryExpression(unwrapped) &&
|
|
292
|
+
unwrapped.operator === ts.SyntaxKind.MinusToken &&
|
|
293
|
+
ts.isNumericLiteral(unwrapped.operand)
|
|
294
|
+
) {
|
|
295
|
+
return -Number(unwrapped.operand.text);
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 顶层值绑定名(import/const/函数/enum)——只用于遮蔽判定 */
|
|
301
|
+
function collectTopLevelBindingNames(sf: ts.SourceFile): Set<string> {
|
|
302
|
+
const names = new Set<string>();
|
|
303
|
+
for (const stmt of sf.statements) {
|
|
304
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
305
|
+
const clause = stmt.importClause;
|
|
306
|
+
if (clause === undefined) continue;
|
|
307
|
+
if (clause.name !== undefined) names.add(clause.name.text);
|
|
308
|
+
if (clause.namedBindings !== undefined) {
|
|
309
|
+
if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
310
|
+
names.add(clause.namedBindings.name.text);
|
|
311
|
+
} else if (ts.isNamedImports(clause.namedBindings)) {
|
|
312
|
+
for (const el of clause.namedBindings.elements)
|
|
313
|
+
names.add(el.name.text);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (ts.isVariableStatement(stmt)) {
|
|
319
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
320
|
+
collectBindingNames(decl.name, names);
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (
|
|
325
|
+
(ts.isFunctionDeclaration(stmt) || ts.isEnumDeclaration(stmt)) &&
|
|
326
|
+
stmt.name !== undefined
|
|
327
|
+
) {
|
|
328
|
+
names.add(stmt.name.text);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return names;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 绑定名递归收集:标识符直取,对象/数组解构展开元素(缺省名、重命名取属性名侧) */
|
|
335
|
+
function collectBindingNames(
|
|
336
|
+
pattern: ts.BindingName,
|
|
337
|
+
names: Set<string>,
|
|
338
|
+
): void {
|
|
339
|
+
if (ts.isIdentifier(pattern)) {
|
|
340
|
+
names.add(pattern.text);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
for (const element of pattern.elements) {
|
|
344
|
+
if (ts.isOmittedExpression(element)) continue;
|
|
345
|
+
if (
|
|
346
|
+
element.propertyName !== undefined &&
|
|
347
|
+
ts.isIdentifier(element.propertyName)
|
|
348
|
+
) {
|
|
349
|
+
names.add(element.propertyName.text);
|
|
350
|
+
}
|
|
351
|
+
collectBindingNames(element.name, names);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块层构建期 define 收集器(编排层段):扫描 Stories.src 各 .ts 模块顶层的
|
|
3
|
+
* define(...) 表达式语句,产出带落点定位的调用清单,与 config.ts 的 define
|
|
4
|
+
* 求值通道汇入同一棵合并树(汇入在 pipeline:mergeDefineCallsInto)。
|
|
5
|
+
*
|
|
6
|
+
* 收集规则(define 域放宽):
|
|
7
|
+
* - 只收顶层表达式语句的 define 调用(函数体内的 define 是运行期词,语义
|
|
8
|
+
* define-once 归编译器/运行期管,此处刻意不碰);
|
|
9
|
+
* - config.ts 走自己的求值通道,根级文件的结构违规与生成物目录由既有诊断负责,
|
|
10
|
+
* 两者都跳过;
|
|
11
|
+
* - 值域 = 字面量/数组/对象/枚举成员引用(const-expr 折叠,AST 侧零求值);
|
|
12
|
+
* - 顶层声明遮蔽 define(同名 import/const/函数/enum)时 fail-closed——构建期
|
|
13
|
+
* define 只识别全局内建,遮蔽即语义歧义。
|
|
14
|
+
*
|
|
15
|
+
* fail-closed:一次报全部,格式与其余管线诊断同构(`- 源文件:行:列 原因`)。
|
|
16
|
+
*/
|
|
17
|
+
import * as ts from "typescript";
|
|
18
|
+
import { CONFIG_FILE } from "../config/index.ts";
|
|
19
|
+
import { StoryBuildError } from "../shared/index.ts";
|
|
20
|
+
import type { DefineCall, JsonValue } from "./const-eval.ts";
|
|
21
|
+
import { type ConstExprContext, evalConstExpr } from "./const-expr.ts";
|
|
22
|
+
|
|
23
|
+
/** 一条模块层 define 调用:求值产物 + 落点(跨文件冲突诊断要带两处位置) */
|
|
24
|
+
export interface LocatedDefineCall {
|
|
25
|
+
readonly call: DefineCall;
|
|
26
|
+
/** `源文件相对路径:行:列`(行列为 1 基,与其余管线诊断同构) */
|
|
27
|
+
readonly origin: string;
|
|
28
|
+
readonly rel: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CollectModuleDefinesInput {
|
|
32
|
+
/** 源目录绝对路径:被 import 的枚举文件按它读盘折叠 */
|
|
33
|
+
readonly sourcesDir: string;
|
|
34
|
+
/** 源树(rel → 源文,walkFiles 结果);收集器只读,不改写 */
|
|
35
|
+
readonly files: ReadonlyMap<string, string>;
|
|
36
|
+
/** 生成物目录名(相对源根):gen 产物不参与收集 */
|
|
37
|
+
readonly generatedDirName: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function collectModuleDefines(
|
|
41
|
+
input: CollectModuleDefinesInput,
|
|
42
|
+
): readonly LocatedDefineCall[] {
|
|
43
|
+
const calls: LocatedDefineCall[] = [];
|
|
44
|
+
const diags: string[] = [];
|
|
45
|
+
for (const rel of [...input.files.keys()].sort()) {
|
|
46
|
+
if (rel === CONFIG_FILE) continue;
|
|
47
|
+
if (!rel.endsWith(".ts")) continue;
|
|
48
|
+
if (!rel.includes("/")) continue; // 根级结构违规由图谱层既有诊断负责
|
|
49
|
+
if (rel.startsWith(`${input.generatedDirName}/`)) continue;
|
|
50
|
+
const text = input.files.get(rel);
|
|
51
|
+
if (text !== undefined)
|
|
52
|
+
collectFromFile(input.sourcesDir, rel, text, calls, diags);
|
|
53
|
+
}
|
|
54
|
+
if (diags.length > 0) {
|
|
55
|
+
throw new StoryBuildError(
|
|
56
|
+
`模块层构建期 define 收集失败(${diags.length} 条):\n${diags.join("\n")}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return calls;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function collectFromFile(
|
|
63
|
+
sourcesDir: string,
|
|
64
|
+
rel: string,
|
|
65
|
+
text: string,
|
|
66
|
+
calls: LocatedDefineCall[],
|
|
67
|
+
diags: string[],
|
|
68
|
+
): void {
|
|
69
|
+
const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true);
|
|
70
|
+
const ctx: ConstExprContext = {
|
|
71
|
+
sourcesDir,
|
|
72
|
+
rel,
|
|
73
|
+
imports: collectImportBindings(sf),
|
|
74
|
+
localEnums: collectLocalEnums(sf),
|
|
75
|
+
enumFiles: new Map(),
|
|
76
|
+
};
|
|
77
|
+
const shadows = collectTopLevelBindingNames(sf).has("define");
|
|
78
|
+
for (const stmt of sf.statements) {
|
|
79
|
+
if (!ts.isExpressionStatement(stmt)) continue;
|
|
80
|
+
const call = stmt.expression;
|
|
81
|
+
if (!isDefineCall(call)) continue;
|
|
82
|
+
const origin = formatOrigin(rel, sf, call);
|
|
83
|
+
if (shadows) {
|
|
84
|
+
diags.push(
|
|
85
|
+
`${origin}define 被本文件顶层声明遮蔽——构建期 define 只识别全局内建,请消除遮蔽`,
|
|
86
|
+
);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
collectCall(call, origin, ctx, calls, diags);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectCall(
|
|
94
|
+
call: ts.CallExpression,
|
|
95
|
+
origin: string,
|
|
96
|
+
ctx: ConstExprContext,
|
|
97
|
+
calls: LocatedDefineCall[],
|
|
98
|
+
diags: string[],
|
|
99
|
+
): void {
|
|
100
|
+
const [first, second] = call.arguments;
|
|
101
|
+
if (first === undefined) {
|
|
102
|
+
diags.push(
|
|
103
|
+
`${origin}define 调用只接受 1 个对象实参或 2 个(路径, 值)实参`,
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (second !== undefined && call.arguments.length > 2) {
|
|
108
|
+
diags.push(
|
|
109
|
+
`${origin}define 调用只接受 1 个对象实参或 2 个(路径, 值)实参`,
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (second === undefined) {
|
|
114
|
+
const spec = evalConstExpr(first, ctx);
|
|
115
|
+
if (!spec.ok) {
|
|
116
|
+
diags.push(`${origin}define({…}):${spec.reason}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!isRecord(spec.value)) {
|
|
120
|
+
diags.push(`${origin}define({…}):对象实参必须是普通对象字面量`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
calls.push({
|
|
124
|
+
call: { kind: "spec", spec: spec.value },
|
|
125
|
+
origin,
|
|
126
|
+
rel: ctx.rel,
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const pathExpr = unwrapExpr(first);
|
|
131
|
+
if (!ts.isStringLiteral(pathExpr)) {
|
|
132
|
+
diags.push(`${origin}define 首参必须是字符串字面量路径,或单参对象实参`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const path = pathExpr.text;
|
|
136
|
+
const value = evalConstExpr(second, ctx);
|
|
137
|
+
if (!value.ok) {
|
|
138
|
+
diags.push(`${origin}define("${path}"):${value.reason}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
calls.push({
|
|
142
|
+
call: { kind: "path", path, value: value.value },
|
|
143
|
+
origin,
|
|
144
|
+
rel: ctx.rel,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isDefineCall(expr: ts.Expression): expr is ts.CallExpression {
|
|
149
|
+
return (
|
|
150
|
+
ts.isCallExpression(expr) &&
|
|
151
|
+
ts.isIdentifier(expr.expression) &&
|
|
152
|
+
expr.expression.text === "define"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function unwrapExpr(expr: ts.Expression): ts.Expression {
|
|
157
|
+
if (
|
|
158
|
+
ts.isParenthesizedExpression(expr) ||
|
|
159
|
+
ts.isAsExpression(expr) ||
|
|
160
|
+
ts.isTypeAssertionExpression(expr)
|
|
161
|
+
) {
|
|
162
|
+
return unwrapExpr(expr.expression);
|
|
163
|
+
}
|
|
164
|
+
return expr;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isRecord(value: JsonValue): value is Record<string, JsonValue> {
|
|
168
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatOrigin(rel: string, sf: ts.SourceFile, node: ts.Node): string {
|
|
172
|
+
const pos = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
173
|
+
return `${rel}:${pos.line + 1}:${pos.character + 1} `;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 顶层值绑定名(import/const/函数/enum)——只用于遮蔽判定 */
|
|
177
|
+
function collectTopLevelBindingNames(sf: ts.SourceFile): Set<string> {
|
|
178
|
+
const names = new Set<string>();
|
|
179
|
+
for (const stmt of sf.statements) {
|
|
180
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
181
|
+
const clause = stmt.importClause;
|
|
182
|
+
if (clause === undefined) continue;
|
|
183
|
+
if (clause.name !== undefined) names.add(clause.name.text);
|
|
184
|
+
if (clause.namedBindings !== undefined) {
|
|
185
|
+
if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
186
|
+
names.add(clause.namedBindings.name.text);
|
|
187
|
+
} else if (ts.isNamedImports(clause.namedBindings)) {
|
|
188
|
+
for (const el of clause.namedBindings.elements)
|
|
189
|
+
names.add(el.name.text);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (ts.isVariableStatement(stmt)) {
|
|
195
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
196
|
+
if (ts.isIdentifier(decl.name)) names.add(decl.name.text);
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) {
|
|
201
|
+
names.add(stmt.name.text);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ts.isEnumDeclaration(stmt)) names.add(stmt.name.text);
|
|
205
|
+
}
|
|
206
|
+
return names;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 模块 import 登记(与 state-enum-ast 同文法;值/type 导入同等可用) */
|
|
210
|
+
function collectImportBindings(
|
|
211
|
+
sf: ts.SourceFile,
|
|
212
|
+
): Map<string, { specifier: string; exportedName: string }> {
|
|
213
|
+
const imports = new Map<
|
|
214
|
+
string,
|
|
215
|
+
{ specifier: string; exportedName: string }
|
|
216
|
+
>();
|
|
217
|
+
for (const stmt of sf.statements) {
|
|
218
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
219
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
220
|
+
const clause = stmt.importClause;
|
|
221
|
+
if (clause === undefined) continue;
|
|
222
|
+
const specifier = stmt.moduleSpecifier.text;
|
|
223
|
+
if (clause.name !== undefined) {
|
|
224
|
+
imports.set(clause.name.text, { specifier, exportedName: "default" });
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
clause.namedBindings !== undefined &&
|
|
228
|
+
ts.isNamedImports(clause.namedBindings)
|
|
229
|
+
) {
|
|
230
|
+
for (const el of clause.namedBindings.elements) {
|
|
231
|
+
imports.set(el.name.text, {
|
|
232
|
+
specifier,
|
|
233
|
+
exportedName: el.propertyName?.text ?? el.name.text,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return imports;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 本文件 enum 声明的成员表(折叠规则与跨文件枚举一致;不可折叠成员不登记) */
|
|
242
|
+
function collectLocalEnums(
|
|
243
|
+
sf: ts.SourceFile,
|
|
244
|
+
): Map<string, ReadonlyMap<string, string | number>> {
|
|
245
|
+
const enums = new Map<string, ReadonlyMap<string, string | number>>();
|
|
246
|
+
for (const stmt of sf.statements) {
|
|
247
|
+
if (!ts.isEnumDeclaration(stmt)) continue;
|
|
248
|
+
const table = new Map<string, string | number>();
|
|
249
|
+
let auto = 0;
|
|
250
|
+
let sawAuto = false;
|
|
251
|
+
let foldable = true;
|
|
252
|
+
for (const member of stmt.members) {
|
|
253
|
+
if (!ts.isIdentifier(member.name)) {
|
|
254
|
+
foldable = false;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
const init = member.initializer;
|
|
258
|
+
if (init === undefined) {
|
|
259
|
+
if (sawAuto) {
|
|
260
|
+
table.set(member.name.text, auto);
|
|
261
|
+
auto += 1;
|
|
262
|
+
} else if (table.size === 0) {
|
|
263
|
+
sawAuto = true;
|
|
264
|
+
table.set(member.name.text, 0);
|
|
265
|
+
auto = 1;
|
|
266
|
+
} else {
|
|
267
|
+
foldable = false;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const value = foldEnumInitializer(init);
|
|
273
|
+
if (value === undefined) {
|
|
274
|
+
foldable = false;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (typeof value === "number") {
|
|
278
|
+
sawAuto = true;
|
|
279
|
+
auto = value + 1;
|
|
280
|
+
}
|
|
281
|
+
table.set(member.name.text, value);
|
|
282
|
+
}
|
|
283
|
+
if (foldable) enums.set(stmt.name.text, table);
|
|
284
|
+
}
|
|
285
|
+
return enums;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function foldEnumInitializer(init: ts.Expression): string | number | undefined {
|
|
289
|
+
const unwrapped = unwrapExpr(init);
|
|
290
|
+
if (ts.isStringLiteral(unwrapped)) return unwrapped.text;
|
|
291
|
+
if (ts.isNumericLiteral(unwrapped)) return Number(unwrapped.text);
|
|
292
|
+
if (
|
|
293
|
+
ts.isPrefixUnaryExpression(unwrapped) &&
|
|
294
|
+
unwrapped.operator === ts.SyntaxKind.MinusToken
|
|
295
|
+
) {
|
|
296
|
+
const operand = unwrapExpr(unwrapped.operand);
|
|
297
|
+
if (ts.isNumericLiteral(operand)) return -Number(operand.text);
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|