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