Skip to content

Plugin Hooks Reference ​

This page documents all six hooks in the TAtscriptPlugin interface — their signatures, when they fire, execution semantics, and practical examples.

Hook Summary ​

HookWhenReturnExecution
config(config)At startup (once)TAtscriptConfig | undefinedSequential, merged with defu
resolve(id)Opening a documentstring | undefinedSequential, last non-undefined wins
load(id)Loading source contentstring | undefinedSequential, first non-undefined wins
onDocument(doc)After parsingvoidSequential, all plugins called
render(doc, format)Code generationTPluginOutput[] | undefinedSequential, outputs concatenated
buildEnd(output, format, repo)After all docs renderedvoidSequential, all plugins called

All hooks can be synchronous or async (return Promise). All are optional — implement only the ones you need.

config() ​

typescript
config?(
  config: TAtscriptConfig
): Promise<TAtscriptConfig | undefined> | TAtscriptConfig | undefined

Called once during PluginManager initialization, before any documents are opened. This is where you register primitives, annotations, and config options.

Execution: Plugins are called sequentially in array order. Each plugin receives the accumulated config. Return values are merged using defu (deep defaults — the first defined value wins).

Example — Register primitives and annotations:

typescript
config() {
  return {
    primitives: {
      geo: {
        extensions: {
          latitude: {
            type: 'number',
            annotations: { 'expect.min': -90, 'expect.max': 90 },
          },
          longitude: {
            type: 'number',
            annotations: { 'expect.min': -180, 'expect.max': 180 },
          },
        },
      },
    },
    annotations: {
      geo: {
        coordinates: new AnnotationSpec({
          nodeType: ['interface'],
          description: 'GeoJSON coordinate system',
        }),
      },
    },
  }
}

Example — Set unknownAnnotation policy:

typescript
config() {
  return {
    unknownAnnotation: 'warn',  // 'error' | 'warn' | 'allow'
  }
}

resolve() ​

typescript
resolve?(id: string): Promise<string | undefined> | string | undefined

Called when the repo opens a document or resolves an import. Allows you to remap module IDs — useful for virtual modules, path aliases, or rewriting import paths.

Execution: All plugins are called. The last non-undefined return wins. If all plugins return undefined, the original ID is used.

Example — Virtual module:

typescript
resolve(id) {
  if (id.includes('virtual:generated-types')) {
    return '/absolute/path/to/generated-types.as'
  }
}

Example — Path alias:

typescript
resolve(id) {
  if (id.startsWith('@models/')) {
    return id.replace('@models/', '/project/src/models/')
  }
}

load() ​

typescript
load?(id: string): Promise<string | undefined> | string | undefined

Called to get the source content for a document. Allows plugins to provide virtual file content without a real file on disk.

Execution: Plugins are called sequentially. The first to return a non-undefined string wins (early exit). If no plugin provides content, the file is read from disk.

Example — Virtual file:

typescript
load(id) {
  if (id.endsWith('generated-enums.as')) {
    return `
      export type Status = "active" | "inactive" | "pending"
      export type Role = "admin" | "user" | "guest"
    `
  }
}

Example — Preprocessing:

typescript
load(id) {
  if (id.endsWith('.as.template')) {
    const raw = readFileSync(id.replace('.template', ''), 'utf8')
    return raw.replace(/\$\{VERSION\}/g, '1.0.0')
  }
}

onDocument() ​

typescript
onDocument?(doc: AtscriptDoc): Promise<void> | void

Called after a document is parsed and its AST is fully built. All plugins receive the call in sequence — there is no early exit.

Use cases: Post-parse processing, injecting virtual properties, running custom validation, patching the AST.

Example — Add computed fields:

typescript
onDocument(doc) {
  for (const node of doc.nodes) {
    if (isInterface(node)) {
      const struc = node.getDefinition()
      if (isStructure(struc) && node.props.has('firstName') && node.props.has('lastName')) {
        struc.addVirtualProp({
          name: 'fullName',
          type: 'string',
          documentation: 'Computed: firstName + lastName',
        })
      }
    }
  }
}

render() ​

