@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,102 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { compareOrdinal } from "@lingfan-engine/core";
|
|
3
|
+
import { script } from "@lingfan-engine/editor";
|
|
4
|
+
|
|
5
|
+
export interface CompileDiag {
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
col: number;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class DiagBag {
|
|
13
|
+
private readonly items = new Map<string, CompileDiag>();
|
|
14
|
+
|
|
15
|
+
add(file: string, sf: ts.SourceFile, node: ts.Node, message: string): void {
|
|
16
|
+
this.addPos(file, sf, node.getStart(sf), message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
addPos(file: string, sf: ts.SourceFile, pos: number, message: string): void {
|
|
20
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(pos);
|
|
21
|
+
const key = `${file}:${line + 1}:${character + 1}:${message}`;
|
|
22
|
+
this.items.set(key, { file, line: line + 1, col: character + 1, message });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
list(): CompileDiag[] {
|
|
26
|
+
return [...this.items.values()].sort(
|
|
27
|
+
(a, b) =>
|
|
28
|
+
compareOrdinal(a.file, b.file) ||
|
|
29
|
+
a.line - b.line ||
|
|
30
|
+
a.col - b.col ||
|
|
31
|
+
compareOrdinal(a.message, b.message),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ConstValue = string | number | boolean | null;
|
|
37
|
+
|
|
38
|
+
export type Binding =
|
|
39
|
+
| { kind: "word"; name: string }
|
|
40
|
+
| { kind: "namespace" }
|
|
41
|
+
| { kind: "function"; file: string; name: string }
|
|
42
|
+
| { kind: "declare"; name: string }
|
|
43
|
+
| { kind: "cell"; guard: string }
|
|
44
|
+
| { kind: "const"; value: ConstValue }
|
|
45
|
+
| { kind: "opaque" }
|
|
46
|
+
| {
|
|
47
|
+
kind: "enum";
|
|
48
|
+
file: string;
|
|
49
|
+
name: string;
|
|
50
|
+
members: ReadonlyMap<string, ConstValue>;
|
|
51
|
+
}
|
|
52
|
+
| { kind: "story"; file: string; name: string };
|
|
53
|
+
|
|
54
|
+
export interface ModuleRecord {
|
|
55
|
+
rel: string;
|
|
56
|
+
sf: ts.SourceFile;
|
|
57
|
+
story: { name: string; node: ts.FunctionDeclaration } | null;
|
|
58
|
+
functions: Map<string, ts.FunctionDeclaration>;
|
|
59
|
+
bindings: Map<string, Binding>;
|
|
60
|
+
exports: Map<string, Binding>;
|
|
61
|
+
imports: ts.ImportDeclaration[];
|
|
62
|
+
consts: ts.VariableStatement[];
|
|
63
|
+
/** node_modules 值导入的本地名登记(扫描期即收集,是否放行由求值喂入决定) */
|
|
64
|
+
npmNames: Set<string>;
|
|
65
|
+
/** 编译期求值喂入:导出名 → JSON 可序列化值;未被求值的文件 = undefined */
|
|
66
|
+
evaluated?: Readonly<Record<string, unknown>>;
|
|
67
|
+
/** 列级字体声明(`export const columnFont = "<名>"`;仅故事文件合法,null = 未声明) */
|
|
68
|
+
columnFont: { value: string; node: ts.Node } | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface StoryConfig {
|
|
72
|
+
readonly folders: readonly string[];
|
|
73
|
+
readonly start: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Graph {
|
|
77
|
+
files: Map<string, string>;
|
|
78
|
+
records: Map<string, ModuleRecord>;
|
|
79
|
+
config: StoryConfig;
|
|
80
|
+
configSf: ts.SourceFile;
|
|
81
|
+
coveredStories: string[];
|
|
82
|
+
/** 状态根名(发现自 `export declare const <根名>: StoryState`;无声明回退 "state") */
|
|
83
|
+
readonly stateRoot: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const ROOT_TS_FILES_ALLOWED = new Set(["config.ts"]);
|
|
87
|
+
export const GEN_PREFIX = "gen/";
|
|
88
|
+
|
|
89
|
+
/** 列级字体声明的保留导出名(`export const columnFont = "<名>"`,仅故事文件顶层) */
|
|
90
|
+
export const COLUMN_FONT_DECL = "columnFont";
|
|
91
|
+
|
|
92
|
+
export const WORD_NAMES: ReadonlySet<string> = new Set(
|
|
93
|
+
Object.keys(script).filter(
|
|
94
|
+
(k) => typeof (script as Record<string, unknown>)[k] === "function",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
export const DEFAULT_STATE_ROOT = "state";
|
|
99
|
+
|
|
100
|
+
export function toPosix(p: string): string {
|
|
101
|
+
return p.replace(/\\/g, "/");
|
|
102
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { Binding, ConstValue, DiagBag } from "./core.ts";
|
|
3
|
+
|
|
4
|
+
function foldNumeric(
|
|
5
|
+
op: string,
|
|
6
|
+
left: number,
|
|
7
|
+
right: number,
|
|
8
|
+
): number | undefined {
|
|
9
|
+
switch (op) {
|
|
10
|
+
case "+":
|
|
11
|
+
return left + right;
|
|
12
|
+
case "-":
|
|
13
|
+
return left - right;
|
|
14
|
+
case "*":
|
|
15
|
+
return left * right;
|
|
16
|
+
case "/":
|
|
17
|
+
return right === 0 ? undefined : left / right;
|
|
18
|
+
case "%":
|
|
19
|
+
return right === 0 ? undefined : left % right;
|
|
20
|
+
default:
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function foldConst(
|
|
26
|
+
expr: ts.Expression,
|
|
27
|
+
sf: ts.SourceFile,
|
|
28
|
+
diags: DiagBag,
|
|
29
|
+
file: string,
|
|
30
|
+
lookup: (name: string) => Binding | undefined,
|
|
31
|
+
): ConstValue | undefined {
|
|
32
|
+
if (ts.isNumericLiteral(expr)) return Number(expr.text);
|
|
33
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
|
|
34
|
+
return expr.text;
|
|
35
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
36
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
37
|
+
if (expr.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
38
|
+
if (ts.isParenthesizedExpression(expr)) {
|
|
39
|
+
return foldConst(expr.expression, sf, diags, file, lookup);
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
ts.isPrefixUnaryExpression(expr) &&
|
|
43
|
+
expr.operator === ts.SyntaxKind.MinusToken
|
|
44
|
+
) {
|
|
45
|
+
const v = foldConst(expr.operand, sf, diags, file, lookup);
|
|
46
|
+
return typeof v === "number" ? -v : undefined;
|
|
47
|
+
}
|
|
48
|
+
if (ts.isTemplateExpression(expr)) {
|
|
49
|
+
const parts: string[] = [expr.head.text];
|
|
50
|
+
for (const span of expr.templateSpans) {
|
|
51
|
+
const v = foldConst(span.expression, sf, diags, file, lookup);
|
|
52
|
+
if (v === undefined) return undefined;
|
|
53
|
+
parts.push(String(v), span.literal.text);
|
|
54
|
+
}
|
|
55
|
+
return parts.join("");
|
|
56
|
+
}
|
|
57
|
+
if (ts.isBinaryExpression(expr)) {
|
|
58
|
+
const op = expr.operatorToken.getText(sf);
|
|
59
|
+
const left = foldConst(expr.left, sf, diags, file, lookup);
|
|
60
|
+
const right = foldConst(expr.right, sf, diags, file, lookup);
|
|
61
|
+
if (left === undefined || right === undefined) return undefined;
|
|
62
|
+
if (typeof left === "string" && typeof right === "string" && op === "+")
|
|
63
|
+
return left + right;
|
|
64
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
65
|
+
const n = foldNumeric(op, left, right);
|
|
66
|
+
return n === undefined || Number.isFinite(n) ? n : undefined;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isConditionalExpression(expr)) {
|
|
71
|
+
const cond = foldConst(expr.condition, sf, diags, file, lookup);
|
|
72
|
+
if (cond === undefined) return undefined;
|
|
73
|
+
return cond
|
|
74
|
+
? foldConst(expr.whenTrue, sf, diags, file, lookup)
|
|
75
|
+
: foldConst(expr.whenFalse, sf, diags, file, lookup);
|
|
76
|
+
}
|
|
77
|
+
if (ts.isIdentifier(expr)) {
|
|
78
|
+
const b = lookup(expr.text);
|
|
79
|
+
return b?.kind === "const" ? b.value : undefined;
|
|
80
|
+
}
|
|
81
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
82
|
+
const b = lookup(expr.expression.text);
|
|
83
|
+
if (b?.kind === "enum") return b.members.get(expr.name.text);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function scanEnum(
|
|
90
|
+
node: ts.EnumDeclaration,
|
|
91
|
+
sf: ts.SourceFile,
|
|
92
|
+
diags: DiagBag,
|
|
93
|
+
file: string,
|
|
94
|
+
): Map<string, ConstValue> | null {
|
|
95
|
+
const members = new Map<string, ConstValue>();
|
|
96
|
+
let next = 0;
|
|
97
|
+
for (const m of node.members) {
|
|
98
|
+
const memberName = m.name.getText(sf);
|
|
99
|
+
if (m.initializer === undefined) {
|
|
100
|
+
members.set(memberName, next);
|
|
101
|
+
next += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const value = foldConst(m.initializer, sf, diags, file, (n) => {
|
|
105
|
+
const v = members.get(n);
|
|
106
|
+
return v === undefined ? undefined : { kind: "const", value: v };
|
|
107
|
+
});
|
|
108
|
+
if (value === undefined) {
|
|
109
|
+
diags.add(
|
|
110
|
+
file,
|
|
111
|
+
sf,
|
|
112
|
+
m,
|
|
113
|
+
"枚举成员初始化仅支持字符串、数字字面量或已声明的枚举成员",
|
|
114
|
+
);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value === "number") next = value + 1;
|
|
118
|
+
members.set(memberName, value);
|
|
119
|
+
}
|
|
120
|
+
return members;
|
|
121
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图谱族出口:读源(`scan/`)与建图(`core.ts` / `build.ts` / `fold.ts` / `state-root.ts`)两段。
|
|
3
|
+
*
|
|
4
|
+
* 轴 = 编译流水线的第一段——先由 `scan/` 把模块读成记录,再由 `build.ts` 连成图,
|
|
5
|
+
* `fold.ts` 求常值、`state-root.ts` 定状态根;`core.ts` 只放共享形状与诊断袋。
|
|
6
|
+
*/
|
|
7
|
+
export type {
|
|
8
|
+
Binding,
|
|
9
|
+
CompileDiag,
|
|
10
|
+
ConstValue,
|
|
11
|
+
Graph,
|
|
12
|
+
ModuleRecord,
|
|
13
|
+
StoryConfig,
|
|
14
|
+
} from "./core.ts";
|
|
15
|
+
export { DiagBag, toPosix } from "./core.ts";
|
|
16
|
+
export { foldConst } from "./fold.ts";
|
|
17
|
+
export { buildGraph, resolveStart } from "./build.ts";
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { foldConst } from "../fold.ts";
|
|
3
|
+
import { WORD_NAMES } from "../core.ts";
|
|
4
|
+
import type { Binding, ConstValue, ModuleRecord, DiagBag } from "../core.ts";
|
|
5
|
+
import {
|
|
6
|
+
defaultStoriesBuildTexts,
|
|
7
|
+
type StoriesBuildTexts,
|
|
8
|
+
} from "../../../../shared/index.ts";
|
|
9
|
+
|
|
10
|
+
export function scanConstStatement(
|
|
11
|
+
stmt: ts.VariableStatement,
|
|
12
|
+
record: ModuleRecord,
|
|
13
|
+
diags: DiagBag,
|
|
14
|
+
texts: StoriesBuildTexts = defaultStoriesBuildTexts,
|
|
15
|
+
): void {
|
|
16
|
+
const { sf, rel } = record;
|
|
17
|
+
const declared =
|
|
18
|
+
stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) ??
|
|
19
|
+
false;
|
|
20
|
+
if (declared) {
|
|
21
|
+
const decl0 = stmt.declarationList.declarations[0];
|
|
22
|
+
if (decl0 !== undefined && ts.isIdentifier(decl0.name)) {
|
|
23
|
+
const exported0 =
|
|
24
|
+
stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ??
|
|
25
|
+
false;
|
|
26
|
+
record.bindings.set(decl0.name.text, {
|
|
27
|
+
kind: "declare",
|
|
28
|
+
name: decl0.name.text,
|
|
29
|
+
});
|
|
30
|
+
if (exported0)
|
|
31
|
+
record.exports.set(decl0.name.text, {
|
|
32
|
+
kind: "declare",
|
|
33
|
+
name: decl0.name.text,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const flags = ts.getCombinedNodeFlags(stmt.declarationList);
|
|
39
|
+
if ((flags & ts.NodeFlags.Const) === 0) {
|
|
40
|
+
diags.add(
|
|
41
|
+
rel,
|
|
42
|
+
sf,
|
|
43
|
+
stmt,
|
|
44
|
+
"模块层只允许 const 声明(let/var 请移入函数体,运行期为局部变量)",
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (stmt.declarationList.declarations.length !== 1) {
|
|
49
|
+
diags.add(rel, sf, stmt, "模块层 const 每条语句只允许一个声明");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const decl = stmt.declarationList.declarations[0];
|
|
53
|
+
if (decl.initializer === undefined) {
|
|
54
|
+
diags.add(rel, sf, decl, "模块层 const 必须带初始化");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const exported =
|
|
58
|
+
stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ??
|
|
59
|
+
false;
|
|
60
|
+
const bind = (name: string, binding: Binding) => {
|
|
61
|
+
record.bindings.set(name, binding);
|
|
62
|
+
if (exported) record.exports.set(name, binding);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
66
|
+
const initIdent = ts.isIdentifier(decl.initializer)
|
|
67
|
+
? record.bindings.get(decl.initializer.text)
|
|
68
|
+
: undefined;
|
|
69
|
+
if (initIdent?.kind !== "namespace") {
|
|
70
|
+
diags.add(
|
|
71
|
+
rel,
|
|
72
|
+
sf,
|
|
73
|
+
decl.initializer,
|
|
74
|
+
"对象解构仅支持词汇层 script 命名空间:const { say, … } = script",
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const el of decl.name.elements) {
|
|
79
|
+
if (
|
|
80
|
+
!ts.isIdentifier(el.name) ||
|
|
81
|
+
el.propertyName !== undefined ||
|
|
82
|
+
el.initializer !== undefined ||
|
|
83
|
+
el.dotDotDotToken !== undefined
|
|
84
|
+
) {
|
|
85
|
+
diags.add(
|
|
86
|
+
rel,
|
|
87
|
+
sf,
|
|
88
|
+
el,
|
|
89
|
+
"词汇层解构只支持 { a, b } 形式(不支持重命名/默认值/剩余)",
|
|
90
|
+
);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const name = el.name.text;
|
|
94
|
+
if (!WORD_NAMES.has(name)) {
|
|
95
|
+
diags.add(
|
|
96
|
+
rel,
|
|
97
|
+
sf,
|
|
98
|
+
el,
|
|
99
|
+
`词汇层不存在词 \`${name}\`(或该名称不是词函数)`,
|
|
100
|
+
);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
bind(name, { kind: "word", name });
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!ts.isIdentifier(decl.name)) {
|
|
108
|
+
diags.add(rel, sf, decl, "模块层 const 不支持数组解构/绑定模式");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const name = decl.name.text;
|
|
112
|
+
const init = decl.initializer;
|
|
113
|
+
|
|
114
|
+
if (ts.isCallExpression(init) && calleeWordName(init, record) === "cell") {
|
|
115
|
+
if (init.arguments.length !== 2 || !ts.isStringLiteral(init.arguments[0])) {
|
|
116
|
+
diags.add(rel, sf, init, "cell(name, impl) 第一实参必须是字符串字面量");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
bind(name, { kind: "cell", guard: init.arguments[0].text });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
ts.isCallExpression(init) &&
|
|
124
|
+
calleeWordName(init, record) === "defineVars"
|
|
125
|
+
) {
|
|
126
|
+
// 换道守卫(config 化):变量键声明统一走 config.vars(config.ts 求值通道)。
|
|
127
|
+
// 图谱层不再收集 defineVars——保留 opaque 绑定只是防级联误报,键不进任何注册表。
|
|
128
|
+
diags.add(
|
|
129
|
+
rel,
|
|
130
|
+
sf,
|
|
131
|
+
init,
|
|
132
|
+
"defineVars 已迁入 config.ts(config.vars)——图谱模块里不再支持 defineVars 调用,请把变量键声明移入 Stories.src/config.ts",
|
|
133
|
+
);
|
|
134
|
+
bind(name, { kind: "opaque" });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const initIdentBinding = ts.isIdentifier(init)
|
|
138
|
+
? record.bindings.get(init.text)
|
|
139
|
+
: undefined;
|
|
140
|
+
if (initIdentBinding !== undefined) {
|
|
141
|
+
if (initIdentBinding.kind === "namespace") {
|
|
142
|
+
bind(name, { kind: "opaque" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (initIdentBinding.kind === "const") {
|
|
146
|
+
bind(name, { kind: "const", value: initIdentBinding.value });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (ts.isPropertyAccessExpression(init) && ts.isIdentifier(init.expression)) {
|
|
151
|
+
const target = record.bindings.get(init.expression.text);
|
|
152
|
+
if (target?.kind === "namespace") {
|
|
153
|
+
bind(name, { kind: "opaque" });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// —— 编译期求值喂入:导出名命中求值结果 ⇒ 直接绑定标量常量,跳过静态
|
|
158
|
+
// 折叠。结构语义分支(cell/defineVars/词解构/namespace)优先于此:同一份代码无论
|
|
159
|
+
// 是否经求值,结构绑定的解释保持一致 —— 求值只接管「常量值」这一条通道。
|
|
160
|
+
const evaluated = record.evaluated?.[name];
|
|
161
|
+
if (evaluated !== undefined) {
|
|
162
|
+
if (!isConstScalar(evaluated)) {
|
|
163
|
+
diags.add(
|
|
164
|
+
rel,
|
|
165
|
+
sf,
|
|
166
|
+
init,
|
|
167
|
+
`编译期求值导出 \`${name}\` 不是标量(string|number|boolean|null)——对象/数组等复合值不可作为常量绑定内联(编译期求值规则)`,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
bind(name, { kind: "const", value: evaluated });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const value = foldConst(init, sf, diags, rel, (n) => record.bindings.get(n));
|
|
175
|
+
if (value === undefined) {
|
|
176
|
+
if (referencesNpmName(init, record.npmNames)) {
|
|
177
|
+
diags.add(rel, sf, init, texts.constNpmReference());
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
diags.add(
|
|
181
|
+
rel,
|
|
182
|
+
sf,
|
|
183
|
+
init,
|
|
184
|
+
"模块层 const 必须内联为可折叠标量(字面量/常量运算/字符串拼接/三元/枚举成员/其他 const)",
|
|
185
|
+
);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
bind(name, { kind: "const", value });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isConstScalar(value: unknown): value is ConstValue {
|
|
192
|
+
return (
|
|
193
|
+
typeof value === "string" ||
|
|
194
|
+
typeof value === "number" ||
|
|
195
|
+
typeof value === "boolean" ||
|
|
196
|
+
value === null
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 取调用表达式指向的**词名**——`cell(...)` 与 `script.cell(...)` 在这里归一到同一个
|
|
202
|
+
* `"cell"`。声明词是绑定表里的普通词(`script` 是命名空间绑定、`cell` 是词绑定),
|
|
203
|
+
* 两条通道都得认:只认裸标识符会让命名空间写法掉进"模块层 const 必须内联为可折叠
|
|
204
|
+
* 标量"的兜底文案,离真正的原因(这是构建期声明)差着十万八千里。
|
|
205
|
+
*
|
|
206
|
+
* 返回 undefined 表示这个 callee 不指向任何已知词(普通函数调用、npm 值调用等),
|
|
207
|
+
* 交给调用点的其他分支处理。
|
|
208
|
+
*/
|
|
209
|
+
function calleeWordName(
|
|
210
|
+
call: ts.CallExpression,
|
|
211
|
+
record: ModuleRecord,
|
|
212
|
+
): string | undefined {
|
|
213
|
+
const callee = call.expression;
|
|
214
|
+
if (ts.isIdentifier(callee)) {
|
|
215
|
+
const binding = record.bindings.get(callee.text);
|
|
216
|
+
if (binding?.kind === "word") return binding.name;
|
|
217
|
+
// `cell` 允许不经绑定直接出现(导入通道未记录时旧行为也认字面名),与 word 通道等价。
|
|
218
|
+
return callee.text === "cell" ? "cell" : undefined;
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
222
|
+
ts.isIdentifier(callee.expression)
|
|
223
|
+
) {
|
|
224
|
+
const target = record.bindings.get(callee.expression.text);
|
|
225
|
+
if (target?.kind === "namespace") return callee.name.text;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function referencesNpmName(
|
|
231
|
+
node: ts.Node,
|
|
232
|
+
npmNames: ReadonlySet<string>,
|
|
233
|
+
): boolean {
|
|
234
|
+
if (npmNames.size === 0) return false;
|
|
235
|
+
let found = false;
|
|
236
|
+
const visit = (n: ts.Node): void => {
|
|
237
|
+
if (found || ts.isTypeNode(n)) return;
|
|
238
|
+
if (ts.isIdentifier(n) && npmNames.has(n.text)) {
|
|
239
|
+
found = true;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
ts.forEachChild(n, visit);
|
|
243
|
+
};
|
|
244
|
+
visit(node);
|
|
245
|
+
return found;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* node_modules 值导入的引用域仅限模块层 const 初始化链。
|
|
250
|
+
* 豁免:导入声明本身、模块层 const/let/var 语句(初始化链语境;非 const 由
|
|
251
|
+
* scanConstStatement 自报)、enum(scanEnum 自校验)与类型位置(与 import type
|
|
252
|
+
* 同语义)。其余引用(含函数体/default 导出体)一律报错。仅对被求值文件生效:
|
|
253
|
+
* 未求值文件的 npm 导入已在 resolveImports 处按原文案拒绝。
|
|
254
|
+
*/
|
|
255
|
+
export function assertNpmNamesConstOnly(
|
|
256
|
+
record: ModuleRecord,
|
|
257
|
+
diags: DiagBag,
|
|
258
|
+
texts: StoriesBuildTexts = defaultStoriesBuildTexts,
|
|
259
|
+
): void {
|
|
260
|
+
if (record.npmNames.size === 0 || record.evaluated === undefined) return;
|
|
261
|
+
const { sf, rel } = record;
|
|
262
|
+
const visit = (node: ts.Node): void => {
|
|
263
|
+
if (
|
|
264
|
+
ts.isImportDeclaration(node) ||
|
|
265
|
+
ts.isVariableStatement(node) ||
|
|
266
|
+
ts.isEnumDeclaration(node)
|
|
267
|
+
)
|
|
268
|
+
return;
|
|
269
|
+
if (ts.isTypeNode(node)) return;
|
|
270
|
+
if (ts.isIdentifier(node) && record.npmNames.has(node.text)) {
|
|
271
|
+
diags.add(rel, sf, node, texts.npmNameConstOnly(node.text));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
ts.forEachChild(node, visit);
|
|
275
|
+
};
|
|
276
|
+
for (const stmt of sf.statements) visit(stmt);
|
|
277
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { WORD_NAMES, GEN_PREFIX, toPosix } from "../core.ts";
|
|
3
|
+
import type { ModuleRecord, DiagBag } from "../core.ts";
|
|
4
|
+
|
|
5
|
+
function resolveModuleFile(rel: string, spec: string): string | null {
|
|
6
|
+
const dirParts = toPosix(rel).split("/").slice(0, -1);
|
|
7
|
+
const specParts = toPosix(spec).split("/");
|
|
8
|
+
const out: string[] = [];
|
|
9
|
+
for (const part of [...dirParts, ...specParts]) {
|
|
10
|
+
if (part === "..") {
|
|
11
|
+
if (out.length === 0) return null;
|
|
12
|
+
out.pop();
|
|
13
|
+
} else if (part !== "." && part !== "") {
|
|
14
|
+
out.push(part);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const base = out.join("/");
|
|
18
|
+
if (base === "" || !base.endsWith(".ts")) return null;
|
|
19
|
+
return base;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveModuleImportPath(rel: string, spec: string): string | null {
|
|
23
|
+
const candidates: string[] = [];
|
|
24
|
+
const normalized = toPosix(spec);
|
|
25
|
+
if (normalized.endsWith(".ts")) {
|
|
26
|
+
candidates.push(normalized);
|
|
27
|
+
} else {
|
|
28
|
+
candidates.push(`${normalized}.ts`, `${normalized}/index.ts`);
|
|
29
|
+
}
|
|
30
|
+
for (const c of candidates) {
|
|
31
|
+
const resolved = resolveModuleFile(rel, c);
|
|
32
|
+
if (resolved !== null) return resolved;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveImports(
|
|
38
|
+
record: ModuleRecord,
|
|
39
|
+
records: Map<string, ModuleRecord>,
|
|
40
|
+
diags: DiagBag,
|
|
41
|
+
editorPass: boolean,
|
|
42
|
+
): void {
|
|
43
|
+
const { sf, rel } = record;
|
|
44
|
+
for (const imp of record.imports) {
|
|
45
|
+
const spec = ts.isStringLiteral(imp.moduleSpecifier)
|
|
46
|
+
? imp.moduleSpecifier.text
|
|
47
|
+
: "";
|
|
48
|
+
if (editorPass !== (spec === "@lingfan-engine/editor")) continue;
|
|
49
|
+
const clause = imp.importClause;
|
|
50
|
+
if (clause === undefined) {
|
|
51
|
+
diags.add(rel, sf, imp, "副作用导入不支持:请显式导入所需符号");
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (spec === "@lingfan-engine/editor") {
|
|
55
|
+
if (clause.name !== undefined) {
|
|
56
|
+
diags.add(
|
|
57
|
+
rel,
|
|
58
|
+
sf,
|
|
59
|
+
clause.name,
|
|
60
|
+
'词汇层请以具名导入(import { say, … } from "@lingfan-engine/editor")或 script 命名空间使用',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
clause.namedBindings !== undefined &&
|
|
65
|
+
ts.isNamespaceImport(clause.namedBindings)
|
|
66
|
+
) {
|
|
67
|
+
record.bindings.set(clause.namedBindings.name.text, {
|
|
68
|
+
kind: "namespace",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
clause.namedBindings !== undefined &&
|
|
73
|
+
ts.isNamedImports(clause.namedBindings)
|
|
74
|
+
) {
|
|
75
|
+
for (const el of clause.namedBindings.elements) {
|
|
76
|
+
if (el.isTypeOnly) continue;
|
|
77
|
+
// `import { script as ct }` 里 ct 是本地名、script 才是被导入的名字。判定与查表
|
|
78
|
+
// 一律用被导入名(propertyName 优先),绑定表的键才用本地名——两者混用会让
|
|
79
|
+
// `import { say as 说 }` 去查一个叫「说」的词(必然不存在)。
|
|
80
|
+
const imported = el.propertyName?.text ?? el.name.text;
|
|
81
|
+
if (imported === "script") {
|
|
82
|
+
record.bindings.set(el.name.text, { kind: "namespace" });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!WORD_NAMES.has(imported)) {
|
|
86
|
+
diags.add(rel, sf, el, `词汇层不存在词 \`${imported}\``);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
record.bindings.set(el.name.text, { kind: "word", name: imported });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (clause.isTypeOnly) continue;
|
|
95
|
+
if (spec === "@lingfan-engine/core") {
|
|
96
|
+
diags.add(rel, sf, imp, "引擎包不参与创作导入:请使用词汇层词函数");
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!spec.startsWith(".")) {
|
|
100
|
+
if (record.evaluated === undefined) {
|
|
101
|
+
diags.add(
|
|
102
|
+
rel,
|
|
103
|
+
sf,
|
|
104
|
+
imp,
|
|
105
|
+
`导入仅限相对路径与词汇层既有出口,不支持外部包 \`${spec}\``,
|
|
106
|
+
);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// 经编译期求值的文件允许 node_modules 值导入——值经求值结果内联,
|
|
110
|
+
// 引用域仅限模块层 const 初始化链(buildGraph 末尾 assertNpmNamesConstOnly 兜底)。
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const targetRel = resolveModuleImportPath(rel, spec);
|
|
114
|
+
if (targetRel === null) {
|
|
115
|
+
diags.add(rel, sf, imp, `模块说明符 \`${spec}\` 必须解析到相对 .ts 文件`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (targetRel.startsWith(GEN_PREFIX)) {
|
|
119
|
+
diags.add(rel, sf, imp, "gen/ 生成物不参与创作导入");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const target = records.get(targetRel);
|
|
123
|
+
if (target === undefined) {
|
|
124
|
+
diags.add(rel, sf, imp, `找不到模块:${spec}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (clause.name !== undefined) {
|
|
128
|
+
if (target.story === null) {
|
|
129
|
+
diags.add(
|
|
130
|
+
rel,
|
|
131
|
+
sf,
|
|
132
|
+
clause.name,
|
|
133
|
+
`纯模块 ${targetRel} 没有 default 导出——跨故事引用请导入故事文件`,
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
record.bindings.set(clause.name.text, {
|
|
137
|
+
kind: "story",
|
|
138
|
+
file: targetRel,
|
|
139
|
+
name: target.story.name,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
clause.namedBindings !== undefined &&
|
|
145
|
+
ts.isNamespaceImport(clause.namedBindings)
|
|
146
|
+
) {
|
|
147
|
+
diags.add(
|
|
148
|
+
rel,
|
|
149
|
+
sf,
|
|
150
|
+
clause.namedBindings,
|
|
151
|
+
"命名空间导入仅支持词汇层 script",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
clause.namedBindings !== undefined &&
|
|
156
|
+
ts.isNamedImports(clause.namedBindings)
|
|
157
|
+
) {
|
|
158
|
+
for (const el of clause.namedBindings.elements) {
|
|
159
|
+
if (el.isTypeOnly) continue;
|
|
160
|
+
const wanted = el.propertyName?.text ?? el.name.text;
|
|
161
|
+
const binding = target.exports.get(wanted);
|
|
162
|
+
if (binding === undefined) {
|
|
163
|
+
diags.add(rel, sf, el, `模块 ${targetRel} 未导出 \`${wanted}\``);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
record.bindings.set(el.name.text, binding);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块扫描族出口:读 TS 源、解析 import、收集常量与绑定声明。
|
|
3
|
+
*
|
|
4
|
+
* 轴 = 读源三部——扫描声明(`module-scan.ts`)、解析 import(`imports.ts`)、
|
|
5
|
+
* 收集常量与绑定(`bindings.ts`)。这一族只产出记录,不建图。
|
|
6
|
+
*/
|
|
7
|
+
export { assertNpmNamesConstOnly, scanConstStatement } from "./bindings.ts";
|
|
8
|
+
export { resolveImports } from "./imports.ts";
|
|
9
|
+
export { scanModule } from "./module-scan.ts";
|