@deepseek-ai/dsh-typert-generator 0.2.1-alpha.2 → 0.2.1-alpha.3
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 +16 -10
- package/README.md +30 -3
- package/README.zh.md +30 -3
- package/lib/index.js +693 -101
- package/lib/types/analyzer.d.ts +34 -2
- package/lib/types/analyzer.js +176 -45
- package/lib/types/artifact-files.d.ts +30 -0
- package/lib/types/artifact-files.js +57 -0
- package/lib/types/build-output-guard.d.ts +61 -0
- package/lib/types/build-output-guard.js +193 -0
- package/lib/types/emitter.d.ts +3 -0
- package/lib/types/emitter.js +148 -48
- package/lib/types/index.d.ts +3 -2
- package/lib/types/index.js +2 -1
- package/lib/types/model.d.ts +6 -2
- package/lib/types/tool-schema.d.ts +20 -0
- package/lib/types/tool-schema.js +156 -0
- package/lib/types/tsdown-plugin.d.ts +21 -6
- package/lib/types/tsdown-plugin.js +171 -72
- package/lib/types/worker.d.ts +28 -0
- package/lib/types/worker.js +49 -0
- package/lib/types/workspace.d.ts +34 -2
- package/lib/types/workspace.js +55 -7
- package/package.json +7 -6
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of workspace build outputs loaded into an analyzed face program,
|
|
3
|
+
* and of workspace imports that resolve to nothing. Typert reads workspace
|
|
4
|
+
* packages only from source when a caller rejects build outputs, so its
|
|
5
|
+
* result cannot depend on whether, or when, package compilation ran: an
|
|
6
|
+
* import that reaches `lib/` on a compiled tree resolves to nothing on a
|
|
7
|
+
* clean one, and both are rejected.
|
|
8
|
+
* @module @deepseek-ai/dsh-typert-generator/build-output-guard
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
11
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
12
|
+
import ts from 'typescript';
|
|
13
|
+
/**
|
|
14
|
+
* List the program files that a workspace package holds under its `lib/`
|
|
15
|
+
* directory. A package is the nearest directory with a `package.json` at or
|
|
16
|
+
* below `root`; files under `node_modules` are external. Importers are found
|
|
17
|
+
* by resolving each program file's import specifiers again, so this pass
|
|
18
|
+
* costs extra only when an output is present.
|
|
19
|
+
* @param program - the analyzed face program.
|
|
20
|
+
* @param root - workspace root.
|
|
21
|
+
* @param host - module resolution host of the program.
|
|
22
|
+
* @returns loads in file-path order; empty when every workspace file comes from source.
|
|
23
|
+
*/
|
|
24
|
+
export function findBuildOutputLoads(program, root, host) {
|
|
25
|
+
const classifier = new BuildOutputClassifier(root);
|
|
26
|
+
const outputs = new Map();
|
|
27
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
28
|
+
if (classifier.isBuildOutput(sourceFile.fileName))
|
|
29
|
+
outputs.set(classifier.canonical(sourceFile.fileName), []);
|
|
30
|
+
}
|
|
31
|
+
if (outputs.size === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const options = program.getCompilerOptions();
|
|
34
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
35
|
+
if (!classifier.isWorkspaceFile(sourceFile.fileName) || classifier.isBuildOutput(sourceFile.fileName))
|
|
36
|
+
continue;
|
|
37
|
+
for (const { fileName: specifier } of ts.preProcessFile(sourceFile.text, true, true).importedFiles) {
|
|
38
|
+
const resolved = ts.resolveModuleName(specifier, sourceFile.fileName, options, host).resolvedModule;
|
|
39
|
+
const importers = resolved === undefined ? undefined : outputs.get(classifier.canonical(resolved.resolvedFileName));
|
|
40
|
+
importers?.push({ file: classifier.display(sourceFile.fileName), specifier });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...outputs]
|
|
44
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
45
|
+
.map(([file, importers]) => ({ file: classifier.display(file), importers, source: classifier.sourceFor(file) }));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* List the imports in workspace program files that name a workspace package
|
|
49
|
+
* and resolve to nothing. A workspace package is one whose `node_modules` link,
|
|
50
|
+
* found from the importing file's directories, leads inside `root` outside any
|
|
51
|
+
* `node_modules`; without that link or a `paths` alias the package cannot be
|
|
52
|
+
* identified, and type checking reports the import. The source of a subpath is
|
|
53
|
+
* derived from the package's `exports` `types` condition under `lib/types/`.
|
|
54
|
+
* @param program - the analyzed face program.
|
|
55
|
+
* @param root - workspace root.
|
|
56
|
+
* @param host - module resolution host of the program.
|
|
57
|
+
* @returns unresolved imports in file and specifier order.
|
|
58
|
+
*/
|
|
59
|
+
export function findUnresolvedWorkspaceImports(program, root, host) {
|
|
60
|
+
const classifier = new BuildOutputClassifier(root);
|
|
61
|
+
const options = program.getCompilerOptions();
|
|
62
|
+
const cache = ts.createModuleResolutionCache(root, file => file, options);
|
|
63
|
+
const found = [];
|
|
64
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
65
|
+
if (!classifier.isWorkspaceFile(sourceFile.fileName) || classifier.isBuildOutput(sourceFile.fileName))
|
|
66
|
+
continue;
|
|
67
|
+
for (const { fileName: specifier } of ts.preProcessFile(sourceFile.text, true, true).importedFiles) {
|
|
68
|
+
if (specifier.startsWith('.') || isAbsolute(specifier) || specifier.includes(':'))
|
|
69
|
+
continue;
|
|
70
|
+
if (ts.resolveModuleName(specifier, sourceFile.fileName, options, host, cache).resolvedModule !== undefined)
|
|
71
|
+
continue;
|
|
72
|
+
const packageRoot = classifier.linkedWorkspacePackage(sourceFile.fileName, packageNameOf(specifier));
|
|
73
|
+
if (packageRoot === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
const source = exportSource(packageRoot, specifier);
|
|
76
|
+
found.push({
|
|
77
|
+
file: classifier.display(sourceFile.fileName),
|
|
78
|
+
specifier,
|
|
79
|
+
source: source === undefined ? undefined : classifier.display(source),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return found.sort((left, right) => left.file.localeCompare(right.file) || left.specifier.localeCompare(right.specifier));
|
|
84
|
+
}
|
|
85
|
+
function packageNameOf(specifier) {
|
|
86
|
+
const parts = specifier.split('/');
|
|
87
|
+
return parts.slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
|
|
88
|
+
}
|
|
89
|
+
function exportSource(packageRoot, specifier) {
|
|
90
|
+
const packageName = packageNameOf(specifier);
|
|
91
|
+
const subpath = specifier === packageName ? '.' : `.${specifier.slice(packageName.length)}`;
|
|
92
|
+
let manifest;
|
|
93
|
+
try {
|
|
94
|
+
manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
catch (_unreadableManifest) {
|
|
97
|
+
// The error names the import without a suggested source path.
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const entry = property(property(manifest, 'exports'), subpath);
|
|
101
|
+
const types = typeof entry === 'string' ? entry : property(entry, 'types');
|
|
102
|
+
if (typeof types !== 'string' || !types.startsWith('./lib/types/'))
|
|
103
|
+
return undefined;
|
|
104
|
+
const stem = join(packageRoot, 'src', types.slice('./lib/types/'.length).replace(/\.d\.([cm]?)ts$/u, '.$1ts'));
|
|
105
|
+
return [stem, `${stem}x`].find(candidate => existsSync(candidate));
|
|
106
|
+
}
|
|
107
|
+
function property(value, key) {
|
|
108
|
+
return typeof value === 'object' && value !== null ? value[key] : undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Classifies program files by their real paths, because module resolution
|
|
112
|
+
* reports symbolic-link targets and Windows long names while the workspace
|
|
113
|
+
* root may be spelled through a link or an 8.3 short name; reported paths are
|
|
114
|
+
* spelled under the caller's root.
|
|
115
|
+
*/
|
|
116
|
+
class BuildOutputClassifier {
|
|
117
|
+
root;
|
|
118
|
+
packageRoots = new Map();
|
|
119
|
+
realPaths = new Map();
|
|
120
|
+
realRoot;
|
|
121
|
+
constructor(root) {
|
|
122
|
+
this.root = root;
|
|
123
|
+
this.realRoot = this.canonical(root);
|
|
124
|
+
}
|
|
125
|
+
canonical(file) {
|
|
126
|
+
const absolute = resolve(file);
|
|
127
|
+
const cached = this.realPaths.get(absolute);
|
|
128
|
+
if (cached !== undefined)
|
|
129
|
+
return cached;
|
|
130
|
+
let real = absolute;
|
|
131
|
+
try {
|
|
132
|
+
real = realpathSync.native(absolute);
|
|
133
|
+
}
|
|
134
|
+
catch (_missingPath) {
|
|
135
|
+
// A path that does not exist, such as an unresolved candidate, keeps its resolved spelling.
|
|
136
|
+
}
|
|
137
|
+
this.realPaths.set(absolute, real);
|
|
138
|
+
return real;
|
|
139
|
+
}
|
|
140
|
+
display(file) {
|
|
141
|
+
const path = relative(this.realRoot, this.canonical(file));
|
|
142
|
+
return path.startsWith('..') || isAbsolute(path) ? resolve(file) : join(this.root, path);
|
|
143
|
+
}
|
|
144
|
+
isWorkspaceFile(file) {
|
|
145
|
+
const path = relative(this.realRoot, this.canonical(file));
|
|
146
|
+
return path !== '' && !path.startsWith('..') && !isAbsolute(path) && !path.split(sep).includes('node_modules');
|
|
147
|
+
}
|
|
148
|
+
isBuildOutput(file) {
|
|
149
|
+
if (!this.isWorkspaceFile(file))
|
|
150
|
+
return false;
|
|
151
|
+
const real = this.canonical(file);
|
|
152
|
+
const packageRoot = this.packageRoot(dirname(real));
|
|
153
|
+
return packageRoot !== undefined && relative(packageRoot, real).split(sep)[0] === 'lib';
|
|
154
|
+
}
|
|
155
|
+
sourceFor(file) {
|
|
156
|
+
const real = this.canonical(file);
|
|
157
|
+
const packageRoot = this.packageRoot(dirname(real));
|
|
158
|
+
if (packageRoot === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
const [lib, types, ...rest] = relative(packageRoot, real).split(sep);
|
|
161
|
+
const last = rest.at(-1);
|
|
162
|
+
if (lib !== 'lib' || types !== 'types' || last === undefined)
|
|
163
|
+
return undefined;
|
|
164
|
+
const stem = join(packageRoot, 'src', ...rest.slice(0, -1), last.replace(/\.d\.([cm]?)ts$/u, '.$1ts'));
|
|
165
|
+
const source = [stem, `${stem}x`].find(candidate => existsSync(candidate));
|
|
166
|
+
return source === undefined ? undefined : this.display(source);
|
|
167
|
+
}
|
|
168
|
+
linkedWorkspacePackage(importer, packageName) {
|
|
169
|
+
let directory = dirname(resolve(importer));
|
|
170
|
+
for (;;) {
|
|
171
|
+
const link = join(directory, 'node_modules', packageName);
|
|
172
|
+
if (existsSync(link)) {
|
|
173
|
+
const target = this.canonical(link);
|
|
174
|
+
return this.isWorkspaceFile(join(target, 'package.json')) ? target : undefined;
|
|
175
|
+
}
|
|
176
|
+
if (this.canonical(directory) === this.realRoot || dirname(directory) === directory)
|
|
177
|
+
return undefined;
|
|
178
|
+
directory = dirname(directory);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
packageRoot(directory) {
|
|
182
|
+
if (this.packageRoots.has(directory))
|
|
183
|
+
return this.packageRoots.get(directory);
|
|
184
|
+
const found = existsSync(join(directory, 'package.json'))
|
|
185
|
+
? directory
|
|
186
|
+
: directory === this.realRoot || dirname(directory) === directory
|
|
187
|
+
? undefined
|
|
188
|
+
: this.packageRoot(dirname(directory));
|
|
189
|
+
this.packageRoots.set(directory, found);
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=build-output-guard.js.map
|
package/lib/types/emitter.d.ts
CHANGED
|
@@ -44,6 +44,9 @@ export declare class FaceModelEmitter {
|
|
|
44
44
|
private renderJs;
|
|
45
45
|
private renderDts;
|
|
46
46
|
private emitRemote;
|
|
47
|
+
private remoteMethodTypes;
|
|
48
|
+
private remoteInputType;
|
|
49
|
+
private remoteInfoLiteral;
|
|
47
50
|
private invocationLiteral;
|
|
48
51
|
private renderRemoteDts;
|
|
49
52
|
private pushRemoteSignature;
|
package/lib/types/emitter.js
CHANGED
|
@@ -8,6 +8,7 @@ import { posix } from 'node:path';
|
|
|
8
8
|
import { GenMapping, addMapping, toEncodedMap } from '@jridgewell/gen-mapping';
|
|
9
9
|
import { TypeGraphRenderer } from "./renderer.js";
|
|
10
10
|
import { childTypeNodeIds } from "./model.js";
|
|
11
|
+
import { projectToolSchema, ToolSchemaError } from "./tool-schema.js";
|
|
11
12
|
/** Failure to project a modeled construct into an emitted artifact. */
|
|
12
13
|
export class TypertEmitError extends Error {
|
|
13
14
|
name = 'TypertEmitError';
|
|
@@ -34,8 +35,7 @@ export class FaceModelEmitter {
|
|
|
34
35
|
if (packageModel === undefined) {
|
|
35
36
|
throw new TypertEmitError(`typert emitter(${this.face.face}): package ${packageName} is not modeled on this face`);
|
|
36
37
|
}
|
|
37
|
-
const
|
|
38
|
-
const schemaArtifact = schemas.emit();
|
|
38
|
+
const schemaArtifact = invocationSchemas(this.renderer, packageModel.schemas, invocationBoundaryRoots(packageModel.invocations));
|
|
39
39
|
const runtimeModel = this.runtimeModel(packageModel);
|
|
40
40
|
const js = this.renderJs(packageModel, schemaArtifact, runtimeModel);
|
|
41
41
|
const dts = this.renderDts(packageModel, schemaArtifact);
|
|
@@ -109,6 +109,9 @@ export class FaceModelEmitter {
|
|
|
109
109
|
];
|
|
110
110
|
if (schemas.definitions.length > 0)
|
|
111
111
|
lines.push('import { z } from \'zod\'', '');
|
|
112
|
+
if (packageModel.invocations.length > 0) {
|
|
113
|
+
lines.push("import { createTypertRemoteInfo } from '@deepseek-ai/dsh-typert-protocol/remote-info'", '');
|
|
114
|
+
}
|
|
112
115
|
lines.push(...schemas.definitions);
|
|
113
116
|
if (schemas.definitions.length > 0)
|
|
114
117
|
lines.push('');
|
|
@@ -116,6 +119,8 @@ export class FaceModelEmitter {
|
|
|
116
119
|
lines.push(`export const ${schema.exportName} = ${schema.internalName}`);
|
|
117
120
|
if (schemas.exports.length > 0)
|
|
118
121
|
lines.push('');
|
|
122
|
+
if (packageModel.invocations.length > 0)
|
|
123
|
+
lines.push(`const $remote = ${this.remoteInfoLiteral(packageModel, schemas)}`, '');
|
|
119
124
|
const model = JSON.stringify(runtimeModel, null, 2);
|
|
120
125
|
lines.push('export const TYPERT = {');
|
|
121
126
|
lines.push(` package: ${quote(packageModel.name)},`);
|
|
@@ -125,11 +130,10 @@ export class FaceModelEmitter {
|
|
|
125
130
|
lines.push(` { name: ${quote(schema.exportName)}, create: ${schema.exportName} },`);
|
|
126
131
|
}
|
|
127
132
|
lines.push(' ],');
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
lines.push(' ],');
|
|
133
|
+
if (packageModel.invocations.length > 0)
|
|
134
|
+
lines.push(' get invocations() { return $remote.descriptors },');
|
|
135
|
+
else
|
|
136
|
+
lines.push(' invocations: [', ' ],');
|
|
133
137
|
lines.push(` model: ${indent(model, 2).trimStart()},`);
|
|
134
138
|
lines.push('}');
|
|
135
139
|
return `${lines.join('\n')}\n`;
|
|
@@ -163,38 +167,53 @@ export class FaceModelEmitter {
|
|
|
163
167
|
return `${lines.join('\n')}\n`;
|
|
164
168
|
}
|
|
165
169
|
emitRemote(packageModel) {
|
|
166
|
-
const schemas =
|
|
170
|
+
const schemas = invocationSchemas(this.renderer, [], invocationBoundaryRoots(packageModel.invocations));
|
|
167
171
|
const lines = [
|
|
168
172
|
'/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */',
|
|
169
173
|
];
|
|
170
174
|
if (schemas.definitions.length > 0)
|
|
171
175
|
lines.push('import { z } from \'zod\'', '');
|
|
176
|
+
lines.push("import { createTypertRemoteInfo } from '@deepseek-ai/dsh-typert-protocol/remote-info'", '');
|
|
172
177
|
lines.push(...schemas.definitions);
|
|
173
178
|
if (schemas.definitions.length > 0)
|
|
174
179
|
lines.push('');
|
|
175
|
-
lines.push(
|
|
176
|
-
lines.push(` package: ${quote(packageModel.name)},`);
|
|
177
|
-
lines.push(' descriptors: [');
|
|
178
|
-
for (const invocation of packageModel.invocations) {
|
|
179
|
-
lines.push(`${indent(this.invocationLiteral(invocation, schemas), 4)},`);
|
|
180
|
-
}
|
|
181
|
-
lines.push(' ],');
|
|
182
|
-
lines.push('}');
|
|
183
|
-
lines.push('');
|
|
180
|
+
lines.push(`export const TYPERT_REMOTE = ${this.remoteInfoLiteral(packageModel, schemas)}`, '');
|
|
184
181
|
lines.push('export default TYPERT_REMOTE');
|
|
185
|
-
const declaration = this.renderRemoteDts(packageModel);
|
|
186
182
|
return {
|
|
187
183
|
js: `${lines.join('\n')}\n`,
|
|
188
|
-
...
|
|
184
|
+
...this.renderRemoteDts(packageModel),
|
|
189
185
|
};
|
|
190
186
|
}
|
|
187
|
+
remoteMethodTypes(packageModel) {
|
|
188
|
+
const names = allocateRemoteImportNames(remoteImports(packageModel.invocations));
|
|
189
|
+
const entries = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'direct').map((invocation) => {
|
|
190
|
+
const input = this.remoteInputType(invocation, names);
|
|
191
|
+
const result = this.renderer.renderType(invocation.result.type, names);
|
|
192
|
+
const uplink = invocation.uplink === undefined ? '' : `, ${this.renderer.renderType(invocation.uplink.boundary.type, names)}`;
|
|
193
|
+
const mode = invocation.mode === 'stream' ? `, ${quote('stream')}${uplink}`
|
|
194
|
+
: containsBytes(this.renderer, invocation.result.codecType) ? `, 'unary', never, RemoteDecoded<${result}>` : '';
|
|
195
|
+
return ` readonly ${quote(`${invocation.namespace}/${invocation.method}`)}: TypertRemoteMethod<${input}, ${result}${mode}>`;
|
|
196
|
+
});
|
|
197
|
+
return `{\n${entries.join('\n')}\n}`;
|
|
198
|
+
}
|
|
199
|
+
remoteInputType(invocation, names) {
|
|
200
|
+
return `{ ${invocation.parameters.map(parameter => `readonly ${quote(parameter.wire)}${parameter.boundary.acceptsUndefined ? '?' : ''}: ${this.renderer.renderType(parameter.boundary.type, names)}`).join('; ')} }`;
|
|
201
|
+
}
|
|
202
|
+
remoteInfoLiteral(packageModel, schemas) {
|
|
203
|
+
return [
|
|
204
|
+
'createTypertRemoteInfo({',
|
|
205
|
+
` package: ${quote(packageModel.name)},`,
|
|
206
|
+
' methods: {',
|
|
207
|
+
...packageModel.invocations.map(invocation => ` ${quote(`${invocation.namespace}/${invocation.method}`)}: ${indent(this.invocationLiteral(invocation, schemas), 4).trimStart()},`),
|
|
208
|
+
' },',
|
|
209
|
+
'}, z)',
|
|
210
|
+
].join('\n');
|
|
211
|
+
}
|
|
191
212
|
invocationLiteral(invocation, schemas) {
|
|
192
213
|
const lines = [
|
|
193
214
|
'{',
|
|
194
|
-
` id: ${quote(invocation.id)},`,
|
|
195
215
|
` service: ${quote(invocation.service)},`,
|
|
196
|
-
`
|
|
197
|
-
` method: ${quote(invocation.method)},`,
|
|
216
|
+
...(invocation.description === undefined ? [] : [` description: ${quote(invocation.description)},`]),
|
|
198
217
|
];
|
|
199
218
|
if (invocation.implementation !== undefined) {
|
|
200
219
|
lines.push(` implementation: ${quote(invocation.implementation)},`);
|
|
@@ -222,13 +241,18 @@ export class FaceModelEmitter {
|
|
|
222
241
|
invocation.parameters.forEach((parameter, index) => {
|
|
223
242
|
lines.push(' {');
|
|
224
243
|
lines.push(` name: ${quote(parameter.name)},`);
|
|
225
|
-
|
|
244
|
+
if (parameter.wire !== parameter.name)
|
|
245
|
+
lines.push(` wire: ${quote(parameter.wire)},`);
|
|
246
|
+
if (parameter.description !== undefined)
|
|
247
|
+
lines.push(` description: ${quote(parameter.description)},`);
|
|
226
248
|
lines.push(` source: ${quote(parameter.source)},`);
|
|
227
249
|
if (parameter.lookup !== undefined)
|
|
228
250
|
lines.push(` lookup: ${quote(parameter.lookup)},`);
|
|
229
251
|
if (parameter.boundary.acceptsUndefined)
|
|
230
252
|
lines.push(' acceptsUndefined: true,');
|
|
231
|
-
|
|
253
|
+
for (const strict of [false, true]) {
|
|
254
|
+
lines.push(` ${strict ? 'strictCodec' : 'codec'}: ${indent(strictCodec(parameter.boundary, schemas.boundary(parameterBoundaryKey(invocation, index, strict)), undefined, toolProjection(() => projectToolSchema(this.renderer, parameter.boundary.codecType, strict, parameter.boundary.acceptsUndefined))), 6).trimStart()},`);
|
|
255
|
+
}
|
|
232
256
|
lines.push(' },');
|
|
233
257
|
});
|
|
234
258
|
lines.push(' ],');
|
|
@@ -240,7 +264,8 @@ export class FaceModelEmitter {
|
|
|
240
264
|
if (invocation.cancellation !== undefined) {
|
|
241
265
|
lines.push(" cancellation: { parameter: 'signal' },");
|
|
242
266
|
}
|
|
243
|
-
|
|
267
|
+
const returns = invocation.tags.find(tag => tag.name === 'returns' || tag.name === 'return')?.comment?.replace(/^[-—–]\s*/, '');
|
|
268
|
+
lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation)), schemas.encoder(resultBoundaryKey(invocation)), toolProjection(() => projectToolSchema(this.renderer, invocation.result.codecType)), returns), 2).trimStart()},`);
|
|
244
269
|
lines.push(` sourceLocation: ${JSON.stringify(invocation.location)},`);
|
|
245
270
|
lines.push('}');
|
|
246
271
|
return lines.join('\n');
|
|
@@ -262,9 +287,11 @@ export class FaceModelEmitter {
|
|
|
262
287
|
const lines = [
|
|
263
288
|
'/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */',
|
|
264
289
|
'import type {',
|
|
290
|
+
...packageModel.invocations.some(invocation => containsBytes(this.renderer, invocation.result.codecType)) ? [' RemoteDecoded,'] : [],
|
|
265
291
|
' RemoteResult,',
|
|
266
292
|
...packageModel.invocations.some(invocation => invocation.mode === 'stream') ? [' RemoteStreamHandle,'] : [],
|
|
267
|
-
'
|
|
293
|
+
' TypertRemoteInfo,',
|
|
294
|
+
...packageModel.invocations.some(invocation => invocation.invocation.kind === 'direct') ? [' TypertRemoteMethod,'] : [],
|
|
268
295
|
'} from \'@deepseek-ai/dsh-typert-protocol\'',
|
|
269
296
|
];
|
|
270
297
|
const sourceMap = new GenMapping({ file: 'typert.remote-client.d.ts' });
|
|
@@ -273,12 +300,6 @@ export class FaceModelEmitter {
|
|
|
273
300
|
lines.push(`import type { ${names.join(', ')} } from ${quote(specifier)}`);
|
|
274
301
|
}
|
|
275
302
|
lines.push('');
|
|
276
|
-
if (packageModel.invocations.some(invocation => containsBytes(this.renderer, invocation.result.codecType))) {
|
|
277
|
-
lines.push('type RemoteDecoded<Value> = Value extends Uint8Array ? Uint8Array<ArrayBuffer>');
|
|
278
|
-
lines.push(' : Value extends string | number | boolean | null | undefined ? Value');
|
|
279
|
-
lines.push(' : { [Key in keyof Value]: RemoteDecoded<Value[Key]> }');
|
|
280
|
-
lines.push('');
|
|
281
|
-
}
|
|
282
303
|
lines.push('declare module \'@deepseek-ai/dsh-typert-protocol\' {');
|
|
283
304
|
const direct = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'direct');
|
|
284
305
|
const scoped = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'context' || invocation.scope !== undefined);
|
|
@@ -310,7 +331,7 @@ export class FaceModelEmitter {
|
|
|
310
331
|
}
|
|
311
332
|
lines.push('}');
|
|
312
333
|
lines.push('');
|
|
313
|
-
lines.push(
|
|
334
|
+
lines.push(`export declare const TYPERT_REMOTE: TypertRemoteInfo<${this.remoteMethodTypes(packageModel)}>`);
|
|
314
335
|
lines.push('export default TYPERT_REMOTE');
|
|
315
336
|
lines.push('//# sourceMappingURL=typert.remote-client.d.ts.map');
|
|
316
337
|
return {
|
|
@@ -385,6 +406,27 @@ function uniqueNamespaces(invocations) {
|
|
|
385
406
|
function remoteNamespaceInterface(namespace) {
|
|
386
407
|
return `TypertRemoteNamespace$${Buffer.from(namespace, 'utf8').toString('hex')}`;
|
|
387
408
|
}
|
|
409
|
+
function toolProjection(project) {
|
|
410
|
+
try {
|
|
411
|
+
return `jsonSchema: ${JSON.stringify(project())}`;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
if (!(error instanceof ToolSchemaError))
|
|
415
|
+
throw error;
|
|
416
|
+
return `jsonSchemaError: ${quote(error.message)}`;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function invocationSchemas(renderer, schemas, roots) {
|
|
420
|
+
const identifiers = new Set();
|
|
421
|
+
const ordinary = new SchemaEmitter(renderer, schemas, roots.filter(root => !root.closedObjects), {}, identifiers).emit();
|
|
422
|
+
const closedRoots = roots.filter(root => root.closedObjects);
|
|
423
|
+
const closed = new SchemaEmitter(renderer, [], closedRoots, { closedObjects: true }, identifiers).emit();
|
|
424
|
+
const keys = new Set(closedRoots.map(root => root.key));
|
|
425
|
+
return { definitions: [...ordinary.definitions, ...closed.definitions], exports: ordinary.exports,
|
|
426
|
+
boundary: key => keys.has(key) ? closed.boundary(key) : ordinary.boundary(key),
|
|
427
|
+
encoder: key => keys.has(key) ? closed.encoder(key) : ordinary.encoder(key),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
388
430
|
class ResultEncoderEmitter {
|
|
389
431
|
renderer;
|
|
390
432
|
names = new Map();
|
|
@@ -504,13 +546,15 @@ class SchemaEmitter {
|
|
|
504
546
|
renderer;
|
|
505
547
|
schemas;
|
|
506
548
|
boundaries;
|
|
549
|
+
options;
|
|
507
550
|
names = new Map();
|
|
508
551
|
boundaryNames = new Map();
|
|
509
552
|
declarations;
|
|
510
|
-
constructor(renderer, schemas, boundaries) {
|
|
553
|
+
constructor(renderer, schemas, boundaries, options = {}, identifiers = new Set()) {
|
|
511
554
|
this.renderer = renderer;
|
|
512
555
|
this.schemas = schemas;
|
|
513
556
|
this.boundaries = boundaries;
|
|
557
|
+
this.options = options;
|
|
514
558
|
const declarations = new Map();
|
|
515
559
|
for (const schema of schemas) {
|
|
516
560
|
for (const declaration of renderer.declarationClosureForTypes([schema.type])) {
|
|
@@ -523,9 +567,8 @@ class SchemaEmitter {
|
|
|
523
567
|
}
|
|
524
568
|
}
|
|
525
569
|
this.declarations = renderer.graph.declarations.filter(declaration => declarations.has(declaration.id));
|
|
526
|
-
const identifiers = new Set();
|
|
527
570
|
for (const declaration of this.declarations) {
|
|
528
|
-
const base = `${safeIdentifier(declaration.name)}$schema`;
|
|
571
|
+
const base = `${options.closedObjects ? 'closed$' : ''}${safeIdentifier(declaration.name)}$schema`;
|
|
529
572
|
let name = base;
|
|
530
573
|
let suffix = 2;
|
|
531
574
|
while (identifiers.has(name))
|
|
@@ -552,7 +595,7 @@ class SchemaEmitter {
|
|
|
552
595
|
if (encode !== undefined)
|
|
553
596
|
encoders.set(boundary.key, encode);
|
|
554
597
|
const name = this.boundaryName(boundary.key);
|
|
555
|
-
definitions.push(
|
|
598
|
+
definitions.push(this.cachedFactory(name, this.typeSchema(boundary.type)));
|
|
556
599
|
}
|
|
557
600
|
definitions.push(...encoder.definitions());
|
|
558
601
|
const exports = this.schemas.map((model) => ({
|
|
@@ -570,12 +613,15 @@ class SchemaEmitter {
|
|
|
570
613
|
declarationDefinition(declaration) {
|
|
571
614
|
const name = this.schemaName(declaration.id);
|
|
572
615
|
if (declaration.typeParameters.length === 0) {
|
|
573
|
-
return
|
|
616
|
+
return this.cachedFactory(name, this.declarationSchema(declaration, new Map()));
|
|
574
617
|
}
|
|
575
618
|
const parameters = declaration.typeParameters.map((parameter, index) => [`type${String(index)}$schema`, parameter.id]);
|
|
576
619
|
const substitutions = new Map(parameters.map(([schema, id]) => [id, schema]));
|
|
577
620
|
return `const ${name} = (${parameters.map(([schema]) => schema).join(', ')}) => ${this.declarationSchema(declaration, substitutions)}`;
|
|
578
621
|
}
|
|
622
|
+
cachedFactory(name, expression) {
|
|
623
|
+
return `let ${name}$value\nconst ${name} = () => (${name}$value ??= ${expression})`;
|
|
624
|
+
}
|
|
579
625
|
declarationSchema(declaration, substitutions) {
|
|
580
626
|
if (declaration.kind === 'enum') {
|
|
581
627
|
this.fail(declaration.name, 'enum declarations have no Zod projection');
|
|
@@ -594,6 +640,8 @@ class SchemaEmitter {
|
|
|
594
640
|
}
|
|
595
641
|
typeSchema(id, substitutions = new Map()) {
|
|
596
642
|
const node = this.renderer.node(id);
|
|
643
|
+
if (node.json)
|
|
644
|
+
return 'z.json()';
|
|
597
645
|
switch (node.kind) {
|
|
598
646
|
case 'keyword': return this.keywordSchema(node.name);
|
|
599
647
|
case 'literal': return `z.literal(${node.text})`;
|
|
@@ -715,6 +763,8 @@ class SchemaEmitter {
|
|
|
715
763
|
objectSchema(members, subject, substitutions) {
|
|
716
764
|
const properties = [];
|
|
717
765
|
const indices = [];
|
|
766
|
+
let stringIndexValue;
|
|
767
|
+
let stringIndexType;
|
|
718
768
|
let symbolMembers = 0;
|
|
719
769
|
for (const member of members) {
|
|
720
770
|
if (member.static || member.visibility !== 'public')
|
|
@@ -731,29 +781,75 @@ class SchemaEmitter {
|
|
|
731
781
|
if (member.signature.parameters.length !== 1 || parameter === undefined) {
|
|
732
782
|
this.fail(subject, 'index signature must have exactly one key parameter');
|
|
733
783
|
}
|
|
734
|
-
|
|
784
|
+
const key = this.typeSchema(parameter.type, substitutions);
|
|
785
|
+
const value = this.typeSchema(member.signature.returns, substitutions);
|
|
786
|
+
if (key === 'z.string()') {
|
|
787
|
+
stringIndexValue = value;
|
|
788
|
+
stringIndexType = member.signature.returns;
|
|
789
|
+
}
|
|
790
|
+
indices.push(this.readonly(`z.record(${key}, ${value})`, member.readonly));
|
|
735
791
|
continue;
|
|
736
792
|
}
|
|
737
793
|
if (member.kind !== 'property')
|
|
738
794
|
this.fail(subject, `${member.kind} member ${member.name} is not data-schema projectable`);
|
|
739
|
-
|
|
740
|
-
const schema = this.readonly(this.typeSchema(member.type, substitutions), member.readonly && !containsBytes(this.renderer, member.type, false));
|
|
741
|
-
const property = this.describe(this.optional(schema, member.optional), member);
|
|
742
|
-
properties.push(`${quote(member.jsonName ?? member.name)}: ${property}`);
|
|
795
|
+
properties.push(member);
|
|
743
796
|
}
|
|
744
797
|
if (indices.length > 1)
|
|
745
798
|
this.fail(subject, 'object type has more than one JSON index signature');
|
|
746
799
|
// A unique-symbol-only object is a compile-time marker and imposes no JSON shape.
|
|
747
800
|
if (properties.length === 0 && indices.length === 0 && symbolMembers > 0)
|
|
748
801
|
return 'z.unknown()';
|
|
749
|
-
const
|
|
802
|
+
const fields = properties.map((member) => {
|
|
803
|
+
// Freezing a union may freeze its populated Uint8Array member.
|
|
804
|
+
const type = this.options.closedObjects && stringIndexType !== undefined
|
|
805
|
+
? this.indexedPropertySchema(member.type, stringIndexType, substitutions)
|
|
806
|
+
: this.typeSchema(member.type, substitutions);
|
|
807
|
+
const schema = this.readonly(type, member.readonly && !containsBytes(this.renderer, member.type, false));
|
|
808
|
+
return `${quote(member.jsonName ?? member.name)}: ${this.describe(this.optional(schema, member.optional), member)}`;
|
|
809
|
+
});
|
|
810
|
+
const object = `z.${this.options.closedObjects && indices.length === 0 ? 'strictObject' : 'object'}({${fields.length === 0 ? '' : `\n${fields.map(field => ` ${field},`).join('\n')}\n`}})`;
|
|
750
811
|
const index = indices[0];
|
|
751
812
|
if (index === undefined)
|
|
752
813
|
return object;
|
|
753
814
|
if (properties.length === 0)
|
|
754
815
|
return index;
|
|
816
|
+
// Named keys enforce both their declared type and the index value; the catchall checks other keys.
|
|
817
|
+
if (this.options.closedObjects && stringIndexValue !== undefined)
|
|
818
|
+
return `${object}.catchall(${stringIndexValue})`;
|
|
755
819
|
return `z.intersection(${object}, ${index})`;
|
|
756
820
|
}
|
|
821
|
+
indexedPropertySchema(declared, indexed, substitutions) {
|
|
822
|
+
if (declared === indexed)
|
|
823
|
+
return this.typeSchema(declared, substitutions);
|
|
824
|
+
const own = this.renderer.node(declared);
|
|
825
|
+
const value = this.renderer.node(indexed);
|
|
826
|
+
if (own.kind === 'union') {
|
|
827
|
+
return `z.union([${own.types.map(type => this.indexedPropertySchema(type, indexed, substitutions)).join(', ')}])`;
|
|
828
|
+
}
|
|
829
|
+
if (value.kind === 'union') {
|
|
830
|
+
return `z.union([${value.types.map(type => this.indexedPropertySchema(declared, type, substitutions)).join(', ')}])`;
|
|
831
|
+
}
|
|
832
|
+
const ownProperties = own.kind === 'object'
|
|
833
|
+
? own.members.filter((member) => member.kind === 'property') : [];
|
|
834
|
+
const valueProperties = value.kind === 'object'
|
|
835
|
+
? value.members.filter((member) => member.kind === 'property') : [];
|
|
836
|
+
if (own.kind === 'object' && value.kind === 'object' && !own.json && !value.json
|
|
837
|
+
&& ownProperties.length === own.members.length && valueProperties.length === value.members.length
|
|
838
|
+
&& [...ownProperties, ...valueProperties].every(member => !member.static
|
|
839
|
+
&& member.visibility === 'public' && member.computed === undefined)) {
|
|
840
|
+
const indexedFields = new Map(valueProperties.map(member => [member.jsonName ?? member.name, member]));
|
|
841
|
+
const overlaps = ownProperties.flatMap((member) => {
|
|
842
|
+
const inherited = indexedFields.get(member.jsonName ?? member.name);
|
|
843
|
+
if (inherited === undefined)
|
|
844
|
+
return [];
|
|
845
|
+
const combined = this.indexedPropertySchema(member.type, inherited.type, substitutions);
|
|
846
|
+
const schema = this.readonly(this.optional(combined, member.optional && inherited.optional), (member.readonly || inherited.readonly) && !containsBytes(this.renderer, member.type, false));
|
|
847
|
+
return [`${quote(member.jsonName ?? member.name)}: ${this.describe(schema, member)}`];
|
|
848
|
+
});
|
|
849
|
+
return `z.strictObject({ ...${this.typeSchema(indexed, substitutions)}.shape, ...${this.typeSchema(declared, substitutions)}.shape${overlaps.length === 0 ? '' : `, ${overlaps.join(', ')}`} })`;
|
|
850
|
+
}
|
|
851
|
+
return `z.intersection(${this.typeSchema(declared, substitutions)}, ${this.typeSchema(indexed, substitutions)})`;
|
|
852
|
+
}
|
|
757
853
|
exportSchemaName(model) {
|
|
758
854
|
const name = this.schemaName(model.symbol);
|
|
759
855
|
const declaration = this.renderer.declaration(model.symbol);
|
|
@@ -822,6 +918,7 @@ function invocationBoundaryRoots(invocations) {
|
|
|
822
918
|
}
|
|
823
919
|
invocation.parameters.forEach((parameter, index) => {
|
|
824
920
|
result.push({ key: parameterBoundaryKey(invocation, index), type: parameter.boundary.codecType });
|
|
921
|
+
result.push({ key: parameterBoundaryKey(invocation, index, true), type: parameter.boundary.codecType, closedObjects: true });
|
|
825
922
|
});
|
|
826
923
|
if (invocation.uplink !== undefined) {
|
|
827
924
|
result.push({ key: uplinkBoundaryKey(invocation), type: invocation.uplink.boundary.codecType });
|
|
@@ -836,8 +933,8 @@ function contextBoundaryKey(invocation) {
|
|
|
836
933
|
function uplinkBoundaryKey(invocation) {
|
|
837
934
|
return `${invocation.id}:uplink`;
|
|
838
935
|
}
|
|
839
|
-
function parameterBoundaryKey(invocation, index) {
|
|
840
|
-
return `${invocation.id}:parameter:${String(index)}`;
|
|
936
|
+
function parameterBoundaryKey(invocation, index, strict = false) {
|
|
937
|
+
return `${invocation.id}:parameter:${String(index)}${strict ? ':strict' : ''}`;
|
|
841
938
|
}
|
|
842
939
|
function resultBoundaryKey(invocation) {
|
|
843
940
|
return `${invocation.id}:result`;
|
|
@@ -865,12 +962,14 @@ function containsBytes(renderer, id, nested = true, visited = new Set()) {
|
|
|
865
962
|
return true;
|
|
866
963
|
return childTypeNodeIds(node).some(child => containsBytes(renderer, child, nested, visited));
|
|
867
964
|
}
|
|
868
|
-
function strictCodec(boundary, schema, encoder) {
|
|
965
|
+
function strictCodec(boundary, schema, encoder, projection, description) {
|
|
869
966
|
return [
|
|
870
967
|
'{',
|
|
871
968
|
' mode: \'strict\',',
|
|
872
969
|
` typeSymbol: ${quote(boundary.typeSymbol)},`,
|
|
873
970
|
` create: ${schema},`,
|
|
971
|
+
...(description === undefined ? [] : [` description: ${quote(description)},`]),
|
|
972
|
+
...(projection === undefined ? [] : [` ${projection},`]),
|
|
874
973
|
...(encoder === undefined ? [] : [
|
|
875
974
|
` decode: value => ${schema}().parse(value),`,
|
|
876
975
|
` encode: (value, writeBytes) => ${encoder}(value, writeBytes, [], new Map()),`,
|
|
@@ -906,7 +1005,8 @@ function allocateRemoteImportNames(imports) {
|
|
|
906
1005
|
'RemoteDecoded',
|
|
907
1006
|
'RemoteResult',
|
|
908
1007
|
'RemoteStreamHandle',
|
|
909
|
-
'
|
|
1008
|
+
'TypertRemoteInfo',
|
|
1009
|
+
'TypertRemoteMethod',
|
|
910
1010
|
'TYPERT_REMOTE',
|
|
911
1011
|
]);
|
|
912
1012
|
const names = new Map();
|
package/lib/types/index.d.ts
CHANGED
|
@@ -10,7 +10,8 @@ export { FaceModelEmitter, TypertEmitError } from './emitter.ts';
|
|
|
10
10
|
export type { ModelEmitResult } from './emitter.ts';
|
|
11
11
|
export * from './cordis-catalog.ts';
|
|
12
12
|
export { TypeGraphRenderer, TypeGraphRenderError } from './renderer.ts';
|
|
13
|
-
export { WorkspaceTypertGenerator } from './workspace.ts';
|
|
14
|
-
export type { WorkspaceEmitResult } from './workspace.ts';
|
|
13
|
+
export { findWorkspaceRoot, WorkspaceTypertGenerator } from './workspace.ts';
|
|
14
|
+
export type { WorkspaceEmitResult, WorkspaceTypertGeneratorOptions } from './workspace.ts';
|
|
15
|
+
export { writePackageArtifacts, writeWorkspaceArtifacts } from './artifact-files.ts';
|
|
15
16
|
export type * from './model.ts';
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.js
CHANGED
|
@@ -8,5 +8,6 @@ export { WorkspaceAnalyzer, WorkspaceCaches, TypertAnalysisError } from "./analy
|
|
|
8
8
|
export { FaceModelEmitter, TypertEmitError } from "./emitter.js";
|
|
9
9
|
export * from "./cordis-catalog.js";
|
|
10
10
|
export { TypeGraphRenderer, TypeGraphRenderError } from "./renderer.js";
|
|
11
|
-
export { WorkspaceTypertGenerator } from "./workspace.js";
|
|
11
|
+
export { findWorkspaceRoot, WorkspaceTypertGenerator } from "./workspace.js";
|
|
12
|
+
export { writePackageArtifacts, writeWorkspaceArtifacts } from "./artifact-files.js";
|
|
12
13
|
//# sourceMappingURL=index.js.map
|