typescript
render?(
  doc: AtscriptDoc,
  format: TAtscriptRenderFormat,
  options?: TAtscriptRenderOptions
): Promise<TPluginOutput[] | undefined> | TPluginOutput[] | undefined

Called once per document per format during the build phase. This is the primary code generation hook.

Options: what the caller says about the render. options.loadCheck is true in the bundler plugin's checked builds (one-shot Vite, Rollup and Rolldown builds, or any build with its loadCheck: true option), false where it must not check (dev server, watch builds, loadCheck: false), and unset otherwise — then the TypeScript plugin checks when the render runs inside withLoadCheck (the asc CLI build, or your own code), which does not set options.loadCheck. The TypeScript plugin loads its generated modules when the check is on; other plugins may ignore the option. A plugin that wraps the TypeScript plugin's render must pass the third argument on.

Execution: All plugins are called. Their outputs are concatenated — multiple plugins can produce output for the same document and format.

Return type: Array of { fileName: string, content: string }. Return an empty array or undefined to produce no output for this format.

Example — Primary output with DEFAULT_FORMAT:

Plugins should handle DEFAULT_FORMAT for output that is essential during development (e.g., type declarations). This format is triggered by the VSCode extension on save and by the CLI when no -f flag is given. See VSCode & Build Integration for details.

typescript
import { DEFAULT_FORMAT } from '@atscript/core'

render(doc, format) {
  if (format === 'dts' || format === DEFAULT_FORMAT) {
    return [{
      fileName: `${doc.name}.d.ts`,
      content: new TypeRenderer(doc).render(),
    }]
  }
  if (format === 'js') {
    return [{
      fileName: `${doc.name}.js`,
      content: new RuntimeRenderer(doc).render(),
    }]
  }
}

Example — Multiple output files per document:

typescript
render(doc, format) {
  if (format === 'graphql') {
    return [
      { fileName: `${doc.name}.graphql`, content: generateSchema(doc) },
      { fileName: `${doc.name}.resolvers.ts`, content: generateResolvers(doc) },
    ]
  }
}

buildEnd() ​

typescript
buildEnd?(
  output: TOutput[],
  format: TAtscriptRenderFormat,
  repo: AtscriptRepo
): Promise<void> | void

Called once after all documents have been rendered for a given format. Use this for cross-document aggregation.

Parameters:

  • output — Mutable array of all output files. You can push new files or modify existing ones.
  • format — The format string (same as passed to render()).
  • repo — The AtscriptRepo instance for querying across all documents.

TOutput extends TPluginOutput with:

  • source — The source document path
  • target — The resolved output file path

Example — Generate an index file:

typescript
async buildEnd(output, format, repo) {
  if (format !== 'python') return

  const exports = output
    .filter(o => o.fileName.endsWith('.py'))
    .map(o => {
      const module = o.fileName.replace('.py', '')
      return `from .${module} import *`
    })

  output.push({
    content: exports.join('\n'),
    fileName: '__init__.py',
    source: '',
    target: '/output/__init__.py',
  })
}

Example — Collect project-wide metadata:

typescript
import { DEFAULT_FORMAT } from '@atscript/core'

async buildEnd(output, format, repo) {
  if (format !== 'dts' && format !== DEFAULT_FORMAT) return

  const annotations = await repo.getUsedAnnotations()
  const tags = await repo.getPrimitivesTags()

  output.push({
    content: generateGlobalTypes(annotations, tags),
    fileName: 'global-types.ext',
    source: '',
    target: '/output/global-types.ext',
  })
}

Plugin Ordering ​

Plugins execute in the order they appear in the plugins array:

typescript
export default defineConfig({
  plugins: [pluginA(), pluginB(), pluginC()],
})

For config(): Plugin A's return merges first. Since defu uses "first defined wins", Plugin A's values take priority over Plugin B's for the same key. Built-in defaults merge last.

For resolve(): All plugins are called; the last non-undefined return wins. So Plugin C's result takes priority if all three return a value.

For load(): First non-undefined return wins. So Plugin A's content takes priority if all three return content.

For render(): All plugins contribute. Their output arrays are concatenated in order (Plugin A's files, then B's, then C's).

For onDocument() and buildEnd(): All plugins are called in order with no return value merging.

Next Steps ​

Released under the MIT License.