@lingfan-engine/stories-build 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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,108 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { checkFreeIdentifiers, collectFileImports, lineOf } from "./imports.ts";
|
|
3
|
+
import type { CellIssue, FileImports } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/** 沿成员/下标链下钻取最左标识符名(`cc.getInfo` → "cc") */
|
|
6
|
+
export function rootIdentifier(node: ts.Node): string | undefined {
|
|
7
|
+
let cursor: ts.Node = node;
|
|
8
|
+
while (
|
|
9
|
+
ts.isPropertyAccessExpression(cursor) ||
|
|
10
|
+
ts.isElementAccessExpression(cursor)
|
|
11
|
+
) {
|
|
12
|
+
cursor = cursor.expression;
|
|
13
|
+
}
|
|
14
|
+
return ts.isIdentifier(cursor) ? cursor.text : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 单文件扫描:收集 `cell(...)` 调用(含校验),fail 项进 issues 不中断其它文件。
|
|
19
|
+
* 实现三形态之外的参数形态按各自文案 fail-closed——判定条件与文案逐字固定。
|
|
20
|
+
*/
|
|
21
|
+
export function scanFile(
|
|
22
|
+
relPath: string,
|
|
23
|
+
text: string,
|
|
24
|
+
globalWhitelist: ReadonlySet<string>,
|
|
25
|
+
issues: CellIssue[],
|
|
26
|
+
): Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
implText: string;
|
|
29
|
+
fingerprint: string;
|
|
30
|
+
imports: FileImports;
|
|
31
|
+
}> {
|
|
32
|
+
const sf = ts.createSourceFile(relPath, text, ts.ScriptTarget.Latest, true);
|
|
33
|
+
const fromDir = relPath.includes("/")
|
|
34
|
+
? relPath.slice(0, relPath.lastIndexOf("/"))
|
|
35
|
+
: "";
|
|
36
|
+
const fileImports = collectFileImports(sf, text, fromDir, issues, relPath);
|
|
37
|
+
const found: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
implText: string;
|
|
40
|
+
fingerprint: string;
|
|
41
|
+
imports: FileImports;
|
|
42
|
+
}> = [];
|
|
43
|
+
const origin = (node: ts.Node): string => `${relPath}:${lineOf(sf, node)}`;
|
|
44
|
+
const visit = (node: ts.Node): void => {
|
|
45
|
+
if (
|
|
46
|
+
ts.isCallExpression(node) &&
|
|
47
|
+
ts.isIdentifier(node.expression) &&
|
|
48
|
+
node.expression.text === "cell"
|
|
49
|
+
) {
|
|
50
|
+
const [nameArg, implArg] = node.arguments;
|
|
51
|
+
if (nameArg === undefined || !ts.isStringLiteralLike(nameArg)) {
|
|
52
|
+
issues.push({
|
|
53
|
+
message:
|
|
54
|
+
'cell 第一个参数必须是字符串字面量名(如 cell("gold-non-negative", …))',
|
|
55
|
+
origin: origin(node),
|
|
56
|
+
});
|
|
57
|
+
} else if (implArg === undefined) {
|
|
58
|
+
issues.push({
|
|
59
|
+
message: "cell 缺第二个参数(实现)",
|
|
60
|
+
origin: origin(node),
|
|
61
|
+
});
|
|
62
|
+
} else if (ts.isCallExpression(implArg)) {
|
|
63
|
+
issues.push({
|
|
64
|
+
message:
|
|
65
|
+
"cell 实现不支持调用表达式(参数固化属 L2)——改为函数字面量或引用导入的函数",
|
|
66
|
+
origin: origin(implArg),
|
|
67
|
+
});
|
|
68
|
+
} else if (
|
|
69
|
+
ts.isArrowFunction(implArg) ||
|
|
70
|
+
ts.isFunctionExpression(implArg)
|
|
71
|
+
) {
|
|
72
|
+
const implText = text.slice(implArg.getStart(sf), implArg.getEnd());
|
|
73
|
+
checkFreeIdentifiers(
|
|
74
|
+
implArg,
|
|
75
|
+
origin(implArg),
|
|
76
|
+
fileImports.bindingNames,
|
|
77
|
+
globalWhitelist,
|
|
78
|
+
issues,
|
|
79
|
+
);
|
|
80
|
+
found.push({
|
|
81
|
+
name: nameArg.text,
|
|
82
|
+
implText,
|
|
83
|
+
fingerprint: implText.trim(),
|
|
84
|
+
imports: fileImports,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
const root = rootIdentifier(implArg);
|
|
88
|
+
if (root === undefined || !fileImports.bindingNames.has(root)) {
|
|
89
|
+
issues.push({
|
|
90
|
+
message: `cell 引用形态的根标识符必须是本文件导入的绑定${root === undefined ? "" : `(收到 "${root}",非导入绑定——本地绑定不搬运)`}`,
|
|
91
|
+
origin: origin(implArg),
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
const implText = text.slice(implArg.getStart(sf), implArg.getEnd());
|
|
95
|
+
found.push({
|
|
96
|
+
name: nameArg.text,
|
|
97
|
+
implText,
|
|
98
|
+
fingerprint: implText.trim(),
|
|
99
|
+
imports: fileImports,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
node.forEachChild(visit);
|
|
105
|
+
};
|
|
106
|
+
visit(sf);
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** 提取产出(书写序;已按(注册面, 名字)+ 实现文本指纹去重) */
|
|
2
|
+
export interface CellScan {
|
|
3
|
+
guards: ReadonlyArray<{ readonly name: string; readonly implText: string }>;
|
|
4
|
+
/** 生成物头部的 import 语句(说明符已按 gen/ 位置重写;跨文件去重,保序) */
|
|
5
|
+
importStatements: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 单条问题(origin = "相对路径:行") */
|
|
9
|
+
export interface CellIssue {
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly origin: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** cell 所在文件的 import 上下文:绑定名 + 重写后的语句文本 */
|
|
15
|
+
export interface FileImports {
|
|
16
|
+
readonly bindingNames: ReadonlySet<string>;
|
|
17
|
+
readonly statements: ReadonlyArray<{
|
|
18
|
+
readonly adjustedText: string;
|
|
19
|
+
readonly bindings: readonly string[];
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `scanCells` 的可配置项(整项缺省 = 内建默认值,调用方只给要覆盖的那一项) */
|
|
24
|
+
export interface ScanCellsOptions {
|
|
25
|
+
/** 允许在内联实现里直接引用的全局名(缺省 = 标准全局白名单 16 项) */
|
|
26
|
+
readonly globalWhitelist?: ReadonlySet<string>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 标准全局白名单(字面量内允许直接引用;刻意最小——需要更多就走 import)。
|
|
3
|
+
* 16 项逐字固定;调用方要放开别的名字,经 `scanCells` 的 `globalWhitelist` 注入。
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_GLOBAL_WHITELIST: ReadonlySet<string> = new Set([
|
|
6
|
+
"JSON",
|
|
7
|
+
"Math",
|
|
8
|
+
"String",
|
|
9
|
+
"Number",
|
|
10
|
+
"Boolean",
|
|
11
|
+
"Object",
|
|
12
|
+
"Array",
|
|
13
|
+
"Date",
|
|
14
|
+
"isNaN",
|
|
15
|
+
"isFinite",
|
|
16
|
+
"parseInt",
|
|
17
|
+
"parseFloat",
|
|
18
|
+
"Infinity",
|
|
19
|
+
"NaN",
|
|
20
|
+
"undefined",
|
|
21
|
+
"console",
|
|
22
|
+
]);
|
package/src/cli/cli.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS 故事源编译 CLI(`pnpm stories:build`,tsx 运行;创作期工具,Node 侧):
|
|
3
|
+
* `Stories.src/config.ts`(布局声明:folders 多目录 + start 入口故事,全局恰一)→
|
|
4
|
+
* 源发现(default 具名故事函数 = 故事文件,仅具名导出 = 模块,覆盖外 default = 报错)→
|
|
5
|
+
* story-compiler(`compileStories` 契约,诊断非空 fail-closed)→ `serializeProject`
|
|
6
|
+
* (单列拆分 + 清单托管键更新——与编辑器写回同一条布局规则;`config.start` →
|
|
7
|
+
* `project.json` entry)→ `assembleProject` 往返自检 → 差量写盘
|
|
8
|
+
* `Resources/Stories/**` + `project.json`(陈旧列文件清理;产物路径 = 源文件路径镜像)。
|
|
9
|
+
*
|
|
10
|
+
* 本文件是**入口与呈现**:流水线在 `./index`(唯一出口),这里只做三件事——解析可选的
|
|
11
|
+
* 位置参数工程根、跑一次编译、把 `BuildReport` 打印成人读的几段(完成行 / 零差量 /
|
|
12
|
+
* 新增 / 更新 / 清理陈旧 / 词汇层提示)。失败一律打到 stderr 并返回退出码 1,便于脚本
|
|
13
|
+
* 链中断。
|
|
14
|
+
*
|
|
15
|
+
* 两种触发方式共用 `runBuildCli`:仓内 `pnpm stories:build` 经底部主模块守卫直接跑本文件
|
|
16
|
+
* (tsx 解释 TS);装了本包的工程经 `bin` 里的 `.mjs` 垫片跑——node_modules 内的 TS 不能靠
|
|
17
|
+
* Node 类型擦除加载,垫片用 tsx 的 `tsImport` 装本文件再调同一个 `runBuildCli`,两条路
|
|
18
|
+
* 因此共用一份呈现逻辑。
|
|
19
|
+
*
|
|
20
|
+
* 用法:`<工程根可省略>`;缺省工程根 = 进程工作目录(调用方脚本已 cd 到宿主工程根,
|
|
21
|
+
* 或经 DSH_STORIES_ROOT 指明——包内没有「谁在调我、宿主工程在哪」这件事实)。
|
|
22
|
+
* 测试不经这里——它们直接调 `buildStories`(纯编排,fs 全在参数 root 之下)。
|
|
23
|
+
*/
|
|
24
|
+
import { resolve } from "node:path";
|
|
25
|
+
import { pathToFileURL } from "node:url";
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_SOURCES_DIR,
|
|
28
|
+
StoryBuildError,
|
|
29
|
+
buildStories,
|
|
30
|
+
} from "../index.ts";
|
|
31
|
+
|
|
32
|
+
export { StoryBuildError, buildStories, type BuildReport } from "../index.ts";
|
|
33
|
+
|
|
34
|
+
/** 工程根:位置参数优先,其次 `DSH_STORIES_ROOT`,最后进程工作目录 */
|
|
35
|
+
export function resolveBuildRoot(argv: readonly string[]): string {
|
|
36
|
+
const envRoot = process.env.DSH_STORIES_ROOT;
|
|
37
|
+
return resolve(
|
|
38
|
+
argv[0] ??
|
|
39
|
+
(envRoot !== undefined && envRoot !== "" ? envRoot : process.cwd()),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 跑一次编译并把结果打到标准输出/标准错误,返回进程退出码。
|
|
45
|
+
* 不调用 `process.exit`——谁来决定退出由调用方(主模块守卫或 bin 垫片)负责。
|
|
46
|
+
*/
|
|
47
|
+
export async function runBuildCli(
|
|
48
|
+
argv: readonly string[] = process.argv.slice(2),
|
|
49
|
+
): Promise<number> {
|
|
50
|
+
const root = resolveBuildRoot(argv);
|
|
51
|
+
try {
|
|
52
|
+
const report = await buildStories(root);
|
|
53
|
+
console.log(
|
|
54
|
+
`TS 故事源编译完成:${DEFAULT_SOURCES_DIR}/${report.source} → story "${report.storyId}"(${report.columns} 列)`,
|
|
55
|
+
);
|
|
56
|
+
if (
|
|
57
|
+
report.added.length === 0 &&
|
|
58
|
+
report.updated.length === 0 &&
|
|
59
|
+
report.removed.length === 0
|
|
60
|
+
) {
|
|
61
|
+
console.log("已是最新(零差量)");
|
|
62
|
+
}
|
|
63
|
+
if (report.added.length > 0) {
|
|
64
|
+
console.log(
|
|
65
|
+
`新增(${report.added.length}):\n ${report.added.join("\n ")}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (report.updated.length > 0) {
|
|
69
|
+
console.log(
|
|
70
|
+
`更新(${report.updated.length}):\n ${report.updated.join("\n ")}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (report.removed.length > 0) {
|
|
74
|
+
console.log(
|
|
75
|
+
`清理陈旧(${report.removed.length}):\n ${report.removed.join("\n ")}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (report.warnings.length > 0) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`词汇层类型提示(${report.warnings.length} 条,不拦构建——引擎求值时将 type-error):\n${report.warnings
|
|
81
|
+
.map((w) => ` - ${w.expression}:${w.message}`)
|
|
82
|
+
.join("\n")}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(
|
|
88
|
+
`[stories:build] 失败:${error instanceof StoryBuildError ? error.message : String(error)}`,
|
|
89
|
+
);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// —— 主模块守卫:仅直接执行时跑(tests 直测 buildStories,bin 垫片直调 runBuildCli)——
|
|
95
|
+
// 可选位置参数 = 工程根(默认 = 工作目录,可由 DSH_STORIES_ROOT 覆盖)
|
|
96
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
97
|
+
const code = await runBuildCli();
|
|
98
|
+
if (code !== 0) process.exit(code);
|
|
99
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS 故事源增量监听 CLI(`pnpm stories:watch`,tsx 运行;创作期工具,Node 侧):
|
|
3
|
+
* 常驻进程入口——首轮全量构建,之后监听源目录递归变更,防抖合并后进程内重跑构建。
|
|
4
|
+
*
|
|
5
|
+
* 本文件是入口与呈现:监听实现在 `../pipeline/watch.ts`。失败轮不退出——
|
|
6
|
+
* 创作期半成品是常态,修复后保存即重试。
|
|
7
|
+
*
|
|
8
|
+
* 与 `./cli.ts` 同一套两路触发:仓内 `pnpm stories:watch` 经主模块守卫直接跑本文件;
|
|
9
|
+
* 装了本包的工程经 `bin` 里 `.mjs` 垫片用 tsx 装载本文件后调 `startWatchCli`。
|
|
10
|
+
*
|
|
11
|
+
* 用法:`<工程根可省略>`;缺省工程根 = 进程工作目录(或 DSH_STORIES_ROOT)。
|
|
12
|
+
*/
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
import { watchStories } from "../pipeline/watch.ts";
|
|
16
|
+
|
|
17
|
+
/** 工程根:位置参数优先,其次 `DSH_STORIES_ROOT`,最后进程工作目录 */
|
|
18
|
+
export function resolveWatchRoot(argv: readonly string[]): string {
|
|
19
|
+
const envRoot = process.env.DSH_STORIES_ROOT;
|
|
20
|
+
return resolve(
|
|
21
|
+
argv[0] ??
|
|
22
|
+
(envRoot !== undefined && envRoot !== "" ? envRoot : process.cwd()),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 起常驻监听并绑定 SIGINT 优雅退出;返回 `watchStories` 给的停止函数 */
|
|
27
|
+
export async function startWatchCli(
|
|
28
|
+
argv: readonly string[] = process.argv.slice(2),
|
|
29
|
+
): Promise<() => void> {
|
|
30
|
+
const root = resolveWatchRoot(argv);
|
|
31
|
+
const stop = await watchStories(root);
|
|
32
|
+
process.on("SIGINT", () => {
|
|
33
|
+
stop();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
36
|
+
return stop;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// —— 主模块守卫:仅直接执行时常驻(tests 直测 watchStories / affectedStories)——
|
|
40
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
41
|
+
await startWatchCli();
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { scanCells, type CellScan } from "../cell/index.ts";
|
|
2
|
+
import { StoryBuildError } from "../shared/index.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* cell 声明扫描(构建期 AST 扫描):`Stories.src` 树(排除生成物目录,仅 `.ts`)。
|
|
6
|
+
* 「名字在数据、实现在代码、build 做名字闭合」:实现住生成模块,故事 JSON 只留名字。
|
|
7
|
+
* 任一条扫描问题都 fail-closed,一次报全部(每条带 `路径:行` 定位)。
|
|
8
|
+
*/
|
|
9
|
+
export function scanCellSources(
|
|
10
|
+
sourcesTree: ReadonlyMap<string, string>,
|
|
11
|
+
generatedDir: string,
|
|
12
|
+
): CellScan {
|
|
13
|
+
const cellSourceTree = new Map<string, string>();
|
|
14
|
+
for (const [rel, text] of sourcesTree) {
|
|
15
|
+
if (rel.startsWith(`${generatedDir}/`) || !rel.endsWith(".ts")) continue;
|
|
16
|
+
cellSourceTree.set(rel, text);
|
|
17
|
+
}
|
|
18
|
+
const cellScanResult = scanCells(cellSourceTree);
|
|
19
|
+
if (cellScanResult.errors.length > 0) {
|
|
20
|
+
throw new StoryBuildError(
|
|
21
|
+
`cell 声明扫描失败:\n${cellScanResult.errors
|
|
22
|
+
.map((issue) => `- ${issue.origin}:${issue.message}`)
|
|
23
|
+
.join("\n")}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return cellScanResult.scan;
|
|
27
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { StoryBuildError } from "../shared/index.ts";
|
|
2
|
+
import type { StoriesBuildTexts } from "../shared/index.ts";
|
|
3
|
+
import type { StorySourceConfig } from "../config/story-config.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* story-compiler 契约(编译器下游的唯一对接面):
|
|
7
|
+
* `compileStories({ root, sourcesDir, stories, modules })` 一次性编译全部故事文件,
|
|
8
|
+
* 产物 = 列粒度条目(`columnId` + `sourceRelPath` + `story`)+ 诊断集。
|
|
9
|
+
* 诊断非空 = fail-closed(编排层一次报全部);`story` 为 parseStory 同形结构。
|
|
10
|
+
*/
|
|
11
|
+
export interface CompiledStoryEntry {
|
|
12
|
+
/** 该条目对应的列 id(须存在于 `story.columns`) */
|
|
13
|
+
readonly columnId: string;
|
|
14
|
+
/** 源文件相对 `Stories.src/` 的 posix 路径(如 `chapters/prologue.ts`) */
|
|
15
|
+
readonly sourceRelPath: string;
|
|
16
|
+
/** parseStory 同形结构(引擎 Story;条目为列粒度) */
|
|
17
|
+
readonly story: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CompilerDiagnostic {
|
|
21
|
+
readonly file: string;
|
|
22
|
+
readonly line: number;
|
|
23
|
+
readonly col: number;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CompiledStories {
|
|
28
|
+
readonly stories: readonly CompiledStoryEntry[];
|
|
29
|
+
readonly diags: readonly CompilerDiagnostic[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 编译期求值喂入:文件相对 `Stories.src/` 的 posix 路径 → 导出名 →
|
|
34
|
+
* 求值出的 JSON 可序列化值。由编排层求值段(const-eval-pass)填充;缺省 =
|
|
35
|
+
* 没有被求值的文件,编译器行为与未接入求值时逐字一致。
|
|
36
|
+
*/
|
|
37
|
+
export type CompilerConstValues = Readonly<
|
|
38
|
+
Record<string, Readonly<Record<string, unknown>>>
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
export interface StoriesCompiler {
|
|
42
|
+
compileStories(input: {
|
|
43
|
+
readonly root: string;
|
|
44
|
+
readonly sourcesDir: string;
|
|
45
|
+
/** 布局声明(config.ts 求值产物;folders/start 必需)——图谱层只消费,不再解析 config */
|
|
46
|
+
readonly config: StorySourceConfig;
|
|
47
|
+
/** 故事文件清单(相对 `Stories.src/`,posix;default 导出 Story 的 .ts) */
|
|
48
|
+
readonly stories: readonly string[];
|
|
49
|
+
/** 模块文件清单(相对 `Stories.src/`,posix;被 import 的支撑 .ts) */
|
|
50
|
+
readonly modules: readonly string[];
|
|
51
|
+
/** 编译期求值喂入;缺省 = 无被求值文件(行为不变) */
|
|
52
|
+
readonly constValues?: CompilerConstValues;
|
|
53
|
+
/** 字体名注册表(Resources/Fonts 文件名去扩展名);缺省 = 空注册表,引用字体名即 fail-closed */
|
|
54
|
+
readonly fontNames?: ReadonlySet<string>;
|
|
55
|
+
/** 全工程角色表(编排层模块层声明收集产物);缺省 = 空表,无前置角色命令 */
|
|
56
|
+
readonly characters?: ReadonlyMap<
|
|
57
|
+
string,
|
|
58
|
+
import("@lingfan-engine/core").CharacterDef
|
|
59
|
+
>;
|
|
60
|
+
/** 文案注入(Partial);缺省 = 内建缺省文案(即构建层的既有文案) */
|
|
61
|
+
readonly texts?: Partial<StoriesBuildTexts>;
|
|
62
|
+
}): CompiledStories | Promise<CompiledStories>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 默认编译器装载:`./story-compiler.ts`(本域内的编译器实现;文件缺席 = fail-closed 带指引)。
|
|
67
|
+
*
|
|
68
|
+
* 这里写死带扩展名的相对路径,不走 bundler 式的无扩展名解析:包内源码由 tsx/Node 直接跑
|
|
69
|
+
* TypeScript,Node 的 ESM 解析不补 `.ts`,无扩展名会解析失败。
|
|
70
|
+
*/
|
|
71
|
+
export async function loadDefaultCompiler(): Promise<StoriesCompiler> {
|
|
72
|
+
const specifier = "./story-compiler.ts";
|
|
73
|
+
try {
|
|
74
|
+
const mod = (await import(specifier)) as {
|
|
75
|
+
compileStories?: StoriesCompiler["compileStories"];
|
|
76
|
+
};
|
|
77
|
+
if (typeof mod.compileStories !== "function") {
|
|
78
|
+
throw new Error("模块缺 compileStories 导出");
|
|
79
|
+
}
|
|
80
|
+
return { compileStories: mod.compileStories };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
throw new StoryBuildError(
|
|
84
|
+
`story-compiler 未就绪(${specifier} 装载失败:${message})——编排契约已按 compileStories 收敛,等待编译器交付对接`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
loadDeclaredExtensions,
|
|
6
|
+
type ExtensionModuleLoader,
|
|
7
|
+
} from "@lingfan-engine/core";
|
|
8
|
+
import { StoryBuildError } from "../shared/index.ts";
|
|
9
|
+
|
|
10
|
+
/** Node 侧缺省装载器:相对说明符按工程根解析(与 Stories.src/Resources 同级),裸说明符交给 Node */
|
|
11
|
+
export function defaultExtensionLoader(root: string): ExtensionModuleLoader {
|
|
12
|
+
return async (specifier: string) =>
|
|
13
|
+
specifier.startsWith("./") || specifier.startsWith("../")
|
|
14
|
+
? import(relativeSpecifierUrl(root, specifier))
|
|
15
|
+
: import(specifier);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 相对说明符 → 可导入的 file URL:Node 的 ESM 解析不给 .ts 补扩展名(包内源码直接跑 TS),
|
|
20
|
+
* 而清单里的相对说明符沿用 bundler 习惯(`./ext/foo`)。这里先按原样试,文件不存在再补 `.ts`,
|
|
21
|
+
* 让两种写法都能落盘——加载失败仍是 fail-closed,只是不再因写法差异误报。
|
|
22
|
+
*/
|
|
23
|
+
function relativeSpecifierUrl(root: string, specifier: string): string {
|
|
24
|
+
const target = resolve(root, specifier);
|
|
25
|
+
const candidates = [target, `${target}.ts`, `${target}/index.ts`];
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
if (existsSync(candidate)) return pathToFileURL(candidate).href;
|
|
28
|
+
}
|
|
29
|
+
return pathToFileURL(target).href;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 清单 `extensions` 声明读取与形状校验(缺席/为空 = 空数组零装载;形状非法 fail-closed) */
|
|
33
|
+
export function readDeclaredExtensions(manifest: unknown): string[] {
|
|
34
|
+
const declared = (manifest as { extensions?: unknown } | null)?.extensions;
|
|
35
|
+
if (declared === undefined) return [];
|
|
36
|
+
if (!Array.isArray(declared)) {
|
|
37
|
+
throw new StoryBuildError(
|
|
38
|
+
`清单 extensions 必须为数组(扩展模块说明符声明),收到 ${JSON.stringify(declared)}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
for (const specifier of declared) {
|
|
42
|
+
if (typeof specifier !== "string" || specifier === "") {
|
|
43
|
+
throw new StoryBuildError(
|
|
44
|
+
`清单 extensions 条目必须为非空模块说明符,收到 ${JSON.stringify(specifier)}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return declared as string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 声明制装载(与运行期同一契约)+ 该轮装载到的全部 op 名(unknown-op 闸门的判定输入)+ 扩展 id */
|
|
52
|
+
export async function loadExtensions(
|
|
53
|
+
root: string,
|
|
54
|
+
manifest: unknown,
|
|
55
|
+
): Promise<{ declared: string[]; opNames: Set<string>; ids: string[] }> {
|
|
56
|
+
const declared = readDeclaredExtensions(manifest);
|
|
57
|
+
const loaded = await loadDeclaredExtensions(
|
|
58
|
+
declared,
|
|
59
|
+
defaultExtensionLoader(root),
|
|
60
|
+
);
|
|
61
|
+
const opNames = new Set(
|
|
62
|
+
loaded.flatMap((extension) => (extension.ops ?? []).map((def) => def.op)),
|
|
63
|
+
);
|
|
64
|
+
// id 供类型面生成器写 `ext.<id>.${string}` 插值键模式(加载期拿到的就是已解析的 id)
|
|
65
|
+
const ids = loaded.map((extension) => extension.id);
|
|
66
|
+
return { declared, opNames, ids };
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编译器域:把 `Stories.src` 的 TS 源编译成引擎故事对象。
|
|
3
|
+
*
|
|
4
|
+
* 对外只有 `loadDefaultCompiler` 与 `compileStories` 两条——宿主既可以
|
|
5
|
+
* 走默认实现,也可以注入自己的编译器(测试就是这么干的)。外部扩展模块的
|
|
6
|
+
* 装载、cell 声明的扫描也在这域:它们是「读源」的三种不同入口,共用同一套
|
|
7
|
+
* fail-closed 口径。
|
|
8
|
+
*/
|
|
9
|
+
export {
|
|
10
|
+
loadDefaultCompiler,
|
|
11
|
+
type CompiledStories,
|
|
12
|
+
type CompiledStoryEntry,
|
|
13
|
+
type CompilerConstValues,
|
|
14
|
+
type CompilerDiagnostic,
|
|
15
|
+
type StoriesCompiler,
|
|
16
|
+
} from "./compiler-contract.ts";
|
|
17
|
+
export {
|
|
18
|
+
loadExtensions,
|
|
19
|
+
readDeclaredExtensions,
|
|
20
|
+
defaultExtensionLoader,
|
|
21
|
+
} from "./extension-load.ts";
|
|
22
|
+
export { scanCellSources } from "./cell-scan.ts";
|