@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,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state 类型面的枚举 type 槽位回收(AST 侧,供 state-render 生成 import type)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:config 求值走 esbuild 打包,标识符在求值产物里消失——`type: Sex` 到了
|
|
5
|
+
* 合并树里只剩枚举对象本身,渲染层无从知道它来自哪个模块,也就生成不出
|
|
6
|
+
* `import type { Sex } from …`。本段用 TS AST 重读源文,把 define(...) 调用、形态 A
|
|
7
|
+
* `export const state` 初始化器与 default 对象(export default { … } satisfies)的
|
|
8
|
+
* state 字段里、descriptor 形对象的 `type` 槽位按点路径回收
|
|
9
|
+
* (esbuild 求值语义保证:同名调用/同形初始化器 = 合并树里的同一条数据),并解析
|
|
10
|
+
* 该标识符的 import 来源。
|
|
11
|
+
*
|
|
12
|
+
* 收集域(define 域定稿):config.ts 收全树;其余模块只收顶层
|
|
13
|
+
* define 表达式语句(函数体内是运行期词,不回收)。模块的 import 说明符相对
|
|
14
|
+
* 各自文件目录,登记前统一折算为「源根相对路径」,config 的说明符相对源根
|
|
15
|
+
* 本就无需折算。
|
|
16
|
+
*
|
|
17
|
+
* 用法:入参 = config.ts 源文本;出参 = 点路径 → 槽位列表(直接槽位与并集成员
|
|
18
|
+
* 槽位按位置累积,同位置首见胜)。渲染层只把「路径能在合并树里对上且槽位存在」
|
|
19
|
+
* 的枚举生成为 import 引用,其余一律回退为内联字面量联合(类型仍精确,只是少了
|
|
20
|
+
* 跨模块引用)。
|
|
21
|
+
*
|
|
22
|
+
* 注意:本文件只做「名称回收」,不做文法校验——槽位形状合法性由求值与降维段统一
|
|
23
|
+
* 判定;qualified name(`Enums.Sex`)与本地声明的枚举不回收(gen 只引用 import 进
|
|
24
|
+
* 源文件的类型模块),走回退。
|
|
25
|
+
*/
|
|
26
|
+
import { posix } from "node:path";
|
|
27
|
+
import * as ts from "typescript";
|
|
28
|
+
|
|
29
|
+
/** 一个可 import 的枚举 type 槽位:源文件里的本地绑定名 + 模块说明符 */
|
|
30
|
+
export interface EnumTypeSlot {
|
|
31
|
+
/** 源文件内使用的本地名(生成物沿用该名,必要时以 `as` 别名导入) */
|
|
32
|
+
readonly localName: string;
|
|
33
|
+
/** 模块出口名;default 导入记为 "default",渲染层生成默认导入形态 */
|
|
34
|
+
readonly importedName: string;
|
|
35
|
+
/** 相对源根的模块路径(渲染层按生成物目录改写相对层级) */
|
|
36
|
+
readonly source: string;
|
|
37
|
+
/**
|
|
38
|
+
* 并集成员下标:type 槽位为多元素数组(并集)时记录枚举所在的成员位置,
|
|
39
|
+
* 渲染层按下标配对;undefined = 直接 type 槽位(非并集)。
|
|
40
|
+
*/
|
|
41
|
+
readonly memberIndex?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 别名回溯上限:有限跳数防环(与 module-scan 状态根别名链同思路) */
|
|
45
|
+
const MAX_ALIAS_HOPS = 10;
|
|
46
|
+
|
|
47
|
+
export function collectEnumTypeSlots(
|
|
48
|
+
configSource: string,
|
|
49
|
+
): ReadonlyMap<string, EnumTypeSlot[]> {
|
|
50
|
+
return collectEnumTypeSlotsForBuild({ configSource, moduleFiles: new Map() });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 多源槽位回收入参:config 源文 + 模块层源文(rel → 源文,不含 config) */
|
|
54
|
+
export interface EnumSlotBuildInput {
|
|
55
|
+
readonly configSource: string;
|
|
56
|
+
readonly moduleFiles: ReadonlyMap<string, string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从 config.ts 与各模块源文回收枚举 type 槽位(求值产物丢标识符,AST 侧按
|
|
61
|
+
* 同一叶路径配对;模块说明符按各自目录折算为源根相对路径)。
|
|
62
|
+
*/
|
|
63
|
+
export function collectEnumTypeSlotsForBuild(
|
|
64
|
+
input: EnumSlotBuildInput,
|
|
65
|
+
): ReadonlyMap<string, EnumTypeSlot[]> {
|
|
66
|
+
const slots = new Map<string, EnumTypeSlot[]>();
|
|
67
|
+
const configSf = ts.createSourceFile(
|
|
68
|
+
"config.ts",
|
|
69
|
+
input.configSource,
|
|
70
|
+
ts.ScriptTarget.Latest,
|
|
71
|
+
true,
|
|
72
|
+
ts.ScriptKind.TS,
|
|
73
|
+
);
|
|
74
|
+
const imports = collectImportBindings(configSf);
|
|
75
|
+
const aliases = collectLocalAliases(configSf);
|
|
76
|
+
ts.forEachChild(configSf, (node) =>
|
|
77
|
+
scanDefineSlots(node, imports, aliases, slots),
|
|
78
|
+
);
|
|
79
|
+
collectStateSlots(configSf, imports, aliases, slots);
|
|
80
|
+
collectDefaultConfigSlots(configSf, imports, aliases, slots);
|
|
81
|
+
for (const [rel, source] of input.moduleFiles) {
|
|
82
|
+
const sf = ts.createSourceFile(
|
|
83
|
+
rel,
|
|
84
|
+
source,
|
|
85
|
+
ts.ScriptTarget.Latest,
|
|
86
|
+
true,
|
|
87
|
+
ts.ScriptKind.TS,
|
|
88
|
+
);
|
|
89
|
+
const moduleImports = collectImportBindings(sf, (spec) =>
|
|
90
|
+
resolveSourceFor(rel, spec),
|
|
91
|
+
);
|
|
92
|
+
const moduleAliases = collectLocalAliases(sf);
|
|
93
|
+
for (const stmt of sf.statements) {
|
|
94
|
+
if (!ts.isExpressionStatement(stmt)) continue; // 只收顶层 define 语句
|
|
95
|
+
scanDefineSlots(stmt, moduleImports, moduleAliases, slots);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return slots;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 形态 A:export const state = { … } 初始化器(与求值侧同一棵树的源文) */
|
|
102
|
+
function collectStateSlots(
|
|
103
|
+
sf: ts.SourceFile,
|
|
104
|
+
imports: ReadonlyMap<string, EnumTypeSlot>,
|
|
105
|
+
aliases: ReadonlyMap<string, string>,
|
|
106
|
+
slots: Map<string, EnumTypeSlot[]>,
|
|
107
|
+
): void {
|
|
108
|
+
for (const stmt of sf.statements) {
|
|
109
|
+
if (!ts.isVariableStatement(stmt)) continue;
|
|
110
|
+
if (!stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
|
|
111
|
+
continue;
|
|
112
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
113
|
+
const initializer = decl.initializer;
|
|
114
|
+
if (
|
|
115
|
+
ts.isIdentifier(decl.name) &&
|
|
116
|
+
decl.name.text === "state" &&
|
|
117
|
+
initializer !== undefined &&
|
|
118
|
+
ts.isObjectLiteralExpression(unwrapExpr(initializer))
|
|
119
|
+
) {
|
|
120
|
+
collectFromValue(unwrapExpr(initializer), "", imports, aliases, slots);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** default 对象形态:export default { … } satisfies … —— state 字段内递归回收槽位 */
|
|
127
|
+
function collectDefaultConfigSlots(
|
|
128
|
+
sf: ts.SourceFile,
|
|
129
|
+
imports: ReadonlyMap<string, EnumTypeSlot>,
|
|
130
|
+
aliases: ReadonlyMap<string, string>,
|
|
131
|
+
slots: Map<string, EnumTypeSlot[]>,
|
|
132
|
+
): void {
|
|
133
|
+
for (const stmt of sf.statements) {
|
|
134
|
+
if (!ts.isExportAssignment(stmt)) continue;
|
|
135
|
+
const expr = ts.isSatisfiesExpression(stmt.expression)
|
|
136
|
+
? stmt.expression.expression
|
|
137
|
+
: stmt.expression;
|
|
138
|
+
const obj = unwrapExpr(expr);
|
|
139
|
+
if (!ts.isObjectLiteralExpression(obj)) continue;
|
|
140
|
+
for (const prop of obj.properties) {
|
|
141
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
142
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name))
|
|
143
|
+
continue;
|
|
144
|
+
if (prop.name.text === "state") {
|
|
145
|
+
collectFromValue(prop.initializer, "", imports, aliases, slots);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 从一个节点起找 define 调用(config 全树 / 模块顶层语句两用) */
|
|
152
|
+
function scanDefineSlots(
|
|
153
|
+
node: ts.Node,
|
|
154
|
+
imports: ReadonlyMap<string, EnumTypeSlot>,
|
|
155
|
+
aliases: ReadonlyMap<string, string>,
|
|
156
|
+
slots: Map<string, EnumTypeSlot[]>,
|
|
157
|
+
): void {
|
|
158
|
+
const visit = (current: ts.Node): void => {
|
|
159
|
+
if (
|
|
160
|
+
ts.isCallExpression(current) &&
|
|
161
|
+
ts.isIdentifier(current.expression) &&
|
|
162
|
+
current.expression.text === "define" &&
|
|
163
|
+
current.arguments.length >= 1
|
|
164
|
+
) {
|
|
165
|
+
const [first, second] = current.arguments;
|
|
166
|
+
if (second !== undefined && ts.isStringLiteral(first)) {
|
|
167
|
+
// define("a.b", value):value 从该点路径起折叠
|
|
168
|
+
collectFromValue(
|
|
169
|
+
unwrapExpr(second),
|
|
170
|
+
first.text,
|
|
171
|
+
imports,
|
|
172
|
+
aliases,
|
|
173
|
+
slots,
|
|
174
|
+
);
|
|
175
|
+
} else if (ts.isObjectLiteralExpression(unwrapExpr(first))) {
|
|
176
|
+
// define({ a: { b: … } }):从根起递归折叠
|
|
177
|
+
collectFromValue(unwrapExpr(first), "", imports, aliases, slots);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
ts.forEachChild(current, visit);
|
|
181
|
+
};
|
|
182
|
+
visit(node);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 模块 import 说明符 → 源根相对路径;相对结果统一带 "./" 前缀,
|
|
187
|
+
* 与包导入(原样保留)在渲染层的换算分支区分开。
|
|
188
|
+
*/
|
|
189
|
+
function resolveSourceFor(rel: string, specifier: string): string {
|
|
190
|
+
if (!specifier.startsWith(".")) return specifier;
|
|
191
|
+
const resolved = posix.normalize(posix.join(posix.dirname(rel), specifier));
|
|
192
|
+
return resolved.startsWith(".") ? resolved : `./${resolved}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 解包 `as const` / 括号包裹(求值语义等价于内层字面量,AST 侧按内层收集) */
|
|
196
|
+
function unwrapExpr(expr: ts.Expression): ts.Expression {
|
|
197
|
+
if (
|
|
198
|
+
ts.isAsExpression(expr) ||
|
|
199
|
+
ts.isParenthesizedExpression(expr) ||
|
|
200
|
+
ts.isTypeAssertionExpression(expr)
|
|
201
|
+
) {
|
|
202
|
+
return unwrapExpr(expr.expression);
|
|
203
|
+
}
|
|
204
|
+
return expr;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 从一个「嵌套分支或 descriptor」值起递归折叠点路径,遇 descriptor 形登记槽位 */
|
|
208
|
+
function collectFromValue(
|
|
209
|
+
valueExpr: ts.Expression,
|
|
210
|
+
path: string,
|
|
211
|
+
imports: ReadonlyMap<string, EnumTypeSlot>,
|
|
212
|
+
aliases: ReadonlyMap<string, string>,
|
|
213
|
+
slots: Map<string, EnumTypeSlot[]>,
|
|
214
|
+
): void {
|
|
215
|
+
const value = unwrapExpr(valueExpr);
|
|
216
|
+
if (!ts.isObjectLiteralExpression(value)) return;
|
|
217
|
+
if (isDescriptorShape(value)) {
|
|
218
|
+
recordTypeSlot(value, path, imports, aliases, slots);
|
|
219
|
+
return; // descriptor 是叶,不再下钻
|
|
220
|
+
}
|
|
221
|
+
for (const prop of value.properties) {
|
|
222
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
223
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) continue;
|
|
224
|
+
const key = prop.name.text;
|
|
225
|
+
collectFromValue(
|
|
226
|
+
prop.initializer,
|
|
227
|
+
path === "" ? key : `${path}.${key}`,
|
|
228
|
+
imports,
|
|
229
|
+
aliases,
|
|
230
|
+
slots,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** descriptor 形 = 含 "type"/"default" 具名属性的普通对象字面量(不安全形态不识别) */
|
|
236
|
+
function isDescriptorShape(obj: ts.ObjectLiteralExpression): boolean {
|
|
237
|
+
let named = false;
|
|
238
|
+
for (const prop of obj.properties) {
|
|
239
|
+
if (!ts.isPropertyAssignment(prop)) return false;
|
|
240
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (prop.name.text === "type" || prop.name.text === "default") named = true;
|
|
244
|
+
}
|
|
245
|
+
return named;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function recordTypeSlot(
|
|
249
|
+
obj: ts.ObjectLiteralExpression,
|
|
250
|
+
path: string,
|
|
251
|
+
imports: ReadonlyMap<string, EnumTypeSlot>,
|
|
252
|
+
aliases: ReadonlyMap<string, string>,
|
|
253
|
+
slots: Map<string, EnumTypeSlot[]>,
|
|
254
|
+
): void {
|
|
255
|
+
if (path === "") return; // 根级路径与合并树键不可能对齐(求值侧同样不产空键)
|
|
256
|
+
const typeProp = obj.properties.find(
|
|
257
|
+
(prop): prop is ts.PropertyAssignment =>
|
|
258
|
+
ts.isPropertyAssignment(prop) &&
|
|
259
|
+
ts.isIdentifier(prop.name) &&
|
|
260
|
+
prop.name.text === "type",
|
|
261
|
+
);
|
|
262
|
+
if (typeProp === undefined) return;
|
|
263
|
+
const init = unwrapExpr(typeProp.initializer);
|
|
264
|
+
if (ts.isIdentifier(init)) {
|
|
265
|
+
const slot = resolveTypeRef(init.text, imports, aliases);
|
|
266
|
+
if (slot !== undefined) addSlot(path, slot, slots);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (ts.isArrayLiteralExpression(init) && init.elements.length >= 2) {
|
|
270
|
+
// 并集成员:逐元素回收枚举标识符并记成员下标(嵌套并集内的枚举不回收,走内联回退)
|
|
271
|
+
init.elements.forEach((element, index) => {
|
|
272
|
+
const expr = unwrapExpr(element);
|
|
273
|
+
if (!ts.isIdentifier(expr)) return;
|
|
274
|
+
const slot = resolveTypeRef(expr.text, imports, aliases);
|
|
275
|
+
if (slot !== undefined)
|
|
276
|
+
addSlot(path, { ...slot, memberIndex: index }, slots);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** 同路径槽位累积:同位置首见胜(重复声明不漂移既有登记) */
|
|
282
|
+
function addSlot(
|
|
283
|
+
path: string,
|
|
284
|
+
slot: EnumTypeSlot,
|
|
285
|
+
slots: Map<string, EnumTypeSlot[]>,
|
|
286
|
+
): void {
|
|
287
|
+
const list = slots.get(path);
|
|
288
|
+
if (list === undefined) {
|
|
289
|
+
slots.set(path, [slot]);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (list.some((existing) => existing.memberIndex === slot.memberIndex))
|
|
293
|
+
return;
|
|
294
|
+
list.push(slot);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function resolveTypeRef(
|
|
298
|
+
name: string,
|
|
299
|
+
imports: ReadonlyMap<string, EnumTypeSlot>,
|
|
300
|
+
aliases: ReadonlyMap<string, string>,
|
|
301
|
+
): EnumTypeSlot | undefined {
|
|
302
|
+
let cur = name;
|
|
303
|
+
for (let hop = 0; hop <= MAX_ALIAS_HOPS; hop += 1) {
|
|
304
|
+
const imported = imports.get(cur);
|
|
305
|
+
if (imported !== undefined) return imported;
|
|
306
|
+
const next = aliases.get(cur);
|
|
307
|
+
if (next === undefined) return undefined;
|
|
308
|
+
cur = next;
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** import 登记:default 与 named 都要(type-only 与值导入同等可用,只取类型) */
|
|
314
|
+
function collectImportBindings(
|
|
315
|
+
sf: ts.SourceFile,
|
|
316
|
+
resolveSource: (specifier: string) => string = (spec) => spec,
|
|
317
|
+
): Map<string, EnumTypeSlot> {
|
|
318
|
+
const imports = new Map<string, EnumTypeSlot>();
|
|
319
|
+
for (const stmt of sf.statements) {
|
|
320
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
321
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
322
|
+
const clause = stmt.importClause;
|
|
323
|
+
if (clause === undefined) continue;
|
|
324
|
+
const source = resolveSource(stmt.moduleSpecifier.text);
|
|
325
|
+
if (clause.name !== undefined) {
|
|
326
|
+
imports.set(clause.name.text, {
|
|
327
|
+
localName: clause.name.text,
|
|
328
|
+
importedName: "default",
|
|
329
|
+
source,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
clause.namedBindings !== undefined &&
|
|
334
|
+
ts.isNamedImports(clause.namedBindings)
|
|
335
|
+
) {
|
|
336
|
+
for (const el of clause.namedBindings.elements) {
|
|
337
|
+
imports.set(el.name.text, {
|
|
338
|
+
localName: el.name.text,
|
|
339
|
+
importedName: el.propertyName?.text ?? el.name.text,
|
|
340
|
+
source,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return imports;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 本地别名表(`const 性别 = Sex;` 一类;本地 enum 声明不登记——gen 不可引用它) */
|
|
349
|
+
function collectLocalAliases(sf: ts.SourceFile): Map<string, string> {
|
|
350
|
+
const aliases = new Map<string, string>();
|
|
351
|
+
for (const stmt of sf.statements) {
|
|
352
|
+
if (!ts.isVariableStatement(stmt)) continue;
|
|
353
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
354
|
+
if (!ts.isIdentifier(decl.name)) continue;
|
|
355
|
+
if (
|
|
356
|
+
decl.initializer === undefined ||
|
|
357
|
+
!ts.isIdentifier(decl.initializer)
|
|
358
|
+
) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
aliases.set(decl.name.text, decl.initializer.text);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return aliases;
|
|
365
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StoryState 类型面渲染器(形态 B gen 类型面)。
|
|
3
|
+
*
|
|
4
|
+
* 输入 = config 求值合并树(形态 A state 树 / 形态 B define 树,与降维段同一棵)+
|
|
5
|
+
* AST 侧回收的枚举 type 槽位(collectEnumTypeSlotsForBuild)。输出 =
|
|
6
|
+
* `Stories.src/gen/state.gen.d.ts` 全文:嵌套对象字面量的 `export type StoryState = { … }`,
|
|
7
|
+
* descriptor 叶按 type 槽位取精确类型——原生标量 / 单元素数组 leaf / 多元素数组并集 /
|
|
8
|
+
* 枚举引用(import type)或内联字面量联合。
|
|
9
|
+
*
|
|
10
|
+
* 用法与注意事项:
|
|
11
|
+
* - 渲染只吃「已过降维文法校验」的树(编排顺序保证:runStateSchemaPass 先行,文法
|
|
12
|
+
* 违规在生成之前已 fail-closed);未知形态按 unknown 兜底而不是猜测。
|
|
13
|
+
* - 恒产出:工程未声明任何状态键时退化为 `Record<string, never>`,桥文件永远有类型
|
|
14
|
+
* 可引(与 stories.gen.d.ts 空集合也落文件同思路)。
|
|
15
|
+
* - 空分支不产键(与降维语义一致:空对象贡献零 defines);键排序去重保证同输入
|
|
16
|
+
* 逐字节同输出。
|
|
17
|
+
*/
|
|
18
|
+
import { posix } from "node:path";
|
|
19
|
+
import { compareOrdinal } from "@lingfan-engine/core";
|
|
20
|
+
import type { EnumTypeSlot } from "./state-enum-ast.ts";
|
|
21
|
+
|
|
22
|
+
export interface StateTypegenInput {
|
|
23
|
+
/** config 求值合并树(形态 A state / 形态 B defineTree,与降维段同一棵) */
|
|
24
|
+
readonly tree: unknown;
|
|
25
|
+
/** 点路径 → 枚举 type 槽位列表(直接槽位与并集成员槽位,collectEnumTypeSlotsForBuild 的结果) */
|
|
26
|
+
readonly enumSlots: ReadonlyMap<string, EnumTypeSlot[]>;
|
|
27
|
+
/** 生成物目录名(相对源根,如 "gen"):import 说明符按它改写相对层级 */
|
|
28
|
+
readonly generatedDir: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const HEADER = [
|
|
32
|
+
"/* GENERATED by stories:build (state-typegen) — 构建期自动生成,手改会在下次构建被覆盖。",
|
|
33
|
+
" state(支持自定义状态根名)为强类型产物,可直接以根名操作关键状态变量;",
|
|
34
|
+
" 运行期 define 动态注入的键不在本类型面。 */",
|
|
35
|
+
].join("\n");
|
|
36
|
+
|
|
37
|
+
const PRIMITIVE_TYPES = new Set(["number", "string", "boolean"]);
|
|
38
|
+
|
|
39
|
+
export function renderStateTypegen(input: StateTypegenInput): string {
|
|
40
|
+
const used = new Map<string, EnumTypeSlot>();
|
|
41
|
+
const body = renderValue(input.tree, "", 0, input.enumSlots, used);
|
|
42
|
+
const lines: string[] = [HEADER, ""];
|
|
43
|
+
const importLines = renderImports(used, input.generatedDir);
|
|
44
|
+
if (importLines.length > 0) lines.push(...importLines, "");
|
|
45
|
+
lines.push(`export type StoryState = ${body ?? "Record<string, never>"};`);
|
|
46
|
+
return `${lines.join("\n")}\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 渲染一个树节点;undefined = 空分支(调用方跳过该键,与降维「不产键」一致) */
|
|
50
|
+
function renderValue(
|
|
51
|
+
value: unknown,
|
|
52
|
+
path: string,
|
|
53
|
+
depth: number,
|
|
54
|
+
slots: ReadonlyMap<string, EnumTypeSlot[]>,
|
|
55
|
+
used: Map<string, EnumTypeSlot>,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
if (isPlainObject(value)) {
|
|
58
|
+
if ("type" in value || "default" in value) {
|
|
59
|
+
return renderTypeSlot(value.type, path, slots, used);
|
|
60
|
+
}
|
|
61
|
+
return renderObject(value, path, depth, slots, used);
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
typeof value === "number" ||
|
|
65
|
+
typeof value === "string" ||
|
|
66
|
+
typeof value === "boolean"
|
|
67
|
+
) {
|
|
68
|
+
return typeof value;
|
|
69
|
+
}
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderObject(
|
|
74
|
+
obj: Record<string, unknown>,
|
|
75
|
+
path: string,
|
|
76
|
+
depth: number,
|
|
77
|
+
slots: ReadonlyMap<string, EnumTypeSlot[]>,
|
|
78
|
+
used: Map<string, EnumTypeSlot>,
|
|
79
|
+
): string | undefined {
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
for (const key of Object.keys(obj).sort()) {
|
|
82
|
+
const childPath = path === "" ? key : `${path}.${key}`;
|
|
83
|
+
const rendered = renderValue(obj[key], childPath, depth + 1, slots, used);
|
|
84
|
+
if (rendered === undefined) continue;
|
|
85
|
+
lines.push(`${" ".repeat(depth + 1)}${JSON.stringify(key)}: ${rendered};`);
|
|
86
|
+
}
|
|
87
|
+
if (lines.length === 0) return undefined;
|
|
88
|
+
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** descriptor 的 type 槽位:原生名 / [leaf] 数组 / 多元素并集 / 枚举对象(引用或内联联合) */
|
|
92
|
+
function renderTypeSlot(
|
|
93
|
+
type: unknown,
|
|
94
|
+
path: string,
|
|
95
|
+
slots: ReadonlyMap<string, EnumTypeSlot[]>,
|
|
96
|
+
used: Map<string, EnumTypeSlot>,
|
|
97
|
+
): string {
|
|
98
|
+
if (typeof type === "string" && PRIMITIVE_TYPES.has(type)) return type;
|
|
99
|
+
if (Array.isArray(type)) {
|
|
100
|
+
if (type.length >= 2) return renderUnionSlot(type, path, slots, used, true);
|
|
101
|
+
if (type.length === 1) {
|
|
102
|
+
return `${parenthesizeUnion(renderTypeSlot(type[0], path, slots, used))}[]`;
|
|
103
|
+
}
|
|
104
|
+
return "unknown";
|
|
105
|
+
}
|
|
106
|
+
if (isPlainObject(type)) {
|
|
107
|
+
const slot = findSlot(path, undefined, slots);
|
|
108
|
+
if (slot !== undefined) {
|
|
109
|
+
used.set(slot.localName, slot);
|
|
110
|
+
return slot.localName;
|
|
111
|
+
}
|
|
112
|
+
return renderEnumUnion(type);
|
|
113
|
+
}
|
|
114
|
+
return "unknown";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 并集成员槽位渲染:成员按作者序取并集(保序 Set 去重,确定性由源序保证)。
|
|
119
|
+
* 枚举成员优先走回收槽位(按成员下标配对)生成 import 引用,取不到则内联
|
|
120
|
+
* 字面量联合;嵌套并集递归渲染但不回收枚举(内联回退)。
|
|
121
|
+
*/
|
|
122
|
+
function renderUnionSlot(
|
|
123
|
+
members: readonly unknown[],
|
|
124
|
+
path: string,
|
|
125
|
+
slots: ReadonlyMap<string, EnumTypeSlot[]>,
|
|
126
|
+
used: Map<string, EnumTypeSlot>,
|
|
127
|
+
recoverEnums: boolean,
|
|
128
|
+
): string {
|
|
129
|
+
const rendered: string[] = [];
|
|
130
|
+
const seen = new Set<string>();
|
|
131
|
+
const push = (text: string): void => {
|
|
132
|
+
if (seen.has(text)) return;
|
|
133
|
+
seen.add(text);
|
|
134
|
+
rendered.push(text);
|
|
135
|
+
};
|
|
136
|
+
members.forEach((member, index) => {
|
|
137
|
+
if (member === null) {
|
|
138
|
+
push("null");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (typeof member === "string" && PRIMITIVE_TYPES.has(member)) {
|
|
142
|
+
push(member);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(member)) {
|
|
146
|
+
if (member.length === 1) {
|
|
147
|
+
push(
|
|
148
|
+
`${parenthesizeUnion(renderTypeSlot(member[0], path, slots, used))}[]`,
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
push(renderUnionSlot(member, path, slots, used, false));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (isPlainObject(member)) {
|
|
156
|
+
const slot = recoverEnums ? findSlot(path, index, slots) : undefined;
|
|
157
|
+
if (slot !== undefined) {
|
|
158
|
+
used.set(slot.localName, slot);
|
|
159
|
+
push(slot.localName);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
push(renderEnumUnion(member));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
push("unknown");
|
|
166
|
+
});
|
|
167
|
+
return rendered.length === 0 ? "unknown" : rendered.join(" | ");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 按路径与成员下标取回收槽位(memberIndex undefined = 直接 type 槽位) */
|
|
171
|
+
function findSlot(
|
|
172
|
+
path: string,
|
|
173
|
+
memberIndex: number | undefined,
|
|
174
|
+
slots: ReadonlyMap<string, EnumTypeSlot[]>,
|
|
175
|
+
): EnumTypeSlot | undefined {
|
|
176
|
+
return slots.get(path)?.find((slot) => slot.memberIndex === memberIndex);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 数组位置包联合:TS 里 `A | B[]` 解析为 `A | (B[])`,叶是联合时必须
|
|
181
|
+
* 括起来才是「联合元素的数组」(如内联枚举 leaf → `("a" | "b")[]`)。
|
|
182
|
+
*/
|
|
183
|
+
function parenthesizeUnion(rendered: string): string {
|
|
184
|
+
return rendered.includes(" | ") ? `(${rendered})` : rendered;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 枚举成员值联合(字符串 JSON 引号、数字原样;排序 + 同值去重保证确定性) */
|
|
188
|
+
function renderEnumUnion(values: Record<string, unknown>): string {
|
|
189
|
+
const rendered = new Set<string>();
|
|
190
|
+
for (const value of Object.values(values)) {
|
|
191
|
+
if (typeof value === "string") rendered.add(JSON.stringify(value));
|
|
192
|
+
else if (typeof value === "number") rendered.add(String(value));
|
|
193
|
+
}
|
|
194
|
+
return rendered.size === 0 ? "unknown" : [...rendered].sort().join(" | ");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderImports(
|
|
198
|
+
used: ReadonlyMap<string, EnumTypeSlot>,
|
|
199
|
+
generatedDir: string,
|
|
200
|
+
): string[] {
|
|
201
|
+
const bySource = new Map<string, EnumTypeSlot[]>();
|
|
202
|
+
for (const slot of used.values()) {
|
|
203
|
+
const list = bySource.get(slot.source) ?? [];
|
|
204
|
+
list.push(slot);
|
|
205
|
+
bySource.set(slot.source, list);
|
|
206
|
+
}
|
|
207
|
+
const lines: string[] = [];
|
|
208
|
+
for (const source of [...bySource.keys()].sort()) {
|
|
209
|
+
const specifier = JSON.stringify(rewriteSpecifier(source, generatedDir));
|
|
210
|
+
const slots = bySource.get(source);
|
|
211
|
+
if (slots === undefined) continue;
|
|
212
|
+
for (const slot of slots.filter((s) => s.importedName === "default")) {
|
|
213
|
+
lines.push(`import type ${slot.localName} from ${specifier};`);
|
|
214
|
+
}
|
|
215
|
+
const named = slots
|
|
216
|
+
.filter((s) => s.importedName !== "default")
|
|
217
|
+
.sort((a, b) => compareOrdinal(a.localName, b.localName))
|
|
218
|
+
.map((s) =>
|
|
219
|
+
s.importedName === s.localName
|
|
220
|
+
? s.localName
|
|
221
|
+
: `${s.importedName} as ${s.localName}`,
|
|
222
|
+
);
|
|
223
|
+
if (named.length > 0) {
|
|
224
|
+
lines.push(`import type { ${named.join(", ")} } from ${specifier};`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return lines;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* import 说明符改写:源根内相对路径("./lib/enums")按生成物目录换算相对层级
|
|
232
|
+
* ("gen" → "../lib/enums");包导入原样保留。
|
|
233
|
+
*/
|
|
234
|
+
function rewriteSpecifier(source: string, generatedDir: string): string {
|
|
235
|
+
if (!source.startsWith(".")) return source;
|
|
236
|
+
const rel = posix.relative(generatedDir, posix.normalize(source));
|
|
237
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
241
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
242
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 类型生成器的输入形状。
|
|
3
|
+
*
|
|
4
|
+
* 生成器只吃「已经算好的字符串域」,不自己去扫源码:列 id 来自编译结果、state 键来自
|
|
5
|
+
* config 状态降维、变量键来自 config.vars、声明名来自产物 op 流、扩展 id 来自清单装载。
|
|
6
|
+
* 这样生成器与编排层的数据来源始终是同一份,不会出现「构建按 A 判定、IDE 按 B 提示」。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 一次类型面生成所需的全部字符串域 */
|
|
10
|
+
export interface TypegenInput {
|
|
11
|
+
/** 列 id:default 导出函数名,jump/option target 的值域 */
|
|
12
|
+
readonly columns: readonly string[];
|
|
13
|
+
/** config 状态(形态 A state / 形态 B define 合并树)降维后的扁平点路径键 */
|
|
14
|
+
readonly schemaKeys: readonly string[];
|
|
15
|
+
/** config.vars(config.ts 的 vars 导出键集对象)登记的变量键 */
|
|
16
|
+
readonly defineKeys: readonly string[];
|
|
17
|
+
/** 产物 op 流里出现过的声明名(let/for/input/store 等) */
|
|
18
|
+
readonly declaredNames: readonly string[];
|
|
19
|
+
/** 清单装载到的扩展 id(存 `demoquest`,不含 `ext.` 前缀与尾点) */
|
|
20
|
+
readonly extensionPrefixes: readonly string[];
|
|
21
|
+
}
|
package/README.md
DELETED