@styx-api/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +7947 -0
- package/dist/index.d.cts +1143 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1143 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +7877 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
- package/src/backend/backend.ts +95 -0
- package/src/backend/boutiques/boutiques.ts +1049 -0
- package/src/backend/boutiques/index.ts +1 -0
- package/src/backend/code-builder.ts +49 -0
- package/src/backend/collect-field-info.ts +50 -0
- package/src/backend/collect-named-types.ts +103 -0
- package/src/backend/collect-output-fields.ts +222 -0
- package/src/backend/find-doc.ts +38 -0
- package/src/backend/find-struct-node.ts +66 -0
- package/src/backend/index.ts +39 -0
- package/src/backend/python/arg-builder.ts +454 -0
- package/src/backend/python/emit.ts +638 -0
- package/src/backend/python/index.ts +9 -0
- package/src/backend/python/outputs-emit.ts +430 -0
- package/src/backend/python/packaging.ts +173 -0
- package/src/backend/python/python.ts +558 -0
- package/src/backend/python/snippet.ts +84 -0
- package/src/backend/python/typemap.ts +131 -0
- package/src/backend/python/types.ts +8 -0
- package/src/backend/python/validate-emit.ts +356 -0
- package/src/backend/resolve-field-binding.ts +41 -0
- package/src/backend/resolve-output-tokens.ts +80 -0
- package/src/backend/schema/index.ts +2 -0
- package/src/backend/schema/jsonschema.ts +303 -0
- package/src/backend/scope.ts +50 -0
- package/src/backend/sig-entries.ts +97 -0
- package/src/backend/snippet-core.ts +185 -0
- package/src/backend/string-case.ts +30 -0
- package/src/backend/styxdefs-compat.ts +21 -0
- package/src/backend/type-keys.ts +52 -0
- package/src/backend/typescript/arg-builder.ts +420 -0
- package/src/backend/typescript/emit.ts +450 -0
- package/src/backend/typescript/index.ts +10 -0
- package/src/backend/typescript/outputs-emit.ts +389 -0
- package/src/backend/typescript/packaging.ts +130 -0
- package/src/backend/typescript/snippet.ts +60 -0
- package/src/backend/typescript/typemap.ts +47 -0
- package/src/backend/typescript/types.ts +8 -0
- package/src/backend/typescript/typescript.ts +507 -0
- package/src/backend/typescript/validate-emit.ts +341 -0
- package/src/backend/union-variants.ts +42 -0
- package/src/backend/validate-walk.ts +111 -0
- package/src/bindings/binding.ts +77 -0
- package/src/bindings/format.ts +176 -0
- package/src/bindings/index.ts +16 -0
- package/src/bindings/output-gate.ts +50 -0
- package/src/bindings/resolved-output.ts +56 -0
- package/src/bindings/types.ts +16 -0
- package/src/frontend/argdump/index.ts +1 -0
- package/src/frontend/argdump/parser.ts +914 -0
- package/src/frontend/boutiques/destruct-template.ts +50 -0
- package/src/frontend/boutiques/index.ts +1 -0
- package/src/frontend/boutiques/parser.ts +676 -0
- package/src/frontend/boutiques/split-command.ts +69 -0
- package/src/frontend/detect-format.ts +42 -0
- package/src/frontend/frontend.ts +31 -0
- package/src/frontend/index.ts +9 -0
- package/src/frontend/workbench/index.ts +1 -0
- package/src/frontend/workbench/parser.ts +351 -0
- package/src/index.ts +41 -0
- package/src/ir/builders.ts +69 -0
- package/src/ir/format.ts +157 -0
- package/src/ir/index.ts +32 -0
- package/src/ir/meta.ts +91 -0
- package/src/ir/node.ts +95 -0
- package/src/ir/passes/canonicalize.ts +108 -0
- package/src/ir/passes/flatten.ts +73 -0
- package/src/ir/passes/index.ts +7 -0
- package/src/ir/passes/pass.ts +86 -0
- package/src/ir/passes/pipeline.ts +21 -0
- package/src/ir/passes/remove-empty.ts +76 -0
- package/src/ir/passes/simplify.ts +179 -0
- package/src/ir/types.ts +15 -0
- package/src/manifest/context.ts +36 -0
- package/src/manifest/index.ts +3 -0
- package/src/manifest/types.ts +15 -0
- package/src/solver/assign-access.ts +218 -0
- package/src/solver/index.ts +4 -0
- package/src/solver/resolve-outputs.ts +233 -0
- package/src/solver/solver.ts +319 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import type { BoundType } from "../../bindings/index.js";
|
|
2
|
+
import type { AppMeta } from "../../ir/index.js";
|
|
3
|
+
import type { CodegenContext, PackageMeta, ProjectMeta } from "../../manifest/index.js";
|
|
4
|
+
import type { AppEntrypoint, Backend, EmitResult, EmittedApp, EmittedPackage } from "../backend.js";
|
|
5
|
+
import type { SigEntry } from "../sig-entries.js";
|
|
6
|
+
import type { NamedType } from "./types.js";
|
|
7
|
+
import { CodeBuilder } from "../code-builder.js";
|
|
8
|
+
import { generatePackageJson, generateRootIndex, generateTsconfig } from "./packaging.js";
|
|
9
|
+
import { Scope } from "../scope.js";
|
|
10
|
+
import { camelCase, pascalCase, screamingSnakeCase, snakeCase } from "../string-case.js";
|
|
11
|
+
import { buildSigEntries } from "../sig-entries.js";
|
|
12
|
+
import {
|
|
13
|
+
emitBuildCargs,
|
|
14
|
+
emitImports,
|
|
15
|
+
emitKwargWrapper,
|
|
16
|
+
emitMetadata,
|
|
17
|
+
emitParamsFactory,
|
|
18
|
+
emitTypeDeclarations,
|
|
19
|
+
emitWrapperFunction,
|
|
20
|
+
tsScrubIdent,
|
|
21
|
+
tsSigOptions,
|
|
22
|
+
} from "./emit.js";
|
|
23
|
+
import { collectFieldInfo } from "./types.js";
|
|
24
|
+
import { emitValidate } from "./validate-emit.js";
|
|
25
|
+
import {
|
|
26
|
+
emitBuildOutputs,
|
|
27
|
+
emitOutputsInterface,
|
|
28
|
+
emitStripExtensionsHelper,
|
|
29
|
+
needsStripExtensionsHelper,
|
|
30
|
+
streamFieldIds,
|
|
31
|
+
} from "./outputs-emit.js";
|
|
32
|
+
import { mapType } from "./typemap.js";
|
|
33
|
+
import { collectNamedTypes, resolveTypeName, structKey, unionKey } from "./types.js";
|
|
34
|
+
|
|
35
|
+
const TS_RESERVED: ReadonlySet<string> = new Set([
|
|
36
|
+
"break",
|
|
37
|
+
"case",
|
|
38
|
+
"catch",
|
|
39
|
+
"class",
|
|
40
|
+
"const",
|
|
41
|
+
"continue",
|
|
42
|
+
"debugger",
|
|
43
|
+
"default",
|
|
44
|
+
"delete",
|
|
45
|
+
"do",
|
|
46
|
+
"else",
|
|
47
|
+
"enum",
|
|
48
|
+
"export",
|
|
49
|
+
"extends",
|
|
50
|
+
"false",
|
|
51
|
+
"finally",
|
|
52
|
+
"for",
|
|
53
|
+
"function",
|
|
54
|
+
"if",
|
|
55
|
+
"import",
|
|
56
|
+
"in",
|
|
57
|
+
"instanceof",
|
|
58
|
+
"new",
|
|
59
|
+
"null",
|
|
60
|
+
"return",
|
|
61
|
+
"super",
|
|
62
|
+
"switch",
|
|
63
|
+
"this",
|
|
64
|
+
"throw",
|
|
65
|
+
"true",
|
|
66
|
+
"try",
|
|
67
|
+
"typeof",
|
|
68
|
+
"undefined",
|
|
69
|
+
"var",
|
|
70
|
+
"void",
|
|
71
|
+
"while",
|
|
72
|
+
"with",
|
|
73
|
+
"yield",
|
|
74
|
+
"let",
|
|
75
|
+
"static",
|
|
76
|
+
"implements",
|
|
77
|
+
"interface",
|
|
78
|
+
"package",
|
|
79
|
+
"private",
|
|
80
|
+
"protected",
|
|
81
|
+
"public",
|
|
82
|
+
"type",
|
|
83
|
+
// Not keywords, but forbidden as binding names in strict mode (ES modules are
|
|
84
|
+
// always strict), so a parameter/local named these is a hard error.
|
|
85
|
+
"arguments",
|
|
86
|
+
"eval",
|
|
87
|
+
"await",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Per-tool public symbol names emitted directly in the flat tool file. Wrapper
|
|
92
|
+
* and function names use camelCase; type names use PascalCase; the metadata
|
|
93
|
+
* constant uses SCREAMING_SNAKE_CASE.
|
|
94
|
+
*/
|
|
95
|
+
export interface PublicNames {
|
|
96
|
+
params: string;
|
|
97
|
+
outputs: string;
|
|
98
|
+
metadata: string;
|
|
99
|
+
cargs: string;
|
|
100
|
+
outputsFn: string;
|
|
101
|
+
paramsFn: string;
|
|
102
|
+
execute: string;
|
|
103
|
+
validate: string;
|
|
104
|
+
wrapper: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Public-name scheme used by the TypeScript backend. Exported so the CLI and tests can use it. */
|
|
108
|
+
export function computePublicNames(appId: string | undefined): PublicNames {
|
|
109
|
+
if (!appId) {
|
|
110
|
+
return {
|
|
111
|
+
params: "Params",
|
|
112
|
+
outputs: "Outputs",
|
|
113
|
+
metadata: "METADATA",
|
|
114
|
+
cargs: "cargs",
|
|
115
|
+
outputsFn: "outputs",
|
|
116
|
+
paramsFn: "buildParams",
|
|
117
|
+
execute: "execute",
|
|
118
|
+
validate: "validate",
|
|
119
|
+
wrapper: "run",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Pre-scrub digit-leading ids so derived case forms produce valid
|
|
123
|
+
// identifiers in a consistent case.
|
|
124
|
+
const id = /^[0-9]/.test(appId) ? "v_" + appId : appId;
|
|
125
|
+
return {
|
|
126
|
+
params: pascalCase(id),
|
|
127
|
+
outputs: pascalCase(id) + "Outputs",
|
|
128
|
+
metadata: screamingSnakeCase(id) + "_METADATA",
|
|
129
|
+
cargs: camelCase(id) + "_cargs",
|
|
130
|
+
outputsFn: camelCase(id) + "_outputs",
|
|
131
|
+
paramsFn: camelCase(id) + "Params",
|
|
132
|
+
execute: camelCase(id) + "Execute",
|
|
133
|
+
validate: camelCase(id) + "Validate",
|
|
134
|
+
wrapper: camelCase(id),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The fully-derived naming/typing model for one tool's TypeScript emission.
|
|
140
|
+
* Computed once by `buildEmitModel` so the file emitter and the call-site snippet
|
|
141
|
+
* renderer share the exact same public names and root typing - the snippet must
|
|
142
|
+
* match the function the generated code actually exposes.
|
|
143
|
+
*/
|
|
144
|
+
export interface TsEmitModel {
|
|
145
|
+
appId: string | undefined;
|
|
146
|
+
pkg: string;
|
|
147
|
+
names: {
|
|
148
|
+
params: string;
|
|
149
|
+
outputs: string;
|
|
150
|
+
metadata: string;
|
|
151
|
+
cargs: string;
|
|
152
|
+
outputsFn: string;
|
|
153
|
+
paramsFn: string;
|
|
154
|
+
execute: string;
|
|
155
|
+
validate: string;
|
|
156
|
+
wrapper: string;
|
|
157
|
+
};
|
|
158
|
+
rootType: BoundType;
|
|
159
|
+
rootIsStruct: boolean;
|
|
160
|
+
namedTypes: Map<string, string>;
|
|
161
|
+
typeDecls: NamedType[];
|
|
162
|
+
rootTypeTag: string | undefined;
|
|
163
|
+
paramsType: string;
|
|
164
|
+
sigEntries: SigEntry[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Derive the public names, named-type declarations, root typing, and per-field
|
|
169
|
+
* signature entries for one tool. Mutates `scope` exactly as the emitter needs
|
|
170
|
+
* (the `reg` registrations and the `sigScope` child), so passing the same scope
|
|
171
|
+
* the emitter continues with keeps later local registrations consistent.
|
|
172
|
+
*/
|
|
173
|
+
export function buildEmitModel(
|
|
174
|
+
ctx: CodegenContext,
|
|
175
|
+
scope: Scope = new Scope(TS_RESERVED),
|
|
176
|
+
): TsEmitModel {
|
|
177
|
+
const appId = ctx.app?.id;
|
|
178
|
+
const pkg = ctx.package?.name ?? "unknown";
|
|
179
|
+
const publicNames = computePublicNames(appId);
|
|
180
|
+
|
|
181
|
+
const rootBinding = ctx.resolve(ctx.expr);
|
|
182
|
+
const rootType: BoundType = rootBinding?.type ?? { kind: "struct", fields: {} };
|
|
183
|
+
// Only treat the root as struct-shaped when there's a real binding. A
|
|
184
|
+
// synthesized empty-struct fallback (no root binding) means the solver
|
|
185
|
+
// collapsed everything away, so the kwarg wrapper has nothing to wrap.
|
|
186
|
+
const rootIsStruct = rootBinding?.type.kind === "struct";
|
|
187
|
+
|
|
188
|
+
// Pre-reserve module-level public names so any IR-derived names colliding
|
|
189
|
+
// with them get suffix-bumped. `params` is intentionally NOT pre-reserved -
|
|
190
|
+
// `collectNamedTypes` claims it for the root struct just below. Each name
|
|
191
|
+
// is scrubbed through `tsScrubIdent` first since the case helpers happily
|
|
192
|
+
// pass through digit-leading app ids.
|
|
193
|
+
const reg = (name: string) => scope.add(tsScrubIdent(name, TS_RESERVED));
|
|
194
|
+
const names = {
|
|
195
|
+
params: tsScrubIdent(publicNames.params, TS_RESERVED),
|
|
196
|
+
outputs: reg(publicNames.outputs),
|
|
197
|
+
metadata: reg(publicNames.metadata),
|
|
198
|
+
cargs: reg(publicNames.cargs),
|
|
199
|
+
outputsFn: reg(publicNames.outputsFn),
|
|
200
|
+
paramsFn: rootIsStruct ? reg(publicNames.paramsFn) : "",
|
|
201
|
+
execute: rootIsStruct ? reg(publicNames.execute) : "",
|
|
202
|
+
validate: reg(publicNames.validate),
|
|
203
|
+
wrapper: reg(publicNames.wrapper),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Prefix nested type names with the tool's root name so a suite's flat barrel
|
|
207
|
+
// doesn't collide same-named types (e.g. `Outputtype`) across tools.
|
|
208
|
+
const { namedTypes, typeDecls } = collectNamedTypes(
|
|
209
|
+
rootType,
|
|
210
|
+
names.params,
|
|
211
|
+
scope,
|
|
212
|
+
pascalCase,
|
|
213
|
+
appId ? names.params : "",
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
names.params =
|
|
217
|
+
(rootType.kind === "struct" ? namedTypes.get(structKey(rootType)) : undefined) ??
|
|
218
|
+
(rootType.kind === "union" ? namedTypes.get(unionKey(rootType)) : undefined) ??
|
|
219
|
+
names.params;
|
|
220
|
+
|
|
221
|
+
const paramsType =
|
|
222
|
+
rootType.kind === "struct" || rootType.kind === "union"
|
|
223
|
+
? names.params
|
|
224
|
+
: mapType(rootType, resolveTypeName(namedTypes));
|
|
225
|
+
|
|
226
|
+
// Build the per-field SigEntry list once - the factory and kwarg wrapper
|
|
227
|
+
// both consume it, so the host names registered here must satisfy both
|
|
228
|
+
// function scopes. Pre-reserve `params` (factory + wrapper body) and
|
|
229
|
+
// `runner` (wrapper signature) so a wire key matching either gets
|
|
230
|
+
// suffix-bumped. `rootType.kind === "struct"` check satisfies the `Extract`
|
|
231
|
+
// constraint when `rootIsStruct` is true.
|
|
232
|
+
const rootTypeTag = appId ? `${pkg}/${appId}` : undefined;
|
|
233
|
+
const sigScope = scope.child(["params", "runner"]);
|
|
234
|
+
const sigEntries =
|
|
235
|
+
rootIsStruct && rootType.kind === "struct"
|
|
236
|
+
? buildSigEntries(
|
|
237
|
+
rootType,
|
|
238
|
+
collectFieldInfo(ctx, rootType),
|
|
239
|
+
(wireKey) => sigScope.add(tsScrubIdent(wireKey, TS_RESERVED)),
|
|
240
|
+
tsSigOptions(resolveTypeName(namedTypes)),
|
|
241
|
+
)
|
|
242
|
+
: [];
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
appId,
|
|
246
|
+
pkg,
|
|
247
|
+
names,
|
|
248
|
+
rootType,
|
|
249
|
+
rootIsStruct,
|
|
250
|
+
namedTypes,
|
|
251
|
+
typeDecls,
|
|
252
|
+
rootTypeTag,
|
|
253
|
+
paramsType,
|
|
254
|
+
sigEntries,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function generateTypeScript(ctx: CodegenContext, packageScope?: Scope): string {
|
|
259
|
+
const cb = new CodeBuilder(" ");
|
|
260
|
+
// A package-shared scope keeps top-level names unique across every tool in the
|
|
261
|
+
// suite barrel; without one (standalone emit) a per-tool scope is enough.
|
|
262
|
+
const scope = packageScope ?? new Scope(TS_RESERVED);
|
|
263
|
+
|
|
264
|
+
const {
|
|
265
|
+
appId,
|
|
266
|
+
pkg,
|
|
267
|
+
names,
|
|
268
|
+
rootType,
|
|
269
|
+
rootIsStruct,
|
|
270
|
+
namedTypes,
|
|
271
|
+
typeDecls,
|
|
272
|
+
rootTypeTag,
|
|
273
|
+
paramsType,
|
|
274
|
+
sigEntries,
|
|
275
|
+
} = buildEmitModel(ctx, scope);
|
|
276
|
+
|
|
277
|
+
// Auto-generated header.
|
|
278
|
+
cb.comment("This file was auto generated by Styx.");
|
|
279
|
+
cb.comment("Do not edit this file directly.");
|
|
280
|
+
cb.blank();
|
|
281
|
+
|
|
282
|
+
// Every tool emits an Outputs object: at minimum the synthetic `root` output
|
|
283
|
+
// directory (outputFile(".")), plus any declared file/mutable outputs and
|
|
284
|
+
// stdout/stderr stream fields. OutputPathType is therefore always imported.
|
|
285
|
+
const emitOutputs = true;
|
|
286
|
+
|
|
287
|
+
emitImports(cb, true);
|
|
288
|
+
cb.blank();
|
|
289
|
+
|
|
290
|
+
emitMetadata(ctx, names.metadata, cb);
|
|
291
|
+
cb.blank();
|
|
292
|
+
|
|
293
|
+
emitTypeDeclarations(typeDecls, namedTypes, ctx, names.params, appId, pkg, cb);
|
|
294
|
+
|
|
295
|
+
if (emitOutputs) {
|
|
296
|
+
emitOutputsInterface(ctx, names.outputs, cb);
|
|
297
|
+
cb.blank();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (emitOutputs && needsStripExtensionsHelper(ctx)) {
|
|
301
|
+
emitStripExtensionsHelper(cb);
|
|
302
|
+
cb.blank();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Params factory (struct-rooted tools only): a kwarg-style builder for the
|
|
306
|
+
// params object. Useful for callers that want to build a params object to
|
|
307
|
+
// mutate before executing.
|
|
308
|
+
if (rootIsStruct) {
|
|
309
|
+
emitParamsFactory(sigEntries, names.paramsFn, paramsType, rootTypeTag, cb);
|
|
310
|
+
cb.blank();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Validation: walks the root binding and throws StyxValidationError on bad
|
|
314
|
+
// input. Called first thing in the dict-style execute (below).
|
|
315
|
+
emitValidate(
|
|
316
|
+
ctx,
|
|
317
|
+
rootType,
|
|
318
|
+
ctx.expr,
|
|
319
|
+
paramsType,
|
|
320
|
+
names.validate,
|
|
321
|
+
resolveTypeName(namedTypes),
|
|
322
|
+
scope,
|
|
323
|
+
cb,
|
|
324
|
+
);
|
|
325
|
+
cb.blank();
|
|
326
|
+
|
|
327
|
+
emitBuildCargs(ctx, rootType, paramsType, names.cargs, cb);
|
|
328
|
+
cb.blank();
|
|
329
|
+
|
|
330
|
+
if (emitOutputs) {
|
|
331
|
+
emitBuildOutputs(ctx, paramsType, names.outputs, names.outputsFn, cb);
|
|
332
|
+
cb.blank();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Dict-style execute function. For struct roots it's the internal
|
|
336
|
+
// `<tool>Execute`; for other roots it doubles as the user-facing wrapper.
|
|
337
|
+
const executeName = rootIsStruct ? names.execute : names.wrapper;
|
|
338
|
+
emitWrapperFunction(
|
|
339
|
+
ctx,
|
|
340
|
+
paramsType,
|
|
341
|
+
executeName,
|
|
342
|
+
names.metadata,
|
|
343
|
+
names.cargs,
|
|
344
|
+
emitOutputs ? names.outputsFn : undefined,
|
|
345
|
+
emitOutputs ? names.outputs : undefined,
|
|
346
|
+
names.validate,
|
|
347
|
+
streamFieldIds(ctx),
|
|
348
|
+
cb,
|
|
349
|
+
);
|
|
350
|
+
cb.blank();
|
|
351
|
+
|
|
352
|
+
// Kwarg-style wrapper (struct roots only): the v1-parity user-facing entry.
|
|
353
|
+
if (rootIsStruct) {
|
|
354
|
+
emitKwargWrapper(
|
|
355
|
+
ctx,
|
|
356
|
+
sigEntries,
|
|
357
|
+
names.wrapper,
|
|
358
|
+
names.paramsFn,
|
|
359
|
+
names.execute,
|
|
360
|
+
emitOutputs ? names.outputs : undefined,
|
|
361
|
+
cb,
|
|
362
|
+
);
|
|
363
|
+
cb.blank();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return cb.toString();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Module name (file stem) for an app: snake_case of app.id, fallback `output`.
|
|
371
|
+
* Scrubbed so digit-leading app ids (e.g. `3dPFM` -> `v_3d_pfm`) and keyword
|
|
372
|
+
* collisions don't break `export * from "./<mod>.js"` in the package index.
|
|
373
|
+
*/
|
|
374
|
+
export function appModuleName(meta: AppMeta | undefined): string {
|
|
375
|
+
if (!meta?.id) return "output";
|
|
376
|
+
return tsScrubIdent(snakeCase(meta.id), TS_RESERVED);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The dispatch entrypoint for one app: its root `@type` (`<package>/<app>`) and
|
|
381
|
+
* the dict-style execute function name. Returns undefined when the id or package
|
|
382
|
+
* is unknown (no stable `@type`), so the app is left out of the suite dispatcher.
|
|
383
|
+
*/
|
|
384
|
+
export function appEntrypoint(ctx: CodegenContext): AppEntrypoint | undefined {
|
|
385
|
+
const appId = ctx.app?.id;
|
|
386
|
+
const pkg = ctx.package?.name;
|
|
387
|
+
if (!appId || !pkg) return undefined;
|
|
388
|
+
const publicNames = computePublicNames(appId);
|
|
389
|
+
const rootIsStruct = ctx.resolve(ctx.expr)?.type.kind === "struct";
|
|
390
|
+
const executeFn = tsScrubIdent(
|
|
391
|
+
rootIsStruct ? publicNames.execute : publicNames.wrapper,
|
|
392
|
+
TS_RESERVED,
|
|
393
|
+
);
|
|
394
|
+
return { type: `${pkg}/${appId}`, executeFn };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Generate the suite-level `index.ts` re-export for a package containing
|
|
399
|
+
* multiple tool modules. Each tool module's public symbols are surfaced via
|
|
400
|
+
* `export * from "./bet.js"`. When apps carry a dispatch entrypoint, a
|
|
401
|
+
* suite-level `execute(params, runner)` is appended that routes a config object
|
|
402
|
+
* to the right tool by its root `@type`.
|
|
403
|
+
*/
|
|
404
|
+
export function generatePackageIndex(apps: EmittedApp[]): string {
|
|
405
|
+
const cb = new CodeBuilder(" ");
|
|
406
|
+
cb.comment("This file was auto generated by Styx.");
|
|
407
|
+
cb.comment("Do not edit this file directly.");
|
|
408
|
+
cb.blank();
|
|
409
|
+
|
|
410
|
+
const sortedApps = [...apps].sort((a, b) =>
|
|
411
|
+
appModuleName(a.meta).localeCompare(appModuleName(b.meta)),
|
|
412
|
+
);
|
|
413
|
+
const dispatch = sortedApps
|
|
414
|
+
.map((a) => ({ entry: a.entrypoint, mod: appModuleName(a.meta) }))
|
|
415
|
+
.filter((x): x is { entry: AppEntrypoint; mod: string } => x.entry !== undefined);
|
|
416
|
+
|
|
417
|
+
if (dispatch.length > 0) {
|
|
418
|
+
cb.line(`import type { Runner } from "styxdefs";`);
|
|
419
|
+
for (const d of dispatch) {
|
|
420
|
+
cb.line(`import { ${d.entry.executeFn} } from "./${d.mod}.js";`);
|
|
421
|
+
}
|
|
422
|
+
cb.blank();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const mod of sortedApps.map((a) => appModuleName(a.meta))) {
|
|
426
|
+
cb.line(`export * from "./${mod}.js";`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (dispatch.length > 0) {
|
|
430
|
+
cb.blank();
|
|
431
|
+
emitPackageDispatch(
|
|
432
|
+
cb,
|
|
433
|
+
dispatch.map((d) => d.entry),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return cb.toString();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Emit the suite-level `execute(params, runner)` dispatcher over `@type`. */
|
|
441
|
+
function emitPackageDispatch(cb: CodeBuilder, dispatch: AppEntrypoint[]): void {
|
|
442
|
+
cb.line("/**");
|
|
443
|
+
cb.line(" * Run a tool in this package from a params object, routed by its `@type`.");
|
|
444
|
+
cb.line(" */");
|
|
445
|
+
cb.line(
|
|
446
|
+
`export function execute(params: { "@type": string }, runner: Runner | null = null): unknown {`,
|
|
447
|
+
);
|
|
448
|
+
cb.indent(() => {
|
|
449
|
+
cb.line("const dispatch: Record<string, (params: any, runner: Runner | null) => unknown> = {");
|
|
450
|
+
cb.indent(() => {
|
|
451
|
+
for (const e of dispatch) {
|
|
452
|
+
cb.line(`${JSON.stringify(e.type)}: ${e.executeFn},`);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
cb.line("};");
|
|
456
|
+
cb.line(`const fn = dispatch[params["@type"]];`);
|
|
457
|
+
cb.line("if (fn === undefined) {");
|
|
458
|
+
cb.indent(() => {
|
|
459
|
+
cb.line("throw new Error(`No tool registered for @type '${params[\"@type\"]}'`);");
|
|
460
|
+
});
|
|
461
|
+
cb.line("}");
|
|
462
|
+
cb.line("return fn(params, runner);");
|
|
463
|
+
});
|
|
464
|
+
cb.line("}");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export class TypeScriptBackend implements Backend {
|
|
468
|
+
readonly name = "typescript";
|
|
469
|
+
readonly target = "typescript";
|
|
470
|
+
|
|
471
|
+
emitApp(ctx: CodegenContext, scope?: Scope): EmittedApp {
|
|
472
|
+
const code = generateTypeScript(ctx, scope);
|
|
473
|
+
const fileName = `${appModuleName(ctx.app)}.ts`;
|
|
474
|
+
return {
|
|
475
|
+
meta: ctx.app,
|
|
476
|
+
entrypoint: appEntrypoint(ctx),
|
|
477
|
+
files: new Map([[fileName, code]]),
|
|
478
|
+
errors: [],
|
|
479
|
+
warnings: [],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
newPackageScope(): Scope {
|
|
484
|
+
return new Scope(TS_RESERVED);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
emitPackage(pkg: PackageMeta, apps: EmittedApp[]): EmittedPackage {
|
|
488
|
+
return {
|
|
489
|
+
meta: pkg,
|
|
490
|
+
files: new Map([["index.ts", generatePackageIndex(apps)]]),
|
|
491
|
+
errors: [],
|
|
492
|
+
warnings: [],
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
emitProject(proj: ProjectMeta, packages: EmittedPackage[]): EmitResult {
|
|
497
|
+
return {
|
|
498
|
+
files: new Map([
|
|
499
|
+
["package.json", generatePackageJson(proj)],
|
|
500
|
+
["index.ts", generateRootIndex(packages)],
|
|
501
|
+
["tsconfig.json", generateTsconfig()],
|
|
502
|
+
]),
|
|
503
|
+
errors: [],
|
|
504
|
+
warnings: [],
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|