@deepseek-ai/dsh-typert-generator 0.1.1-rc.2 → 0.1.2-alpha.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/README.i18n.yaml +2 -2
- package/README.md +116 -18
- package/README.zh.md +117 -19
- package/lib/index.js +49 -21
- package/lib/types/analyzer.js +52 -19
- package/lib/types/cordis-catalog.js +1 -1
- package/lib/types/emitter.js +7 -3
- package/lib/types/model.d.ts +1 -0
- package/lib/types/tsdown-plugin.js +6 -2
- package/lib/types/workspace.d.ts +16 -1
- package/lib/types/workspace.js +10 -2
- package/package.json +7 -7
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/typert/generator/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: c6a419467f8f9da71c789dee2b969f69841b57cc
|
|
6
|
+
README.zh.md: 196af16d97c9c6eb2774e615840bfaed2454e7f2
|
package/README.md
CHANGED
|
@@ -1,41 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The build-time Typert generator: source type analysis, compiler-independent models, and artifact emission for maintainers wiring Typert publication or consuming generated artifacts."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-typert-generator
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-typert-generator` turns source TypeScript into compiler-independent data and runnable artifacts at build time: it analyzes a workspace's package type trees, produces a `FaceModel` and type graph, and emits executable JavaScript with supported Zod schemas and a `TYPERT` reflection contribution, plus matching declarations. It is a build-time library, not a plugin — it never runs inside a live agent session. The repository's Host tsdown runs it automatically; a business package opts in by exporting `./typert` and `./client/typert` entries, and the generator validates those exports and published file lists. Static consumers can also call the analyzer directly for type inspection or catalog generation without publishing anything.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
This package is for package and repository maintainers who wire Typert generation into a build or consume generated artifacts. Publication is opt-in: declare the export entries, run the build, and the artifacts appear in `lib/`; static analysis needs no artifact.
|
|
29
|
+
|
|
30
|
+
### Publishing Typert artifacts from a package
|
|
31
|
+
|
|
32
|
+
A contributing package declares the host-face artifact export in `package.json`; packages that contribute both faces also declare `./client/typert`, and packages with Remote methods declare `./remote`:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
exports:
|
|
36
|
+
"./typert":
|
|
37
|
+
types: "./lib/typert.host.d.ts"
|
|
38
|
+
default: "./lib/typert.host.js"
|
|
39
|
+
files:
|
|
40
|
+
- "lib/typert.host.js"
|
|
41
|
+
- "lib/typert.host.d.ts"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After the build, `lib/typert.host.js` and `lib/typert.host.d.ts` exist and the [loader](../loader/README.md) registers the contribution in Loader compositions. The generated declaration file exposes `TYPERT` as `unknown`, so contributing packages never depend on the runtime registry. The generator fails the build when a declaration is missing, points at the wrong file, or publishes Remote artifacts without Remote methods; unsupported Zod projections fail with a `TypertEmitError` naming the construct instead of flattening or weakening the source type.
|
|
45
|
+
|
|
46
|
+
### Analyzing a workspace statically
|
|
47
|
+
|
|
48
|
+
Static consumers call `WorkspaceAnalyzer` directly against the workspace's `tsconfig.host.json` and `tsconfig.client.json` aggregates, select a face and package subset, and read the resulting `FaceModel` and type graph without emitting or loading runtime artifacts. `analyzeInBatches()` processes a large package selection through bounded compiler programs with the same model shape, and `discoverPackages()` finds contributing packages without building a type-checker program.
|
|
49
|
+
|
|
50
|
+
### Running generation inside a tsdown build
|
|
51
|
+
|
|
52
|
+
The package's `./tsdown` subpath provides `typertPlugin()` for the root tsdown config: it lowers standard decorators in TypeScript dependencies before bundling and emits the model-driven face artifacts at the package output root. In `package` mode it emits only the bundled package; in `workspace` mode it emits every explicit contributor once.
|
|
53
|
+
|
|
54
|
+
-----
|
|
55
|
+
|
|
56
|
+
<a id="understand-the-implementation"></a>
|
|
57
|
+
## Understand the implementation
|
|
6
58
|
|
|
7
|
-
|
|
59
|
+
<details>
|
|
60
|
+
<summary>Implementation internals — click to expand</summary>
|
|
8
61
|
|
|
9
|
-
|
|
62
|
+
This section explains how the generator reaches a compiler-independent model and what it emits; the observable build behavior is covered in [Use this package](#use-this-package).
|
|
10
63
|
|
|
11
|
-
|
|
64
|
+
### Design concept
|
|
12
65
|
|
|
13
|
-
`WorkspaceAnalyzer`
|
|
66
|
+
The generator is built on one separation: extraction and emission are decoupled through the compiler-independent model. `WorkspaceAnalyzer` reads TypeScript programs seeded from the face aggregate tsconfigs and produces `FaceModel` and `TypeGraph` data; `FaceModelEmitter` consumes only that model and never receives compiler nodes. The model retains declaration identity, generic parameters and applications, explicit inheritance, conditional and mapped types, import attributes, abstract modifiers, and source JSDoc, and excludes constructors, static members, and non-public members.
|
|
14
67
|
|
|
15
|
-
|
|
68
|
+
### Source map
|
|
16
69
|
|
|
17
|
-
|
|
70
|
+
| File | Role |
|
|
71
|
+
|---|---|
|
|
72
|
+
| [`src/index.ts`](src/index.ts) | Public API: analyzer, emitter, workspace generator, renderer, catalog projection |
|
|
73
|
+
| [`src/analyzer.ts`](src/analyzer.ts) | `WorkspaceAnalyzer`: face programs, check/write modes, batching, discovery, source index |
|
|
74
|
+
| [`src/model.ts`](src/model.ts) | Compiler-independent model types |
|
|
75
|
+
| [`src/emitter.ts`](src/emitter.ts) | `FaceModelEmitter`: Zod schema and declaration emission, Remote declarations |
|
|
76
|
+
| [`src/workspace.ts`](src/workspace.ts) | `WorkspaceTypertGenerator`: discovery, generation, export and files validation |
|
|
77
|
+
| [`src/tsdown-plugin.ts`](src/tsdown-plugin.ts) | tsdown plugin face: decorator lowering and artifact emission |
|
|
78
|
+
| [`src/cordis-catalog.ts`](src/cordis-catalog.ts) | Catalog projection used by the generated Cordis catalogs |
|
|
18
79
|
|
|
19
|
-
|
|
80
|
+
### Analysis and faces
|
|
20
81
|
|
|
21
|
-
|
|
82
|
+
Host and client are independent TypeScript programs. Direct project references establish compiler-face membership, while `dsh.client` package subpaths establish runtime-face contribution; `package.json#exports` marks every cross-package public boundary, and imports or re-exports are the only cross-face edges. `check` mode fails on syntax or semantic diagnostics, missing public annotations, private cross-package references, and reachable declaration merges the model cannot retain losslessly; `write` mode inserts checker-derived annotations and returns a clean check-mode model. Types owned by NPM dependencies remain `external` references instead of being expanded.
|
|
22
83
|
|
|
23
|
-
|
|
84
|
+
### Emission and publication contract
|
|
24
85
|
|
|
25
|
-
|
|
86
|
+
`FaceModelEmitter` emits executable JavaScript containing supported Zod schemas and the `TYPERT` contribution, plus a declaration file whose schemas are typed `z.ZodType<SourceType>` through the package's public export; unsupported Zod projections fail. The Host face with Remote methods additionally emits `typert.remote-client.*` projections of Host Remote contracts for the Client. `WorkspaceTypertGenerator` validates each contributor's `package.json`: `./typert` and `./client/typert` (and `./remote` when Remote methods exist) must point at the exact generated files, and the `files` list must include them.
|
|
26
87
|
|
|
88
|
+
### Catalog projection
|
|
89
|
+
|
|
90
|
+
The root export includes the model-driven extraction, completeness checks, and deterministic text renderers used by this repository's Cordis catalogs. They accept a `CordisCatalogPolicy`; repository-owned type links, foundation and exemption classifications, and inherited Cordis entries stay in `scripts/gen-cordis-catalog.ts` and are passed in explicitly, so this package contains projection mechanics, not a hidden copy of the repository's documentation taxonomy.
|
|
91
|
+
|
|
92
|
+
</details>
|
|
93
|
+
|
|
94
|
+
-----
|
|
95
|
+
|
|
96
|
+
<a id="further-exploration"></a>
|
|
97
|
+
## Further Exploration
|
|
98
|
+
|
|
99
|
+
Read these pages when the package-level contract is not enough; they move from the generated model to the runtime and the Remote-call path.
|
|
100
|
+
|
|
101
|
+
- [Typert subsystem reference](../../../docs/subsystems/typert.md) — the Remote contracts and registry interfaces the generator models.
|
|
102
|
+
- [Typert protocol](../protocol/README.md) — the declarations generated artifacts extend and consume.
|
|
103
|
+
- [Typert registry](../registry/README.md) — the runtime store the emitted artifacts feed.
|
|
104
|
+
- [API Gateway reference](../../../docs/api-gateway.md) — how generated Remote descriptors are invoked end to end.
|
|
105
|
+
- [Compiler-independent model Agent Note](../../../.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md) — the model design, alternatives, and consequences.
|
|
106
|
+
|
|
107
|
+
-----
|
|
108
|
+
|
|
109
|
+
<a id="model-experience"></a>
|
|
27
110
|
## Model Experience
|
|
28
111
|
|
|
29
|
-
None, as
|
|
112
|
+
None, as the build-time generator runs outside any agent runtime and touches no model request.
|
|
30
113
|
|
|
31
114
|
#### KV Cache effect
|
|
32
115
|
|
|
33
|
-
|
|
116
|
+
No direct effect; generated artifacts reach a request only when a consumer places them in one.
|
|
34
117
|
|
|
35
118
|
## Known Limitations and Deferred Work
|
|
36
119
|
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
120
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
These limits define what the generator cannot model or emit; they are current package constraints, not a task backlog.
|
|
124
|
+
|
|
125
|
+
- **Package export patterns are skipped** — contributing packages need concrete export targets; wildcard export patterns are not analyzed.
|
|
126
|
+
- **Namespace re-exports across faces fail** — named and star re-exports produce links, but a namespace re-export cannot be represented until `TypeTargetModel` can model a module namespace without flattening it.
|
|
127
|
+
- **The Zod emitter supports a deliberate subset** — generic schema declarations and computed constructs such as conditional or mapped schema roots fail until a concrete schema-factory policy exists.
|
|
128
|
+
- **No generated schema imports across faces** — cross-face links are represented for analysis, but no generated schema requires a runtime cross-face Zod import.
|
|
129
|
+
- **Discovery covers concrete public exports only** — declarations neither exported nor imported by the reachable graph are intentionally outside the package model.
|
|
130
|
+
|
|
131
|
+
<a id="dev-note"></a>
|
|
132
|
+
### Dev Note
|
|
133
|
+
|
|
134
|
+
<details>
|
|
135
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
136
|
+
|
|
137
|
+
None.
|
|
138
|
+
|
|
139
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,41 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "构建时 Typert 生成器:源代码类型分析、与编译器无关的模型与产物生成,供接入 Typert 发布或消费生成产物的维护者阅读。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-typert-generator
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-typert-generator` 在构建时把源代码 TypeScript 转换为与编译器无关的数据与可运行产物:它分析工作区各包的类型树,生成 `FaceModel` 与类型图,并输出包含受支持 Zod schema 与 `TYPERT` 反射贡献的可执行 JavaScript,以及配套声明文件。它是构建时库而非插件——绝不会在实时 agent 会话中运行。仓库的 Host tsdown 会自动运行它;业务包通过导出 `./typert` 与 `./client/typert` 入口选择加入,生成器会校验这些导出与发布文件清单。静态消费方也可以直接调用分析器进行类型检查或目录生成,无需发布任何内容。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
本包供把 Typert 生成接入构建或消费生成产物的包维护者与仓库维护者使用。发布是选择加入的:声明导出入口、运行构建,产物即出现在 `lib/` 中;静态分析则不需要产物。
|
|
29
|
+
|
|
30
|
+
### 从包中发布 Typert 产物
|
|
31
|
+
|
|
32
|
+
参与贡献的包在 `package.json` 中声明宿主侧产物导出;同时贡献两侧的包还要声明 `./client/typert`,含 Remote 方法的包还要声明 `./remote`:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
exports:
|
|
36
|
+
"./typert":
|
|
37
|
+
types: "./lib/typert.host.d.ts"
|
|
38
|
+
default: "./lib/typert.host.js"
|
|
39
|
+
files:
|
|
40
|
+
- "lib/typert.host.js"
|
|
41
|
+
- "lib/typert.host.d.ts"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
构建完成后,`lib/typert.host.js` 与 `lib/typert.host.d.ts` 即存在,[loader](../loader/README.zh.md) 会在 Loader 组合中注册该贡献。生成的声明文件把 `TYPERT` 暴露为 `unknown`,因此参与贡献的包永远不会依赖运行时注册表。当声明缺失、指向错误文件,或在没有 Remote 方法的情况下发布 Remote 产物时,生成器会使构建失败;不支持的 Zod 投影会以 `TypertEmitError` 指明具体构造并失败,而不会展平或弱化源类型。
|
|
45
|
+
|
|
46
|
+
### 静态分析工作区
|
|
47
|
+
|
|
48
|
+
静态消费方直接以工作区的 `tsconfig.host.json` 与 `tsconfig.client.json` aggregate 调用 `WorkspaceAnalyzer`,选择 face 与包子集,并在不生成或加载运行时产物的前提下读取生成的 `FaceModel` 与类型图。`analyzeInBatches()` 通过有界的编译器程序处理大批量包选择,模型形态保持一致;`discoverPackages()` 无需构建类型检查程序即可找出参与贡献的包。
|
|
49
|
+
|
|
50
|
+
### 在 tsdown 构建中运行生成
|
|
51
|
+
|
|
52
|
+
包的 `./tsdown` 子路径为根 tsdown 配置提供 `typertPlugin()`:它在打包前降低 TypeScript 依赖中的标准装饰器,并在包输出根目录生成模型驱动的 face 产物。`package` 模式只生成当前打包的包;`workspace` 模式对每个显式贡献方各生成一次。
|
|
53
|
+
|
|
54
|
+
-----
|
|
55
|
+
|
|
56
|
+
<a id="understand-the-implementation"></a>
|
|
57
|
+
## 理解实现
|
|
6
58
|
|
|
7
|
-
|
|
59
|
+
<details>
|
|
60
|
+
<summary>实现细节——点击展开</summary>
|
|
8
61
|
|
|
9
|
-
|
|
62
|
+
本节解释生成器如何得到与编译器无关的模型以及它生成什么;可观察的构建行为已在[使用本包](#use-this-package)中说明。
|
|
10
63
|
|
|
11
|
-
|
|
64
|
+
### 设计理念
|
|
12
65
|
|
|
13
|
-
|
|
66
|
+
生成器建立在一个分离之上:提取与生成通过与编译器无关的模型解耦。`WorkspaceAnalyzer` 读取以 face aggregate tsconfig 为种子的 TypeScript 程序,产出 `FaceModel` 与 `TypeGraph` 数据;`FaceModelEmitter` 只消费该模型,绝不接收编译器节点。模型保留声明标识、泛型参数及应用、显式继承、条件类型与映射类型、导入属性、abstract 修饰符与源码 JSDoc,并排除构造函数、静态成员与非公共成员。
|
|
14
67
|
|
|
15
|
-
|
|
68
|
+
### 源码地图
|
|
16
69
|
|
|
17
|
-
|
|
70
|
+
| 文件 | 职责 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| [`src/index.ts`](src/index.ts) | 公共 API:分析器、生成器、工作区生成器、渲染器、目录投影 |
|
|
73
|
+
| [`src/analyzer.ts`](src/analyzer.ts) | `WorkspaceAnalyzer`:face 程序、check/write 模式、分批、发现、源码索引 |
|
|
74
|
+
| [`src/model.ts`](src/model.ts) | 与编译器无关的模型类型 |
|
|
75
|
+
| [`src/emitter.ts`](src/emitter.ts) | `FaceModelEmitter`:Zod schema 与声明生成、Remote 声明 |
|
|
76
|
+
| [`src/workspace.ts`](src/workspace.ts) | `WorkspaceTypertGenerator`:发现、生成、导出与文件清单校验 |
|
|
77
|
+
| [`src/tsdown-plugin.ts`](src/tsdown-plugin.ts) | tsdown 插件面:装饰器降低与产物生成 |
|
|
78
|
+
| [`src/cordis-catalog.ts`](src/cordis-catalog.ts) | 生成 Cordis 目录所用的目录投影 |
|
|
18
79
|
|
|
19
|
-
|
|
80
|
+
### 分析与 face
|
|
20
81
|
|
|
21
|
-
|
|
82
|
+
Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face 的边只能来自导入或重新导出。`check` 模式遇到语法或语义诊断、缺失的公开类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时都会失败;`write` 模式插入类型检查器推导出的标注,并返回无诊断的 check 模式模型。NPM 依赖拥有的类型继续以 `external` 引用表示,不会被展开。
|
|
22
83
|
|
|
23
|
-
|
|
84
|
+
### 生成与发布约定
|
|
24
85
|
|
|
25
|
-
|
|
86
|
+
`FaceModelEmitter` 输出包含受支持 Zod schema 与 `TYPERT` 贡献的可执行 JavaScript,以及把 schema 通过包的公开导出标注为 `z.ZodType<SourceType>` 的声明文件;不支持的 Zod 投影会失败。含 Remote 方法的 Host face 还会额外为 Client 生成 Host Remote 约定的 `typert.remote-client.*` 投影。`WorkspaceTypertGenerator` 校验每个贡献方的 `package.json`:`./typert` 与 `./client/typert`(存在 Remote 方法时还有 `./remote`)必须指向精确的生成文件,且 `files` 清单必须包含它们。
|
|
26
87
|
|
|
88
|
+
### 目录投影
|
|
89
|
+
|
|
90
|
+
根导出包含本仓库 Cordis 目录使用的模型驱动提取逻辑、完整性检查与确定性文本渲染器。它们接受 `CordisCatalogPolicy`;由仓库持有的类型链接、基础类型/豁免分类与继承的 Cordis 条目仍位于 `scripts/gen-cordis-catalog.ts`,由调用方显式传入,因此本包只包含投影机制,不会隐式复制仓库的文档分类体系。
|
|
91
|
+
|
|
92
|
+
</details>
|
|
93
|
+
|
|
94
|
+
-----
|
|
95
|
+
|
|
96
|
+
<a id="further-exploration"></a>
|
|
97
|
+
## 进一步探索
|
|
98
|
+
|
|
99
|
+
当包级约定不够用时阅读以下页面;它们从生成模型逐步进入运行时与 Remote 调用路径。
|
|
100
|
+
|
|
101
|
+
- [Typert 子系统参考](../../../docs/subsystems/typert.zh.md)——生成器建模的 Remote 约定与注册表接口。
|
|
102
|
+
- [Typert 协议](../protocol/README.zh.md)——生成产物所扩展并消费的声明。
|
|
103
|
+
- [Typert 注册表](../registry/README.zh.md)——生成产物所供给的运行时存储。
|
|
104
|
+
- [API Gateway 参考](../../../docs/api-gateway.zh.md)——生成的 Remote 描述符如何端到端被调用。
|
|
105
|
+
- [Compiler-independent model Agent Note](../../../.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md)——模型设计、备选方案与后果。
|
|
106
|
+
|
|
107
|
+
-----
|
|
108
|
+
|
|
109
|
+
<a id="model-experience"></a>
|
|
27
110
|
## 模型体验
|
|
28
111
|
|
|
29
|
-
|
|
112
|
+
无,因为构建时生成器在任何 agent 运行时之外运行,不触及任何模型请求。
|
|
30
113
|
|
|
31
114
|
#### KV Cache 影响
|
|
32
115
|
|
|
33
|
-
|
|
116
|
+
无直接影响;生成产物只有在消费方将其放入请求时才会触及请求。
|
|
34
117
|
|
|
35
|
-
##
|
|
118
|
+
## 已知限制与延期工作
|
|
119
|
+
|
|
120
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
这些限制说明生成器无法建模或生成的构造;它们是当前包约束,不是任务积压。
|
|
124
|
+
|
|
125
|
+
- **包导出模式会被跳过**——参与贡献的包需要具体的导出目标;通配符导出模式不会被分析。
|
|
126
|
+
- **跨 face 的命名空间重新导出会失败**——具名与星号重新导出会生成链接,但在 `TypeTargetModel` 能够不经展平表示模块命名空间之前,命名空间重新导出无法表示。
|
|
127
|
+
- **Zod 生成器只支持有意限定的子集**——泛型 schema 声明,以及以条件类型或映射类型为 schema 根的计算构造,都会失败,直到存在明确的 schema 工厂策略。
|
|
128
|
+
- **没有生成的 schema 跨 face 导入**——跨 face 链接会在模型中表示以供分析,但生成的 schema 均不需要跨 face 的运行时 Zod 导入。
|
|
129
|
+
- **发现范围只覆盖具体公开导出**——既未导出、也未由可达图导入的声明按设计排除在包模型之外。
|
|
130
|
+
|
|
131
|
+
<a id="dev-note"></a>
|
|
132
|
+
### 开发备注
|
|
133
|
+
|
|
134
|
+
<details>
|
|
135
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
136
|
+
|
|
137
|
+
无。
|
|
36
138
|
|
|
37
|
-
|
|
38
|
-
- 跨 face 的具名重新导出和星号重新导出会生成链接;在 `TypeTargetModel` 能够不经展平便表示模块命名空间之前,命名空间重新导出会失败。
|
|
39
|
-
- Zod 产物生成组件仅支持 TypeScript 类型图中有意限定的部分。泛型 schema 声明,以及以条件类型或映射类型为 schema 根的计算构造,都会失败,直到存在明确的 schema 工厂策略。
|
|
40
|
-
- 跨 face 链接会在模型中表示以供分析,但当前生成的 schema 均不需要跨 face 的运行时 Zod 导入。
|
|
41
|
-
- 发现过程会遍历从具体公开导出可达的源文件;既未导出、也未由该图导入的声明会按设计排除在包模型之外。
|
|
139
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { GenMapping, addMapping, toEncodedMap } from "@jridgewell/gen-mapping";
|
|
|
10
10
|
* details; callers receive only the model declared in {@link ./model.ts}.
|
|
11
11
|
* @module @deepseek-ai/dsh-typert-generator/analyzer
|
|
12
12
|
*/
|
|
13
|
+
const PUBLIC_REMOTE_TYPE_ROOTS = new Set(["@deepseek-ai/dsh-util-values"]);
|
|
13
14
|
/** Analysis failure with a source-oriented diagnostic. */
|
|
14
15
|
var TypertAnalysisError = class extends Error {
|
|
15
16
|
name = "TypertAnalysisError";
|
|
@@ -705,13 +706,15 @@ var FaceAnalyzer = class {
|
|
|
705
706
|
};
|
|
706
707
|
}
|
|
707
708
|
}
|
|
708
|
-
const
|
|
709
|
+
const mode = invocation.kind === "direct" ? invocation.mode : void 0;
|
|
710
|
+
const resultType = this.remoteResultType(method, mode);
|
|
709
711
|
return {
|
|
710
712
|
id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
|
|
711
713
|
service: binding.service,
|
|
712
714
|
namespace: binding.namespace,
|
|
713
715
|
method: exportedMethod,
|
|
714
716
|
...exportedMethod === methodName ? {} : { implementation: methodName },
|
|
717
|
+
...mode === void 0 ? {} : { mode },
|
|
715
718
|
invocation: receiver,
|
|
716
719
|
...scope === void 0 ? {} : { scope },
|
|
717
720
|
parameters,
|
|
@@ -781,13 +784,26 @@ var FaceAnalyzer = class {
|
|
|
781
784
|
let marker;
|
|
782
785
|
if (this.isTypeMetaSymbol(expression, "Remote")) marker = { kind: "direct" };
|
|
783
786
|
else if (ts.isCallExpression(expression) && this.isTypeMetaSymbol(expression.expression, "Remote")) {
|
|
784
|
-
if (expression.arguments.length !== 1) this.fail(expression, "Remote() requires one
|
|
785
|
-
const
|
|
786
|
-
if (
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
exportName
|
|
790
|
-
|
|
787
|
+
if (expression.arguments.length !== 1) this.fail(expression, "Remote() requires one name or options object");
|
|
788
|
+
const argument = expression.arguments[0];
|
|
789
|
+
if (argument === void 0) this.fail(expression, "Remote() requires one name or options object");
|
|
790
|
+
const exportName = stringLiteralValue(argument);
|
|
791
|
+
if (exportName !== void 0) {
|
|
792
|
+
if (!isRemoteSegment(exportName)) this.fail(argument, "Remote() name must contain only RPC endpoint segment characters");
|
|
793
|
+
marker = {
|
|
794
|
+
kind: "direct",
|
|
795
|
+
exportName
|
|
796
|
+
};
|
|
797
|
+
} else {
|
|
798
|
+
if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
|
|
799
|
+
const [property] = argument.properties;
|
|
800
|
+
if (property === void 0) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
|
|
801
|
+
if (!ts.isPropertyAssignment(property) || memberName(property.name) !== "mode" || stringLiteralValue(property.initializer) !== "stream") this.fail(property, "Remote() options must contain exactly mode: \"stream\"");
|
|
802
|
+
marker = {
|
|
803
|
+
kind: "direct",
|
|
804
|
+
mode: "stream"
|
|
805
|
+
};
|
|
806
|
+
}
|
|
791
807
|
} else if (ts.isCallExpression(expression) && this.isTypeMetaSymbol(expression.expression, "RemoteScope")) {
|
|
792
808
|
if (expression.arguments.length < 1 || expression.arguments.length > 2) this.fail(expression, "RemoteScope() requires a Context key and optional exported method name");
|
|
793
809
|
const context = stringLiteralValue(expression.arguments[0]);
|
|
@@ -806,16 +822,18 @@ var FaceAnalyzer = class {
|
|
|
806
822
|
}
|
|
807
823
|
return found;
|
|
808
824
|
}
|
|
809
|
-
remoteResultType(method) {
|
|
825
|
+
remoteResultType(method, mode) {
|
|
810
826
|
const authored = this.requiredType(method, method.type, "return");
|
|
811
|
-
if (
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
827
|
+
if (ts.isTypeReferenceNode(authored)) {
|
|
828
|
+
const symbol = this.checker.getSymbolAtLocation(authored.typeName);
|
|
829
|
+
const resolved = symbol === void 0 ? void 0 : this.resolveSymbol(symbol);
|
|
830
|
+
const resultType = authored.typeArguments?.[0];
|
|
831
|
+
const wrappers = mode === "stream" ? ["Iterable", "AsyncIterable"] : ["Promise"];
|
|
832
|
+
const declaration = resolved === void 0 ? void 0 : preferredDeclaration(resolved);
|
|
833
|
+
if (resolved !== void 0 && wrappers.includes(resolved.name) && resultType !== void 0 && authored.typeArguments?.length === 1 && declaration !== void 0 && isStandardLibraryFile(declaration.getSourceFile().fileName)) return resultType;
|
|
834
|
+
}
|
|
835
|
+
if (mode === "stream") this.fail(method, "stream Remote methods must return Iterable<T> or AsyncIterable<T>");
|
|
836
|
+
return authored;
|
|
819
837
|
}
|
|
820
838
|
isGlobalAbortSignal(type) {
|
|
821
839
|
const symbol = this.symbolAtType(type);
|
|
@@ -1247,7 +1265,7 @@ var FaceAnalyzer = class {
|
|
|
1247
1265
|
if (registration === void 0) this.fail(site, `type ${symbol.name} is not owned by a workspace package`);
|
|
1248
1266
|
const candidates = [];
|
|
1249
1267
|
for (const [subpath, target] of packageExportTargets(registration.manifest)) {
|
|
1250
|
-
if (subpath === "." || subpath === "./package.json" || subpath === "./typert" || subpath === "./client/typert" || subpath === "./remote" || target.includes("*")) continue;
|
|
1268
|
+
if (subpath === "." && !PUBLIC_REMOTE_TYPE_ROOTS.has(registration.name) || subpath === "./package.json" || subpath === "./typert" || subpath === "./client/typert" || subpath === "./remote" || target.includes("*")) continue;
|
|
1251
1269
|
const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
|
|
1252
1270
|
if (sourceFile === void 0) continue;
|
|
1253
1271
|
const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
|
|
@@ -2783,6 +2801,7 @@ var FaceModelEmitter = class {
|
|
|
2783
2801
|
` method: ${quote$1(invocation.method)},`
|
|
2784
2802
|
];
|
|
2785
2803
|
if (invocation.implementation !== void 0) lines.push(` implementation: ${quote$1(invocation.implementation)},`);
|
|
2804
|
+
if (invocation.mode !== void 0) lines.push(` mode: ${quote$1(invocation.mode)},`);
|
|
2786
2805
|
if (invocation.invocation.kind === "direct") lines.push(" invocation: { kind: 'direct' },");
|
|
2787
2806
|
else {
|
|
2788
2807
|
lines.push(" invocation: {");
|
|
@@ -2912,6 +2931,7 @@ var FaceModelEmitter = class {
|
|
|
2912
2931
|
const parameters = invocation.parameters.filter((parameter) => !scoped || invocation.invocation.kind === "context" || parameter.wire !== invocation.scope?.wire).map((parameter) => `${safeIdentifier(parameter.wire)}${parameter.optional === true ? "?" : ""}: ${this.renderer.renderType(parameter.boundary.type, referenceNames)}`);
|
|
2913
2932
|
if (invocation.cancellation !== void 0) parameters.push("signal?: AbortSignal");
|
|
2914
2933
|
const result = this.renderer.renderType(invocation.result.type, referenceNames);
|
|
2934
|
+
if (invocation.mode === "stream") return `(${parameters.join(", ")}) => AsyncIterable<${result}>`;
|
|
2915
2935
|
return `(${parameters.join(", ")}) => Promise<RemoteResult<${result}>>`;
|
|
2916
2936
|
}
|
|
2917
2937
|
};
|
|
@@ -3824,7 +3844,7 @@ function renderRuntimeApi(services, events, types, inheritedServices) {
|
|
|
3824
3844
|
}
|
|
3825
3845
|
lines.push("]", "", "/** The inherited `ctx` API (cordis core + loader/hmr/timer), in curated order. */", "export const INHERITED_CTX_API: readonly InheritedApiEntry[] = [");
|
|
3826
3846
|
for (const inherited of inheritedServices) lines.push(` { name: ${quote(inherited.name)}, summary: ${quote(inherited.summary)} },`);
|
|
3827
|
-
lines.push("]", "", "function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {", " const included = new Set<string>()", " let frontier = [...seeds]", " while (frontier.length > 0) {", " const next: string[] = []", " for (const entry of TYPE_API) {", " if (included.has(entry.name)) continue", " const pattern = new RegExp(
|
|
3847
|
+
lines.push("]", "", "function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {", " const included = new Set<string>()", " let frontier = [...seeds]", " while (frontier.length > 0) {", " const next: string[] = []", " for (const entry of TYPE_API) {", " if (included.has(entry.name)) continue", " const pattern = new RegExp(`\\\\b${entry.name}\\\\b`)", " if (!frontier.some(text => pattern.test(text))) continue", " included.add(entry.name)", " next.push(entry.declaration)", " }", " frontier = next", " }", " return TYPE_API.filter(entry => included.has(entry.name))", "}", "", "function contextProperty(key: string): string {", " return /^[A-Za-z_$][\\w$]*$/.test(key) ? `ctx.${key}` : `ctx[${JSON.stringify(key)}]`", "}", "", "/**", " * Project the Service Catalog as a compact directory or one exact coding contract.", " * @param key - exact Service key; omit it to list all Services and method signatures.", " * @param services - platform-specific visible Service entries.", " * @returns compact navigation data or one detailed Service with its referenced type closure.", " */", "export function queryServiceApi(key?: string, services: readonly ServiceApiEntry[] = SERVICE_API): object {", " if (key === undefined) {", " return {", " mode: 'catalog',", " services: services.map(service => ({", " key: service.key,", " description: service.summary,", " methods: service.methods.map(method => ({ signature: method.signature })),", " })),", " }", " }", " const service = services.find(candidate => candidate.key === key)", " if (service === undefined) throw new Error(`no catalogued Service named \"${key}\"`)", " return {", " mode: 'service',", " service: {", " key: service.key,", " description: service.description,", " access: {", " optional: { expression: `ctx.get(${JSON.stringify(service.key)})`, requiresUndefinedCheck: true },", " hardDependency: { inject: [service.key], expression: contextProperty(service.key) },", " },", " methods: service.methods,", " },", " referencedTypes: referencedTypeClosure(service.methods.map(method => method.signature)),", " }", "}", "", "/**", " * Project the Event Catalog as a compact directory or one exact listener contract.", " * @param name - exact Event name; omit it to list all Events and listener signatures.", " * @param events - platform-specific visible Event entries.", " * @returns compact navigation data or one detailed Event with its referenced type closure.", " */", "export function queryEventApi(name?: string, events: readonly EventApiEntry[] = EVENT_API): object {", " if (name === undefined) {", " return {", " mode: 'catalog',", " events: events.map(event => ({", " name: event.name,", " description: event.summary,", " mode: event.mode,", " signature: event.signature,", " })),", " }", " }", " const event = events.find(candidate => candidate.name === name)", " if (event === undefined) throw new Error(`no catalogued Event named \"${name}\"`)", " return {", " mode: 'event',", " event: {", " name: event.name,", " description: event.description,", " mode: event.mode,", " signature: event.signature,", " parameters: event.parameters,", " },", " referencedTypes: referencedTypeClosure([event.signature]),", " }", "}", "/* jscpd:ignore-end */", "");
|
|
3828
3848
|
return lines.join("\n");
|
|
3829
3849
|
}
|
|
3830
3850
|
/** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */
|
|
@@ -3972,12 +3992,17 @@ function renderInheritedPage(policy) {
|
|
|
3972
3992
|
/** Discover, analyze, and emit package reflection from independent faces. */
|
|
3973
3993
|
var WorkspaceTypertGenerator = class {
|
|
3974
3994
|
root;
|
|
3995
|
+
options;
|
|
3996
|
+
/** Parsed-config and program-host state shared by every analyzer this generator creates. */
|
|
3997
|
+
caches = new WorkspaceCaches();
|
|
3975
3998
|
/**
|
|
3976
3999
|
* Bind generation to one workspace root.
|
|
3977
4000
|
* @param root - directory containing face aggregate tsconfigs.
|
|
4001
|
+
* @param options - behavior switches applied to every pass of this generator.
|
|
3978
4002
|
*/
|
|
3979
|
-
constructor(root) {
|
|
4003
|
+
constructor(root, options = {}) {
|
|
3980
4004
|
this.root = root;
|
|
4005
|
+
this.options = options;
|
|
3981
4006
|
}
|
|
3982
4007
|
/**
|
|
3983
4008
|
* Find public package faces that contribute Cordis services/events or
|
|
@@ -3988,6 +4013,7 @@ var WorkspaceTypertGenerator = class {
|
|
|
3988
4013
|
discover(faces) {
|
|
3989
4014
|
return new WorkspaceAnalyzer({
|
|
3990
4015
|
root: this.root,
|
|
4016
|
+
caches: this.caches,
|
|
3991
4017
|
...faces === void 0 ? {} : { faces }
|
|
3992
4018
|
}).discoverPackages();
|
|
3993
4019
|
}
|
|
@@ -4002,7 +4028,9 @@ var WorkspaceTypertGenerator = class {
|
|
|
4002
4028
|
const workspace = new WorkspaceAnalyzer({
|
|
4003
4029
|
root: this.root,
|
|
4004
4030
|
packages: selected,
|
|
4005
|
-
|
|
4031
|
+
caches: this.caches,
|
|
4032
|
+
...faces === void 0 ? {} : { faces },
|
|
4033
|
+
...this.options.checkDiagnostics === void 0 ? {} : { checkDiagnostics: this.options.checkDiagnostics }
|
|
4006
4034
|
}).analyze();
|
|
4007
4035
|
const artifacts = [];
|
|
4008
4036
|
for (const face of workspace.faces) {
|
package/lib/types/analyzer.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
9
9
|
import ts from 'typescript';
|
|
10
|
+
const PUBLIC_REMOTE_TYPE_ROOTS = new Set([
|
|
11
|
+
'@deepseek-ai/dsh-util-values',
|
|
12
|
+
]);
|
|
10
13
|
/** Analysis failure with a source-oriented diagnostic. */
|
|
11
14
|
export class TypertAnalysisError extends Error {
|
|
12
15
|
name = 'TypertAnalysisError';
|
|
@@ -895,13 +898,15 @@ class FaceAnalyzer {
|
|
|
895
898
|
scope = { context: context.key, wire: parameter.wire };
|
|
896
899
|
}
|
|
897
900
|
}
|
|
898
|
-
const
|
|
901
|
+
const mode = invocation.kind === 'direct' ? invocation.mode : undefined;
|
|
902
|
+
const resultType = this.remoteResultType(method, mode);
|
|
899
903
|
return {
|
|
900
904
|
id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
|
|
901
905
|
service: binding.service,
|
|
902
906
|
namespace: binding.namespace,
|
|
903
907
|
method: exportedMethod,
|
|
904
908
|
...(exportedMethod === methodName ? {} : { implementation: methodName }),
|
|
909
|
+
...(mode === undefined ? {} : { mode }),
|
|
905
910
|
invocation: receiver,
|
|
906
911
|
...(scope === undefined ? {} : { scope }),
|
|
907
912
|
parameters,
|
|
@@ -1009,12 +1014,31 @@ class FaceAnalyzer {
|
|
|
1009
1014
|
else if (ts.isCallExpression(expression)
|
|
1010
1015
|
&& this.isTypeMetaSymbol(expression.expression, 'Remote')) {
|
|
1011
1016
|
if (expression.arguments.length !== 1)
|
|
1012
|
-
this.fail(expression, 'Remote() requires one
|
|
1013
|
-
const
|
|
1014
|
-
if (
|
|
1015
|
-
this.fail(expression
|
|
1017
|
+
this.fail(expression, 'Remote() requires one name or options object');
|
|
1018
|
+
const argument = expression.arguments[0];
|
|
1019
|
+
if (argument === undefined)
|
|
1020
|
+
this.fail(expression, 'Remote() requires one name or options object');
|
|
1021
|
+
const exportName = stringLiteralValue(argument);
|
|
1022
|
+
if (exportName !== undefined) {
|
|
1023
|
+
if (!isRemoteSegment(exportName)) {
|
|
1024
|
+
this.fail(argument, 'Remote() name must contain only RPC endpoint segment characters');
|
|
1025
|
+
}
|
|
1026
|
+
marker = { kind: 'direct', exportName };
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) {
|
|
1030
|
+
this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
|
|
1031
|
+
}
|
|
1032
|
+
const [property] = argument.properties;
|
|
1033
|
+
if (property === undefined)
|
|
1034
|
+
this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
|
|
1035
|
+
if (!ts.isPropertyAssignment(property)
|
|
1036
|
+
|| memberName(property.name) !== 'mode'
|
|
1037
|
+
|| stringLiteralValue(property.initializer) !== 'stream') {
|
|
1038
|
+
this.fail(property, 'Remote() options must contain exactly mode: "stream"');
|
|
1039
|
+
}
|
|
1040
|
+
marker = { kind: 'direct', mode: 'stream' };
|
|
1016
1041
|
}
|
|
1017
|
-
marker = { kind: 'direct', exportName };
|
|
1018
1042
|
}
|
|
1019
1043
|
else if (ts.isCallExpression(expression)
|
|
1020
1044
|
&& this.isTypeMetaSymbol(expression.expression, 'RemoteScope')) {
|
|
@@ -1041,19 +1065,27 @@ class FaceAnalyzer {
|
|
|
1041
1065
|
}
|
|
1042
1066
|
return found;
|
|
1043
1067
|
}
|
|
1044
|
-
remoteResultType(method) {
|
|
1068
|
+
remoteResultType(method, mode) {
|
|
1045
1069
|
const authored = this.requiredType(method, method.type, 'return');
|
|
1046
|
-
if (
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1070
|
+
if (ts.isTypeReferenceNode(authored)) {
|
|
1071
|
+
const symbol = this.checker.getSymbolAtLocation(authored.typeName);
|
|
1072
|
+
const resolved = symbol === undefined ? undefined : this.resolveSymbol(symbol);
|
|
1073
|
+
const resultType = authored.typeArguments?.[0];
|
|
1074
|
+
const wrappers = mode === 'stream' ? ['Iterable', 'AsyncIterable'] : ['Promise'];
|
|
1075
|
+
const declaration = resolved === undefined ? undefined : preferredDeclaration(resolved);
|
|
1076
|
+
if (resolved !== undefined
|
|
1077
|
+
&& wrappers.includes(resolved.name)
|
|
1078
|
+
&& resultType !== undefined
|
|
1079
|
+
&& authored.typeArguments?.length === 1
|
|
1080
|
+
&& declaration !== undefined
|
|
1081
|
+
&& isStandardLibraryFile(declaration.getSourceFile().fileName)) {
|
|
1082
|
+
return resultType;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (mode === 'stream') {
|
|
1086
|
+
this.fail(method, 'stream Remote methods must return Iterable<T> or AsyncIterable<T>');
|
|
1087
|
+
}
|
|
1088
|
+
return authored;
|
|
1057
1089
|
}
|
|
1058
1090
|
isGlobalAbortSignal(type) {
|
|
1059
1091
|
const symbol = this.symbolAtType(type);
|
|
@@ -1542,7 +1574,8 @@ class FaceAnalyzer {
|
|
|
1542
1574
|
this.fail(site, `type ${symbol.name} is not owned by a workspace package`);
|
|
1543
1575
|
const candidates = [];
|
|
1544
1576
|
for (const [subpath, target] of packageExportTargets(registration.manifest)) {
|
|
1545
|
-
if (subpath === '.'
|
|
1577
|
+
if ((subpath === '.' && !PUBLIC_REMOTE_TYPE_ROOTS.has(registration.name))
|
|
1578
|
+
|| subpath === './package.json' || subpath === './typert'
|
|
1546
1579
|
|| subpath === './client/typert' || subpath === './remote' || target.includes('*'))
|
|
1547
1580
|
continue;
|
|
1548
1581
|
const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
|
|
@@ -663,7 +663,7 @@ function renderRuntimeApi(services, events, types, inheritedServices) {
|
|
|
663
663
|
for (const inherited of inheritedServices) {
|
|
664
664
|
lines.push(` { name: ${quote(inherited.name)}, summary: ${quote(inherited.summary)} },`);
|
|
665
665
|
}
|
|
666
|
-
lines.push(']', '', 'function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {', ' const included = new Set<string>()', ' let frontier = [...seeds]', ' while (frontier.length > 0) {', ' const next: string[] = []', ' for (const entry of TYPE_API) {', ' if (included.has(entry.name)) continue', ' const pattern = new RegExp(
|
|
666
|
+
lines.push(']', '', 'function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {', ' const included = new Set<string>()', ' let frontier = [...seeds]', ' while (frontier.length > 0) {', ' const next: string[] = []', ' for (const entry of TYPE_API) {', ' if (included.has(entry.name)) continue', ' const pattern = new RegExp(`\\\\b${entry.name}\\\\b`)', ' if (!frontier.some(text => pattern.test(text))) continue', ' included.add(entry.name)', ' next.push(entry.declaration)', ' }', ' frontier = next', ' }', ' return TYPE_API.filter(entry => included.has(entry.name))', '}', '', 'function contextProperty(key: string): string {', ' return /^[A-Za-z_$][\\w$]*$/.test(key) ? `ctx.${key}` : `ctx[${JSON.stringify(key)}]`', '}', '', '/**', ' * Project the Service Catalog as a compact directory or one exact coding contract.', ' * @param key - exact Service key; omit it to list all Services and method signatures.', ' * @param services - platform-specific visible Service entries.', ' * @returns compact navigation data or one detailed Service with its referenced type closure.', ' */', 'export function queryServiceApi(key?: string, services: readonly ServiceApiEntry[] = SERVICE_API): object {', ' if (key === undefined) {', ' return {', " mode: 'catalog',", ' services: services.map(service => ({', ' key: service.key,', ' description: service.summary,', ' methods: service.methods.map(method => ({ signature: method.signature })),', ' })),', ' }', ' }', ' const service = services.find(candidate => candidate.key === key)', ' if (service === undefined) throw new Error(`no catalogued Service named "${key}"`)', ' return {', " mode: 'service',", ' service: {', ' key: service.key,', ' description: service.description,', ' access: {', ' optional: { expression: `ctx.get(${JSON.stringify(service.key)})`, requiresUndefinedCheck: true },', ' hardDependency: { inject: [service.key], expression: contextProperty(service.key) },', ' },', ' methods: service.methods,', ' },', ' referencedTypes: referencedTypeClosure(service.methods.map(method => method.signature)),', ' }', '}', '', '/**', ' * Project the Event Catalog as a compact directory or one exact listener contract.', ' * @param name - exact Event name; omit it to list all Events and listener signatures.', ' * @param events - platform-specific visible Event entries.', ' * @returns compact navigation data or one detailed Event with its referenced type closure.', ' */', 'export function queryEventApi(name?: string, events: readonly EventApiEntry[] = EVENT_API): object {', ' if (name === undefined) {', ' return {', " mode: 'catalog',", ' events: events.map(event => ({', ' name: event.name,', ' description: event.summary,', ' mode: event.mode,', ' signature: event.signature,', ' })),', ' }', ' }', ' const event = events.find(candidate => candidate.name === name)', ' if (event === undefined) throw new Error(`no catalogued Event named "${name}"`)', ' return {', " mode: 'event',", ' event: {', ' name: event.name,', ' description: event.description,', ' mode: event.mode,', ' signature: event.signature,', ' parameters: event.parameters,', ' },', ' referencedTypes: referencedTypeClosure([event.signature]),', ' }', '}', '/* jscpd:ignore-end */', '');
|
|
667
667
|
return lines.join('\n');
|
|
668
668
|
}
|
|
669
669
|
/** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */
|
package/lib/types/emitter.js
CHANGED
|
@@ -198,6 +198,8 @@ export class FaceModelEmitter {
|
|
|
198
198
|
if (invocation.implementation !== undefined) {
|
|
199
199
|
lines.push(` implementation: ${quote(invocation.implementation)},`);
|
|
200
200
|
}
|
|
201
|
+
if (invocation.mode !== undefined)
|
|
202
|
+
lines.push(` mode: ${quote(invocation.mode)},`);
|
|
201
203
|
if (invocation.invocation.kind === 'direct') {
|
|
202
204
|
lines.push(' invocation: { kind: \'direct\' },');
|
|
203
205
|
}
|
|
@@ -341,9 +343,11 @@ export class FaceModelEmitter {
|
|
|
341
343
|
if (invocation.cancellation !== undefined)
|
|
342
344
|
parameters.push('signal?: AbortSignal');
|
|
343
345
|
const result = this.renderer.renderType(invocation.result.type, referenceNames);
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
346
|
+
if (invocation.mode === 'stream') {
|
|
347
|
+
return `(${parameters.join(', ')}) => AsyncIterable<${result}>`;
|
|
348
|
+
}
|
|
349
|
+
// The unary Client Remote face delivers the carrier's outcome, so every
|
|
350
|
+
// generated consumer signature resolves to a result the caller reads.
|
|
347
351
|
return `(${parameters.join(', ')}) => Promise<RemoteResult<${result}>>`;
|
|
348
352
|
}
|
|
349
353
|
}
|
package/lib/types/model.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
10
10
|
import ts from 'typescript';
|
|
11
11
|
import { WorkspaceTypertGenerator } from "./workspace.js";
|
|
12
12
|
const DECORATOR_SYNTAX = /^\s*@[A-Za-z_$][\w$]*/m;
|
|
13
|
+
// This plugin consumes tsc-emitted `lib/types` output, so every project it
|
|
14
|
+
// would re-diagnose has already passed the workspace tsc build in the same
|
|
15
|
+
// orchestration; the generator skips its per-package diagnostic pass here.
|
|
16
|
+
const TSC_VERIFIED_INPUT = { checkDiagnostics: false };
|
|
13
17
|
/**
|
|
14
18
|
* Create the decorator-lowering and typert-generation plugin for the root tsdown config.
|
|
15
19
|
* @param pluginOptions - package/workspace emission mode and independent program faces.
|
|
@@ -60,7 +64,7 @@ export function typertPlugin(pluginOptions = {}) {
|
|
|
60
64
|
return;
|
|
61
65
|
let artifacts = artifactsByRoot.get(root);
|
|
62
66
|
if (artifacts === undefined) {
|
|
63
|
-
const generator = new WorkspaceTypertGenerator(root);
|
|
67
|
+
const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT);
|
|
64
68
|
artifacts = pluginOptions.faces === undefined
|
|
65
69
|
? generator.generate()
|
|
66
70
|
: generator.generate(undefined, pluginOptions.faces);
|
|
@@ -70,7 +74,7 @@ export function typertPlugin(pluginOptions = {}) {
|
|
|
70
74
|
},
|
|
71
75
|
};
|
|
72
76
|
function emitWorkspace(root, faces) {
|
|
73
|
-
const generator = new WorkspaceTypertGenerator(root);
|
|
77
|
+
const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT);
|
|
74
78
|
const packages = generator.discover(faces)
|
|
75
79
|
.filter(candidate => hasTypertExport(readManifest(join(root, candidate.root)).exports))
|
|
76
80
|
.map(candidate => candidate.package);
|
package/lib/types/workspace.d.ts
CHANGED
|
@@ -9,14 +9,29 @@ import type { TypertFace } from './model.ts';
|
|
|
9
9
|
export interface WorkspaceEmitResult extends ModelEmitResult {
|
|
10
10
|
readonly packageRoot: string;
|
|
11
11
|
}
|
|
12
|
+
/** Behavior switches for one {@link WorkspaceTypertGenerator}. */
|
|
13
|
+
export interface WorkspaceTypertGeneratorOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Run the per-package syntactic/semantic diagnostic pass before analysis
|
|
16
|
+
* (default true). Pass false only when the same orchestration already
|
|
17
|
+
* verified the workspace with tsc; the Typert-specific analysis checks
|
|
18
|
+
* (annotation coverage, private cross-package references, unretainable
|
|
19
|
+
* merges) run regardless.
|
|
20
|
+
*/
|
|
21
|
+
readonly checkDiagnostics?: boolean;
|
|
22
|
+
}
|
|
12
23
|
/** Discover, analyze, and emit package reflection from independent faces. */
|
|
13
24
|
export declare class WorkspaceTypertGenerator {
|
|
14
25
|
private readonly root;
|
|
26
|
+
private readonly options;
|
|
27
|
+
/** Parsed-config and program-host state shared by every analyzer this generator creates. */
|
|
28
|
+
private readonly caches;
|
|
15
29
|
/**
|
|
16
30
|
* Bind generation to one workspace root.
|
|
17
31
|
* @param root - directory containing face aggregate tsconfigs.
|
|
32
|
+
* @param options - behavior switches applied to every pass of this generator.
|
|
18
33
|
*/
|
|
19
|
-
constructor(root: string);
|
|
34
|
+
constructor(root: string, options?: WorkspaceTypertGeneratorOptions);
|
|
20
35
|
/**
|
|
21
36
|
* Find public package faces that contribute Cordis services/events or
|
|
22
37
|
* explicitly tagged Typert roots.
|
package/lib/types/workspace.js
CHANGED
|
@@ -4,17 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
|
-
import { TypertAnalysisError, WorkspaceAnalyzer } from "./analyzer.js";
|
|
7
|
+
import { TypertAnalysisError, WorkspaceAnalyzer, WorkspaceCaches } from "./analyzer.js";
|
|
8
8
|
import { FaceModelEmitter } from "./emitter.js";
|
|
9
9
|
/** Discover, analyze, and emit package reflection from independent faces. */
|
|
10
10
|
export class WorkspaceTypertGenerator {
|
|
11
11
|
root;
|
|
12
|
+
options;
|
|
13
|
+
/** Parsed-config and program-host state shared by every analyzer this generator creates. */
|
|
14
|
+
caches = new WorkspaceCaches();
|
|
12
15
|
/**
|
|
13
16
|
* Bind generation to one workspace root.
|
|
14
17
|
* @param root - directory containing face aggregate tsconfigs.
|
|
18
|
+
* @param options - behavior switches applied to every pass of this generator.
|
|
15
19
|
*/
|
|
16
|
-
constructor(root) {
|
|
20
|
+
constructor(root, options = {}) {
|
|
17
21
|
this.root = root;
|
|
22
|
+
this.options = options;
|
|
18
23
|
}
|
|
19
24
|
/**
|
|
20
25
|
* Find public package faces that contribute Cordis services/events or
|
|
@@ -25,6 +30,7 @@ export class WorkspaceTypertGenerator {
|
|
|
25
30
|
discover(faces) {
|
|
26
31
|
return new WorkspaceAnalyzer({
|
|
27
32
|
root: this.root,
|
|
33
|
+
caches: this.caches,
|
|
28
34
|
...(faces === undefined ? {} : { faces }),
|
|
29
35
|
}).discoverPackages();
|
|
30
36
|
}
|
|
@@ -39,7 +45,9 @@ export class WorkspaceTypertGenerator {
|
|
|
39
45
|
const workspace = new WorkspaceAnalyzer({
|
|
40
46
|
root: this.root,
|
|
41
47
|
packages: selected,
|
|
48
|
+
caches: this.caches,
|
|
42
49
|
...(faces === undefined ? {} : { faces }),
|
|
50
|
+
...(this.options.checkDiagnostics === undefined ? {} : { checkDiagnostics: this.options.checkDiagnostics }),
|
|
43
51
|
}).analyze();
|
|
44
52
|
const artifacts = [];
|
|
45
53
|
for (const face of workspace.faces) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-typert-generator",
|
|
3
3
|
"description": "TypeScript project analyzer and model-driven Typert artifact generator",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"typescript": "^6.0.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@deepseek-ai/
|
|
45
|
-
"@deepseek-ai/
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
45
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"zod": "^4.4.3",
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-typert-registry": "^0.1.
|
|
52
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
49
|
+
"@deepseek-ai/dsh-tool-cordis": "^0.1.2-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-typert-registry": "^0.1.2-alpha.2",
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
53
53
|
}
|
|
54
54
|
}
|