@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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 单轮求值:worker 生命周期(超时/错误兜底)与打包/执行结果翻译成诊断。
|
|
3
|
+
*/
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Worker } from "node:worker_threads";
|
|
6
|
+
import type { CompilerDiagnostic } from "../../compiler/index.ts";
|
|
7
|
+
import type {
|
|
8
|
+
ConstEvalResult,
|
|
9
|
+
DefineCall,
|
|
10
|
+
DefineBad,
|
|
11
|
+
JsonValue,
|
|
12
|
+
} from "../contracts.ts";
|
|
13
|
+
import { locateExport } from "../diag/index.ts";
|
|
14
|
+
import { WORKER_SOURCE } from "./source.ts";
|
|
15
|
+
|
|
16
|
+
interface WorkerExportsMessage {
|
|
17
|
+
kind: "exports";
|
|
18
|
+
values: Record<string, unknown>;
|
|
19
|
+
bad: { key: string; reason: string }[];
|
|
20
|
+
defineCalls: DefineCall[];
|
|
21
|
+
defineBad: DefineBad[];
|
|
22
|
+
}
|
|
23
|
+
interface WorkerEsbuildMessage {
|
|
24
|
+
kind: "esbuild-error";
|
|
25
|
+
diags: CompilerDiagnostic[];
|
|
26
|
+
}
|
|
27
|
+
interface WorkerRuntimeMessage {
|
|
28
|
+
kind: "runtime-error";
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
type WorkerOutcome =
|
|
32
|
+
| WorkerExportsMessage
|
|
33
|
+
| WorkerEsbuildMessage
|
|
34
|
+
| WorkerRuntimeMessage
|
|
35
|
+
| { kind: "timeout" };
|
|
36
|
+
|
|
37
|
+
export async function evaluateOnce(
|
|
38
|
+
absEntry: string,
|
|
39
|
+
esbuildPath: string,
|
|
40
|
+
timeoutMs: number,
|
|
41
|
+
): Promise<ConstEvalResult> {
|
|
42
|
+
const outcome = await runWorkerRound({
|
|
43
|
+
entry: absEntry,
|
|
44
|
+
esbuildPath,
|
|
45
|
+
workDir: path.dirname(absEntry),
|
|
46
|
+
timeoutMs,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
switch (outcome.kind) {
|
|
50
|
+
case "exports": {
|
|
51
|
+
if (outcome.bad.length > 0) {
|
|
52
|
+
return {
|
|
53
|
+
diags: outcome.bad.map((item) => {
|
|
54
|
+
const at = locateExport(absEntry, item.key);
|
|
55
|
+
return {
|
|
56
|
+
file: absEntry,
|
|
57
|
+
line: at.line,
|
|
58
|
+
col: at.col,
|
|
59
|
+
message: `导出 "${item.key}" 非 JSON 可序列化(${item.reason})——编译期求值产物仅取 JSON 可序列化值`,
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (outcome.defineBad.length > 0) {
|
|
65
|
+
return {
|
|
66
|
+
diags: outcome.defineBad.map((item) => ({
|
|
67
|
+
file: absEntry,
|
|
68
|
+
line: 1,
|
|
69
|
+
col: 1,
|
|
70
|
+
message: `define("${item.path}"):${item.reason}——define 值文法与 state 同规(仅 JSON 可序列化值;函数/undefined/symbol/bigint 拒)`,
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// worker 分类已保证 JSON 安全,边界处收窄类型
|
|
75
|
+
return {
|
|
76
|
+
values: outcome.values as Record<string, JsonValue>,
|
|
77
|
+
defineCalls: outcome.defineCalls,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case "esbuild-error":
|
|
81
|
+
return { diags: outcome.diags };
|
|
82
|
+
case "runtime-error":
|
|
83
|
+
return {
|
|
84
|
+
diags: [
|
|
85
|
+
{
|
|
86
|
+
file: absEntry,
|
|
87
|
+
line: 1,
|
|
88
|
+
col: 1,
|
|
89
|
+
message: `编译期求值失败:模块顶层执行抛错 —— ${outcome.message}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
case "timeout":
|
|
94
|
+
return {
|
|
95
|
+
diags: [
|
|
96
|
+
{
|
|
97
|
+
file: absEntry,
|
|
98
|
+
line: 1,
|
|
99
|
+
col: 1,
|
|
100
|
+
message: `编译期求值超时(>${timeoutMs}ms)——模块顶层代码未在时限内完成求值`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function runWorkerRound(data: {
|
|
108
|
+
entry: string;
|
|
109
|
+
esbuildPath: string;
|
|
110
|
+
workDir: string;
|
|
111
|
+
timeoutMs: number;
|
|
112
|
+
}): Promise<WorkerOutcome> {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const worker = new Worker(WORKER_SOURCE, { eval: true, workerData: data });
|
|
115
|
+
let settled = false;
|
|
116
|
+
const finish = (outcome: WorkerOutcome): void => {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
settled = true;
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
void worker.terminate();
|
|
121
|
+
resolve(outcome);
|
|
122
|
+
};
|
|
123
|
+
const timer = setTimeout(() => finish({ kind: "timeout" }), data.timeoutMs);
|
|
124
|
+
worker.on("message", (message: { kind?: unknown }) => {
|
|
125
|
+
if (
|
|
126
|
+
message !== null &&
|
|
127
|
+
typeof message === "object" &&
|
|
128
|
+
typeof message.kind === "string"
|
|
129
|
+
) {
|
|
130
|
+
finish(message as WorkerOutcome);
|
|
131
|
+
} else {
|
|
132
|
+
finish({ kind: "runtime-error", message: "worker 返回了未知消息形态" });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
worker.on("error", (error: unknown) => {
|
|
136
|
+
finish({
|
|
137
|
+
kind: "runtime-error",
|
|
138
|
+
message: error instanceof Error ? error.message : String(error),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
worker.on("exit", (code: number) => {
|
|
142
|
+
if (!settled)
|
|
143
|
+
finish({
|
|
144
|
+
kind: "runtime-error",
|
|
145
|
+
message: `worker 异常退出(exit code ${code})`,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worker 源码模板:`String.raw` + 文件内无 `$` 插值起点,搬迁时内容必须逐字保持。
|
|
3
|
+
*/
|
|
4
|
+
export const WORKER_SOURCE = String.raw`
|
|
5
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
6
|
+
const { createRequire } = require("node:module");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
|
|
9
|
+
/** 返回 null = JSON 可序列化;否则返回不可序列化原因。 */
|
|
10
|
+
function classify(value, seen) {
|
|
11
|
+
if (value === undefined) return "undefined";
|
|
12
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return null;
|
|
13
|
+
if (typeof value === "number") return Number.isFinite(value) ? null : "非有限数值(NaN/Infinity)";
|
|
14
|
+
if (typeof value === "function") return "函数(function)";
|
|
15
|
+
if (typeof value === "bigint") return "bigint";
|
|
16
|
+
if (typeof value === "symbol") return "symbol";
|
|
17
|
+
if (typeof value !== "object") return "不可序列化类型(" + String(typeof value) + ")";
|
|
18
|
+
if (seen.has(value)) return "循环引用";
|
|
19
|
+
seen.add(value);
|
|
20
|
+
try {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
for (let i = 0; i < value.length; i++) {
|
|
23
|
+
if (!(i in value)) return "数组空洞(undefined 元素)";
|
|
24
|
+
const element = classify(value[i], seen);
|
|
25
|
+
if (element !== null) return element;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const proto = Object.getPrototypeOf(value);
|
|
30
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
31
|
+
const name = (proto.constructor && proto.constructor.name) || "unknown";
|
|
32
|
+
return "非普通对象(" + name + " 实例:Date/Map/Set/class 等不保证可序列化)";
|
|
33
|
+
}
|
|
34
|
+
for (const key of Object.keys(value)) {
|
|
35
|
+
const member = classify(value[key], seen);
|
|
36
|
+
if (member !== null) return member;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
} finally {
|
|
40
|
+
seen.delete(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
(async () => {
|
|
45
|
+
try {
|
|
46
|
+
const requireEsbuild = createRequire(workerData.esbuildPath);
|
|
47
|
+
const esbuild = requireEsbuild(workerData.esbuildPath);
|
|
48
|
+
const result = await esbuild.build({
|
|
49
|
+
entryPoints: [workerData.entry],
|
|
50
|
+
bundle: true,
|
|
51
|
+
write: false,
|
|
52
|
+
format: "cjs",
|
|
53
|
+
platform: "node",
|
|
54
|
+
logLevel: "silent",
|
|
55
|
+
absWorkingDir: workerData.workDir,
|
|
56
|
+
});
|
|
57
|
+
const code = result.outputFiles[0].text;
|
|
58
|
+
|
|
59
|
+
const moduleObj = { exports: {} };
|
|
60
|
+
const runRequire = createRequire(workerData.entry);
|
|
61
|
+
|
|
62
|
+
// define / defineVars:config.ts 构建期内建(求值环境注入,无运行时实体)。
|
|
63
|
+
// define 二态:字符串首参 = 点路径单值;普通对象首参 = 子树声明;值过 classify
|
|
64
|
+
// 才入列(非法值进 defineBad,由求值段翻成诊断)。defineVars 返回实参本身,
|
|
65
|
+
// 使 export const vars = defineVars({...}) 的导出值 = 键集对象(vars 键来源)。
|
|
66
|
+
const defineCalls = [];
|
|
67
|
+
const defineBad = [];
|
|
68
|
+
function define(pathOrSpec, value) {
|
|
69
|
+
if (typeof pathOrSpec === "string") {
|
|
70
|
+
const reason = classify(value, new Set());
|
|
71
|
+
if (reason !== null) {
|
|
72
|
+
defineBad.push({ path: pathOrSpec, reason: "值不可序列化(" + reason + ")" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
defineCalls.push({ kind: "path", path: pathOrSpec, value: value });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (pathOrSpec !== null && typeof pathOrSpec === "object" && !Array.isArray(pathOrSpec)) {
|
|
79
|
+
const reason = classify(pathOrSpec, new Set());
|
|
80
|
+
if (reason !== null) {
|
|
81
|
+
defineBad.push({ path: "(对象实参)", reason: "对象实参不可序列化(" + reason + ")" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
defineCalls.push({ kind: "spec", spec: pathOrSpec });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
defineBad.push({ path: "(实参)", reason: "首参须为点路径字符串或对象字面量" });
|
|
88
|
+
}
|
|
89
|
+
function defineVars(spec) {
|
|
90
|
+
return spec;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const factory = new Function(
|
|
94
|
+
"module",
|
|
95
|
+
"exports",
|
|
96
|
+
"require",
|
|
97
|
+
"__filename",
|
|
98
|
+
"__dirname",
|
|
99
|
+
"define",
|
|
100
|
+
"defineVars",
|
|
101
|
+
code,
|
|
102
|
+
);
|
|
103
|
+
factory(moduleObj, moduleObj.exports, runRequire, workerData.entry, path.dirname(workerData.entry), define, defineVars);
|
|
104
|
+
try { delete moduleObj.exports.__esModule; } catch { /* 不可删则忽略 */ }
|
|
105
|
+
|
|
106
|
+
const values = {};
|
|
107
|
+
const bad = [];
|
|
108
|
+
for (const key of Object.keys(moduleObj.exports)) {
|
|
109
|
+
const reason = classify(moduleObj.exports[key], new Set());
|
|
110
|
+
if (reason === null) values[key] = moduleObj.exports[key];
|
|
111
|
+
else bad.push({ key, reason });
|
|
112
|
+
}
|
|
113
|
+
parentPort.postMessage({ kind: "exports", values, bad, defineCalls, defineBad });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error !== null && typeof error === "object" && Array.isArray(error.errors)) {
|
|
116
|
+
const diags = error.errors.map((item) => {
|
|
117
|
+
const at = item.location ?? null;
|
|
118
|
+
return {
|
|
119
|
+
file: (at && at.file) || workerData.entry,
|
|
120
|
+
line: (at && at.line) || 1,
|
|
121
|
+
col: ((at && at.column) ?? 0) + 1,
|
|
122
|
+
message: item.text ?? String(item),
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
parentPort.postMessage({ kind: "esbuild-error", diags });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
parentPort.postMessage({
|
|
129
|
+
kind: "runtime-error",
|
|
130
|
+
message: error instanceof Error ? error.message : String(error),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
`;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state 降维段(编排层):把状态初始声明递归降维成扁平点路径 defines
|
|
3
|
+
* (引擎初始化通道,session 无条件 Set,零引擎改动),经 mergeDefines 并入工程 defines。
|
|
4
|
+
*
|
|
5
|
+
* 输入双形态(二选一,由 config 求值段保证互斥;皆缺省 = 无 schema 贡献):
|
|
6
|
+
* - 形态 A:`config.state` 导出的嵌套树;
|
|
7
|
+
* - 形态 B:define() 调用合并出的纯嵌套树(config-eval/define-merge 的产物)。
|
|
8
|
+
* 两棵树走同一条降维文法(合流点唯一),本段自身不再读文件、不再求值。
|
|
9
|
+
*
|
|
10
|
+
* 降维文法(与包出口的 InferStateSchema 一一对应):
|
|
11
|
+
* - 嵌套对象递归拼接点路径(源形态只有嵌套一种,点路径键 fail-closed);descriptor
|
|
12
|
+
* { type, default } 取 default;
|
|
13
|
+
* - descriptor type ∈ "number"|"string"|"boolean" | 枚举对象(值全为 string|number)|
|
|
14
|
+
* 单元素数组 [leaf],default 与 type 做一致性校验;多元素数组 = 成员槽位并集
|
|
15
|
+
* (成员:保留字标量/枚举对象/null/[leaf]/嵌套并集;default 须落在至少一个成员
|
|
16
|
+
* 值域内,未知成员 fail-closed 一次报全部);
|
|
17
|
+
* - 形态非法 fail-closed(一次报全部);值形态无法保留源位置,诊断统一挂
|
|
18
|
+
* `config.ts:1:1`,以键路径上下文定位。
|
|
19
|
+
*/
|
|
20
|
+
import { CONFIG_FILE } from "../config/story-config.ts";
|
|
21
|
+
import { StoryBuildError } from "../shared/index.ts";
|
|
22
|
+
|
|
23
|
+
const PRIMITIVE_TYPES: ReadonlySet<string> = new Set([
|
|
24
|
+
"number",
|
|
25
|
+
"string",
|
|
26
|
+
"boolean",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 把 config 求值产物(形态 A 的 state 树 / 形态 B 的 define 合并树)降维为扁平 defines。
|
|
31
|
+
* 同步纯函数:求值已在上游完成,此处只做文法校验与展平。
|
|
32
|
+
*/
|
|
33
|
+
export function runStateSchemaPass(
|
|
34
|
+
stateTree: unknown,
|
|
35
|
+
defineTree: Record<string, unknown>,
|
|
36
|
+
sourcesDirName: string,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
const origin = { line: 1, col: 1 };
|
|
39
|
+
const errors: string[] = [];
|
|
40
|
+
const defines: Record<string, unknown> = {};
|
|
41
|
+
if (stateTree !== undefined)
|
|
42
|
+
flattenRoot(stateTree, defines, errors, origin, "state");
|
|
43
|
+
flattenRoot(defineTree, defines, errors, origin, "define");
|
|
44
|
+
if (errors.length > 0) {
|
|
45
|
+
throw new StoryBuildError(
|
|
46
|
+
`state schema 降维失败(${errors.length} 条):\n${errors
|
|
47
|
+
.map(
|
|
48
|
+
(message) =>
|
|
49
|
+
`- ${sourcesDirName}/${CONFIG_FILE}:${origin.line}:${origin.col} ${message}`,
|
|
50
|
+
)
|
|
51
|
+
.join("\n")}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return defines;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function flattenRoot(
|
|
58
|
+
schema: unknown,
|
|
59
|
+
out: Record<string, unknown>,
|
|
60
|
+
errors: string[],
|
|
61
|
+
origin: { line: number; col: number },
|
|
62
|
+
rootLabel: string,
|
|
63
|
+
): void {
|
|
64
|
+
if (!isPlainObject(schema)) {
|
|
65
|
+
errors.push(`${rootLabel} 根必须是键集对象`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
69
|
+
if (key.includes(".")) {
|
|
70
|
+
errors.push(dotKeyError(key));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
flattenValue(value, key, out, errors, origin);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 点路径键统一拒绝:扁平 defines 键由降维展平产生,源形态只有嵌套一种。 */
|
|
78
|
+
const dotKeyError = (key: string): string =>
|
|
79
|
+
`${key}:点路径键不支持(源形态统一纯嵌套对象,扁平 defines 键由降维展平产生)`;
|
|
80
|
+
|
|
81
|
+
function flattenValue(
|
|
82
|
+
value: unknown,
|
|
83
|
+
path: string,
|
|
84
|
+
out: Record<string, unknown>,
|
|
85
|
+
errors: string[],
|
|
86
|
+
origin: { line: number; col: number },
|
|
87
|
+
): void {
|
|
88
|
+
if (isPlainObject(value)) {
|
|
89
|
+
if ("type" in value || "default" in value) {
|
|
90
|
+
if (validateDescriptor(value, path, errors)) out[path] = value.default;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const [key, child] of Object.entries(value)) {
|
|
94
|
+
if (key.includes(".")) {
|
|
95
|
+
errors.push(dotKeyError(`${path}.${key}`));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
flattenValue(child, `${path}.${key}`, out, errors, origin);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
errors.push(
|
|
104
|
+
`${path}:数组值请用 descriptor { type: [leaf], default: [...] }`,
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
value === undefined ||
|
|
110
|
+
typeof value === "function" ||
|
|
111
|
+
typeof value === "symbol" ||
|
|
112
|
+
typeof value === "bigint"
|
|
113
|
+
) {
|
|
114
|
+
errors.push(`${path}:不可序列化的 schema 值`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
out[path] = value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 校验 descriptor;返回 true 表示合法(default 已随之写入 defines)。
|
|
122
|
+
* 诊断文本只带路径不带行列(`config.ts` 的挂点在整条消息末尾统一给出),
|
|
123
|
+
* 所以这里不收 origin——多一个用不上的参数只会让调用方以为它能定位。
|
|
124
|
+
*/
|
|
125
|
+
function validateDescriptor(
|
|
126
|
+
descriptor: Record<string, unknown>,
|
|
127
|
+
path: string,
|
|
128
|
+
errors: string[],
|
|
129
|
+
): boolean {
|
|
130
|
+
if (!("default" in descriptor)) {
|
|
131
|
+
errors.push(`${path}:descriptor 缺 default`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const type = descriptor.type;
|
|
135
|
+
const def = descriptor.default;
|
|
136
|
+
if (typeof type === "string" && PRIMITIVE_TYPES.has(type)) {
|
|
137
|
+
const ok =
|
|
138
|
+
(type === "number" && typeof def === "number" && Number.isFinite(def)) ||
|
|
139
|
+
(type === "string" && typeof def === "string") ||
|
|
140
|
+
(type === "boolean" && typeof def === "boolean");
|
|
141
|
+
if (!ok)
|
|
142
|
+
errors.push(`${path}:descriptor type "${type}" 与 default 不一致`);
|
|
143
|
+
return ok;
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(type)) {
|
|
146
|
+
if (type.length >= 2) return validateUnion(type, def, path, errors);
|
|
147
|
+
if (type.length !== 1) {
|
|
148
|
+
errors.push(
|
|
149
|
+
`${path}:数组 descriptor 的 type 不能为空数组(单元素 [leaf] 或多元素并集)`,
|
|
150
|
+
);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(def)) {
|
|
154
|
+
errors.push(`${path}:数组 descriptor 的 default 必须是数组`);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const bad = def.filter((element) => !leafMatches(type[0], element)).length;
|
|
158
|
+
if (bad > 0)
|
|
159
|
+
errors.push(`${path}:数组 default 有 ${bad} 个元素与 leaf 类型不一致`);
|
|
160
|
+
return bad === 0;
|
|
161
|
+
}
|
|
162
|
+
const enumValues = asEnumValues(type);
|
|
163
|
+
if (enumValues !== undefined) {
|
|
164
|
+
if (
|
|
165
|
+
(typeof def !== "string" && typeof def !== "number") ||
|
|
166
|
+
!enumValues.includes(def)
|
|
167
|
+
) {
|
|
168
|
+
errors.push(`${path}:descriptor default 不在枚举值域内`);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
errors.push(`${path}:无法识别的 descriptor type`);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function leafMatches(leaf: unknown, value: unknown): boolean {
|
|
178
|
+
if (typeof leaf === "string" && PRIMITIVE_TYPES.has(leaf)) {
|
|
179
|
+
return (
|
|
180
|
+
(leaf === "number" &&
|
|
181
|
+
typeof value === "number" &&
|
|
182
|
+
Number.isFinite(value)) ||
|
|
183
|
+
(leaf === "string" && typeof value === "string") ||
|
|
184
|
+
(leaf === "boolean" && typeof value === "boolean")
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const enumValues = asEnumValues(leaf);
|
|
188
|
+
return (
|
|
189
|
+
enumValues !== undefined &&
|
|
190
|
+
(typeof value === "string" || typeof value === "number") &&
|
|
191
|
+
enumValues.includes(value)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 并集成员槽位校验(type 为多元素数组):逐成员按文法归类,未知成员
|
|
197
|
+
* fail-closed 一次报全部;default 落在至少一个成员值域内即合法。
|
|
198
|
+
*/
|
|
199
|
+
function validateUnion(
|
|
200
|
+
members: readonly unknown[],
|
|
201
|
+
def: unknown,
|
|
202
|
+
path: string,
|
|
203
|
+
errors: string[],
|
|
204
|
+
): boolean {
|
|
205
|
+
let unknownCount = 0;
|
|
206
|
+
members.forEach((member, index) => {
|
|
207
|
+
if (isKnownUnionMember(member)) return;
|
|
208
|
+
errors.push(
|
|
209
|
+
`${path}:并集第 ${index + 1} 个成员无法识别(只允许 "number"/"string"/"boolean"、枚举对象、null 与 [leaf])`,
|
|
210
|
+
);
|
|
211
|
+
unknownCount += 1;
|
|
212
|
+
});
|
|
213
|
+
if (unknownCount > 0) return false;
|
|
214
|
+
if (members.some((member) => unionMatches(member, def))) return true;
|
|
215
|
+
errors.push(`${path}:descriptor default 不在并集值域内`);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** 并集成员文法判定:null / 保留字标量 / 枚举对象 / [leaf] / 嵌套并集(递归)。 */
|
|
220
|
+
function isKnownUnionMember(member: unknown): boolean {
|
|
221
|
+
if (member === null) return true;
|
|
222
|
+
if (typeof member === "string") return PRIMITIVE_TYPES.has(member);
|
|
223
|
+
if (Array.isArray(member)) {
|
|
224
|
+
if (member.length === 1) return isValidLeaf(member[0]);
|
|
225
|
+
return member.every(isKnownUnionMember);
|
|
226
|
+
}
|
|
227
|
+
return asEnumValues(member) !== undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** default 是否落在单个并集成员的值域内(与 isKnownUnionMember 同构逐分支匹配)。 */
|
|
231
|
+
function unionMatches(member: unknown, value: unknown): boolean {
|
|
232
|
+
if (member === null) return value === null;
|
|
233
|
+
if (typeof member === "string" && PRIMITIVE_TYPES.has(member))
|
|
234
|
+
return leafMatches(member, value);
|
|
235
|
+
if (Array.isArray(member)) {
|
|
236
|
+
if (member.length === 1)
|
|
237
|
+
return (
|
|
238
|
+
Array.isArray(value) &&
|
|
239
|
+
value.every((element) => leafMatches(member[0], element))
|
|
240
|
+
);
|
|
241
|
+
return member.some((nested) => unionMatches(nested, value));
|
|
242
|
+
}
|
|
243
|
+
const enumValues = asEnumValues(member);
|
|
244
|
+
return (
|
|
245
|
+
enumValues !== undefined &&
|
|
246
|
+
(typeof value === "string" || typeof value === "number") &&
|
|
247
|
+
enumValues.includes(value)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** leaf 槽位是否合法(保留字标量或枚举对象)——并集数组成员复用。 */
|
|
252
|
+
function isValidLeaf(leaf: unknown): boolean {
|
|
253
|
+
if (typeof leaf === "string" && PRIMITIVE_TYPES.has(leaf)) return true;
|
|
254
|
+
return asEnumValues(leaf) !== undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 枚举对象判定:非空普通对象且值全为 string|number(descriptor 不落此形——type 槽位专用)。 */
|
|
258
|
+
function asEnumValues(value: unknown): (string | number)[] | undefined {
|
|
259
|
+
if (!isPlainObject(value)) return undefined;
|
|
260
|
+
const values = Object.values(value);
|
|
261
|
+
if (values.length === 0) return undefined;
|
|
262
|
+
return values.every((v) => typeof v === "string" || typeof v === "number")
|
|
263
|
+
? (values as (string | number)[])
|
|
264
|
+
: undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
268
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
269
|
+
}
|