Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
node-version: [20, 22]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install pnpm
uses: pnpm/action-setup@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install pnpm
uses: pnpm/action-setup@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install pnpm
uses: pnpm/action-setup@v4
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,10 @@ plugin documentation {
| Option | Type | Default | Description |
|---|---|---|---|
| `output` | `string` | ZenStack default output path | Directory to write generated docs |
| `cleanOutput` | `boolean` | `false` | When `true`, the output directory is removed before generation to ensure a clean build (use with caution) |
| `title` | `string` | `"Schema Documentation"` | Heading on the index page |
| `fieldOrder` | `"declaration"` or `"alphabetical"` | `"declaration"` | How fields are ordered in tables |
| `includeInternalModels` | `boolean` | `false` | Include models marked `@@ignore` in output |
| `includeInternalModels` | `boolean` | `false` | Include models marked `@@ignore` or annotated with `@@meta('doc:ignore', true)` in output |
| `includeRelationships` | `boolean` | `true` | Generate relationship sections and `relationships.md` |
| `includePolicies` | `boolean` | `true` | Generate access policy tables |
| `includeValidation` | `boolean` | `true` | Generate validation rule tables |
Expand Down
19 changes: 18 additions & 1 deletion src/extractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,24 @@ export function isFieldRequired(field: DataField): boolean {
* Returns `true` if the model has the `@@ignore` attribute.
*/
export function isIgnoredModel(model: DataModel): boolean {
return model.attributes.some((a) => a.decl.ref?.name === '@@ignore');
if (model.attributes.some((a) => a.decl.ref?.name === '@@ignore')) {
return true;
}

// Support @@meta('doc:ignore', true)
for (const attribute of model.attributes) {
if (attribute.decl.ref?.name !== '@@meta') {
continue;
}

const key = stripQuotes(attribute.args[0]?.$cstNode?.text ?? '');
const value = stripQuotes(attribute.args[1]?.$cstNode?.text ?? '');
if (key === 'doc:ignore' && value === 'true') {
return true;
}
}

return false;
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ export async function generate(context: CliGeneratorContext): Promise<void> {
options.genCtx = genCtx;

try {
if (pluginOptions.cleanOutput) {
try {
const root = path.parse(outputDir).root;
if (outputDir === root) {
throw new Error('Refusing to remove root directory');
}

if (fs.existsSync(outputDir)) {
fs.rmSync(outputDir, { force: true, recursive: true });
}
} catch (error) {
throw new Error(
`Failed to clean output directory "${outputDir}": ${error instanceof Error ? error.message : String(error)}`,
);
}
}

fs.mkdirSync(outputDir, { recursive: true });
} catch (error) {
throw new Error(
Expand Down Expand Up @@ -302,6 +319,7 @@ function resolveOutputDir(options: PluginOptions, defaultPath: string): string {
*/
function resolvePluginOptions(raw: Record<string, unknown>): PluginOptions {
return {
cleanOutput: raw['cleanOutput'] === true,
diagramEmbed: (['file', 'inline'] as const).includes(
raw['diagramEmbed'] as 'file' | 'inline',
)
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export type PluginOptions = {
* inline in the markdown (`'inline'`). Only applies when `diagramFormat` is
* `'svg'` or `'both'`.
*/
/**
* If true, delete the output directory before generating files.
*/
cleanOutput?: boolean;
diagramEmbed?: 'file' | 'inline';
diagramFormat?: 'both' | 'mermaid' | 'svg';
erdFormat?: 'both' | 'mmd' | 'svg';
Expand Down
10 changes: 5 additions & 5 deletions test/generator/__snapshots__/snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`documentation plugin: snapshot > snapshot: full representative schema o
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
// Source: schema.zmodel · Generated: 2026-03-08 //
// Source: schema.zmodel · Generated: <REDACTED> //
//////////////////////////////////////////////////////////////////////////////////////////////
\`\`\`

Expand Down Expand Up @@ -73,7 +73,7 @@ exports[`documentation plugin: snapshot > snapshot: full representative schema o
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
// Source: schema.zmodel · Generated: 2026-03-08 //
// Source: schema.zmodel · Generated: <REDACTED> //
//////////////////////////////////////////////////////////////////////////////////////////////
\`\`\`

Expand Down Expand Up @@ -125,7 +125,7 @@ exports[`documentation plugin: snapshot > snapshot: full representative schema o
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
// Source: schema.zmodel · Generated: 2026-03-08 //
// Source: schema.zmodel · Generated: <REDACTED> //
//////////////////////////////////////////////////////////////////////////////////////////////
\`\`\`

Expand Down Expand Up @@ -218,7 +218,7 @@ exports[`documentation plugin: snapshot > snapshot: full representative schema o
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
// Source: schema.zmodel · Generated: 2026-03-08 //
// Source: schema.zmodel · Generated: <REDACTED> //
//////////////////////////////////////////////////////////////////////////////////////////////
\`\`\`

Expand Down Expand Up @@ -349,7 +349,7 @@ exports[`documentation plugin: snapshot > snapshot: full representative schema o
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
// Source: schema.zmodel · Generated: 2026-03-08 //
// Source: schema.zmodel · Generated: <REDACTED> //
//////////////////////////////////////////////////////////////////////////////////////////////
\`\`\`

Expand Down
61 changes: 61 additions & 0 deletions test/generator/plugin-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import plugin from '../../src';
import { loadSchema } from '../utils';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';

describe('documentation plugin: plugin options', () => {
it('cleanOutput deletes existing files in the output directory before generation', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-plugin-'));
// create a stale file that should be removed by cleanOutput
fs.writeFileSync(path.join(tmpDir, 'stale.txt'), 'stale');

const model = await loadSchema(`
model A {
id String @id @default(cuid())
}
`);

await plugin.generate({
defaultOutputPath: tmpDir,
model,
pluginOptions: { cleanOutput: true, output: tmpDir },
schemaFile: 'schema.zmodel',
});

// ensure output dir is prepopulated and removed by cleanOutput

expect(fs.existsSync(path.join(tmpDir, 'stale.txt'))).toBe(false);
expect(fs.existsSync(path.join(tmpDir, 'index.md'))).toBe(true);
});
it("models with @@meta('doc:ignore', true) are treated as internal and excluded when includeInternalModels is false", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-plugin-'));

const schema = `
model Public {
id String @id @default(cuid())
}

model Internal {
id String @id @default(cuid())
@@meta('doc:ignore', true)
}
`;

const model = await loadSchema(schema);

await plugin.generate({
defaultOutputPath: tmpDir,
model,
pluginOptions: { includeInternalModels: false, output: tmpDir },
schemaFile: 'schema.zmodel',
});

// Public model should be present, internal model should be excluded
expect(fs.existsSync(path.join(tmpDir, 'models', 'Public.md'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'models', 'Internal.md'))).toBe(
false,
);
});
});
6 changes: 2 additions & 4 deletions test/generator/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ function stabilize(content: string): string {
/\*\*Generated\*\* \| \d{4}-\d{2}-\d{2}/gu,
'**Generated** | <REDACTED>',
)
.replaceAll(
/Generated:\*\* \d{4}-\d{2}-\d{2}/gu,
'Generated:** <REDACTED>',
);
.replaceAll(/Generated:\*\* \d{4}-\d{2}-\d{2}/gu, 'Generated:** <REDACTED>')
.replaceAll(/Generated: \d{4}-\d{2}-\d{2}/gu, 'Generated: <REDACTED>');
}

describe('documentation plugin: snapshot', () => {
Expand Down