preRenderHooks
In Kanel, "rendering" means translating a bunch of declarations into arrays of strings that will ultimately become files. You can apply hooks before and after this step if you want to make some custom modifications or additions to the final output.
Two Types of Pre-Render Hooks
In V4, there are two types of pre-render hooks depending on where you configure them:
1. Global Pre-Render Hooks
Global hooks are configured at the top level of your config and run after all generators complete:
export type PreRenderHook = (
outputAcc: Output,
) => Awaitable<Output>;Use these for operations that need to work on the complete accumulated output from all generators:
import { makePgTsGenerator } from 'kanel';
const config = {
generators: [makePgTsGenerator()],
preRenderHooks: [
// This runs AFTER all generators complete
(outputAcc) => {
// Transform all output here
return outputAcc;
},
],
};To access context (config, schemas, etc.) in a global hook, use useKanelContext():
import { useKanelContext } from 'kanel';
const myHook = (outputAcc) => {
const context = useKanelContext();
// Access context.config, context.schemas, context.typescriptConfig
return outputAcc;
};See useKanelContext for complete documentation.
2. PgTs Generator-Specific Pre-Render Hooks
These hooks are configured inside makePgTsGenerator() and receive the PgTsGeneratorContext as a parameter:
export type PgTsPreRenderHook = (
outputAcc: Output,
context: PgTsGeneratorContext,
) => Awaitable<Output>;These hooks run immediately after the PgTs generator completes, before output is merged with other generators. They receive the generator's context directly as a parameter, giving type-safe access to the PgTs-specific configuration:
import { makePgTsGenerator } from 'kanel';
import { makeKyselyHook } from 'kanel-kysely';
import { generateZodSchemas } from 'kanel-zod';
const config = {
generators: [
makePgTsGenerator({
// These hooks receive PgTsGeneratorContext as a parameter
preRenderHooks: [
makeKyselyHook(),
generateZodSchemas,
(outputAcc, pgTsContext) => {
// Access pgTsContext.typeMap, pgTsContext.getMetadata, etc.
return outputAcc;
},
],
}),
],
};Popular extension hooks like makeKyselyHook(), generateZodSchemas, and generateKnexTablesModule are PgTs generator-specific hooks and must be placed in PgTsGeneratorConfig.preRenderHooks.
V3 to V4 Migration
In V3, all pre-render hooks received InstantiatedConfig as the second parameter and were configured at the top level. In V4:
- Global hooks no longer receive config as a parameter — use
useKanelContext()instead - PgTs-specific hooks are configured inside
makePgTsGenerator()and receivePgTsGeneratorContextas a parameter
V3 pattern (deprecated):
import { makeKyselyHook } from 'kanel-kysely';
module.exports = {
preRenderHooks: [
(outputAcc, instantiatedConfig) => {
// Access instantiatedConfig here
return outputAcc;
},
makeKyselyHook(),
],
};V4 pattern:
import { makePgTsGenerator, useKanelContext } from 'kanel';
import { makeKyselyHook } from 'kanel-kysely';
module.exports = {
generators: [
makePgTsGenerator({
// PgTs-specific hooks go here
preRenderHooks: [makeKyselyHook()],
}),
],
// Global hooks go here (optional)
preRenderHooks: [
(outputAcc) => {
const context = useKanelContext(); // Access context if needed
return outputAcc;
},
],
};Working with Output
The outputAcc (short for accumulated output) will contain the output as it looks when calling this hook. The hook should return the entire output as it should appear after processing.
An Output instance is a record of paths (strings) and Declarations. Each entry represents a file that will be written to the file system. Declarations can be either typeDeclaration, interface or generic. If you want to create just any code, use the last kind (generic) where you can just add raw string lines.
Any declaration can contain a number of comment lines and a number of type imports. Comment lines go into the property comment, and they will be rendered as JSDoc comments, i.e. with /** ... */ syntax. The type imports can specify if the declaration needs to import something from another package (not necessarily only types).
To understand declarations better, look at the ts-declaration-types.ts file:
import type { default as Output, Path } from "../Output";
import type TypeImport from "./TypeImport";
export type DeclarationBase = {
comment?: string[];
typeImports?: TypeImport[];
};
export type TypeDeclaration = DeclarationBase & {
declarationType: "typeDeclaration";
name: string;
/** Must be valid TypeScript */
typeDefinition: string[];
exportAs: "named" | "default";
};
export type InterfacePropertyDeclaration = DeclarationBase & {
name: string;
dimensions: number;
isNullable: boolean;
isOptional: boolean;
typeName: string;
};
export type InterfaceDeclaration = DeclarationBase & {
declarationType: "interface";
name: string;
base?: string;
properties: InterfacePropertyDeclaration[];
exportAs: "named" | "default";
};
export type EnumDeclaration = DeclarationBase & {
declarationType: "enum";
name: string;
values: string[];
exportAs: "named" | "default";
};
export type ConstantDeclaration = DeclarationBase & {
declarationType: "constant";
name: string;
/** Must be valid TypeScript */
type: string | undefined;
value: string | string[];
exportAs: "named" | "default";
};
export type GenericDeclaration = DeclarationBase & {
declarationType: "generic";
lines: string[];
};
export type TsDeclaration =
| TypeDeclaration
| InterfaceDeclaration
| EnumDeclaration
| ConstantDeclaration
| GenericDeclaration;
export function registerTsDeclaration(
output: Output,
path: Path,
declaration: TsDeclaration,
): Output {
const newOutput = { ...output };
if (newOutput[path]) {
if (newOutput[path].fileType !== "typescript") {
throw new Error(
`Path ${path} already exists and is not a typescript file`,
);
}
newOutput[path].declarations.push(declaration);
} else {
newOutput[path] = { fileType: "typescript", declarations: [declaration] };
}
return newOutput;
}Execution Flow
Understanding when hooks run is important:
- Generator 1 (PgTs) runs → produces output
- Generator 1's
preRenderHooksrun on its output (receivePgTsGeneratorContext) - Generator 2 runs → produces output (if any)
- Generator 2's
preRenderHooksrun on its output - All output is accumulated (merged)
- Global
preRenderHooksrun on everything (useuseKanelContext()) - Files are rendered to strings
postRenderHooksrun on rendered lines
Examples
Example: Generate Index File (Global Hook)
import { generateIndexFile, makePgTsGenerator } from 'kanel';
const config = {
generators: [makePgTsGenerator()],
// Run after all generators complete
preRenderHooks: [generateIndexFile],
};Example: Kysely + Zod (PgTs-Specific Hooks)
import { makePgTsGenerator } from 'kanel';
import { makeKyselyHook } from 'kanel-kysely';
import { generateZodSchemas } from 'kanel-zod';
const config = {
generators: [
makePgTsGenerator({
// These transform the PgTs generator's output
preRenderHooks: [
makeKyselyHook(),
generateZodSchemas,
],
}),
],
};For more examples, see the examples folder in the repository.