@contractspec/tool.create-contractspec-plugin 1.57.0 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1081 @@
1
+ // @bun
2
+ // src/templates/example-generator.ts
3
+ function createExampleGeneratorTemplate() {
4
+ return {
5
+ files: {
6
+ "package.json": `{
7
+ "name": "{{integrationPackageName}}",
8
+ "version": "{{version}}",
9
+ "description": "{{description}}",
10
+ "keywords": [
11
+ "contractspec",
12
+ "plugin",
13
+ "generator",
14
+ "markdown",
15
+ "documentation",
16
+ "typescript"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": "./dist/index.js",
23
+ "./types": "./dist/types.js",
24
+ "./generator": "./dist/generator.js",
25
+ "./config": "./dist/config.js",
26
+ "./*": "./*"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "scripts": {
34
+ "publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
35
+ "publish:pkg:canary": "bun publish:pkg --tag canary",
36
+ "prebuild": "contractspec-bun-build prebuild",
37
+ "build": "bun run prebuild && bun run build:bundle && bun run build:types",
38
+ "build:bundle": "contractspec-bun-build transpile",
39
+ "build:types": "contractspec-bun-build types",
40
+ "typecheck": "tsc --noEmit",
41
+ "dev": "contractspec-bun-build dev",
42
+ "clean": "rimraf dist .turbo",
43
+ "lint": "bun lint:fix",
44
+ "lint:fix": "eslint src --fix",
45
+ "lint:check": "eslint src",
46
+ "test": "bun test",
47
+ "test:watch": "bun test --watch",
48
+ "test:coverage": "bun test --coverage",
49
+ "test:smoke": "bun test/smoke.test.ts"
50
+ },
51
+ "dependencies": {
52
+ "@contractspec/lib.contracts": "workspace:*",
53
+ "@contractspec/lib.schema": "workspace:*",
54
+ "zod": "catalog:"
55
+ },
56
+ "devDependencies": {
57
+ "@contractspec/tool.bun": "workspace:*",
58
+ "@contractspec/tool.typescript": "workspace:*",
59
+ "typescript": "catalog:",
60
+ "@types/node": "^22.0.0",
61
+ "rimraf": "^6.0.1"
62
+ },
63
+ "peerDependencies": {
64
+ "@contractspec/lib.contracts": "^1.0.0",
65
+ "@contractspec/lib.schema": "^1.0.0"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public",
69
+ "registry": "https://registry.npmjs.org/"
70
+ },
71
+ "license": "MIT",
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "https://github.com/lssm-tech/contractspec.git",
75
+ "directory": "packages/integrations/{{name}}"
76
+ },
77
+ "homepage": "https://contractspec.io",
78
+ "bugs": {
79
+ "url": "https://github.com/lssm-tech/contractspec/issues"
80
+ },
81
+ "author": {
82
+ "name": "{{author}}"
83
+ },
84
+ "engines": {
85
+ "node": ">=18.0.0",
86
+ "bun": ">=1.0.0"
87
+ }
88
+ }`,
89
+ "README.md": `# {{integrationPackageName}}
90
+
91
+ {{description}}
92
+
93
+ ## Overview
94
+
95
+ This is a ContractSpec plugin that generates markdown documentation from ContractSpec specifications. It transforms structured spec definitions into human-readable documentation that can be used for API docs, user guides, or technical specifications.
96
+
97
+ ## Features
98
+
99
+ - \uD83D\uDE80 **Spec-First Generation**: Converts ContractSpec specs to markdown automatically
100
+ - \uD83D\uDCDD **Rich Formatting**: Supports tables, lists, and detail views
101
+ - \uD83D\uDD27 **Configurable Output**: Customize formatting, field selection, and styling
102
+ - \uD83D\uDCCA **Data Integration**: Works with schema models and instance data
103
+ - \uD83C\uDFAF **Type Safe**: Full TypeScript support with proper type definitions
104
+ - \uD83E\uDDEA **Well Tested**: Comprehensive test suite included
105
+
106
+ ## Installation
107
+
108
+ \`\`\`bash
109
+ npm install {{integrationPackageName}}
110
+ \`\`\`
111
+
112
+ ## Usage
113
+
114
+ ### Basic Usage
115
+
116
+ \`\`\`typescript
117
+ import { {{className}} } from "{{integrationPackageName}}";
118
+
119
+ const generator = new {{className}}({
120
+ outputDir: "./docs",
121
+ format: "table", // or "list", "detail", "auto"
122
+ includeFields: ["id", "name", "description"],
123
+ });
124
+
125
+ // Generate markdown from specs
126
+ await generator.generateFromSpec(specPath, outputPath);
127
+ \`\`\`
128
+
129
+ ### Advanced Configuration
130
+
131
+ \`\`\`typescript
132
+ import { {{className}} } from "{{integrationPackageName}}";
133
+
134
+ const generator = new {{className}}({
135
+ outputDir: "./docs",
136
+ format: "auto",
137
+ title: "API Documentation",
138
+ description: "Auto-generated API documentation",
139
+ maxItems: 100,
140
+ maxDepth: 3,
141
+ fieldLabels: {
142
+ id: "ID",
143
+ createdAt: "Created Date",
144
+ updatedAt: "Last Modified"
145
+ },
146
+ summaryFields: ["id", "name", "status"],
147
+ excludeFields: ["internalNotes", "metadata"]
148
+ });
149
+ \`\`\`
150
+
151
+ ## Configuration Options
152
+
153
+ | Option | Type | Default | Description |
154
+ |--------|------|---------|-------------|
155
+ | \`outputDir\` | \`string\` | \`"./docs"\` | Directory for generated files |
156
+ | \`format\` | \`string\` | \`"auto"\` | Output format: \`"table"\`, \`"list"\`, \`"detail"\`, \`"auto"\` |
157
+ | \`title\` | \`string\` | undefined | Document title |
158
+ | \`description\` | \`string\` | undefined | Document description |
159
+ | \`maxItems\` | \`number\` | \`100\` | Maximum items to render in tables |
160
+ | \`maxDepth\` | \`number\` | \`2\` | Maximum nesting depth for objects |
161
+ | \`includeFields\` | \`string[]\` | undefined | Only include these fields |
162
+ | \`excludeFields\` | \`string[]\` | \`[]\` | Exclude these fields from output |
163
+ | \`fieldLabels\` | \`Record<string, string>\` | undefined | Custom field labels |
164
+ | \`summaryFields\` | \`string[]\` | undefined | Fields for list summaries |
165
+
166
+ ## Plugin Interface
167
+
168
+ This plugin implements the ContractSpec generator interface:
169
+
170
+ \`\`\`typescript
171
+ interface GeneratorPlugin {
172
+ readonly id: string;
173
+ readonly name: string;
174
+ readonly version: string;
175
+
176
+ initialize(config: GeneratorConfig): Promise<void>;
177
+ generate(spec: SpecDefinition, context: GeneratorContext): Promise<GeneratorResult>;
178
+ cleanup(): Promise<void>;
179
+ }
180
+ \`\`\`
181
+
182
+ ## Development
183
+
184
+ ### Setup
185
+
186
+ \`\`\`bash
187
+ # Clone the repository
188
+ git clone https://github.com/lssm-tech/contractspec.git
189
+ cd contractspec/packages/libs/plugins/{{name}}
190
+
191
+ # Install dependencies
192
+ bun install
193
+
194
+ # Run tests
195
+ bun test
196
+
197
+ # Build the plugin
198
+ bun run build
199
+ \`\`\`
200
+
201
+ ### Testing
202
+
203
+ \`\`\`bash
204
+ # Run all tests
205
+ bun test
206
+
207
+ # Run tests in watch mode
208
+ bun test:watch
209
+
210
+ # Run tests with coverage
211
+ bun test:coverage
212
+ \`\`\`
213
+
214
+ ### Building
215
+
216
+ \`\`\`bash
217
+ # Build the plugin
218
+ bun run build
219
+
220
+ # Build types only
221
+ bun run build:types
222
+
223
+ # Build bundle only
224
+ bun run build:bundle
225
+ \`\`\`
226
+
227
+ ## Contributing
228
+
229
+ We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details.
230
+
231
+ ## License
232
+
233
+ MIT \xA9 {{author}}
234
+
235
+ ## Support
236
+
237
+ - \uD83D\uDCD6 [Documentation](https://contractspec.io/docs)
238
+ - \uD83D\uDC1B [Issues](https://github.com/lssm-tech/contractspec/issues)
239
+ - \uD83D\uDCAC [Discussions](https://github.com/lssm-tech/contractspec/discussions)`,
240
+ "src/index.ts": `/**
241
+ * {{integrationPackageName}}
242
+ * {{description}}
243
+
244
+ */
245
+
246
+ export { {{className}} } from "./generator.js";
247
+ export type { {{className}}Config, GeneratorResult } from "./types.js";
248
+ export { defaultConfig } from "./config.js";`,
249
+ "src/types.ts": `import type { AnySchemaModel } from "@contractspec/lib.schema";
250
+ import type { SpecDefinition } from "@contractspec/lib.contracts";
251
+
252
+ /**
253
+ * Configuration for the {{className}} plugin
254
+ */
255
+ export interface {{className}}Config {
256
+ /** Directory where markdown files will be generated */
257
+ outputDir: string;
258
+ /** Output format: table, list, detail, or auto */
259
+ format?: "table" | "list" | "detail" | "auto";
260
+ /** Title for the generated documentation */
261
+ title?: string;
262
+ /** Description to include below the title */
263
+ description?: string;
264
+ /** Maximum number of items to render in tables */
265
+ maxItems?: number;
266
+ /** Maximum nesting depth for nested objects */
267
+ maxDepth?: number;
268
+ /** Only include these fields (if not specified, all fields are included) */
269
+ includeFields?: string[];
270
+ /** Exclude these fields from output */
271
+ excludeFields?: string[];
272
+ /** Custom field labels (field name -> display label) */
273
+ fieldLabels?: Record<string, string>;
274
+ /** Fields to use for summary in list format */
275
+ summaryFields?: string[];
276
+ }
277
+
278
+ /**
279
+ * Context provided during generation
280
+ */
281
+ export interface GeneratorContext {
282
+ /** The spec definition being processed */
283
+ spec: SpecDefinition;
284
+ /** Schema models from the spec */
285
+ schemas: Record<string, AnySchemaModel>;
286
+ /** Instance data (optional) */
287
+ data?: unknown;
288
+ /** Additional metadata */
289
+ metadata?: Record<string, unknown>;
290
+ }
291
+
292
+ /**
293
+ * Result of generation
294
+ */
295
+ export interface GeneratorResult {
296
+ /** Path to the generated file */
297
+ outputPath: string;
298
+ /** Number of items processed */
299
+ itemCount: number;
300
+ /** Generation metadata */
301
+ metadata: {
302
+ specId: string;
303
+ generatedAt: Date;
304
+ format: string;
305
+ config: Partial<{{className}}Config>;
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Plugin metadata
311
+ */
312
+ export interface PluginMetadata {
313
+ readonly id: string;
314
+ readonly name: string;
315
+ readonly version: string;
316
+ readonly description: string;
317
+ readonly author: string;
318
+ readonly homepage?: string;
319
+ }
320
+
321
+ /**
322
+ * Error types for the plugin
323
+ */
324
+ export class {{className}}Error extends Error {
325
+ constructor(
326
+ message: string,
327
+ public readonly code: string,
328
+ public readonly details?: unknown
329
+ ) {
330
+ super(message);
331
+ this.name = "{{className}}Error";
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Validation errors
337
+ */
338
+ export class ValidationError extends {{className}}Error {
339
+ constructor(message: string, details?: unknown) {
340
+ super(message, "VALIDATION_ERROR", details);
341
+ this.name = "ValidationError";
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Configuration errors
347
+ */
348
+ export class ConfigurationError extends {{className}}Error {
349
+ constructor(message: string, details?: unknown) {
350
+ super(message, "CONFIGURATION_ERROR", details);
351
+ this.name = "ConfigurationError";
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Generation errors
357
+ */
358
+ export class GenerationError extends {{className}}Error {
359
+ constructor(message: string, details?: unknown) {
360
+ super(message, "GENERATION_ERROR", details);
361
+ this.name = "GenerationError";
362
+ }
363
+ }`,
364
+ "src/config.ts": `import type { {{className}}Config } from "./types.js";
365
+
366
+ /**
367
+ * Default configuration for the {{className}} plugin
368
+ */
369
+ export const defaultConfig: {{className}}Config = {
370
+ outputDir: "./docs",
371
+ format: "auto",
372
+ maxItems: 100,
373
+ maxDepth: 2,
374
+ excludeFields: [],
375
+ };
376
+
377
+ /**
378
+ * Merge user config with defaults
379
+ */
380
+ export function mergeConfig(userConfig: Partial<{{className}}Config>): {{className}}Config {
381
+ return {
382
+ ...defaultConfig,
383
+ ...userConfig,
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Validate configuration
389
+ */
390
+ export function validateConfig(config: {{className}}Config): void {
391
+ if (!config.outputDir) {
392
+ throw new Error("outputDir is required");
393
+ }
394
+
395
+ if (config.format && !["table", "list", "detail", "auto"].includes(config.format)) {
396
+ throw new Error("format must be one of: table, list, detail, auto");
397
+ }
398
+
399
+ if (config.maxItems !== undefined && config.maxItems < 1) {
400
+ throw new Error("maxItems must be greater than 0");
401
+ }
402
+
403
+ if (config.maxDepth !== undefined && config.maxDepth < 1) {
404
+ throw new Error("maxDepth must be greater than 0");
405
+ }
406
+ }`,
407
+ "src/generator.ts": `import { existsSync, mkdirSync, writeFileSync } from "fs";
408
+ import { join, dirname } from "path";
409
+ import type {
410
+ {{className}}Config,
411
+ GeneratorContext,
412
+ GeneratorResult,
413
+ PluginMetadata,
414
+ ConfigurationError,
415
+ GenerationError
416
+ } from "./types.js";
417
+ import { schemaToMarkdown } from "@contractspec/lib.contracts";
418
+ import { validateConfig, mergeConfig } from "./config.js";
419
+
420
+ /**
421
+ * {{className}} - Markdown Documentation Generator
422
+ *
423
+ * Generates markdown documentation from ContractSpec specifications.
424
+ */
425
+ export class {{className}} {
426
+ private readonly metadata: PluginMetadata;
427
+ private config: {{className}}Config;
428
+
429
+ constructor(config: Partial<{{className}}Config> = {}) {
430
+ this.metadata = {
431
+ id: "{{name}}",
432
+ name: "{{integrationPackageName}}",
433
+ version: "{{version}}",
434
+ description: "{{description}}",
435
+ author: "{{author}}",
436
+ homepage: "https://contractspec.io",
437
+ };
438
+
439
+ this.config = mergeConfig(config);
440
+ validateConfig(this.config);
441
+
442
+ // Ensure output directory exists
443
+ if (!existsSync(this.config.outputDir)) {
444
+ mkdirSync(this.config.outputDir, { recursive: true });
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Get plugin metadata
450
+ */
451
+ getMetadata(): PluginMetadata {
452
+ return this.metadata;
453
+ }
454
+
455
+ /**
456
+ * Generate markdown documentation from a spec and optional data
457
+ */
458
+ async generate(context: GeneratorContext): Promise<GeneratorResult> {
459
+ try {
460
+ const { spec, schemas, data } = context;
461
+
462
+ if (!spec) {
463
+ throw new GenerationError("Spec definition is required");
464
+ }
465
+
466
+ // Find the primary schema (first one by convention)
467
+ const schemaNames = Object.keys(schemas);
468
+ if (schemaNames.length === 0) {
469
+ throw new GenerationError("No schemas found in spec");
470
+ }
471
+
472
+ const primarySchemaName = schemaNames[0];
473
+ const primarySchema = schemas[primarySchemaName];
474
+
475
+ // Generate markdown
476
+ const markdown = schemaToMarkdown(primarySchema, data, {
477
+ title: this.config.title,
478
+ description: this.config.description,
479
+ format: this.config.format,
480
+ maxItems: this.config.maxItems,
481
+ maxDepth: this.config.maxDepth,
482
+ includeFields: this.config.includeFields,
483
+ excludeFields: this.config.excludeFields,
484
+ fieldLabels: this.config.fieldLabels,
485
+ summaryFields: this.config.summaryFields,
486
+ });
487
+
488
+ // Determine output file path
489
+ const fileName = this.generateFileName(spec.id || primarySchemaName);
490
+ const outputPath = join(this.config.outputDir, fileName);
491
+
492
+ // Write markdown file
493
+ writeFileSync(outputPath, markdown, "utf8");
494
+
495
+ const itemCount = Array.isArray(data) ? data.length : data ? 1 : 0;
496
+
497
+ return {
498
+ outputPath,
499
+ itemCount,
500
+ metadata: {
501
+ specId: spec.id || "unknown",
502
+ generatedAt: new Date(),
503
+ format: this.config.format || "auto",
504
+ config: {
505
+ format: this.config.format,
506
+ maxItems: this.config.maxItems,
507
+ maxDepth: this.config.maxDepth,
508
+ },
509
+ },
510
+ };
511
+ } catch (error) {
512
+ if (error instanceof GenerationError) {
513
+ throw error;
514
+ }
515
+ throw new GenerationError("Failed to generate documentation", error);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Generate documentation from a spec file path
521
+ */
522
+ async generateFromSpec(specPath: string, outputPath?: string): Promise<GeneratorResult> {
523
+ try {
524
+ // This would typically load the spec from file
525
+ // For now, we'll throw an error indicating this needs implementation
526
+ throw new Error("generateFromSpec needs to be implemented with spec loading logic");
527
+ } catch (error) {
528
+ if (error instanceof GenerationError) {
529
+ throw error;
530
+ }
531
+ throw new GenerationError("Failed to generate from spec file", error);
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Generate appropriate filename for the output
537
+ */
538
+ private generateFileName(specId: string): string {
539
+ // Convert spec ID to a filename-friendly format
540
+ const fileName = specId
541
+ .replace(/[^a-zA-Z0-9-_]/g, "-")
542
+ .replace(/-+/g, "-")
543
+ .toLowerCase();
544
+
545
+ return fileName.endsWith(".md")
546
+ ? fileName
547
+ : fileName + ".md";
548
+ }
549
+
550
+ /**
551
+ * Update configuration
552
+ */
553
+ updateConfig(newConfig: Partial<{{className}}Config>): void {
554
+ this.config = mergeConfig({ ...this.config, ...newConfig });
555
+ validateConfig(this.config);
556
+
557
+ // Ensure output directory exists if it changed
558
+ if (!existsSync(this.config.outputDir)) {
559
+ mkdirSync(this.config.outputDir, { recursive: true });
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Get current configuration
565
+ */
566
+ getConfig(): {{className}}Config {
567
+ return { ...this.config };
568
+ }
569
+
570
+ /**
571
+ * Cleanup resources (no-op for this plugin)
572
+ */
573
+ async cleanup(): Promise<void> {
574
+ // No resources to cleanup for this plugin
575
+ }
576
+ }`,
577
+ "src/utils/test-utils.ts": `
578
+
579
+ import type { AnySchemaModel } from "@contractspec/lib.schema";
580
+ import type { {{className}}Config } from "../types.js";
581
+
582
+ /**
583
+ * Create a mock schema for testing
584
+ */
585
+ export function createMockSchema(overrides: Partial<AnySchemaModel> = {}): AnySchemaModel {
586
+ return {
587
+ config: {
588
+ fields: {
589
+ id: { type: "string", isOptional: false },
590
+ name: { type: "string", isOptional: false },
591
+ description: { type: "string", isOptional: true },
592
+ status: { type: "string", isOptional: true },
593
+ createdAt: { type: "date", isOptional: false },
594
+ updatedAt: { type: "date", isOptional: true },
595
+ },
596
+ ...overrides.config,
597
+ },
598
+ ...overrides,
599
+ };
600
+ }
601
+
602
+ /**
603
+ * Create mock data for testing
604
+ */
605
+ export function createMockData(count: number = 3) {
606
+ return Array.from({ length: count }, (_, i) => ({
607
+ id: "item-" + (i + 1),
608
+ name: "Test Item " + (i + 1),
609
+ description: "Description for test item " + (i + 1),
610
+ status: i % 2 === 0 ? "active" : "inactive",
611
+ createdAt: new Date(2024, 0, i + 1),
612
+ updatedAt: i % 3 === 0 ? undefined : new Date(2024, 0, i + 2),
613
+ }));
614
+ }
615
+
616
+
617
+ /**
618
+ * Create test configuration
619
+ */
620
+ export function createTestConfig(overrides: Partial<Config> = {}): {{className}}Config {
621
+ return {
622
+ outputDir: "./test-docs",
623
+ format: "table",
624
+ maxItems: 10,
625
+ maxDepth: 2,
626
+ excludeFields: [],
627
+ ...overrides,
628
+ };
629
+ }`,
630
+ "tests/generator.test.ts": `import { describe, it, expect, beforeEach, afterEach } from "bun:test";
631
+ import { existsSync, unlinkSync, readFileSync } from "fs";
632
+ import { join } from "path";
633
+ import { {{className}}, ConfigurationError, GenerationError } from "../src/generator.js";
634
+ import type { {{className}}Config } from "../src/types.js";
635
+ import { createMockSchema, createMockData, createTestConfig } from "../src/utils/test-utils.js";
636
+
637
+ describe("{{className}}", () => {
638
+ let generator: {{className}};
639
+ let testConfig: {{className}}Config;
640
+ const testOutputDir = "./test-output";
641
+
642
+ beforeEach(() => {
643
+ testConfig = createTestConfig({ outputDir: testOutputDir });
644
+ generator = new {{className}}(testConfig);
645
+ });
646
+
647
+ afterEach(() => {
648
+ // Clean up test files
649
+ if (existsSync(testOutputDir)) {
650
+ // In a real implementation, you'd recursively delete the directory
651
+ console.log("Cleaning up test directory: " + testOutputDir);
652
+ }
653
+ });
654
+
655
+ describe("constructor", () => {
656
+ it("should create a generator with default config", () => {
657
+ const gen = new {{className}}();
658
+ const config = gen.getConfig();
659
+
660
+ expect(config.outputDir).toBe("./docs");
661
+ expect(config.format).toBe("auto");
662
+ expect(config.maxItems).toBe(100);
663
+ });
664
+
665
+ it("should create a generator with custom config", () => {
666
+ const customConfig = {
667
+ outputDir: "./custom-docs",
668
+ format: "table" as const,
669
+ maxItems: 50,
670
+ };
671
+
672
+ const gen = new {{className}}(customConfig);
673
+ const config = gen.getConfig();
674
+
675
+ expect(config.outputDir).toBe("./custom-docs");
676
+ expect(config.format).toBe("table");
677
+ expect(config.maxItems).toBe(50);
678
+ });
679
+
680
+ it("should throw ConfigurationError for invalid config", () => {
681
+ expect(() => {
682
+ new {{className}}({ outputDir: "" });
683
+ }).toThrow(ConfigurationError);
684
+ });
685
+ });
686
+
687
+ describe("getMetadata", () => {
688
+ it("should return correct plugin metadata", () => {
689
+ const metadata = generator.getMetadata();
690
+
691
+ expect(metadata.id).toBe("{{name}}");
692
+ expect(metadata.name).toBe("{{integrationPackageName}}");
693
+ expect(metadata.description).toBe("{{description}}");
694
+ expect(metadata.author).toBe("{{author}}");
695
+ });
696
+ });
697
+
698
+ describe("generate", () => {
699
+ it("should generate markdown from schema and data", async () => {
700
+ const schema = createMockSchema();
701
+ const data = createMockData(3);
702
+
703
+ const result = await generator.generate({
704
+ spec: { id: "test-spec" } as any,
705
+ schemas: { TestItem: schema },
706
+ data,
707
+ });
708
+
709
+ expect(result.outputPath).toContain(testOutputDir);
710
+ expect(result.itemCount).toBe(3);
711
+ expect(existsSync(result.outputPath)).toBe(true);
712
+
713
+ // Check generated content
714
+ const content = readFileSync(result.outputPath, "utf8");
715
+ expect(content).toContain("item-1");
716
+ expect(content).toContain("Test Item 1");
717
+ });
718
+
719
+ it("should handle empty data", async () => {
720
+ const schema = createMockSchema();
721
+
722
+ const result = await generator.generate({
723
+ spec: { id: "test-spec" } as any,
724
+ schemas: { TestItem: schema },
725
+ data: [],
726
+ });
727
+
728
+ expect(result.itemCount).toBe(0);
729
+ expect(existsSync(result.outputPath)).toBe(true);
730
+ });
731
+
732
+ it("should throw GenerationError for missing spec", async () => {
733
+ await expect(
734
+ generator.generate({
735
+ spec: null as any,
736
+ schemas: {},
737
+ data: [],
738
+ })
739
+ ).rejects.toThrow(GenerationError);
740
+ });
741
+
742
+ it("should throw GenerationError for missing schemas", async () => {
743
+ await expect(
744
+ generator.generate({
745
+ spec: { id: "test-spec" } as any,
746
+ schemas: {},
747
+ data: [],
748
+ })
749
+ ).rejects.toThrow(GenerationError);
750
+ });
751
+ });
752
+
753
+ describe("updateConfig", () => {
754
+ it("should update configuration", () => {
755
+ generator.updateConfig({ maxItems: 25 });
756
+
757
+ const config = generator.getConfig();
758
+ expect(config.maxItems).toBe(25);
759
+ });
760
+
761
+ it("should throw ConfigurationError for invalid update", () => {
762
+ expect(() => {
763
+ generator.updateConfig({ format: "invalid" as any });
764
+ }).toThrow(ConfigurationError);
765
+ });
766
+ });
767
+
768
+ describe("cleanup", () => {
769
+ it("should cleanup successfully", async () => {
770
+ await expect(generator.cleanup()).resolves.not.toThrow();
771
+ });
772
+ });
773
+ });`,
774
+ "tests/utils.test.ts": `import { describe, it, expect } from "bun:test";
775
+ import { createMockSchema, createMockData, createTestConfig } from "../src/utils/test-utils.js";
776
+ import type { {{className}}Config } from "../src/types.js";
777
+
778
+ describe("Test Utils", () => {
779
+ describe("createMockSchema", () => {
780
+ it("should create a mock schema with default fields", () => {
781
+ const schema = createMockSchema();
782
+
783
+ expect(schema.config.fields).toBeDefined();
784
+ expect(schema.config.fields.id).toBeDefined();
785
+ expect(schema.config.fields.name).toBeDefined();
786
+ expect(schema.config.fields.description).toBeDefined();
787
+ });
788
+
789
+ it("should apply overrides", () => {
790
+ const overrides = {
791
+ config: {
792
+ fields: {
793
+ customField: { type: "number", isOptional: false },
794
+ },
795
+ },
796
+ };
797
+
798
+ const schema = createMockSchema(overrides);
799
+
800
+ expect(schema.config.fields.customField).toBeDefined();
801
+ expect(schema.config.fields.customField.type).toBe("number");
802
+ });
803
+ });
804
+
805
+ describe("createMockData", () => {
806
+ it("should create mock data with specified count", () => {
807
+ const data = createMockData(5);
808
+
809
+ expect(data).toHaveLength(5);
810
+ expect(data[0].id).toBe("item-1");
811
+ expect(data[4].id).toBe("item-5");
812
+ });
813
+
814
+ it("should create default count when not specified", () => {
815
+ const data = createMockData();
816
+
817
+ expect(data).toHaveLength(3);
818
+ });
819
+ });
820
+
821
+ describe("createTestConfig", () => {
822
+ it("should create test config with defaults", () => {
823
+ const config = createTestConfig();
824
+
825
+ expect(config.outputDir).toBe("./test-docs");
826
+ expect(config.format).toBe("table");
827
+ expect(config.maxItems).toBe(10);
828
+ });
829
+
830
+ it("should apply overrides", () => {
831
+ const overrides: Partial<{{className}}Config> = {
832
+ format: "list",
833
+ maxItems: 20,
834
+ };
835
+
836
+ const config = createTestConfig(overrides);
837
+
838
+ expect(config.format).toBe("list");
839
+ expect(config.maxItems).toBe(20);
840
+ });
841
+ });
842
+ });`,
843
+ ".github/workflows/ci.yml": `name: CI
844
+
845
+ on:
846
+ push:
847
+ branches: [ main, develop ]
848
+ pull_request:
849
+ branches: [ main ]
850
+
851
+ jobs:
852
+ smoke-test:
853
+ runs-on: ubuntu-latest
854
+ steps:
855
+ - uses: actions/checkout@v4
856
+
857
+ - name: Setup Bun
858
+ uses: oven-sh/setup-bun@v1
859
+ with:
860
+ bun-version: latest
861
+
862
+ - name: Install dependencies
863
+ run: bun install
864
+
865
+ - name: Run smoke test
866
+ run: bun run test:smoke
867
+
868
+ test:
869
+ runs-on: ubuntu-latest
870
+
871
+ strategy:
872
+ matrix:
873
+ node-version: [18, 20]
874
+
875
+ steps:
876
+ - uses: actions/checkout@v4
877
+
878
+ - name: Setup Bun
879
+ uses: oven-sh/setup-bun@v1
880
+ with:
881
+ bun-version: latest
882
+
883
+ - name: Install dependencies
884
+ run: bun install
885
+
886
+ - name: Run tests
887
+ run: bun test
888
+
889
+ - name: Run tests with coverage
890
+ run: bun test --coverage
891
+
892
+ - name: Build
893
+ run: bun run build
894
+
895
+ - name: Lint
896
+ run: bun run lint:check
897
+
898
+ publish:
899
+ needs: test
900
+ runs-on: ubuntu-latest
901
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
902
+
903
+ steps:
904
+ - uses: actions/checkout@v4
905
+
906
+ - name: Setup Bun
907
+ uses: oven-sh/setup-bun@v1
908
+ with:
909
+ bun-version: latest
910
+
911
+ - name: Install dependencies
912
+ run: bun install
913
+
914
+ - name: Build
915
+ run: bun run build
916
+
917
+ - name: Publish to NPM
918
+ run: bun run publish:pkg
919
+ env:
920
+ NPM_TOKEN: \${{ secrets.NPM_TOKEN }}`,
921
+ "tests/smoke.test.ts": `import { describe, it, expect, beforeAll, afterAll } from "bun:test";
922
+ import { existsSync, mkdirSync, rmSync } from "fs";
923
+ import { {{className}} } from "../src/generator.js";
924
+
925
+ const SMOKE_OUTPUT_DIR = "./.smoke-test-output";
926
+
927
+ describe("{{className}} Smoke Test", () => {
928
+ beforeAll(() => {
929
+ if (!existsSync(SMOKE_OUTPUT_DIR)) {
930
+ mkdirSync(SMOKE_OUTPUT_DIR, { recursive: true });
931
+ }
932
+ });
933
+
934
+ afterAll(() => {
935
+ if (existsSync(SMOKE_OUTPUT_DIR)) {
936
+ rmSync(SMOKE_OUTPUT_DIR, { recursive: true, force: true });
937
+ }
938
+ });
939
+
940
+ it("should instantiate without errors", () => {
941
+ const generator = new {{className}}({ outputDir: SMOKE_OUTPUT_DIR });
942
+ expect(generator).toBeDefined();
943
+ expect(typeof generator.generate).toBe("function");
944
+ expect(typeof generator.getMetadata).toBe("function");
945
+ expect(typeof generator.getConfig).toBe("function");
946
+ expect(typeof generator.cleanup).toBe("function");
947
+ });
948
+
949
+ it("should generate output file", async () => {
950
+ const generator = new {{className}}({ outputDir: SMOKE_OUTPUT_DIR });
951
+
952
+ const result = await generator.generate({
953
+ spec: { id: "smoke-test-spec" } as any,
954
+ schemas: {
955
+ TestEntity: {
956
+ config: {
957
+ fields: {
958
+ id: { type: "string", isOptional: false },
959
+ name: { type: "string", isOptional: false },
960
+ },
961
+ },
962
+ },
963
+ },
964
+ data: [{ id: "test-1", name: "Smoke Test Entity" }],
965
+ });
966
+
967
+ expect(result.outputPath).toBeDefined();
968
+ expect(result.itemCount).toBe(1);
969
+ expect(existsSync(result.outputPath)).toBe(true);
970
+ });
971
+
972
+ it("should handle config updates", () => {
973
+ const generator = new {{className}}();
974
+ generator.updateConfig({ format: "list", maxItems: 50 });
975
+ const config = generator.getConfig();
976
+ expect(config.format).toBe("list");
977
+ expect(config.maxItems).toBe(50);
978
+ });
979
+
980
+ it("should provide valid metadata", () => {
981
+ const generator = new {{className}}();
982
+ const metadata = generator.getMetadata();
983
+ expect(metadata.id).toBeDefined();
984
+ expect(metadata.name).toBeDefined();
985
+ expect(metadata.version).toBeDefined();
986
+ expect(typeof metadata.id).toBe("string");
987
+ expect(typeof metadata.name).toBe("string");
988
+ expect(typeof metadata.version).toBe("string");
989
+ });
990
+
991
+ it("should cleanup without errors", async () => {
992
+ const generator = new {{className}}({ outputDir: SMOKE_OUTPUT_DIR });
993
+ await expect(generator.cleanup()).resolves.not.toThrow();
994
+ });
995
+ });`,
996
+ ".eslintrc.json": `{
997
+ "extends": [
998
+ "@contractspec/eslint-config-typescript"
999
+ ],
1000
+ "parser": "@typescript-eslint/parser",
1001
+ "plugins": ["@typescript-eslint"],
1002
+ "rules": {
1003
+ "@typescript-eslint/no-unused-vars": "error",
1004
+ "@typescript-eslint/explicit-function-return-type": "warn",
1005
+ "@typescript-eslint/no-explicit-any": "warn"
1006
+ }
1007
+ }`,
1008
+ "tsconfig.json": `{
1009
+ "extends": "@contractspec/tsconfig-base",
1010
+ "compilerOptions": {
1011
+ "outDir": "./dist",
1012
+ "rootDir": "./src",
1013
+ "declaration": true,
1014
+ "declarationMap": true,
1015
+ "sourceMap": true
1016
+ },
1017
+ "include": [
1018
+ "src/**/*"
1019
+ ],
1020
+ "exclude": [
1021
+ "node_modules",
1022
+ "dist",
1023
+ "tests"
1024
+ ]
1025
+ }`,
1026
+ "tsdown.config.js": `import { defineConfig, nodeLib } from "@contractspec/tool.bun";
1027
+
1028
+ export default defineConfig(() => ({
1029
+ ...nodeLib,
1030
+ entry: ["src/index.ts"],
1031
+ }));`,
1032
+ LICENSE: `MIT License
1033
+
1034
+ Copyright (c) {{currentYear}} {{author}}
1035
+
1036
+ Permission is hereby granted, free of charge, to any person obtaining a copy
1037
+ of this software and associated documentation files (the "Software"), to deal
1038
+ in the Software without restriction, including without limitation the rights
1039
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1040
+ copies of the Software, and to permit persons to whom the Software is
1041
+ furnished to do so, subject to the following conditions:
1042
+
1043
+ The above copyright notice and this permission notice shall be included in all
1044
+ copies or substantial portions of the Software.
1045
+
1046
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1047
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1048
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1049
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1050
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1051
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1052
+ SOFTWARE.`,
1053
+ "src/templates/index.ts": `/**
1054
+ * Template registry for the create-contractspec-plugin tool
1055
+ */
1056
+
1057
+ export { createExampleGeneratorTemplate } from "./example-generator.js";
1058
+ export type { Template, TemplateFile } from "./types.js";`,
1059
+ "src/templates/types.ts": `/**
1060
+ * Template types
1061
+ */
1062
+
1063
+ export interface TemplateFile {
1064
+ content: string;
1065
+ path: string;
1066
+ }
1067
+
1068
+ export interface Template {
1069
+ name: string;
1070
+ description: string;
1071
+ files: Record<string, string>;
1072
+ dependencies?: string[];
1073
+ devDependencies?: string[];
1074
+ }
1075
+ `
1076
+ }
1077
+ };
1078
+ }
1079
+ export {
1080
+ createExampleGeneratorTemplate
1081
+ };