@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
package/lib/types/model.d.ts
CHANGED
|
@@ -95,9 +95,10 @@ export interface InvocationParameterModel {
|
|
|
95
95
|
/** Authored as an optional parameter, so consumers may omit the wire field. */
|
|
96
96
|
readonly optional?: true;
|
|
97
97
|
readonly boundary: RemoteBoundaryModel;
|
|
98
|
+
readonly description?: string;
|
|
98
99
|
}
|
|
99
100
|
/** One strictly analyzed Host method exported through Typert Gateway. */
|
|
100
|
-
export interface InvocationModel {
|
|
101
|
+
export interface InvocationModel extends DocumentationModel {
|
|
101
102
|
readonly id: string;
|
|
102
103
|
readonly service: string;
|
|
103
104
|
readonly namespace: string;
|
|
@@ -302,7 +303,7 @@ export interface TemplateSpanModel {
|
|
|
302
303
|
readonly text: string;
|
|
303
304
|
}
|
|
304
305
|
/** Compiler-independent TypeScript type expression. */
|
|
305
|
-
export type TypeNodeModel = {
|
|
306
|
+
export type TypeNodeModel = ({
|
|
306
307
|
readonly id: TypeNodeId;
|
|
307
308
|
readonly kind: 'keyword';
|
|
308
309
|
readonly name: KeywordTypeName;
|
|
@@ -403,6 +404,9 @@ export type TypeNodeModel = {
|
|
|
403
404
|
} | {
|
|
404
405
|
readonly id: TypeNodeId;
|
|
405
406
|
readonly kind: 'this';
|
|
407
|
+
}) & {
|
|
408
|
+
/** Canonical lossless JsonValue, represented without recursive tool-schema references. */
|
|
409
|
+
readonly json?: true;
|
|
406
410
|
};
|
|
407
411
|
/**
|
|
408
412
|
* Return the direct type-expression edges owned by one node.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Lossless projection into the harness tool-schema subset. */
|
|
2
|
+
import type { TypeNodeId } from './model.ts';
|
|
3
|
+
import { TypeGraphRenderer } from './renderer.ts';
|
|
4
|
+
/** A resolved codec whose type the tool-schema subset cannot represent without weakening constraints. */
|
|
5
|
+
export declare class ToolSchemaError extends Error {
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Project one resolved type into the tool-schema subset that `@deepseek-ai/dsh-tools`
|
|
10
|
+
* enforces on tool parameters, tool output, and the generated PTC SDK. A type the
|
|
11
|
+
* subset cannot express without weakening constraints raises {@link ToolSchemaError},
|
|
12
|
+
* which callers report as an absent projection rather than a narrower schema.
|
|
13
|
+
* @param renderer - complete analyzed type graph.
|
|
14
|
+
* @param type - resolved wire type.
|
|
15
|
+
* @param closedObjects - close declared request objects with `additionalProperties: false`.
|
|
16
|
+
* @param optional - omit a top-level undefined alternative supplied by an optional argument.
|
|
17
|
+
* @returns an acyclic schema in the tool-schema subset.
|
|
18
|
+
*/
|
|
19
|
+
export declare function projectToolSchema(renderer: TypeGraphRenderer, type: TypeNodeId, closedObjects?: boolean, optional?: boolean): Record<string, unknown>;
|
|
20
|
+
//# sourceMappingURL=tool-schema.d.ts.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Lossless projection into the harness tool-schema subset. */
|
|
2
|
+
/** A resolved codec whose type the tool-schema subset cannot represent without weakening constraints. */
|
|
3
|
+
export class ToolSchemaError extends Error {
|
|
4
|
+
name = 'ToolSchemaError';
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Project one resolved type into the tool-schema subset that `@deepseek-ai/dsh-tools`
|
|
8
|
+
* enforces on tool parameters, tool output, and the generated PTC SDK. A type the
|
|
9
|
+
* subset cannot express without weakening constraints raises {@link ToolSchemaError},
|
|
10
|
+
* which callers report as an absent projection rather than a narrower schema.
|
|
11
|
+
* @param renderer - complete analyzed type graph.
|
|
12
|
+
* @param type - resolved wire type.
|
|
13
|
+
* @param closedObjects - close declared request objects with `additionalProperties: false`.
|
|
14
|
+
* @param optional - omit a top-level undefined alternative supplied by an optional argument.
|
|
15
|
+
* @returns an acyclic schema in the tool-schema subset.
|
|
16
|
+
*/
|
|
17
|
+
export function projectToolSchema(renderer, type, closedObjects = false, optional = false) {
|
|
18
|
+
const active = new Set();
|
|
19
|
+
const fail = (subject) => { throw new ToolSchemaError(`typert tool schema: ${subject}`); };
|
|
20
|
+
const project = (id, optional = false) => {
|
|
21
|
+
const node = renderer.node(id);
|
|
22
|
+
if (node.json)
|
|
23
|
+
return {};
|
|
24
|
+
if (active.has(id))
|
|
25
|
+
return fail('recursive selected type requires a supported JSON value declaration');
|
|
26
|
+
active.add(id);
|
|
27
|
+
try {
|
|
28
|
+
switch (node.kind) {
|
|
29
|
+
case 'keyword': {
|
|
30
|
+
if (['string', 'number', 'boolean'].includes(node.name))
|
|
31
|
+
return { type: node.name };
|
|
32
|
+
return fail(`unsupported ${node.name} value`);
|
|
33
|
+
}
|
|
34
|
+
case 'literal': {
|
|
35
|
+
if (typeof node.value === 'bigint')
|
|
36
|
+
return fail('bigint is not JSON');
|
|
37
|
+
return { type: node.value === null ? 'null' : typeof node.value, const: node.value };
|
|
38
|
+
}
|
|
39
|
+
case 'parenthesized': return project(node.type, optional);
|
|
40
|
+
case 'array': return { type: 'array', items: project(node.element) };
|
|
41
|
+
case 'object': return object(node.members);
|
|
42
|
+
case 'reference': {
|
|
43
|
+
if (node.target.kind !== 'declaration' || node.arguments.length !== 0) {
|
|
44
|
+
return fail(`unsupported selected reference ${node.name}`);
|
|
45
|
+
}
|
|
46
|
+
const declaration = renderer.declaration(node.target.symbol);
|
|
47
|
+
const schema = declaration.kind === 'alias' && declaration.type !== undefined
|
|
48
|
+
? project(declaration.type, optional)
|
|
49
|
+
: object(declaration.members);
|
|
50
|
+
if (declaration.extends.length > 0)
|
|
51
|
+
return fail('unresolved inherited selected type');
|
|
52
|
+
return declaration.description === undefined ? schema : { ...schema, description: declaration.description };
|
|
53
|
+
}
|
|
54
|
+
case 'union': {
|
|
55
|
+
const branches = node.types.filter((item) => {
|
|
56
|
+
const branch = renderer.node(item);
|
|
57
|
+
return !(optional && branch.kind === 'keyword' && branch.name === 'undefined');
|
|
58
|
+
}).map(item => project(item));
|
|
59
|
+
return union(branches);
|
|
60
|
+
}
|
|
61
|
+
default: return fail(`unsupported selected ${node.kind} type`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
active.delete(id);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const object = (members) => {
|
|
69
|
+
const properties = {};
|
|
70
|
+
const required = [];
|
|
71
|
+
let indexed = false;
|
|
72
|
+
for (const member of members) {
|
|
73
|
+
if (member.static || member.visibility !== 'public' || member.computed === 'symbol')
|
|
74
|
+
continue;
|
|
75
|
+
if (member.kind === 'index') {
|
|
76
|
+
const key = member.signature.parameters[0];
|
|
77
|
+
if (key === undefined || project(key.type).type !== 'string'
|
|
78
|
+
|| Object.keys(project(member.signature.returns)).length !== 0) {
|
|
79
|
+
return fail('selected dictionaries require string keys and unconstrained JSON values');
|
|
80
|
+
}
|
|
81
|
+
indexed = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (member.kind !== 'property' || member.computed === 'dynamic')
|
|
85
|
+
return fail(`unsupported member ${member.name}`);
|
|
86
|
+
const name = member.jsonName ?? member.name;
|
|
87
|
+
const schema = project(member.type, member.optional);
|
|
88
|
+
properties[name] = member.description === undefined ? schema : { ...schema, description: member.description };
|
|
89
|
+
if (!member.optional)
|
|
90
|
+
required.push(name);
|
|
91
|
+
}
|
|
92
|
+
return { type: 'object', properties, required, additionalProperties: indexed || !closedObjects };
|
|
93
|
+
};
|
|
94
|
+
const union = (input) => {
|
|
95
|
+
if (input.some(branch => Object.keys(branch).length === 0))
|
|
96
|
+
return {};
|
|
97
|
+
const branches = [];
|
|
98
|
+
for (const branch of input) {
|
|
99
|
+
const scalar = branch.type !== undefined && !['object', 'array'].includes(branch.type);
|
|
100
|
+
const previous = scalar ? branches.find(candidate => candidate.type === branch.type) : undefined;
|
|
101
|
+
if (previous === undefined) {
|
|
102
|
+
branches.push(branch);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const values = literals(previous);
|
|
106
|
+
const next = literals(branch);
|
|
107
|
+
if (values === undefined || next === undefined) {
|
|
108
|
+
delete previous.const;
|
|
109
|
+
delete previous.enum;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
delete previous.const;
|
|
113
|
+
previous.enum = [...new Set([...values, ...next])];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (branches.length === 1)
|
|
117
|
+
return branches[0];
|
|
118
|
+
if (branches.length === 0)
|
|
119
|
+
return fail('empty selected union');
|
|
120
|
+
for (const [index, left] of branches.entries()) {
|
|
121
|
+
for (const right of branches.slice(index + 1)) {
|
|
122
|
+
if (!disjoint(left, right))
|
|
123
|
+
return fail('selected union branches overlap or have no required discriminator');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { oneOf: branches };
|
|
127
|
+
};
|
|
128
|
+
return project(type, optional);
|
|
129
|
+
}
|
|
130
|
+
function literals(schema) {
|
|
131
|
+
if (schema.const !== undefined)
|
|
132
|
+
return [schema.const];
|
|
133
|
+
return schema.enum;
|
|
134
|
+
}
|
|
135
|
+
function disjoint(left, right) {
|
|
136
|
+
if (left.oneOf !== undefined)
|
|
137
|
+
return left.oneOf.every(branch => disjoint(branch, right));
|
|
138
|
+
if (right.oneOf !== undefined)
|
|
139
|
+
return right.oneOf.every(branch => disjoint(left, branch));
|
|
140
|
+
if (left.type !== right.type)
|
|
141
|
+
return left.type !== undefined && right.type !== undefined;
|
|
142
|
+
if (left.type !== 'object' || right.type !== 'object')
|
|
143
|
+
return false;
|
|
144
|
+
return (left.required ?? []).some((name) => {
|
|
145
|
+
if (!right.required?.includes(name))
|
|
146
|
+
return false;
|
|
147
|
+
const a = left.properties?.[name];
|
|
148
|
+
const b = right.properties?.[name];
|
|
149
|
+
if (a === undefined || b === undefined)
|
|
150
|
+
return false;
|
|
151
|
+
const first = literals(a);
|
|
152
|
+
const second = literals(b);
|
|
153
|
+
return first !== undefined && second !== undefined && first.every(value => !second.includes(value));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=tool-schema.js.map
|
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
* Optional tsdown (rolldown) plugin face of the typert generator. It lowers
|
|
3
3
|
* standard decorators in TypeScript dependencies before bundling, then emits
|
|
4
4
|
* model-driven face artifacts at the package output root. Packages without a
|
|
5
|
-
* Typert or Remote export are skipped.
|
|
5
|
+
* Typert or Remote export are skipped. Workspace mode generates in a Worker
|
|
6
|
+
* Thread started by the pass's first `buildStart`, so bundling continues on the
|
|
7
|
+
* main thread while Typert analyzes; the pass's first `writeBundle` awaits the
|
|
8
|
+
* result and writes every artifact once.
|
|
6
9
|
* @module @deepseek-ai/dsh-typert-generator/tsdown
|
|
7
10
|
*/
|
|
8
11
|
import type { TypertFace } from './model.ts';
|
|
9
12
|
/** The subset of the rolldown plugin contract used here (structural; avoids a rolldown type dependency). */
|
|
10
13
|
interface TypertPlugin {
|
|
11
14
|
name: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
buildStart: (options: {
|
|
16
|
+
cwd: string;
|
|
17
|
+
}) => void;
|
|
18
|
+
transform: {
|
|
19
|
+
filter: {
|
|
20
|
+
id: RegExp;
|
|
21
|
+
};
|
|
22
|
+
handler: (code: string, id: string) => {
|
|
23
|
+
code: string;
|
|
24
|
+
map: string | undefined;
|
|
25
|
+
} | undefined;
|
|
26
|
+
};
|
|
16
27
|
writeBundle: (options: {
|
|
17
28
|
dir?: string;
|
|
18
|
-
}) => void
|
|
29
|
+
}) => Promise<void>;
|
|
19
30
|
}
|
|
20
31
|
/** Generation scope selected by a tsdown build phase. */
|
|
21
32
|
export interface TypertPluginOptions {
|
|
@@ -24,6 +35,10 @@ export interface TypertPluginOptions {
|
|
|
24
35
|
/** Independent TypeScript program faces included in this phase. */
|
|
25
36
|
readonly faces?: readonly TypertFace[];
|
|
26
37
|
}
|
|
38
|
+
/** A workspace-mode generation failure reported by, or observed on, the Typert Worker Thread. */
|
|
39
|
+
export declare class TypertWorkerError extends Error {
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
27
42
|
/**
|
|
28
43
|
* Create the decorator-lowering and typert-generation plugin for the root tsdown config.
|
|
29
44
|
* @param pluginOptions - package/workspace emission mode and independent program faces.
|
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
* Optional tsdown (rolldown) plugin face of the typert generator. It lowers
|
|
3
3
|
* standard decorators in TypeScript dependencies before bundling, then emits
|
|
4
4
|
* model-driven face artifacts at the package output root. Packages without a
|
|
5
|
-
* Typert or Remote export are skipped.
|
|
5
|
+
* Typert or Remote export are skipped. Workspace mode generates in a Worker
|
|
6
|
+
* Thread started by the pass's first `buildStart`, so bundling continues on the
|
|
7
|
+
* main thread while Typert analyzes; the pass's first `writeBundle` awaits the
|
|
8
|
+
* result and writes every artifact once.
|
|
6
9
|
* @module @deepseek-ai/dsh-typert-generator/tsdown
|
|
7
10
|
*/
|
|
8
|
-
import { existsSync,
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
12
|
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { Worker } from 'node:worker_threads';
|
|
10
14
|
import ts from 'typescript';
|
|
11
|
-
import {
|
|
15
|
+
import { writePackageArtifacts, writeWorkspaceArtifacts } from "./artifact-files.js";
|
|
16
|
+
import { findWorkspaceRoot, hasTypertExport, WorkspaceTypertGenerator } from "./workspace.js";
|
|
17
|
+
/** A module id whose path (before any `?query`) names a TypeScript source. */
|
|
18
|
+
const TYPESCRIPT_MODULE = /^[^?]*\.[cm]?tsx?(?:\?|$)/;
|
|
12
19
|
const DECORATOR_SYNTAX = /^\s*@[A-Za-z_$][\w$]*/m;
|
|
13
20
|
// Full builds and typecheck own TypeScript diagnostics. Benchmark-only emission
|
|
14
21
|
// defers them to the required full-build CI jobs; Typert generates reflection
|
|
15
22
|
// without repeating those diagnostics.
|
|
16
23
|
const TSC_OWNED_DIAGNOSTICS = { checkDiagnostics: false };
|
|
24
|
+
/** A workspace-mode generation failure reported by, or observed on, the Typert Worker Thread. */
|
|
25
|
+
export class TypertWorkerError extends Error {
|
|
26
|
+
name = 'TypertWorkerError';
|
|
27
|
+
}
|
|
17
28
|
/**
|
|
18
29
|
* Create the decorator-lowering and typert-generation plugin for the root tsdown config.
|
|
19
30
|
* @param pluginOptions - package/workspace emission mode and independent program faces.
|
|
@@ -21,39 +32,68 @@ const TSC_OWNED_DIAGNOSTICS = { checkDiagnostics: false };
|
|
|
21
32
|
*/
|
|
22
33
|
export function typertPlugin(pluginOptions = {}) {
|
|
23
34
|
const artifactsByRoot = new Map();
|
|
35
|
+
const generations = new Map();
|
|
24
36
|
const emittedWorkspaces = new Set();
|
|
37
|
+
const workspaceGeneration = (root) => {
|
|
38
|
+
let generation = generations.get(root);
|
|
39
|
+
if (generation === undefined) {
|
|
40
|
+
generation = generateInWorker({
|
|
41
|
+
root,
|
|
42
|
+
...(pluginOptions.faces === undefined ? {} : { faces: pluginOptions.faces }),
|
|
43
|
+
options: TSC_OWNED_DIAGNOSTICS,
|
|
44
|
+
});
|
|
45
|
+
generations.set(root, generation);
|
|
46
|
+
}
|
|
47
|
+
return generation;
|
|
48
|
+
};
|
|
25
49
|
return {
|
|
26
50
|
name: 'dsh-typert-generator',
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
fileName: file,
|
|
33
|
-
compilerOptions: {
|
|
34
|
-
target: ts.ScriptTarget.ES2024,
|
|
35
|
-
module: ts.ModuleKind.ESNext,
|
|
36
|
-
...(file.endsWith('x') ? { jsx: ts.JsxEmit.ReactJSX } : {}),
|
|
37
|
-
sourceMap: true,
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
return {
|
|
41
|
-
code: result.outputText.replace(/\n?\/\/# sourceMappingURL=.*$/u, '\n'),
|
|
42
|
-
map: result.sourceMapText,
|
|
43
|
-
};
|
|
51
|
+
buildStart(inputOptions) {
|
|
52
|
+
// tsdown builds every workspace package concurrently through this one
|
|
53
|
+
// plugin instance; the first package build to start launches generation.
|
|
54
|
+
if (pluginOptions.mode === 'workspace')
|
|
55
|
+
void workspaceGeneration(findWorkspaceRoot(inputOptions.cwd));
|
|
44
56
|
},
|
|
45
|
-
|
|
57
|
+
transform: {
|
|
58
|
+
// Rolldown evaluates the id filter natively, so non-TypeScript modules
|
|
59
|
+
// never cross into this JavaScript hook.
|
|
60
|
+
filter: { id: TYPESCRIPT_MODULE },
|
|
61
|
+
handler(code, id) {
|
|
62
|
+
if (!TYPESCRIPT_MODULE.test(id) || !DECORATOR_SYNTAX.test(code))
|
|
63
|
+
return;
|
|
64
|
+
const file = id.replace(/\?.*$/su, '');
|
|
65
|
+
const result = ts.transpileModule(code, {
|
|
66
|
+
fileName: file,
|
|
67
|
+
compilerOptions: {
|
|
68
|
+
target: ts.ScriptTarget.ES2024,
|
|
69
|
+
module: ts.ModuleKind.ESNext,
|
|
70
|
+
...(file.endsWith('x') ? { jsx: ts.JsxEmit.ReactJSX } : {}),
|
|
71
|
+
sourceMap: true,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
code: result.outputText.replace(/\n?\/\/# sourceMappingURL=.*$/u, '\n'),
|
|
76
|
+
map: result.sourceMapText,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
async writeBundle(bundleOptions) {
|
|
46
81
|
// options.dir is the package's absolute outDir (<package>/lib); its
|
|
47
82
|
// nearest package.json owns the bundle even when a custom config writes
|
|
48
83
|
// a nested output such as <package>/lib/dev.
|
|
49
84
|
if (bundleOptions.dir === undefined)
|
|
50
85
|
return;
|
|
51
|
-
const root =
|
|
86
|
+
const root = findWorkspaceRoot(bundleOptions.dir);
|
|
52
87
|
if (emittedWorkspaces.has(root))
|
|
53
88
|
return;
|
|
54
89
|
if (pluginOptions.mode === 'workspace') {
|
|
55
|
-
|
|
90
|
+
// The first bundle to finish claims emission and reports any failure;
|
|
91
|
+
// later bundles of the pass return without waiting.
|
|
56
92
|
emittedWorkspaces.add(root);
|
|
93
|
+
const outcome = await workspaceGeneration(root);
|
|
94
|
+
if (!outcome.ok)
|
|
95
|
+
throw outcome.error;
|
|
96
|
+
writeWorkspaceArtifacts(root, outcome.artifacts);
|
|
57
97
|
return;
|
|
58
98
|
}
|
|
59
99
|
const packageDir = packageRoot(bundleOptions.dir, root);
|
|
@@ -70,53 +110,122 @@ export function typertPlugin(pluginOptions = {}) {
|
|
|
70
110
|
: generator.generate(undefined, pluginOptions.faces);
|
|
71
111
|
artifactsByRoot.set(root, artifacts);
|
|
72
112
|
}
|
|
73
|
-
|
|
113
|
+
writePackageArtifacts(packageDir, artifacts.filter(candidate => candidate.package === manifest.name));
|
|
74
114
|
},
|
|
75
115
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run workspace generation in a Worker Thread. The outcome never rejects, and
|
|
119
|
+
* every terminal path terminates the worker before the outcome settles.
|
|
120
|
+
*/
|
|
121
|
+
function generateInWorker(request) {
|
|
122
|
+
const worker = spawnWorker(request);
|
|
123
|
+
return new Promise((settle) => {
|
|
124
|
+
let settled = false;
|
|
125
|
+
const finish = (outcome) => {
|
|
126
|
+
if (settled)
|
|
127
|
+
return;
|
|
128
|
+
settled = true;
|
|
129
|
+
worker.terminate().then(() => { settle(outcome); }, (error) => { settle({ ok: false, error: workerError(request, 'could not terminate its worker', error) }); });
|
|
130
|
+
};
|
|
131
|
+
const fail = (problem, cause) => {
|
|
132
|
+
finish({ ok: false, error: workerError(request, problem, cause) });
|
|
133
|
+
};
|
|
134
|
+
worker.once('message', (value) => {
|
|
135
|
+
const response = parseWorkerResponse(value);
|
|
136
|
+
if (response === undefined) {
|
|
137
|
+
fail('received a malformed worker response');
|
|
138
|
+
}
|
|
139
|
+
else if (response.kind === 'failure') {
|
|
140
|
+
const cause = new Error(response.message);
|
|
141
|
+
cause.name = response.name;
|
|
142
|
+
if (response.stack !== undefined)
|
|
143
|
+
cause.stack = response.stack;
|
|
144
|
+
fail(`failed: ${response.name}: ${response.message}`, cause);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
finish({ ok: true, artifacts: response.artifacts });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
worker.once('messageerror', (error) => { fail('received a worker response that could not be deserialized', error); });
|
|
151
|
+
worker.once('error', (error) => { fail(`crashed: ${error.message}`, error); });
|
|
152
|
+
worker.once('exit', (code) => { fail(`stopped: its worker exited with code ${String(code)} before reporting artifacts`); });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function workerError(request, problem, cause) {
|
|
156
|
+
return new TypertWorkerError(`typert-generator: workspace generation for ${request.root} ${problem}`, cause === undefined ? undefined : { cause });
|
|
157
|
+
}
|
|
158
|
+
function spawnWorker(request) {
|
|
159
|
+
/* v8 ignore next 3 -- the tsdown host build runs the built worker entry. */
|
|
160
|
+
if (!import.meta.url.endsWith('.ts')) {
|
|
161
|
+
return new Worker(new URL('./worker.js', import.meta.url), { workerData: request });
|
|
86
162
|
}
|
|
163
|
+
// Source runs load the TypeScript worker entry through tsx.
|
|
164
|
+
const bootstrap = [
|
|
165
|
+
`import { register } from ${JSON.stringify(import.meta.resolve('tsx/esm/api'))}`,
|
|
166
|
+
'register()',
|
|
167
|
+
`await import(${JSON.stringify(new URL('./worker.ts', import.meta.url).href)})`,
|
|
168
|
+
].join('\n');
|
|
169
|
+
return new Worker(new URL(`data:text/javascript,${encodeURIComponent(bootstrap)}`), { workerData: request, execArgv: [] });
|
|
87
170
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
171
|
+
function parseWorkerResponse(value) {
|
|
172
|
+
if (!isRecord(value))
|
|
173
|
+
return undefined;
|
|
174
|
+
if (value.kind === 'failure') {
|
|
175
|
+
if (typeof value.name !== 'string' || typeof value.message !== 'string'
|
|
176
|
+
|| (value.stack !== undefined && typeof value.stack !== 'string'))
|
|
177
|
+
return undefined;
|
|
178
|
+
return {
|
|
179
|
+
kind: 'failure',
|
|
180
|
+
name: value.name,
|
|
181
|
+
message: value.message,
|
|
182
|
+
...(value.stack === undefined ? {} : { stack: value.stack }),
|
|
183
|
+
};
|
|
101
184
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
185
|
+
if (value.kind !== 'artifacts' || !Array.isArray(value.artifacts))
|
|
186
|
+
return undefined;
|
|
187
|
+
const artifacts = [];
|
|
188
|
+
for (const artifact of value.artifacts) {
|
|
189
|
+
const parsed = parseArtifact(artifact);
|
|
190
|
+
if (parsed === undefined)
|
|
191
|
+
return undefined;
|
|
192
|
+
artifacts.push(parsed);
|
|
109
193
|
}
|
|
194
|
+
return { kind: 'artifacts', artifacts };
|
|
110
195
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
196
|
+
function parseArtifact(value) {
|
|
197
|
+
if (!isRecord(value)
|
|
198
|
+
|| typeof value.package !== 'string'
|
|
199
|
+
|| typeof value.packageRoot !== 'string'
|
|
200
|
+
|| (value.face !== 'host' && value.face !== 'client')
|
|
201
|
+
|| !Array.isArray(value.exports)
|
|
202
|
+
|| !value.exports.every((entry) => typeof entry === 'string')
|
|
203
|
+
|| typeof value.js !== 'string'
|
|
204
|
+
|| typeof value.dts !== 'string')
|
|
205
|
+
return undefined;
|
|
206
|
+
const remote = parseRemoteArtifact(value.remote);
|
|
207
|
+
if (value.remote !== undefined && remote === undefined)
|
|
208
|
+
return undefined;
|
|
209
|
+
return {
|
|
210
|
+
package: value.package,
|
|
211
|
+
packageRoot: value.packageRoot,
|
|
212
|
+
face: value.face,
|
|
213
|
+
exports: value.exports,
|
|
214
|
+
js: value.js,
|
|
215
|
+
dts: value.dts,
|
|
216
|
+
...(remote === undefined ? {} : { remote }),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function parseRemoteArtifact(value) {
|
|
220
|
+
if (!isRecord(value)
|
|
221
|
+
|| typeof value.js !== 'string'
|
|
222
|
+
|| typeof value.dts !== 'string'
|
|
223
|
+
|| typeof value.dtsMap !== 'string')
|
|
224
|
+
return undefined;
|
|
225
|
+
return { js: value.js, dts: value.dts, dtsMap: value.dtsMap };
|
|
113
226
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
return false;
|
|
117
|
-
return Object.hasOwn(exportsField, './typert')
|
|
118
|
-
|| Object.hasOwn(exportsField, './client/typert')
|
|
119
|
-
|| Object.hasOwn(exportsField, './remote');
|
|
227
|
+
function isRecord(value) {
|
|
228
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
120
229
|
}
|
|
121
230
|
function packageRoot(start, workspace) {
|
|
122
231
|
let current = resolve(start);
|
|
@@ -127,14 +236,4 @@ function packageRoot(start, workspace) {
|
|
|
127
236
|
}
|
|
128
237
|
return undefined;
|
|
129
238
|
}
|
|
130
|
-
function workspaceRoot(start) {
|
|
131
|
-
let current = resolve(start);
|
|
132
|
-
while (!existsSync(join(current, 'tsconfig.host.json'))) {
|
|
133
|
-
const parent = dirname(current);
|
|
134
|
-
if (parent === current)
|
|
135
|
-
throw new Error(`typert-generator: cannot find workspace root above ${start}`);
|
|
136
|
-
current = parent;
|
|
137
|
-
}
|
|
138
|
-
return current;
|
|
139
|
-
}
|
|
140
239
|
//# sourceMappingURL=tsdown-plugin.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Thread entry for workspace-mode Typert generation. It generates every
|
|
3
|
+
* explicit contributor of the requested workspace, posts exactly one
|
|
4
|
+
* {@link WorkspaceWorkerResponse}, and exits.
|
|
5
|
+
* @module @deepseek-ai/dsh-typert-generator/worker
|
|
6
|
+
*/
|
|
7
|
+
import type { WorkspaceEmitResult, WorkspaceTypertGeneratorOptions } from './workspace.ts';
|
|
8
|
+
import type { TypertFace } from './model.ts';
|
|
9
|
+
/** Generation inputs passed as `workerData`. */
|
|
10
|
+
export interface WorkspaceWorkerRequest {
|
|
11
|
+
/** Workspace root containing the face aggregate tsconfigs. */
|
|
12
|
+
readonly root: string;
|
|
13
|
+
/** Faces to generate; every face when absent. */
|
|
14
|
+
readonly faces?: readonly TypertFace[];
|
|
15
|
+
/** Generator switches applied to the pass. */
|
|
16
|
+
readonly options: WorkspaceTypertGeneratorOptions;
|
|
17
|
+
}
|
|
18
|
+
/** The single message the worker posts: every generated artifact, or the failure that stopped generation. */
|
|
19
|
+
export type WorkspaceWorkerResponse = {
|
|
20
|
+
readonly kind: 'artifacts';
|
|
21
|
+
readonly artifacts: readonly WorkspaceEmitResult[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'failure';
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly stack?: string;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Thread entry for workspace-mode Typert generation. It generates every
|
|
3
|
+
* explicit contributor of the requested workspace, posts exactly one
|
|
4
|
+
* {@link WorkspaceWorkerResponse}, and exits.
|
|
5
|
+
* @module @deepseek-ai/dsh-typert-generator/worker
|
|
6
|
+
*/
|
|
7
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
8
|
+
import { WorkspaceTypertGenerator } from "./workspace.js";
|
|
9
|
+
function parseRequest(value) {
|
|
10
|
+
const request = typeof value === 'object' && value !== null
|
|
11
|
+
? value
|
|
12
|
+
: {};
|
|
13
|
+
const { root, faces, options } = request;
|
|
14
|
+
const checkDiagnostics = typeof options === 'object' && options !== null
|
|
15
|
+
? options.checkDiagnostics
|
|
16
|
+
: null;
|
|
17
|
+
if (typeof root !== 'string'
|
|
18
|
+
|| (faces !== undefined
|
|
19
|
+
&& (!Array.isArray(faces) || !faces.every((face) => face === 'host' || face === 'client')))
|
|
20
|
+
|| (checkDiagnostics !== undefined && typeof checkDiagnostics !== 'boolean')) {
|
|
21
|
+
throw new TypeError('typert-generator: malformed workspace worker request');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
root,
|
|
25
|
+
...(faces === undefined ? {} : { faces }),
|
|
26
|
+
options: checkDiagnostics === undefined ? {} : { checkDiagnostics },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function respond() {
|
|
30
|
+
try {
|
|
31
|
+
const request = parseRequest(workerData);
|
|
32
|
+
const generator = new WorkspaceTypertGenerator(request.root, request.options);
|
|
33
|
+
return { kind: 'artifacts', artifacts: generator.generateContributors(request.faces) };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (!(error instanceof Error))
|
|
37
|
+
return { kind: 'failure', name: 'Error', message: String(error) };
|
|
38
|
+
return {
|
|
39
|
+
kind: 'failure',
|
|
40
|
+
name: error.name,
|
|
41
|
+
message: error.message,
|
|
42
|
+
...(error.stack === undefined ? {} : { stack: error.stack }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (parentPort === null)
|
|
47
|
+
throw new Error('typert-generator: the workspace worker entry must run in a Worker Thread');
|
|
48
|
+
parentPort.postMessage(respond());
|
|
49
|
+
//# sourceMappingURL=worker.js.map
|