@deepseek-ai/dsh-typert-generator 0.0.1-rc.1
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 +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +41 -0
- package/README.zh.md +41 -0
- package/lib/index.js +3922 -0
- package/lib/invariant.js +24 -0
- package/lib/types/analyzer.d.ts +143 -0
- package/lib/types/analyzer.js +2723 -0
- package/lib/types/cordis-catalog.d.ts +148 -0
- package/lib/types/cordis-catalog.js +695 -0
- package/lib/types/emitter.d.ts +55 -0
- package/lib/types/emitter.js +745 -0
- package/lib/types/index.d.ts +16 -0
- package/lib/types/index.js +12 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/model.d.ts +409 -0
- package/lib/types/model.js +48 -0
- package/lib/types/renderer.d.ts +90 -0
- package/lib/types/renderer.js +351 -0
- package/lib/types/tsdown-plugin.d.ts +34 -0
- package/lib/types/tsdown-plugin.js +136 -0
- package/lib/types/workspace.d.ts +36 -0
- package/lib/types/workspace.js +114 -0
- package/package.json +54 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering and traversal over the compiler-independent TypeGraph. Emitters
|
|
3
|
+
* use this module instead of reaching back into TypeScript AST nodes.
|
|
4
|
+
* @module @deepseek-ai/dsh-typert-generator/renderer
|
|
5
|
+
*/
|
|
6
|
+
import { childTypeNodeIds } from "./model.js";
|
|
7
|
+
/** Failure to render or traverse an internally inconsistent TypeGraph. */
|
|
8
|
+
export class TypeGraphRenderError extends Error {
|
|
9
|
+
name = 'TypeGraphRenderError';
|
|
10
|
+
}
|
|
11
|
+
/** Read and render one TypeGraph without compiler objects. */
|
|
12
|
+
export class TypeGraphRenderer {
|
|
13
|
+
graph;
|
|
14
|
+
nodes;
|
|
15
|
+
declarations;
|
|
16
|
+
members;
|
|
17
|
+
parameterNames = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Index one complete graph.
|
|
20
|
+
* @param graph - compiler-independent graph to render.
|
|
21
|
+
*/
|
|
22
|
+
constructor(graph) {
|
|
23
|
+
this.graph = graph;
|
|
24
|
+
this.nodes = new Map(graph.nodes.map(node => [node.id, node]));
|
|
25
|
+
this.declarations = new Map(graph.declarations.map(declaration => [declaration.id, declaration]));
|
|
26
|
+
this.members = new Map(graph.declarations.flatMap(declaration => declaration.members.map(member => [member.id, member])));
|
|
27
|
+
for (const declaration of graph.declarations) {
|
|
28
|
+
this.indexParameters(declaration.typeParameters);
|
|
29
|
+
for (const member of declaration.members) {
|
|
30
|
+
if ('signature' in member)
|
|
31
|
+
this.indexParameters(member.signature.typeParameters);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a node id or fail with the broken edge.
|
|
37
|
+
* @param id - graph-local type node id.
|
|
38
|
+
* @returns the referenced node.
|
|
39
|
+
*/
|
|
40
|
+
node(id) {
|
|
41
|
+
const node = this.nodes.get(id);
|
|
42
|
+
if (node === undefined)
|
|
43
|
+
throw new TypeGraphRenderError(`type graph references missing node ${id}`);
|
|
44
|
+
return node;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a declaration id or fail with the broken edge.
|
|
48
|
+
* @param id - workspace symbol id.
|
|
49
|
+
* @returns the referenced declaration.
|
|
50
|
+
*/
|
|
51
|
+
declaration(id) {
|
|
52
|
+
const declaration = this.declarations.get(id);
|
|
53
|
+
if (declaration === undefined)
|
|
54
|
+
throw new TypeGraphRenderError(`type graph references missing declaration ${id}`);
|
|
55
|
+
return declaration;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a public member id.
|
|
59
|
+
* @param id - declaration member id.
|
|
60
|
+
* @returns the referenced member.
|
|
61
|
+
*/
|
|
62
|
+
member(id) {
|
|
63
|
+
const member = this.members.get(id);
|
|
64
|
+
if (member === undefined)
|
|
65
|
+
throw new TypeGraphRenderError(`type graph references missing member ${id}`);
|
|
66
|
+
return member;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Render one type expression from the retained source structure.
|
|
70
|
+
* @param id - type node id.
|
|
71
|
+
* @param references - optional generated names for declaration references.
|
|
72
|
+
* @returns TypeScript type text.
|
|
73
|
+
*/
|
|
74
|
+
renderType(id, references) {
|
|
75
|
+
const node = this.node(id);
|
|
76
|
+
switch (node.kind) {
|
|
77
|
+
case 'keyword': return node.name;
|
|
78
|
+
case 'literal': return node.text;
|
|
79
|
+
case 'parenthesized': return `(${this.renderType(node.type, references)})`;
|
|
80
|
+
case 'reference': {
|
|
81
|
+
const name = node.target.kind === 'type-parameter'
|
|
82
|
+
? this.parameterNames.get(node.target.parameter) ?? node.name
|
|
83
|
+
: node.target.kind === 'declaration'
|
|
84
|
+
? references?.get(node.target.symbol) ?? node.name
|
|
85
|
+
: node.name;
|
|
86
|
+
return node.arguments.length === 0
|
|
87
|
+
? name
|
|
88
|
+
: `${name}<${node.arguments.map(argument => this.renderType(argument, references)).join(', ')}>`;
|
|
89
|
+
}
|
|
90
|
+
case 'union': return node.types.map(type => this.renderType(type, references)).join(' | ');
|
|
91
|
+
case 'intersection': return node.types.map(type => this.renderType(type, references)).join(' & ');
|
|
92
|
+
case 'array': {
|
|
93
|
+
const element = this.renderType(node.element, references);
|
|
94
|
+
const wrapped = needsArrayParentheses(this.node(node.element)) ? `(${element})` : element;
|
|
95
|
+
return `${wrapped}[]`;
|
|
96
|
+
}
|
|
97
|
+
case 'tuple': {
|
|
98
|
+
const elements = node.elements.map((element) => {
|
|
99
|
+
const type = this.renderType(element.type, references);
|
|
100
|
+
if (element.name !== undefined) {
|
|
101
|
+
return `${element.rest ? '...' : ''}${element.name}${element.optional ? '?' : ''}: ${type}`;
|
|
102
|
+
}
|
|
103
|
+
return `${element.rest ? '...' : ''}${type}${element.optional ? '?' : ''}`;
|
|
104
|
+
});
|
|
105
|
+
return `[${elements.join(', ')}]`;
|
|
106
|
+
}
|
|
107
|
+
case 'object': return this.renderObject(node.members, references);
|
|
108
|
+
case 'function': return `${this.renderSignatureHead(node.signature, references)} => ${this.renderType(node.signature.returns, references)}`;
|
|
109
|
+
case 'constructor': return `${node.abstract ? 'abstract ' : ''}new ${this.renderSignatureHead(node.signature, references)} => ${this.renderType(node.signature.returns, references)}`;
|
|
110
|
+
case 'indexed-access': return `${this.renderType(node.object, references)}[${this.renderType(node.index, references)}]`;
|
|
111
|
+
case 'operator': return `${node.operator} ${this.renderType(node.type, references)}`;
|
|
112
|
+
case 'conditional': {
|
|
113
|
+
return `${this.renderType(node.check, references)} extends ${this.renderType(node.extends, references)} ? ${this.renderType(node.whenTrue, references)} : ${this.renderType(node.whenFalse, references)}`;
|
|
114
|
+
}
|
|
115
|
+
case 'infer': return `infer ${this.renderTypeParameter(node.parameter, false, references)}`;
|
|
116
|
+
case 'mapped': {
|
|
117
|
+
const readonly = node.readonly === 'preserve' ? '' : node.readonly === 'remove' ? '-readonly ' : 'readonly ';
|
|
118
|
+
const optional = node.optional === 'preserve' ? '' : node.optional === 'remove' ? '-?' : '?';
|
|
119
|
+
if (node.parameter.constraint === undefined) {
|
|
120
|
+
throw new TypeGraphRenderError(`mapped type parameter ${node.parameter.name} has no constraint`);
|
|
121
|
+
}
|
|
122
|
+
const parameter = `${node.parameter.name} in ${this.renderType(node.parameter.constraint, references)}`;
|
|
123
|
+
const nameType = node.nameType === undefined ? '' : ` as ${this.renderType(node.nameType, references)}`;
|
|
124
|
+
const value = node.value === undefined ? 'unknown' : this.renderType(node.value, references);
|
|
125
|
+
return `{ ${readonly}[${parameter}${nameType}]${optional}: ${value} }`;
|
|
126
|
+
}
|
|
127
|
+
case 'template-literal': {
|
|
128
|
+
const spans = node.spans.map(span => `\${${this.renderType(span.type, references)}}${escapeTemplate(span.text)}`).join('');
|
|
129
|
+
return `\`${escapeTemplate(node.head)}${spans}\``;
|
|
130
|
+
}
|
|
131
|
+
case 'type-query': {
|
|
132
|
+
const argumentsText = node.arguments.length === 0
|
|
133
|
+
? ''
|
|
134
|
+
: `<${node.arguments.map(argument => this.renderType(argument, references)).join(', ')}>`;
|
|
135
|
+
return `typeof ${node.expression}${argumentsText}`;
|
|
136
|
+
}
|
|
137
|
+
case 'import-type': {
|
|
138
|
+
const attributes = node.attributes === undefined ? '' : `, ${node.attributes}`;
|
|
139
|
+
const imported = `import(${quote(node.module)}${attributes})${node.qualifier === undefined ? '' : `.${node.qualifier}`}`;
|
|
140
|
+
const argumentsText = node.arguments.length === 0
|
|
141
|
+
? ''
|
|
142
|
+
: `<${node.arguments.map(argument => this.renderType(argument, references)).join(', ')}>`;
|
|
143
|
+
return `${node.typeof ? 'typeof ' : ''}${imported}${argumentsText}`;
|
|
144
|
+
}
|
|
145
|
+
case 'predicate': {
|
|
146
|
+
const assertion = node.asserts ? 'asserts ' : '';
|
|
147
|
+
return node.type === undefined
|
|
148
|
+
? `${assertion}${node.parameter}`
|
|
149
|
+
: `${assertion}${node.parameter} is ${this.renderType(node.type, references)}`;
|
|
150
|
+
}
|
|
151
|
+
case 'this': return 'this';
|
|
152
|
+
default: return assertNever(node);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Render a callable signature without a member name.
|
|
157
|
+
* @param signature - modeled signature.
|
|
158
|
+
* @param references - optional generated names for declaration references.
|
|
159
|
+
* @returns parameter list and return type.
|
|
160
|
+
*/
|
|
161
|
+
renderSignature(signature, references) {
|
|
162
|
+
return `${this.renderSignatureHead(signature, references)}: ${this.renderType(signature.returns, references)}`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Render one class/interface member as a body-free declaration.
|
|
166
|
+
* @param member - modeled member.
|
|
167
|
+
* @param sourceModifiers - retain source-only modifiers for reflection text.
|
|
168
|
+
* @param references - optional generated names for declaration references.
|
|
169
|
+
* @returns one-line TypeScript member text.
|
|
170
|
+
*/
|
|
171
|
+
renderMember(member, sourceModifiers = false, references) {
|
|
172
|
+
if (sourceModifiers)
|
|
173
|
+
return member.text;
|
|
174
|
+
const name = renderPropertyName(member.name);
|
|
175
|
+
const optional = member.optional ? '?' : '';
|
|
176
|
+
const readonly = member.readonly ? 'readonly ' : '';
|
|
177
|
+
const abstract = member.abstract ? 'abstract ' : '';
|
|
178
|
+
switch (member.kind) {
|
|
179
|
+
case 'property': return `${abstract}${readonly}${name}${optional}: ${this.renderType(member.type, references)}`;
|
|
180
|
+
case 'method': return `${abstract}${name}${optional}${this.renderSignature(member.signature, references)}`;
|
|
181
|
+
case 'getter': return `${abstract}get ${name}()${this.renderReturn(member.signature, references)}`;
|
|
182
|
+
case 'setter': return `${abstract}set ${name}${this.renderSignatureHead(member.signature, references)}`;
|
|
183
|
+
case 'call': return this.renderSignature(member.signature, references);
|
|
184
|
+
case 'construct': return `new ${this.renderSignature(member.signature, references)}`;
|
|
185
|
+
case 'index': {
|
|
186
|
+
const parameters = member.signature.parameters.map(parameter => this.renderParameter(parameter, references)).join(', ');
|
|
187
|
+
return `${readonly}[${parameters}]: ${this.renderType(member.signature.returns, references)}`;
|
|
188
|
+
}
|
|
189
|
+
default: return assertNever(member);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Render a named declaration without JSDoc.
|
|
194
|
+
* @param id - declaration symbol id.
|
|
195
|
+
* @returns exported TypeScript declaration text.
|
|
196
|
+
*/
|
|
197
|
+
renderDeclaration(id) {
|
|
198
|
+
const declaration = this.declaration(id);
|
|
199
|
+
const parameters = this.renderTypeParameters(declaration.typeParameters);
|
|
200
|
+
if (declaration.kind === 'enum') {
|
|
201
|
+
const members = declaration.enumMembers?.map(member => ` ${renderPropertyName(member.name)}${member.initializer === undefined ? '' : ` = ${member.initializer}`},`) ?? [];
|
|
202
|
+
return [`export enum ${declaration.name} {`, ...members, '}'].join('\n');
|
|
203
|
+
}
|
|
204
|
+
if (declaration.kind === 'alias') {
|
|
205
|
+
if (declaration.type === undefined)
|
|
206
|
+
throw new TypeGraphRenderError(`alias ${id} has no type node`);
|
|
207
|
+
return `export type ${declaration.name}${parameters} = ${this.renderType(declaration.type)};`;
|
|
208
|
+
}
|
|
209
|
+
const extendsTypes = declaration.extends.map(type => this.renderType(type));
|
|
210
|
+
const implementsTypes = declaration.implements.map(type => this.renderType(type));
|
|
211
|
+
const heritage = [
|
|
212
|
+
extendsTypes.length === 0 ? '' : ` extends ${extendsTypes.join(', ')}`,
|
|
213
|
+
implementsTypes.length === 0 ? '' : ` implements ${implementsTypes.join(', ')}`,
|
|
214
|
+
].join('');
|
|
215
|
+
const prefix = declaration.kind === 'class' && declaration.abstract ? 'abstract ' : '';
|
|
216
|
+
const members = declaration.members.map(member => ` ${this.renderMember(member)};`);
|
|
217
|
+
return [`export ${prefix}${declaration.kind} ${declaration.name}${parameters}${heritage} {`, ...members, '}'].join('\n');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Find the transitive declaration closure referenced by members.
|
|
221
|
+
* @param memberIds - business-surface member ids.
|
|
222
|
+
* @returns declarations in graph order, excluding no roots implicitly.
|
|
223
|
+
*/
|
|
224
|
+
declarationClosureForMembers(memberIds) {
|
|
225
|
+
return this.declarationClosure(memberIds, []);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Find the transitive declaration closure referenced by type roots.
|
|
229
|
+
* @param typeIds - graph type roots.
|
|
230
|
+
* @returns declarations in graph order.
|
|
231
|
+
*/
|
|
232
|
+
declarationClosureForTypes(typeIds) {
|
|
233
|
+
return this.declarationClosure([], typeIds);
|
|
234
|
+
}
|
|
235
|
+
declarationClosure(memberIds, typeIds) {
|
|
236
|
+
const found = new Set();
|
|
237
|
+
const visiting = new Set();
|
|
238
|
+
const visitNode = (id) => {
|
|
239
|
+
const node = this.node(id);
|
|
240
|
+
if (node.kind === 'reference' && node.target.kind === 'declaration')
|
|
241
|
+
visitDeclaration(node.target.symbol);
|
|
242
|
+
if (node.kind === 'import-type' && node.target?.kind === 'declaration')
|
|
243
|
+
visitDeclaration(node.target.symbol);
|
|
244
|
+
for (const child of childTypeNodeIds(node))
|
|
245
|
+
visitNode(child);
|
|
246
|
+
for (const signature of nodeSignatures(node))
|
|
247
|
+
visitSignature(signature);
|
|
248
|
+
if (node.kind === 'object')
|
|
249
|
+
for (const member of node.members)
|
|
250
|
+
visitMember(member);
|
|
251
|
+
};
|
|
252
|
+
const visitSignature = (signature) => {
|
|
253
|
+
for (const parameter of signature.typeParameters) {
|
|
254
|
+
if (parameter.constraint !== undefined)
|
|
255
|
+
visitNode(parameter.constraint);
|
|
256
|
+
if (parameter.default !== undefined)
|
|
257
|
+
visitNode(parameter.default);
|
|
258
|
+
}
|
|
259
|
+
for (const parameter of signature.parameters)
|
|
260
|
+
visitNode(parameter.type);
|
|
261
|
+
visitNode(signature.returns);
|
|
262
|
+
};
|
|
263
|
+
const visitMember = (member) => {
|
|
264
|
+
if (member.kind === 'property')
|
|
265
|
+
visitNode(member.type);
|
|
266
|
+
else
|
|
267
|
+
visitSignature(member.signature);
|
|
268
|
+
};
|
|
269
|
+
const visitDeclaration = (id) => {
|
|
270
|
+
if (found.has(id) || visiting.has(id))
|
|
271
|
+
return;
|
|
272
|
+
visiting.add(id);
|
|
273
|
+
const declaration = this.declaration(id);
|
|
274
|
+
for (const parameter of declaration.typeParameters) {
|
|
275
|
+
if (parameter.constraint !== undefined)
|
|
276
|
+
visitNode(parameter.constraint);
|
|
277
|
+
if (parameter.default !== undefined)
|
|
278
|
+
visitNode(parameter.default);
|
|
279
|
+
}
|
|
280
|
+
for (const type of [...declaration.extends, ...declaration.implements])
|
|
281
|
+
visitNode(type);
|
|
282
|
+
if (declaration.type !== undefined)
|
|
283
|
+
visitNode(declaration.type);
|
|
284
|
+
for (const member of declaration.members)
|
|
285
|
+
visitMember(member);
|
|
286
|
+
visiting.delete(id);
|
|
287
|
+
found.add(id);
|
|
288
|
+
};
|
|
289
|
+
for (const id of memberIds)
|
|
290
|
+
visitMember(this.member(id));
|
|
291
|
+
for (const id of typeIds)
|
|
292
|
+
visitNode(id);
|
|
293
|
+
return this.graph.declarations.filter(declaration => found.has(declaration.id));
|
|
294
|
+
}
|
|
295
|
+
renderSignatureHead(signature, references) {
|
|
296
|
+
return `${this.renderTypeParameters(signature.typeParameters, references)}(${signature.parameters.map(parameter => this.renderParameter(parameter, references)).join(', ')})`;
|
|
297
|
+
}
|
|
298
|
+
renderReturn(signature, references) {
|
|
299
|
+
return `: ${this.renderType(signature.returns, references)}`;
|
|
300
|
+
}
|
|
301
|
+
renderParameter(parameter, references) {
|
|
302
|
+
const name = parameter.binding === 'identifier' ? renderPropertyName(parameter.name) : parameter.name;
|
|
303
|
+
const optional = parameter.initializer === undefined && parameter.optional && !parameter.rest ? '?' : '';
|
|
304
|
+
const initializer = parameter.initializer === undefined ? '' : ` = ${parameter.initializer}`;
|
|
305
|
+
return `${parameter.rest ? '...' : ''}${name}${optional}: ${this.renderType(parameter.type, references)}${initializer}`;
|
|
306
|
+
}
|
|
307
|
+
renderTypeParameters(parameters, references) {
|
|
308
|
+
return parameters.length === 0
|
|
309
|
+
? ''
|
|
310
|
+
: `<${parameters.map(parameter => this.renderTypeParameter(parameter, true, references)).join(', ')}>`;
|
|
311
|
+
}
|
|
312
|
+
renderTypeParameter(parameter, includeDefault, references) {
|
|
313
|
+
const variance = parameter.variance === undefined ? '' : `${parameter.variance === 'in-out' ? 'in out' : parameter.variance} `;
|
|
314
|
+
const constModifier = parameter.const ? 'const ' : '';
|
|
315
|
+
const constraint = parameter.constraint === undefined ? '' : ` extends ${this.renderType(parameter.constraint, references)}`;
|
|
316
|
+
const fallback = !includeDefault || parameter.default === undefined ? '' : ` = ${this.renderType(parameter.default, references)}`;
|
|
317
|
+
return `${constModifier}${variance}${parameter.name}${constraint}${fallback}`;
|
|
318
|
+
}
|
|
319
|
+
renderObject(members, references) {
|
|
320
|
+
if (members.length === 0)
|
|
321
|
+
return '{}';
|
|
322
|
+
return `{ ${members.map(member => `${this.renderMember(member, false, references)};`).join(' ')} }`;
|
|
323
|
+
}
|
|
324
|
+
indexParameters(parameters) {
|
|
325
|
+
for (const parameter of parameters)
|
|
326
|
+
this.parameterNames.set(parameter.id, parameter.name);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function nodeSignatures(node) {
|
|
330
|
+
return node.kind === 'function' || node.kind === 'constructor' ? [node.signature] : [];
|
|
331
|
+
}
|
|
332
|
+
function needsArrayParentheses(node) {
|
|
333
|
+
return node.kind === 'union' || node.kind === 'intersection' || node.kind === 'function' || node.kind === 'constructor' || node.kind === 'conditional';
|
|
334
|
+
}
|
|
335
|
+
function renderPropertyName(name) {
|
|
336
|
+
if (name.startsWith('[') && name.endsWith(']'))
|
|
337
|
+
return name;
|
|
338
|
+
if (/^(?:[$A-Z_a-z][$\w]*|\d+)$/u.test(name))
|
|
339
|
+
return name;
|
|
340
|
+
return quote(name);
|
|
341
|
+
}
|
|
342
|
+
function quote(value) {
|
|
343
|
+
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'").replaceAll('\n', '\\n')}'`;
|
|
344
|
+
}
|
|
345
|
+
function escapeTemplate(value) {
|
|
346
|
+
return value.replaceAll('\\', '\\\\').replaceAll('`', '\\`').replaceAll('${', '\\${');
|
|
347
|
+
}
|
|
348
|
+
function assertNever(value) {
|
|
349
|
+
throw new TypeGraphRenderError(`unsupported model variant ${JSON.stringify(value)}`);
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=renderer.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional tsdown (rolldown) plugin face of the typert generator. It lowers
|
|
3
|
+
* standard decorators in TypeScript dependencies before bundling, then emits
|
|
4
|
+
* model-driven face artifacts at the package output root. Packages without a
|
|
5
|
+
* Typert or Remote export are skipped.
|
|
6
|
+
* @module @deepseek-ai/dsh-typert-generator/tsdown
|
|
7
|
+
*/
|
|
8
|
+
import type { TypertFace } from './model.ts';
|
|
9
|
+
/** The subset of the rolldown plugin contract used here (structural; avoids a rolldown type dependency). */
|
|
10
|
+
interface TypertPlugin {
|
|
11
|
+
name: string;
|
|
12
|
+
transform: (code: string, id: string) => {
|
|
13
|
+
code: string;
|
|
14
|
+
map: string | undefined;
|
|
15
|
+
} | undefined;
|
|
16
|
+
writeBundle: (options: {
|
|
17
|
+
dir?: string;
|
|
18
|
+
}) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Generation scope selected by a tsdown build phase. */
|
|
21
|
+
export interface TypertPluginOptions {
|
|
22
|
+
/** Package mode emits only the package being bundled; workspace mode emits every explicit contributor once. */
|
|
23
|
+
readonly mode?: 'package' | 'workspace';
|
|
24
|
+
/** Independent TypeScript program faces included in this phase. */
|
|
25
|
+
readonly faces?: readonly TypertFace[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create the decorator-lowering and typert-generation plugin for the root tsdown config.
|
|
29
|
+
* @param pluginOptions - package/workspace emission mode and independent program faces.
|
|
30
|
+
* @returns a rolldown-compatible plugin that lowers source decorators and emits local and Host-for-Client artifacts.
|
|
31
|
+
*/
|
|
32
|
+
export declare function typertPlugin(pluginOptions?: TypertPluginOptions): TypertPlugin;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=tsdown-plugin.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional tsdown (rolldown) plugin face of the typert generator. It lowers
|
|
3
|
+
* standard decorators in TypeScript dependencies before bundling, then emits
|
|
4
|
+
* model-driven face artifacts at the package output root. Packages without a
|
|
5
|
+
* Typert or Remote export are skipped.
|
|
6
|
+
* @module @deepseek-ai/dsh-typert-generator/tsdown
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
import { WorkspaceTypertGenerator } from "./workspace.js";
|
|
12
|
+
const DECORATOR_SYNTAX = /^\s*@[A-Za-z_$][\w$]*/m;
|
|
13
|
+
/**
|
|
14
|
+
* Create the decorator-lowering and typert-generation plugin for the root tsdown config.
|
|
15
|
+
* @param pluginOptions - package/workspace emission mode and independent program faces.
|
|
16
|
+
* @returns a rolldown-compatible plugin that lowers source decorators and emits local and Host-for-Client artifacts.
|
|
17
|
+
*/
|
|
18
|
+
export function typertPlugin(pluginOptions = {}) {
|
|
19
|
+
const artifactsByRoot = new Map();
|
|
20
|
+
const emittedWorkspaces = new Set();
|
|
21
|
+
return {
|
|
22
|
+
name: 'dsh-typert-generator',
|
|
23
|
+
transform(code, id) {
|
|
24
|
+
const file = id.split('?', 1)[0] ?? id;
|
|
25
|
+
if (!/\.[cm]?tsx?$/.test(file) || !DECORATOR_SYNTAX.test(code))
|
|
26
|
+
return;
|
|
27
|
+
const result = ts.transpileModule(code, {
|
|
28
|
+
fileName: file,
|
|
29
|
+
compilerOptions: {
|
|
30
|
+
target: ts.ScriptTarget.ES2024,
|
|
31
|
+
module: ts.ModuleKind.ESNext,
|
|
32
|
+
...(file.endsWith('x') ? { jsx: ts.JsxEmit.ReactJSX } : {}),
|
|
33
|
+
sourceMap: true,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
code: result.outputText.replace(/\n?\/\/# sourceMappingURL=.*$/u, '\n'),
|
|
38
|
+
map: result.sourceMapText,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
writeBundle(bundleOptions) {
|
|
42
|
+
// options.dir is the package's absolute outDir (<package>/lib); its
|
|
43
|
+
// nearest package.json owns the bundle even when a custom config writes
|
|
44
|
+
// a nested output such as <package>/lib/dev.
|
|
45
|
+
if (bundleOptions.dir === undefined)
|
|
46
|
+
return;
|
|
47
|
+
const root = workspaceRoot(bundleOptions.dir);
|
|
48
|
+
if (emittedWorkspaces.has(root))
|
|
49
|
+
return;
|
|
50
|
+
if (pluginOptions.mode === 'workspace') {
|
|
51
|
+
emitWorkspace(root, pluginOptions.faces);
|
|
52
|
+
emittedWorkspaces.add(root);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const packageDir = packageRoot(bundleOptions.dir, root);
|
|
56
|
+
if (packageDir === undefined)
|
|
57
|
+
return;
|
|
58
|
+
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
59
|
+
if (manifest.name === undefined || !hasTypertExport(manifest.exports))
|
|
60
|
+
return;
|
|
61
|
+
let artifacts = artifactsByRoot.get(root);
|
|
62
|
+
if (artifacts === undefined) {
|
|
63
|
+
const generator = new WorkspaceTypertGenerator(root);
|
|
64
|
+
artifacts = pluginOptions.faces === undefined
|
|
65
|
+
? generator.generate()
|
|
66
|
+
: generator.generate(undefined, pluginOptions.faces);
|
|
67
|
+
artifactsByRoot.set(root, artifacts);
|
|
68
|
+
}
|
|
69
|
+
emitArtifacts(packageDir, artifacts.filter(candidate => candidate.package === manifest.name));
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
function emitWorkspace(root, faces) {
|
|
73
|
+
const generator = new WorkspaceTypertGenerator(root);
|
|
74
|
+
const packages = generator.discover(faces)
|
|
75
|
+
.filter(candidate => hasTypertExport(readManifest(join(root, candidate.root)).exports))
|
|
76
|
+
.map(candidate => candidate.package);
|
|
77
|
+
if (packages.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
for (const artifact of generator.generate(packages, faces)) {
|
|
80
|
+
emitArtifacts(join(root, artifact.packageRoot), [artifact]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function emitArtifacts(packageDir, artifacts) {
|
|
85
|
+
const output = join(packageDir, 'lib');
|
|
86
|
+
mkdirSync(output, { recursive: true });
|
|
87
|
+
let emittedRemote = false;
|
|
88
|
+
for (const artifact of artifacts) {
|
|
89
|
+
writeFileSync(join(output, `typert.${artifact.face}.js`), artifact.js);
|
|
90
|
+
writeFileSync(join(output, `typert.${artifact.face}.d.ts`), artifact.dts);
|
|
91
|
+
if (artifact.remote !== undefined) {
|
|
92
|
+
emittedRemote = true;
|
|
93
|
+
writeFileSync(join(output, 'typert.remote-client.js'), artifact.remote.js);
|
|
94
|
+
writeFileSync(join(output, 'typert.remote-client.d.ts'), artifact.remote.dts);
|
|
95
|
+
writeFileSync(join(output, 'typert.remote-client.d.ts.map'), artifact.remote.dtsMap);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!emittedRemote && artifacts.some(artifact => artifact.face === 'host')) {
|
|
99
|
+
for (const file of [
|
|
100
|
+
'typert.remote-client.js',
|
|
101
|
+
'typert.remote-client.d.ts',
|
|
102
|
+
'typert.remote-client.d.ts.map',
|
|
103
|
+
])
|
|
104
|
+
rmSync(join(output, file), { force: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function readManifest(packageDir) {
|
|
108
|
+
return JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
109
|
+
}
|
|
110
|
+
function hasTypertExport(exportsField) {
|
|
111
|
+
if (exportsField === null || typeof exportsField !== 'object' || Array.isArray(exportsField))
|
|
112
|
+
return false;
|
|
113
|
+
return Object.hasOwn(exportsField, './typert')
|
|
114
|
+
|| Object.hasOwn(exportsField, './client/typert')
|
|
115
|
+
|| Object.hasOwn(exportsField, './remote');
|
|
116
|
+
}
|
|
117
|
+
function packageRoot(start, workspace) {
|
|
118
|
+
let current = resolve(start);
|
|
119
|
+
while (current !== workspace) {
|
|
120
|
+
if (existsSync(join(current, 'package.json')))
|
|
121
|
+
return current;
|
|
122
|
+
current = dirname(current);
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function workspaceRoot(start) {
|
|
127
|
+
let current = resolve(start);
|
|
128
|
+
while (!existsSync(join(current, 'tsconfig.host.json'))) {
|
|
129
|
+
const parent = dirname(current);
|
|
130
|
+
if (parent === current)
|
|
131
|
+
throw new Error(`typert-generator: cannot find workspace root above ${start}`);
|
|
132
|
+
current = parent;
|
|
133
|
+
}
|
|
134
|
+
return current;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=tsdown-plugin.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-level discovery and model-driven Typert generation.
|
|
3
|
+
* @module @deepseek-ai/dsh-typert-generator/workspace
|
|
4
|
+
*/
|
|
5
|
+
import type { DiscoveredTypertPackage } from './analyzer.ts';
|
|
6
|
+
import type { ModelEmitResult } from './emitter.ts';
|
|
7
|
+
import type { TypertFace } from './model.ts';
|
|
8
|
+
/** One emitted artifact paired with its source package root. */
|
|
9
|
+
export interface WorkspaceEmitResult extends ModelEmitResult {
|
|
10
|
+
readonly packageRoot: string;
|
|
11
|
+
}
|
|
12
|
+
/** Discover, analyze, and emit package reflection from independent faces. */
|
|
13
|
+
export declare class WorkspaceTypertGenerator {
|
|
14
|
+
private readonly root;
|
|
15
|
+
/**
|
|
16
|
+
* Bind generation to one workspace root.
|
|
17
|
+
* @param root - directory containing face aggregate tsconfigs.
|
|
18
|
+
*/
|
|
19
|
+
constructor(root: string);
|
|
20
|
+
/**
|
|
21
|
+
* Find public package faces that contribute Cordis services/events or
|
|
22
|
+
* explicitly tagged Typert roots.
|
|
23
|
+
* @param faces - optional independent program faces to inspect.
|
|
24
|
+
* @returns discovered packages in stable package-name order.
|
|
25
|
+
*/
|
|
26
|
+
discover(faces?: readonly TypertFace[]): DiscoveredTypertPackage[];
|
|
27
|
+
/**
|
|
28
|
+
* Generate all discovered contributors, or an explicit package subset.
|
|
29
|
+
* @param packages - optional exact package names for a focused pass.
|
|
30
|
+
* @param faces - optional independent program faces to analyze.
|
|
31
|
+
* @returns one artifact per package face.
|
|
32
|
+
*/
|
|
33
|
+
generate(packages?: readonly string[], faces?: readonly TypertFace[]): WorkspaceEmitResult[];
|
|
34
|
+
private validateExport;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-level discovery and model-driven Typert generation.
|
|
3
|
+
* @module @deepseek-ai/dsh-typert-generator/workspace
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { TypertAnalysisError, WorkspaceAnalyzer } from "./analyzer.js";
|
|
8
|
+
import { FaceModelEmitter } from "./emitter.js";
|
|
9
|
+
/** Discover, analyze, and emit package reflection from independent faces. */
|
|
10
|
+
export class WorkspaceTypertGenerator {
|
|
11
|
+
root;
|
|
12
|
+
/**
|
|
13
|
+
* Bind generation to one workspace root.
|
|
14
|
+
* @param root - directory containing face aggregate tsconfigs.
|
|
15
|
+
*/
|
|
16
|
+
constructor(root) {
|
|
17
|
+
this.root = root;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Find public package faces that contribute Cordis services/events or
|
|
21
|
+
* explicitly tagged Typert roots.
|
|
22
|
+
* @param faces - optional independent program faces to inspect.
|
|
23
|
+
* @returns discovered packages in stable package-name order.
|
|
24
|
+
*/
|
|
25
|
+
discover(faces) {
|
|
26
|
+
return new WorkspaceAnalyzer({
|
|
27
|
+
root: this.root,
|
|
28
|
+
...(faces === undefined ? {} : { faces }),
|
|
29
|
+
}).discoverPackages();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate all discovered contributors, or an explicit package subset.
|
|
33
|
+
* @param packages - optional exact package names for a focused pass.
|
|
34
|
+
* @param faces - optional independent program faces to analyze.
|
|
35
|
+
* @returns one artifact per package face.
|
|
36
|
+
*/
|
|
37
|
+
generate(packages, faces) {
|
|
38
|
+
const selected = packages ?? this.discover(faces).map(candidate => candidate.package);
|
|
39
|
+
const workspace = new WorkspaceAnalyzer({
|
|
40
|
+
root: this.root,
|
|
41
|
+
packages: selected,
|
|
42
|
+
...(faces === undefined ? {} : { faces }),
|
|
43
|
+
}).analyze();
|
|
44
|
+
const artifacts = [];
|
|
45
|
+
for (const face of workspace.faces) {
|
|
46
|
+
const emitter = new FaceModelEmitter(face);
|
|
47
|
+
for (const packageModel of face.packages) {
|
|
48
|
+
const artifact = {
|
|
49
|
+
...emitter.emit(packageModel.name),
|
|
50
|
+
packageRoot: packageModel.root,
|
|
51
|
+
};
|
|
52
|
+
this.validateExport(artifact);
|
|
53
|
+
artifacts.push(artifact);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return artifacts;
|
|
57
|
+
}
|
|
58
|
+
validateExport(artifact) {
|
|
59
|
+
const manifestPath = resolve(this.root, artifact.packageRoot, 'package.json');
|
|
60
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
61
|
+
const subpath = artifact.face === 'host' ? './typert' : './client/typert';
|
|
62
|
+
const expected = {
|
|
63
|
+
types: `./lib/typert.${artifact.face}.d.ts`,
|
|
64
|
+
default: `./lib/typert.${artifact.face}.js`,
|
|
65
|
+
};
|
|
66
|
+
const actual = manifest.exports !== null && typeof manifest.exports === 'object'
|
|
67
|
+
? manifest.exports[subpath]
|
|
68
|
+
: undefined;
|
|
69
|
+
if (!sameExport(actual, expected)) {
|
|
70
|
+
throw new TypertAnalysisError(`typert(${artifact.face}): ${artifact.package} must export ${subpath} as ${JSON.stringify(expected)}`);
|
|
71
|
+
}
|
|
72
|
+
const files = Array.isArray(manifest.files) ? manifest.files : [];
|
|
73
|
+
for (const file of [`lib/typert.${artifact.face}.js`, `lib/typert.${artifact.face}.d.ts`]) {
|
|
74
|
+
if (!files.includes(file)) {
|
|
75
|
+
throw new TypertAnalysisError(`typert(${artifact.face}): ${artifact.package} package files must include ${file}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (artifact.face !== 'host')
|
|
79
|
+
return;
|
|
80
|
+
const remoteExpected = {
|
|
81
|
+
types: './lib/typert.remote-client.d.ts',
|
|
82
|
+
default: './lib/typert.remote-client.js',
|
|
83
|
+
};
|
|
84
|
+
const remoteActual = manifest.exports !== null && typeof manifest.exports === 'object'
|
|
85
|
+
? manifest.exports['./remote']
|
|
86
|
+
: undefined;
|
|
87
|
+
const remoteFiles = [
|
|
88
|
+
'lib/typert.remote-client.js',
|
|
89
|
+
'lib/typert.remote-client.d.ts',
|
|
90
|
+
'lib/typert.remote-client.d.ts.map',
|
|
91
|
+
];
|
|
92
|
+
if (artifact.remote === undefined) {
|
|
93
|
+
if (remoteActual !== undefined || remoteFiles.some(file => files.includes(file))) {
|
|
94
|
+
throw new TypertAnalysisError(`typert(host): ${artifact.package} publishes Remote artifacts but has no Remote methods`);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!sameExport(remoteActual, remoteExpected)) {
|
|
99
|
+
throw new TypertAnalysisError(`typert(host): ${artifact.package} must export ./remote as ${JSON.stringify(remoteExpected)}`);
|
|
100
|
+
}
|
|
101
|
+
for (const file of remoteFiles) {
|
|
102
|
+
if (!files.includes(file)) {
|
|
103
|
+
throw new TypertAnalysisError(`typert(host): ${artifact.package} package files must include ${file}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function sameExport(actual, expected) {
|
|
109
|
+
if (actual === null || typeof actual !== 'object' || Array.isArray(actual))
|
|
110
|
+
return false;
|
|
111
|
+
const value = actual;
|
|
112
|
+
return value.types === expected.types && value.default === expected.default;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=workspace.js.map
|