toArtifacts
The top-level engine function that runs the SKMTC generation pipeline end-to-end: Parse → Generate → Render. Returns the generated artifacts and the manifest. This is what the Worker invokes on…
Everything before toArtifacts (the CLI, the Worker bootstrap, the generator
map construction) is orchestration. Everything after it is the engine. The
function takes a parsed OAS-or-GQL document, a client settings object, and a
generator map factory — and produces the artifacts and manifest.
Source
skmtc/deno/core/run/toArtifacts.ts
Signature
export async function toArtifacts<E = undefined>(
args: TransformArgs<E>,
): Promise<{
artifacts: Record<string, string>;
manifest: ManifestContent;
}>;
export type TransformArgs<E> = {
traceId: string;
spanId: string;
document: SkmtcDocumentInput;
settings: ClientSettings | undefined;
logsPath?: string;
stackTrail: StackTrail;
toGeneratorConfigMap: () => GeneratorsMapContainer<E>;
startAt: number;
silent: boolean;
};Parameters
traceId and spanId
Tracing IDs propagated from the CLI invocation. They thread through the
StackTrail and end up in log lines (when logging is enabled), making it
possible to correlate engine output with CLI runs.
The CLI generates a fresh traceId per invocation and a fresh spanId per
child operation. Direct callers can pass any unique-enough strings (e.g.,
timestamps + nonces).
document
The parsed input document. Accepts either an OAS document object or a GraphQL schema string:
type SkmtcDocumentInput =
| { type: "oas"; value: OpenAPIV3.Document<Record<string, never>> }
| { type: "gql"; value: string | GraphQLSchema };Both variants carry the source as value — the field name is uniform across
protocols. For OAS, value is a normalized OpenAPIV3.Document (Swagger 2 /
OAS 3.1 inputs are auto-converted to 3.0 by @skmtc/convert before reaching
this boundary). For GraphQL, value is either an SDL string (parsed via
buildSchema inside the pipeline) or a pre-built GraphQLSchema instance (used
as-is).
The post-parse internal shape is the parallel
SkmtcParsedDocument union, which also
keys the protocol payload on value (OasDocument or GqlDocument).
settings
The validated ClientSettings from
client.json. Carries:
basePath— the output directory (relative to the manifest root)paths— per-operation path overridesenrichments— per-generator enrichment payloadsskip— operations to skipexclude— components to exclude
Pass undefined when running without a settings file (rare; most callers have
at least minimal settings).
toGeneratorConfigMap
A factory function that returns the generator map. Called by toArtifacts
to obtain the current set of installed generators:
type GeneratorsMapContainer<E> = {
generators: Record<string, GeneratorConfigInput<E>>;
};
toGeneratorConfigMap: (() => GeneratorsMapContainer<E>);Why a factory function rather than the map directly? It defers construction until the engine is ready to consume it. This lets the generator-bundling step (which may run lazily) happen at the right point in the pipeline.
Each generator config carries its Entry (the toOasOperationEntry /
toModelEntry result) and its enrichment schema. The engine iterates the map to
dispatch work.
stackTrail
A StackTrail instance threading through Parse and Generate. Carries the
current "where in the document/generator graph are we" context for error
messages and diagnostics. See API: StackTrail.
The CLI creates an empty StackTrail at the top of toArtifacts and passes it
down.
silent, logsPath
logsPath
When set, structured logs are written to this path during the run. When unset, logs are written via console only.
The CLI sets logsPath to a per-invocation directory under the project's
.skmtc/logs/. Direct callers typically omit it.
silent
When true, suppresses log output. The CLI sets silent: false for normal
runs; bench scripts and tests typically set silent: true.
startAt
A performance.now() (or Date.now()) snapshot taken before toArtifacts is
called. Used for the duration calculation in the returned ManifestContent. The
engine subtracts startAt from Date.now() at the end of the run to compute
total elapsed time.
Returns
Promise<{
artifacts: Record<string, string>;
manifest: ManifestContent;
}>;artifacts
Map from absolute artifact path → file contents (string). The path keys include the basePath prefix when settings provide it; otherwise they're relative to the artifact root.
The CLI writes each [path, contents] pair to disk as the post-engine step. The
engine itself doesn't touch the filesystem — toArtifacts returns a pure value.
manifest
type ManifestContent = {
files: Record<string, ManifestFileEntry>;
durationMs: number;
diagnostics: DiagnosticItem[];
// ...
};The manifest summarizes what was generated:
files— per-artifact metadata (generator key, source operation or component, generation time)durationMs— total elapsed (usingstartAt)diagnostics— parse and generate issues, severity-ranked
The CLI writes the manifest to manifest.json alongside the artifacts. Direct
callers can inspect it for diagnostics or skip writing it.
Behavior
Internally, toArtifacts runs three sequential phases:
toArtifacts
├── 1. Parse: document → OasDocument | GqlSchema (parsed model)
├── 2. Generate: parsed model → File/Projection/Definition tree
└── 3. Render: file tree → artifacts (Record<path, contents>)Each phase creates and tears down its own context (ParseContext,
GenerateContext, RenderContext). See
the three phases concept for the detailed
flow.
The phases are sequential and synchronous within the engine (the function is
async for I/O readiness, not concurrency). Errors from Parse cascade to
Generate via removeErroredItems (one-hop pruning); Generate errors don't
cascade to Render — the partial output is returned.
Example
From a bench script
When you want to measure engine performance without the CLI/worker overhead:
import { StackTrail, toArtifacts } from "@skmtc/core";
import { toGeneratorConfigMap } from "./my-generators.ts";
const startAt = performance.now();
const result = await toArtifacts({
traceId: "bench-001",
spanId: "bench-001",
document: { type: "oas", value: openApiSpec },
settings: clientSettings,
stackTrail: new StackTrail(),
toGeneratorConfigMap,
startAt,
silent: true,
});
console.log(
`Generated ${
Object.keys(result.artifacts).length
} files in ${result.manifest.durationMs}ms`,
);From the Worker
The standard production path. Worker boots, receives GENERATE message, calls
toArtifacts, posts result back:
self.addEventListener("message", async (event) => {
if (event.data.type === "GENERATE") {
const { artifacts, manifest } = await toArtifacts(event.data.args);
self.postMessage({ type: "RESULT", artifacts, manifest });
}
});This is what skmtc generate triggers.
From tests
Tests of generator behavior typically build a minimal openApiSpec fixture,
call toArtifacts, and assert on result.artifacts['some/path.ts']:
test("zod generator produces userBody for User schema", async () => {
const result = await toArtifacts({
traceId: "t",
spanId: "t",
document: { type: "oas", value: minimalSpec },
settings: undefined,
stackTrail: new StackTrail(),
toGeneratorConfigMap: () => ({ generators: { zod: ZodEntry } }),
startAt: 0,
silent: true,
});
expect(result.artifacts["models/User.generated.ts"]).toContain("z.object");
});Common questions
Why does toArtifacts need a toGeneratorConfigMap function rather than just a map?
Deferred construction. The map factory is called once, at the top of the engine run, after Parse has produced the document. This lets generator initialization happen lazily — useful when generator construction is expensive, when generator selection depends on runtime state, or when the engine needs to control the timing of side effects in generator entry code.
In practice, most callers' factories are () => ({ generators: {...} }) with a
static map. The function indirection has minimal cost.
Does toArtifacts write to disk?
No. The function is pure: it returns artifacts as in-memory
Record<path, contents>. Disk writes happen in the CLI's worker-result handler,
after toArtifacts returns.
This separation makes the engine testable (no fs cleanup) and embeddable (callers can route artifacts to any sink — git commits, in-memory diffs, S3, etc.).
What's the relationship between toArtifacts and the CLI?
The CLI handles:
- Bootstrap (load
deno.json,client.json) - Pre-parse OAS host-side (for the
structuredClonereason) - Spawn the worker
- Pass the args (which include the pre-parsed document) to the worker
- Worker calls
toArtifacts - CLI receives the result and persists artifacts + manifest
toArtifacts is step 5. Steps 1–4 and 6 are CLI orchestration. See
the three phases for the full lifecycle.
Why async if the phases are synchronous?
The async is there for the few I/O moments the engine might invoke (e.g.,
dynamic generator loading in some paths). It's not used for concurrency between
phases — the phases run sequentially.
The async also gives the function a future-compatible signature: if any phase
later needs awaitable work, the caller signature doesn't change.
Can I run multiple toArtifacts invocations in parallel?
Yes, but each call should get fresh StackTrail and unique traceId instances.
The function itself is reentrant — its in-memory state lives in the contexts it
creates, not in module globals.
In practice, parallelism across invocations is uncommon. The Worker serves one request at a time, and bench scripts typically run sequentially.
Related types
type SkmtcDocumentInput =
| { type: "oas"; value: OpenAPIV3.Document<Record<string, never>> }
| { type: "gql"; value: string | GraphQLSchema };
type GeneratorsMapContainer<E> = {
generators: Record<string, GeneratorConfigInput<E>>;
};
type ManifestContent = {
files: Record<string, ManifestFileEntry>;
durationMs: number;
diagnostics: DiagnosticItem[];
};See also
- The three phases concept — Parse → Generate → Render in detail
- The worker runtime concept — how toArtifacts is invoked
- API: ParseContext — phase 1
- API: GenerateContext — phase 2
- API: RenderContext — phase 3
- API: StackTrail — tracing parameter
- Reference: client.json schema — what
settingscarries - Glossary: toArtifacts, Manifest, Artifact