05Project
Custom plugin example
Two small plugins applied to a Swagger 2.0 document (swagger.json):
- plugins/sdk-names.ts names operations after an
x-sdk-namevendor extension (Users_List→listUsers) with theoperationNamehook.Teams_ListMembershas no extension and keeps the default name,teamsListMembers. - plugins/operation-catalog.ts adds a
@seelink to each operation’s documentation with thejsdochook and emitscatalog.ts, a typed constant listing every operation, with thegeneratehook.
pnpm example:custom-plugin # from the repository root: build swaggerz, generate, print the catalogOr, inside this directory once swaggerz is built:
pnpm generate # src/generated/api, including catalog.tspnpm start # prints the catalog (src/catalog-report.ts, run by Node.js directly)pnpm check # type-checks src/, the plugins and the configPlugin files are owned by the generator like built-in files: they are recorded in the manifest and removed when a later run no longer produces them. See docs/plugins.md for every hook.
Config
import { defineConfig } from 'swaggerz';import { operationCatalog } from './plugins/operation-catalog.ts';import { sdkNames } from './plugins/sdk-names.ts';
export default defineConfig({ spec: { version: '2.0', input: './swagger.json' }, output: { directory: './src/generated/api' }, plugins: [sdkNames(), operationCatalog({ docsBaseUrl: 'https://docs.example.com/directory' })],});Sources
// Prints the typed operation catalog that the operation-catalog plugin emits. Node.js runs this// file directly (type stripping), so it imports the generated catalog with its `.ts` extension.import { operations } from './generated/api/catalog.ts';
const width = Math.max(...operations.map((operation) => operation.method.length));for (const operation of operations) { console.log(`${operation.method.padEnd(width)} ${operation.path} → ${operation.name}()`); console.log(`${' '.repeat(width)} ${operation.summary}`); console.log(`${' '.repeat(width)} ${operation.docs}`);}import { client, getUser, listUsers, teamsListMembers, type User } from './generated/api/index.js';import { operations, type OperationName } from './generated/api/catalog.js';
client.setConfig({ baseUrl: process.env['DIRECTORY_API_URL'] ?? 'https://api.example.com/v2' });
// Named by the sdk-names plugin from `x-sdk-name`; `Teams_ListMembers` has none and keeps the// default name.export async function adminNames(): Promise<string[]> { const admins: User[] = await listUsers({ role: 'admin', top: 10 }); return admins.map((user) => user.name);}
export async function teamEmails(teamId: string): Promise<string[]> { const members = await teamsListMembers({ teamId }); return members.flatMap((member) => (member.email == null ? [] : [member.email]));}
export const fetchUser = (userId: string) => getUser({ userId });
// The catalog emitted by the operation-catalog plugin is fully typed.export const readOperations: OperationName[] = operations .filter((operation) => operation.method === 'GET') .map((operation) => operation.name);
export function describe(name: OperationName): string { const operation = operations.find((candidate) => candidate.name === name); return operation === undefined ? name : `${operation.method} ${operation.path} — ${operation.summary} (${operation.docs})`;}import { definePlugin, type GeneratedFile } from 'swaggerz';
export interface OperationCatalogOptions { /** Base URL of the API reference; each operation links to `<base>#<operationId>`. */ readonly docsBaseUrl: string;}
/** * Adds a `@see` link to the reference documentation of every operation and emits `catalog.ts` * (or `catalog.js`), a typed list of all operations for building menus, permissions or logs. */export function operationCatalog(options: OperationCatalogOptions) { const docsUrl = (operationId: string | undefined, fallback: string) => `${options.docsBaseUrl}#${encodeURIComponent(operationId ?? fallback)}`;
return definePlugin({ name: 'operation-catalog',
jsdoc(block, target) { if (target.kind === 'model') return; const { operation } = target; return { ...block, tags: [...block.tags, { tag: 'see', text: docsUrl(operation.operationId, operation.key) }], }; },
generate(ctx): GeneratedFile[] { const entries = ctx.project.operations.map((symbol) => ({ name: symbol.name, method: symbol.operation.method.toUpperCase(), path: symbol.operation.path, summary: symbol.operation.summary ?? '', tags: symbol.operation.tags, docs: docsUrl(symbol.operation.operationId, symbol.key), })); // JSON.stringify produces valid, safely escaped JavaScript literals. const list = JSON.stringify(entries, null, 2); const typescript = ctx.config.output.language === 'typescript'; const content = typescript ? `// @generated by the operation-catalog plugin.\n\nexport const operations = ${list} as const;\n\nexport type OperationName = (typeof operations)[number]['name'];\n` : `// @generated by the operation-catalog plugin.\n\n/** @type {const} */\nexport const operations = ${list};\n`; return [{ path: typescript ? 'catalog.ts' : 'catalog.js', content, origin: '' }]; }, });}import { definePlugin } from 'swaggerz';
/** * Names operations after an `x-sdk-name` vendor extension when the specification provides one, * e.g. `Users_List` → `listUsers`. Other operations keep swaggerz's default name. */export function sdkNames(extension = 'x-sdk-name') { return definePlugin({ name: 'sdk-names', operationName(operation) { const name = operation.extensions[extension]; return typeof name === 'string' && /^[A-Za-z_$][\w$]*$/.test(name) ? name : undefined; }, });}Generated files
17 files for 4 operations and 3 models, generated with this config when the site was built:
catalog.ts, client.ts, index.tsmodels/3 filesruntime/7 filesteams/1 filesusers/3 files