@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,389 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Binding,
|
|
3
|
+
BindingId,
|
|
4
|
+
BoundType,
|
|
5
|
+
GateAtom,
|
|
6
|
+
ResolvedToken,
|
|
7
|
+
} from "../../bindings/index.js";
|
|
8
|
+
import { outputGate } from "../../bindings/index.js";
|
|
9
|
+
import { collectFieldInfo } from "../collect-field-info.js";
|
|
10
|
+
import type { CodegenContext } from "../../manifest/index.js";
|
|
11
|
+
import { CodeBuilder } from "../code-builder.js";
|
|
12
|
+
import {
|
|
13
|
+
type EmittedOutput,
|
|
14
|
+
type OutputShape,
|
|
15
|
+
collectMutableOutputs,
|
|
16
|
+
collectOutputFields,
|
|
17
|
+
outputShape,
|
|
18
|
+
rootOutput,
|
|
19
|
+
streamFields,
|
|
20
|
+
} from "../collect-output-fields.js";
|
|
21
|
+
import { emitJsDoc, renderAccess, tsPropAccess } from "./emit.js";
|
|
22
|
+
import { renderTsLiteral } from "./typemap.js";
|
|
23
|
+
|
|
24
|
+
// The output-field/stream/mutable collection is language-agnostic and shared
|
|
25
|
+
// with the Python and JSON Schema backends; re-export the predicates the
|
|
26
|
+
// backend entry point consumes so its import surface stays `./outputs-emit.js`.
|
|
27
|
+
export { hasAnyOutputs, hasMutableInputs, hasStreamOutputs } from "../collect-output-fields.js";
|
|
28
|
+
|
|
29
|
+
function outputTypeExpr(shape: OutputShape): string {
|
|
30
|
+
if (shape.kind === "list") return "OutputPathType[]";
|
|
31
|
+
return shape.optional ? "OutputPathType | null" : "OutputPathType";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function initialValue(shape: OutputShape): string {
|
|
35
|
+
if (shape.kind === "list") return "[]";
|
|
36
|
+
// Required fields get a non-null assertion placeholder; the linear-flow
|
|
37
|
+
// assignment below makes the placeholder unobservable.
|
|
38
|
+
return shape.optional ? "null" : "null!";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Raw field names for stdout/stderr (in declaration order), for wrapper wiring. */
|
|
42
|
+
export function streamFieldIds(ctx: CodegenContext): { stdout?: string; stderr?: string } {
|
|
43
|
+
const fields = streamFields(ctx, jsId);
|
|
44
|
+
const res: { stdout?: string; stderr?: string } = {};
|
|
45
|
+
let idx = 0;
|
|
46
|
+
if (ctx.app?.stdout) res.stdout = fields[idx++]!.name;
|
|
47
|
+
if (ctx.app?.stderr) res.stderr = fields[idx++]!.name;
|
|
48
|
+
return res;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Emit the `export interface <outputsType> { ... }` declaration. */
|
|
52
|
+
export function emitOutputsInterface(
|
|
53
|
+
ctx: CodegenContext,
|
|
54
|
+
outputsType: string,
|
|
55
|
+
cb: CodeBuilder,
|
|
56
|
+
): void {
|
|
57
|
+
cb.line(`export interface ${outputsType} {`);
|
|
58
|
+
cb.indent(() => {
|
|
59
|
+
for (const field of collectOutputFields(ctx, jsId)) {
|
|
60
|
+
emitJsDoc(cb, field.doc);
|
|
61
|
+
cb.line(`${field.id}: ${outputTypeExpr(field.shape)};`);
|
|
62
|
+
}
|
|
63
|
+
for (const s of streamFields(ctx, jsId)) {
|
|
64
|
+
emitJsDoc(cb, s.doc);
|
|
65
|
+
cb.line(`${s.id}: string[];`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
cb.line(`}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Substitutions for ref access while inside an iteration loop. When emitting
|
|
73
|
+
* `for (const item of foo)`, refs to `foo` inside should resolve to `item`.
|
|
74
|
+
* This is also how `iter` segments in a binding's access path are resolved.
|
|
75
|
+
*/
|
|
76
|
+
type IterScope = Map<BindingId, string>;
|
|
77
|
+
|
|
78
|
+
interface OutputEmitCtx {
|
|
79
|
+
ctx: CodegenContext;
|
|
80
|
+
iter: IterScope;
|
|
81
|
+
/**
|
|
82
|
+
* Merged shape per emitted field id (jsId of the output name), across every
|
|
83
|
+
* contributing scope. An output name can be declared in several scopes with
|
|
84
|
+
* different shapes - e.g. a list scope (repeatable option) and a single scope
|
|
85
|
+
* (plain option) both writing `volume_out`. The field's type follows the
|
|
86
|
+
* merged shape (a list if any contributor iterates), so the *write* must too:
|
|
87
|
+
* a single-scope contributor pushes one element into a list field rather than
|
|
88
|
+
* assigning. Keyed identically to `collectOutputFields`.
|
|
89
|
+
*/
|
|
90
|
+
fieldShapes: Map<string, OutputShape>;
|
|
91
|
+
/**
|
|
92
|
+
* Rendered TS default literals for root-level defaulted fields, keyed by field
|
|
93
|
+
* name. An output path interpolating such a field (e.g. an output basename
|
|
94
|
+
* `maskfile`) reads it via `(access ?? default)` so an absent key substitutes
|
|
95
|
+
* the default rather than stringifying `undefined`. Mirrors the cargs builder.
|
|
96
|
+
*/
|
|
97
|
+
defaults: ReadonlyMap<string, string>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The rendered default for a binding iff it is a root-level defaulted field. */
|
|
101
|
+
function rootFieldDefault(
|
|
102
|
+
binding: Binding | undefined,
|
|
103
|
+
defaults: ReadonlyMap<string, string>,
|
|
104
|
+
): string | undefined {
|
|
105
|
+
if (!binding) return undefined;
|
|
106
|
+
const a = binding.access;
|
|
107
|
+
if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Build the field-name -> rendered-default map for the struct root (else empty).
|
|
112
|
+
* Includes only non-optional defaulted fields (optional fields are
|
|
113
|
+
* presence-guarded; their default comes from the factory's kwarg signature). */
|
|
114
|
+
function collectDefaults(ctx: CodegenContext): Map<string, string> {
|
|
115
|
+
const out = new Map<string, string>();
|
|
116
|
+
const rootType = ctx.resolve(ctx.expr)?.type;
|
|
117
|
+
if (rootType?.kind !== "struct") return out;
|
|
118
|
+
for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
|
|
119
|
+
if (fi.defaultValue === undefined) continue;
|
|
120
|
+
if (rootType.fields[name]?.kind === "optional") continue;
|
|
121
|
+
out.set(name, renderTsLiteral(fi.defaultValue));
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Render one output's wrapper stack and emit the assignment inside the
|
|
128
|
+
* innermost wrapper. Nesting is done via recursive callbacks so the
|
|
129
|
+
* CodeBuilder's auto-indentation tracks correctly.
|
|
130
|
+
*/
|
|
131
|
+
function emitOneOutput(
|
|
132
|
+
output: EmittedOutput,
|
|
133
|
+
gate: GateAtom[],
|
|
134
|
+
ec: OutputEmitCtx,
|
|
135
|
+
cb: CodeBuilder,
|
|
136
|
+
): void {
|
|
137
|
+
const occShape = outputShape(gate);
|
|
138
|
+
// Push-vs-assign follows the *field's* merged shape, not this occurrence's:
|
|
139
|
+
// when the same name is a list in one scope and single in another, the field
|
|
140
|
+
// is a list, so a single-scope contributor must push (one element) rather
|
|
141
|
+
// than assign a scalar into the array. Falls back to the occurrence shape for
|
|
142
|
+
// a name that somehow has no merged entry.
|
|
143
|
+
const shape = ec.fieldShapes.get(jsId(output.name)) ?? occShape;
|
|
144
|
+
// Bracket-access for non-identifier field names (e.g. `in-file`, `4d`); dot
|
|
145
|
+
// for valid identifiers. The interface key is jsId-quoted to match.
|
|
146
|
+
const fieldRef = tsPropAccess("outputs", output.name);
|
|
147
|
+
|
|
148
|
+
function nest(remaining: GateAtom[], child: OutputEmitCtx): void {
|
|
149
|
+
if (remaining.length === 0) {
|
|
150
|
+
const pathExpr = renderPathExpr(output.tokens, child);
|
|
151
|
+
// A mutable input's writable copy is surfaced via mutableCopy (its host
|
|
152
|
+
// path); a regular output resolves a local path via outputFile. The
|
|
153
|
+
// optional `, true` arg only applies to a single (non-list) field.
|
|
154
|
+
const optionalArg =
|
|
155
|
+
!output.mutable &&
|
|
156
|
+
shape.kind === "single" &&
|
|
157
|
+
occShape.kind === "single" &&
|
|
158
|
+
occShape.optional
|
|
159
|
+
? ", true"
|
|
160
|
+
: "";
|
|
161
|
+
const call = output.mutable
|
|
162
|
+
? `execution.mutableCopy(${pathExpr})`
|
|
163
|
+
: `execution.outputFile(${pathExpr}${optionalArg})`;
|
|
164
|
+
if (shape.kind === "list") {
|
|
165
|
+
cb.line(`${fieldRef}.push(${call});`);
|
|
166
|
+
} else {
|
|
167
|
+
cb.line(`${fieldRef} = ${call};`);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const [head, ...rest] = remaining;
|
|
172
|
+
if (!head) return;
|
|
173
|
+
const wrapper = renderWrapperOpen(head, child);
|
|
174
|
+
cb.line(wrapper.open);
|
|
175
|
+
cb.indent(() => {
|
|
176
|
+
const inner =
|
|
177
|
+
head.kind === "iter"
|
|
178
|
+
? { ...child, iter: new Map(child.iter).set(head.binding, wrapper.loopVar!) }
|
|
179
|
+
: child;
|
|
180
|
+
nest(rest, inner);
|
|
181
|
+
});
|
|
182
|
+
cb.line(wrapper.close);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
nest(gate, ec);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface WrapperRender {
|
|
189
|
+
open: string;
|
|
190
|
+
close: string;
|
|
191
|
+
loopVar?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let loopCounter = 0;
|
|
195
|
+
|
|
196
|
+
function renderWrapperOpen(atom: GateAtom, ec: OutputEmitCtx): WrapperRender {
|
|
197
|
+
if (atom.kind === "iter") {
|
|
198
|
+
const access = bindingAccess(atom.binding, ec);
|
|
199
|
+
const v = `__o${loopCounter++}`;
|
|
200
|
+
return { open: `for (const ${v} of ${access}) {`, close: `}`, loopVar: v };
|
|
201
|
+
}
|
|
202
|
+
if (atom.kind === "variant") {
|
|
203
|
+
const access = bindingAccess(atom.binding, ec);
|
|
204
|
+
return {
|
|
205
|
+
open: `if (${access}["@type"] === ${JSON.stringify(atom.variant)}) {`,
|
|
206
|
+
close: `}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// present
|
|
210
|
+
const binding = ec.ctx.bindings.get(atom.binding);
|
|
211
|
+
const access = bindingAccess(atom.binding, ec);
|
|
212
|
+
const cond = presentCondition(binding?.type, access);
|
|
213
|
+
return { open: `if (${cond}) {`, close: `}` };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function presentCondition(type: BoundType | undefined, access: string): string {
|
|
217
|
+
if (!type) return access;
|
|
218
|
+
switch (type.kind) {
|
|
219
|
+
case "optional":
|
|
220
|
+
return `${access} !== null && ${access} !== undefined`;
|
|
221
|
+
case "bool":
|
|
222
|
+
return access;
|
|
223
|
+
case "count":
|
|
224
|
+
return `${access} > 0`;
|
|
225
|
+
default:
|
|
226
|
+
return access;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function bindingAccess(id: BindingId, ec: OutputEmitCtx): string {
|
|
231
|
+
// The binding is itself the currently-iterated element (a `ref` to the list
|
|
232
|
+
// being looped, or a scalar list element): use its loop variable directly.
|
|
233
|
+
const subst = ec.iter.get(id);
|
|
234
|
+
if (subst) return subst;
|
|
235
|
+
const binding = ec.ctx.bindings.get(id);
|
|
236
|
+
if (binding) {
|
|
237
|
+
// Solver-assigned path; `iter` segments resolve to the loop variable bound
|
|
238
|
+
// by the surrounding `iter` gate atom (always open by the time a ref to a
|
|
239
|
+
// binding under that repeat renders).
|
|
240
|
+
return renderAccess(binding.access, (b) => ec.iter.get(b) ?? unresolvedLoopVar(b));
|
|
241
|
+
}
|
|
242
|
+
// Fallback: shouldn't happen for well-formed IR, but emit a comment-style
|
|
243
|
+
// placeholder so the generated code surfaces the issue.
|
|
244
|
+
return `/* unresolved binding ${id} */ null as any`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function unresolvedLoopVar(binding: BindingId): string {
|
|
248
|
+
return `/* unresolved loop var ${binding} */ (null as any)`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderPathExpr(tokens: ResolvedToken[], ec: OutputEmitCtx): string {
|
|
252
|
+
if (tokens.length === 0) return `""`;
|
|
253
|
+
if (tokens.length === 1) return renderToken(tokens[0]!, ec);
|
|
254
|
+
// Template-literal join: \`${...}${...}\`
|
|
255
|
+
let result = "`";
|
|
256
|
+
for (const tok of tokens) {
|
|
257
|
+
if (tok.kind === "literal") {
|
|
258
|
+
result += tok.value.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
259
|
+
} else {
|
|
260
|
+
result += "${";
|
|
261
|
+
result += renderRefValue(tok, ec);
|
|
262
|
+
result += "}";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
result += "`";
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderToken(tok: ResolvedToken, ec: OutputEmitCtx): string {
|
|
270
|
+
if (tok.kind === "literal") return JSON.stringify(tok.value);
|
|
271
|
+
return renderRefValue(tok, ec);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function renderRefValue(tok: Extract<ResolvedToken, { kind: "ref" }>, ec: OutputEmitCtx): string {
|
|
275
|
+
// A defaulted root field interpolated into an output path is read absent-safe
|
|
276
|
+
// via `(access ?? default)` (it is `?:`); other refs render normally.
|
|
277
|
+
const def = rootFieldDefault(ec.ctx.bindings.get(tok.binding), ec.defaults);
|
|
278
|
+
let expr =
|
|
279
|
+
def !== undefined && !ec.iter.has(tok.binding)
|
|
280
|
+
? `(${bindingAccess(tok.binding, ec)} ?? ${def})`
|
|
281
|
+
: bindingAccess(tok.binding, ec);
|
|
282
|
+
// Optional refs with a fallback substitute the fallback on null/undefined.
|
|
283
|
+
if (tok.fallback !== undefined) {
|
|
284
|
+
expr = `(${expr} ?? ${JSON.stringify(tok.fallback)})`;
|
|
285
|
+
}
|
|
286
|
+
// stripExtensions: cut listed suffixes from the value (longest match first).
|
|
287
|
+
if (tok.stripExtensions && tok.stripExtensions.length > 0) {
|
|
288
|
+
const sorted = [...tok.stripExtensions].sort((a, b) => b.length - a.length);
|
|
289
|
+
const lits = sorted.map((s) => JSON.stringify(s)).join(", ");
|
|
290
|
+
expr = `stripExtensions(${expr}, [${lits}])`;
|
|
291
|
+
}
|
|
292
|
+
return expr;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function jsId(name: string): string {
|
|
296
|
+
// Output ids may contain characters that aren't valid JS identifier chars.
|
|
297
|
+
// Quote when needed.
|
|
298
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) return name;
|
|
299
|
+
return JSON.stringify(name);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Emit a standalone `_outputs(params, execution)` function that builds and
|
|
304
|
+
* returns the `Outputs` object. Mirrors the `_cargs` function structurally so
|
|
305
|
+
* the wrapper can just call both.
|
|
306
|
+
*/
|
|
307
|
+
export function emitBuildOutputs(
|
|
308
|
+
ctx: CodegenContext,
|
|
309
|
+
paramsType: string,
|
|
310
|
+
outputsType: string,
|
|
311
|
+
funcName: string,
|
|
312
|
+
cb: CodeBuilder,
|
|
313
|
+
): void {
|
|
314
|
+
cb.line(
|
|
315
|
+
`export function ${funcName}(params: ${paramsType}, execution: Execution): ${outputsType} {`,
|
|
316
|
+
);
|
|
317
|
+
cb.indent(() => {
|
|
318
|
+
loopCounter = 0;
|
|
319
|
+
const fields = collectOutputFields(ctx, jsId);
|
|
320
|
+
const ec: OutputEmitCtx = {
|
|
321
|
+
ctx,
|
|
322
|
+
iter: new Map(),
|
|
323
|
+
fieldShapes: new Map(fields.map((f) => [f.id, f.shape])),
|
|
324
|
+
defaults: collectDefaults(ctx),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Initialize the outputs object with defaults so wrapper code can assign or
|
|
328
|
+
// push into it without conditional construction. Deduped by field id so
|
|
329
|
+
// same-named outputs (e.g. union arms) yield one initializer entry.
|
|
330
|
+
cb.line(`const outputs: ${outputsType} = {`);
|
|
331
|
+
cb.indent(() => {
|
|
332
|
+
for (const field of fields) {
|
|
333
|
+
cb.line(`${field.id}: ${initialValue(field.shape)},`);
|
|
334
|
+
}
|
|
335
|
+
// Stream fields start empty; the wrapper pushes onto them via the
|
|
336
|
+
// handleStdout / handleStderr callbacks passed to execution.run.
|
|
337
|
+
for (const s of streamFields(ctx, jsId)) {
|
|
338
|
+
cb.line(`${s.id}: [],`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
cb.line(`};`);
|
|
342
|
+
|
|
343
|
+
const emitContributor = (output: EmittedOutput, scopeGate: GateAtom[]): void => {
|
|
344
|
+
const gate = outputGate(scopeGate, output, ctx.bindings);
|
|
345
|
+
emitOneOutput(output, gate, ec, cb);
|
|
346
|
+
};
|
|
347
|
+
// The always-present root output directory, assigned before any declared
|
|
348
|
+
// output (matches its first position in collectOutputFields).
|
|
349
|
+
emitContributor(rootOutput(ctx, jsId), []);
|
|
350
|
+
for (const scope of ctx.outputScopes) {
|
|
351
|
+
const scopeBinding = ctx.bindings.get(scope.scope);
|
|
352
|
+
const scopeGate = scopeBinding?.gate ?? [];
|
|
353
|
+
for (const output of scope.outputs) emitContributor(output, scopeGate);
|
|
354
|
+
}
|
|
355
|
+
for (const output of collectMutableOutputs(ctx)) emitContributor(output, []);
|
|
356
|
+
|
|
357
|
+
cb.line(`return outputs;`);
|
|
358
|
+
});
|
|
359
|
+
cb.line(`}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Whether the generated module needs a `stripExtensions` helper (any ref
|
|
364
|
+
* token has stripExtensions set).
|
|
365
|
+
*/
|
|
366
|
+
export function needsStripExtensionsHelper(ctx: CodegenContext): boolean {
|
|
367
|
+
for (const scope of ctx.outputScopes) {
|
|
368
|
+
for (const output of scope.outputs) {
|
|
369
|
+
for (const tok of output.tokens) {
|
|
370
|
+
if (tok.kind === "ref" && tok.stripExtensions?.length) return true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Emit a small `stripExtensions` helper used by ref tokens that strip suffixes. */
|
|
378
|
+
export function emitStripExtensionsHelper(cb: CodeBuilder): void {
|
|
379
|
+
cb.line(`function stripExtensions(value: string, exts: string[]): string {`);
|
|
380
|
+
cb.indent(() => {
|
|
381
|
+
cb.line(`for (const ext of exts) {`);
|
|
382
|
+
cb.indent(() => {
|
|
383
|
+
cb.line(`if (value.endsWith(ext)) return value.slice(0, value.length - ext.length);`);
|
|
384
|
+
});
|
|
385
|
+
cb.line(`}`);
|
|
386
|
+
cb.line(`return value;`);
|
|
387
|
+
});
|
|
388
|
+
cb.line(`}`);
|
|
389
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ProjectMeta } from "../../manifest/index.js";
|
|
2
|
+
import type { EmittedPackage } from "../backend.js";
|
|
3
|
+
import { CodeBuilder } from "../code-builder.js";
|
|
4
|
+
import { Scope } from "../scope.js";
|
|
5
|
+
import { STYXDEFS_COMPAT } from "../styxdefs-compat.js";
|
|
6
|
+
|
|
7
|
+
// JS keywords that are illegal as the binding name in `export * as <name>`
|
|
8
|
+
// (`default` is intentionally allowed - it is valid there).
|
|
9
|
+
const NS_RESERVED = [
|
|
10
|
+
"import",
|
|
11
|
+
"export",
|
|
12
|
+
"class",
|
|
13
|
+
"function",
|
|
14
|
+
"const",
|
|
15
|
+
"let",
|
|
16
|
+
"var",
|
|
17
|
+
"return",
|
|
18
|
+
"new",
|
|
19
|
+
"delete",
|
|
20
|
+
"typeof",
|
|
21
|
+
"void",
|
|
22
|
+
"in",
|
|
23
|
+
"instanceof",
|
|
24
|
+
"enum",
|
|
25
|
+
"extends",
|
|
26
|
+
"super",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function description(proj: ProjectMeta): string {
|
|
30
|
+
if (proj.doc?.description) return proj.doc.description;
|
|
31
|
+
return `Styx generated wrappers for ${proj.doc?.title ?? proj.name ?? "tools"}.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** npm names must be lowercase and URL-safe; normalize or fall back. */
|
|
35
|
+
function npmName(name: string | undefined): string {
|
|
36
|
+
const normalized = (name ?? "")
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9-._]/g, "-")
|
|
39
|
+
.replace(/^[-._]+/, "");
|
|
40
|
+
return normalized || "styx-wrappers";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Single npm `package.json` for the whole project (TypeScript ships one package
|
|
45
|
+
* spanning all suites, unlike Python's per-suite distributions). The styxdefs
|
|
46
|
+
* floor is baked in as the lone runtime dependency.
|
|
47
|
+
*/
|
|
48
|
+
export function generatePackageJson(proj: ProjectMeta): string {
|
|
49
|
+
const pkg = {
|
|
50
|
+
name: npmName(proj.name),
|
|
51
|
+
version: proj.version ?? "0.0.0",
|
|
52
|
+
description: description(proj),
|
|
53
|
+
type: "module",
|
|
54
|
+
types: "./dist/index.d.ts",
|
|
55
|
+
main: "./dist/index.js",
|
|
56
|
+
exports: {
|
|
57
|
+
".": {
|
|
58
|
+
types: "./dist/index.d.ts",
|
|
59
|
+
import: "./dist/index.js",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
files: ["dist"],
|
|
63
|
+
scripts: {
|
|
64
|
+
build: "tsc",
|
|
65
|
+
prepublishOnly: "npm run build",
|
|
66
|
+
},
|
|
67
|
+
license: proj.license?.description ?? "unknown",
|
|
68
|
+
dependencies: {
|
|
69
|
+
styxdefs: STYXDEFS_COMPAT.npm,
|
|
70
|
+
},
|
|
71
|
+
devDependencies: {
|
|
72
|
+
typescript: "^5.6.0",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Turn a suite directory name into a valid JS namespace identifier. */
|
|
79
|
+
function nsIdent(name: string): string {
|
|
80
|
+
const cleaned = name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
81
|
+
return /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Root barrel namespacing each per-suite `index.ts` by suite
|
|
86
|
+
* (`export * as afni from "./afni/index.js"`). Namespacing avoids cross-suite
|
|
87
|
+
* collisions when the same tool id appears in two suites, and keeps the root
|
|
88
|
+
* from flattening every tool into one giant namespace.
|
|
89
|
+
*/
|
|
90
|
+
export function generateRootIndex(packages: EmittedPackage[]): string {
|
|
91
|
+
const cb = new CodeBuilder(" ");
|
|
92
|
+
cb.comment("This file was auto generated by Styx.");
|
|
93
|
+
cb.comment("Do not edit this file directly.");
|
|
94
|
+
cb.blank();
|
|
95
|
+
|
|
96
|
+
const dirs = packages
|
|
97
|
+
.map((p) => p.meta?.name)
|
|
98
|
+
.filter((name): name is string => !!name)
|
|
99
|
+
.sort();
|
|
100
|
+
|
|
101
|
+
// Dodge namespace collisions (two dirs cleaning to the same identifier, or a
|
|
102
|
+
// dir named like a keyword) the same way per-tool names are dodged.
|
|
103
|
+
const nsScope = new Scope(NS_RESERVED);
|
|
104
|
+
for (const dir of dirs) {
|
|
105
|
+
const ns = nsScope.add(nsIdent(dir));
|
|
106
|
+
cb.line(`export * as ${ns} from "./${dir}/index.js";`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return cb.toString() + "\n";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Minimal `tsconfig.json` producing ESM + declarations into `dist/`. */
|
|
113
|
+
export function generateTsconfig(): string {
|
|
114
|
+
const config = {
|
|
115
|
+
compilerOptions: {
|
|
116
|
+
target: "ES2020",
|
|
117
|
+
module: "NodeNext",
|
|
118
|
+
moduleResolution: "NodeNext",
|
|
119
|
+
declaration: true,
|
|
120
|
+
outDir: "./dist",
|
|
121
|
+
strict: true,
|
|
122
|
+
esModuleInterop: true,
|
|
123
|
+
skipLibCheck: true,
|
|
124
|
+
forceConsistentCasingInFileNames: true,
|
|
125
|
+
},
|
|
126
|
+
include: ["**/*.ts"],
|
|
127
|
+
exclude: ["dist", "node_modules"],
|
|
128
|
+
};
|
|
129
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
130
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CodegenContext } from "../../manifest/index.js";
|
|
2
|
+
import type { SnippetDialect, SnippetOptions } from "../snippet-core.js";
|
|
3
|
+
import { renderStructLiteral, renderValue } from "../snippet-core.js";
|
|
4
|
+
import { tsObjKey } from "./emit.js";
|
|
5
|
+
import { buildEmitModel } from "./typescript.js";
|
|
6
|
+
|
|
7
|
+
/** Snippet rendering hooks for TypeScript (object literals, `true`/`null`). */
|
|
8
|
+
const tsDialect: SnippetDialect = {
|
|
9
|
+
indentUnit: " ",
|
|
10
|
+
string: (s) => JSON.stringify(s),
|
|
11
|
+
boolean: (b) => (b ? "true" : "false"),
|
|
12
|
+
number: (n) => (Number.isFinite(n) ? String(n) : "NaN"),
|
|
13
|
+
null: "null",
|
|
14
|
+
objKey: tsObjKey,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render a TypeScript call snippet for one tool from a config object (keyed by
|
|
19
|
+
* Boutiques wire names).
|
|
20
|
+
*
|
|
21
|
+
* The generated v2 kwarg wrapper takes *positional* arguments, which can't skip
|
|
22
|
+
* a middle optional - so the runnable object-style entry is the dict-style
|
|
23
|
+
* `<tool>Execute(params)` (struct roots). The snippet builds the params object
|
|
24
|
+
* literal (wire-keyed, with the root `@type` injected) and passes it there.
|
|
25
|
+
* Union- (or otherwise non-struct-) rooted tools call the dict-style `<tool>`
|
|
26
|
+
* entry the same way. Nested structs / union variants / lists-of-structs have no
|
|
27
|
+
* constructor in the generated code and render as object literals.
|
|
28
|
+
*
|
|
29
|
+
* The snippet matches the *standalone* (single-descriptor) emission of the same
|
|
30
|
+
* context - which is how the hub compiles - not a catalog emission where a
|
|
31
|
+
* shared package scope could suffix-bump a name.
|
|
32
|
+
*
|
|
33
|
+
* @param ctx - The compiled context (compile -> pipeline -> solve ->
|
|
34
|
+
* resolveOutputs -> createContext, as in the CLI's `readAndCompile`).
|
|
35
|
+
* @param config - The params object, keyed by Boutiques *wire* names. Every
|
|
36
|
+
* union-typed value - including the root of a union-rooted tool - must carry
|
|
37
|
+
* its `@type` discriminator so the variant can be matched; the root struct's
|
|
38
|
+
* `@type` is supplied by the renderer, so omit it there.
|
|
39
|
+
* @param opts - Import and package-root options.
|
|
40
|
+
*/
|
|
41
|
+
export function renderTypeScriptCall(
|
|
42
|
+
ctx: CodegenContext,
|
|
43
|
+
config: Record<string, unknown>,
|
|
44
|
+
opts: SnippetOptions = {},
|
|
45
|
+
): string {
|
|
46
|
+
const model = buildEmitModel(ctx);
|
|
47
|
+
const pkg = ctx.package?.name;
|
|
48
|
+
const fnName = model.rootIsStruct ? model.names.execute : model.names.wrapper;
|
|
49
|
+
const callee = pkg ? `${pkg}.${fnName}` : fnName;
|
|
50
|
+
|
|
51
|
+
const arg =
|
|
52
|
+
model.rootIsStruct && model.rootType.kind === "struct"
|
|
53
|
+
? renderStructLiteral(config, model.rootType, "", tsDialect, model.rootTypeTag)
|
|
54
|
+
: renderValue(config, model.rootType, "", tsDialect);
|
|
55
|
+
const call = `${callee}(${arg})`;
|
|
56
|
+
|
|
57
|
+
if (opts.includeImport === false || !pkg) return call;
|
|
58
|
+
const root = opts.packageRoot ?? ctx.project?.name ?? "niwrap";
|
|
59
|
+
return `import { ${pkg} } from "${root}";\n\n${call}`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BoundType } from "../../bindings/index.js";
|
|
2
|
+
|
|
3
|
+
export function mapType(type: BoundType, resolve: (type: BoundType) => string | undefined): string {
|
|
4
|
+
switch (type.kind) {
|
|
5
|
+
case "scalar":
|
|
6
|
+
return { int: "number", float: "number", str: "string", path: "InputPathType" }[type.scalar];
|
|
7
|
+
case "bool":
|
|
8
|
+
return "boolean";
|
|
9
|
+
case "count":
|
|
10
|
+
return "number";
|
|
11
|
+
case "literal":
|
|
12
|
+
return typeof type.value === "string" ? JSON.stringify(type.value) : String(type.value);
|
|
13
|
+
case "optional":
|
|
14
|
+
// The solver has no nullable type: `optional` means "omittable" (the key
|
|
15
|
+
// may be absent), never "the value may be null". Omittability is expressed
|
|
16
|
+
// structurally at the field level (`?:` on the interface key, NotRequired
|
|
17
|
+
// in Python); the value type itself is just the inner type. So in any
|
|
18
|
+
// value position (nested list/union arm, validator messages) we render the
|
|
19
|
+
// inner type with no `| null | undefined`.
|
|
20
|
+
return mapType(type.inner, resolve);
|
|
21
|
+
case "list": {
|
|
22
|
+
const inner = mapType(type.item, resolve);
|
|
23
|
+
return inner.includes("|") ? `Array<${inner}>` : `${inner}[]`;
|
|
24
|
+
}
|
|
25
|
+
case "struct": {
|
|
26
|
+
const name = resolve(type);
|
|
27
|
+
if (name) return name;
|
|
28
|
+
const fields = Object.entries(type.fields)
|
|
29
|
+
.filter(([, v]) => v.kind !== "literal")
|
|
30
|
+
.map(([k, v]) => `${k}: ${mapType(v, resolve)}`)
|
|
31
|
+
.join("; ");
|
|
32
|
+
return `{ ${fields} }`;
|
|
33
|
+
}
|
|
34
|
+
case "union": {
|
|
35
|
+
const name = resolve(type);
|
|
36
|
+
if (name) return name;
|
|
37
|
+
return type.variants.map((v) => mapType(v.type, resolve)).join(" | ");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Render a JS default value as a TypeScript literal (signatures, `?? default`). */
|
|
43
|
+
export function renderTsLiteral(value: string | number | boolean): string {
|
|
44
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
45
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NaN";
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Re-export shared utilities for backwards compatibility and convenience.
|
|
2
|
+
// All logic has been extracted to shared backend modules.
|
|
3
|
+
|
|
4
|
+
export { structKey, unionKey } from "../type-keys.js";
|
|
5
|
+
export type { NamedType } from "../collect-named-types.js";
|
|
6
|
+
export { collectNamedTypes, resolveTypeName } from "../collect-named-types.js";
|
|
7
|
+
export type { FieldInfo } from "../collect-field-info.js";
|
|
8
|
+
export { collectFieldInfo } from "../collect-field-info.js";
|