skmtcdocs

RenderContext

The Render-phase context. Serializes the file map produced by GenerateContext into { path: content } artifact strings. Thin wrapper around file.toString() and path resolution.

Source

skmtc/deno/core/context/RenderContext.ts

Class

class RenderContext {
  files: Map<string, FileBase>
  previews: Record<string, Preview>
  basePath: string | undefined
  logger: Logger
  captureCurrentResult: (result: ResultType, stackTrail: StackTrail) => void
  attribution: AttributionState | undefined

  constructor(args: {
    files: Map<string, FileBase>
    previews: Record<string, Preview>
    basePath: string | undefined
    logger: Logger
    captureCurrentResult: ...
    attribution?: AttributionState     // gen-maps emission config
    captureChannel?: CaptureChannel    // shared with GenerateContext
  })

  render(stackTrail: StackTrail): RenderPhaseResult
  collate(stackTrail: StackTrail): FilesRenderResult
  getFile(filePath: string): FileBase
  pick(args: PickArgs): DefinitionBase | undefined
}

Output is unformatted by design

Render does not invoke any formatter. The renderFile helper takes the content produced by file.toString(), resolves the artifact path, and returns the metadata-wrapped result without modification:

const renderFile = ({ content, destinationPath, basePath }: RenderFileArgs): FileObject => {
  const path = toResolvedArtifactPath({ basePath, destinationPath })
  return {
    content,                            // ← unmodified
    path,
    destinationPath,
    lines: content.split('\n').length,
    characters: content.length
  }
}

Generated output is unformatted by design. Consumers run their own formatter (Prettier, Biome, deno fmt) as a separate step — typically in a pre-commit hook or build script.

Constructor

Constructed internally by toArtifacts. Not typically instantiated by generators.

Properties

files: Map<string, FileBase>

The file map produced by GenerateContext. Keys are unresolved destination paths (before basePath application). Values are the concrete FileBase subclasses — the language code files (TsFile), JsonFile, MarkdownFile.

previews: Record<string, Preview>

Per-Projection preview metadata, forwarded from the Generate phase. Render passes it through unchanged.

basePath: string | undefined

The base path under which generated files land. Pulled from client.json#settings.basePath. Applied by toResolvedArtifactPath to compute final on-disk paths.

logger: Logger

Structured logger.

captureCurrentResult: (result, stackTrail) => void

Records per-file render outcomes into the manifest. Render-phase results are typically all 'success' because the heavy lifting happened in Generate.

Methods

render(stackTrail: StackTrail): RenderPhaseResult

The Render-phase entry point. Calls collate, packages the result with previews. Returns:

type RenderResult = {
  artifacts: Record<string, string>      // resolved path → content
  files: Record<string, {                // resolved path → metadata
    destinationPath: string
    lines: number
    characters: number
  }>
  previews: Record<string, Preview>
  results: ResultsItem
}

type RenderPhaseResult = Omit<RenderResult, 'results'> & {
  sidecars?: Record<string, Sidecar>          // attribution runs only
  generationMap?: GenerationMapEntry[]        // attribution runs only
}

(The omitted results field comes from the manifest layer above. sidecars / generationMap appear only when the run configured attribution.postPass.)

collate(stackTrail: StackTrail): FilesRenderResult

The core serialization loop. Iterates files, calls file.toString() on each, computes line/character metadata, resolves paths against basePath.

type FilesRenderResult = {
  artifacts: Record<string, string>
  files: Record<string, FileMetadata>
}

Each file is stringified independently. There's no cross-file operation in Render — by the time Render runs, every file's content is fully determined by file.toString().

getFile(filePath: string): FileBase

Look up a file by its (normalized) destination path. Throws if the file isn't in the map.

const file = renderContext.getFile('./src/types/User.ts')
console.log(file.toString())  // the rendered content

pick({ name, exportPath }: PickArgs): DefinitionBase | undefined

Look up a specific Definition by name within a file. Convenience for inspecting generated content from outside the standard flow.

const userDef = renderContext.pick({
  name: 'User',
  exportPath: './src/models/User.ts'
})

Examples

What Render actually does (conceptually)

// Pseudocode of the core Render loop
for (const [path, file] of files) {
  const content = file.toString()  // ← imports + reExports + definitions joined
  const resolvedPath = join(basePath, path)
  artifacts[resolvedPath] = content
}

That's the entire transformation. No analysis, no per-file optimization, no formatting.

file.toString() shape

A File.toString() produces the assembled file content:

[reExports, imports, definitions]
  .filter(section => section.length > 0)
  .map(section => section.join('\n'))
  .join('\n\n')

Three sections separated by blank lines, in order: re-exports, then imports, then definitions. Each section is empty-skipped if it has no entries.

Common questions

Why doesn't Render run Prettier?

Two reasons:

  1. Format is the consumer's concern. SKMTC produces syntactically valid TypeScript; aesthetic choices (semis, trailing commas, quote style) belong to the consumer's existing tooling.
  2. Coupling to a specific formatter would create friction. Different consumer projects use different formatters (Prettier, Biome, dprint, custom). Picking one in SKMTC would force every consumer to either accept SKMTC's choice or post-process anyway.

The pragmatic stance is "produce valid, let the consumer format."

Can I add formatting back into the pipeline?

You could — call Prettier in the host after writeGeneratedFiles. The CLI doesn't do this, but a consumer running the engine programmatically could. The cleanest pattern is a pre-commit hook on the generated output directory, which runs Prettier independent of SKMTC.

Why is Render a separate phase if it's so simple?

For the same reason Parse and Generate are separate: the phase boundary creates an invariant. Once Generate finishes, the file map is the source of truth — no further mutation. Render reads; Render doesn't transform.

This makes the manifest's structure consistent (every file in the output was in the map at the end of Generate). It also makes debugging easier — you can inspect the file map after Generate and predict what Render will produce.

Can I inject content during Render?

No — by design. If you want to inject content, do it during Generate via register({ ... }) calls. Render reads the file map and serializes; it doesn't accept new contributions.

How does JsonFile differ from the code files?

JsonFile is used for JSON output. Its toString() returns JSON.stringify(content, null, 2) rather than the reExports + imports + definitions join. Generators register JSON content via registerJson rather than register.

Code paths (.ts, .tsx) hold the language's concrete code file (TsFile from @skmtc/lang-typescript), created by the lang package's register function on first write.

type RenderFileArgs = {
  content: string
  destinationPath: string
  basePath?: string
}

type FileObject = {
  content: string
  path: string
  destinationPath: string
  lines: number
  characters: number
}

type PickArgs = {
  name: string
  exportPath: string
}

See also

On this page