wip league admin tools
This commit is contained in:
@@ -12,17 +12,21 @@
|
||||
* Or use: npm run api:sync-types (runs both)
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { createHash } from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function sha256OfFile(filePath: string): Promise<string> {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
return createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
async function generateTypes() {
|
||||
const openapiPath = path.join(__dirname, '../apps/api/openapi.json');
|
||||
const outputDir = path.join(__dirname, '../apps/website/lib/types/generated');
|
||||
const outputFile = path.join(outputDir, 'api.ts');
|
||||
|
||||
console.log('🔄 Generating TypeScript types from OpenAPI spec...');
|
||||
|
||||
@@ -31,7 +35,7 @@ async function generateTypes() {
|
||||
await fs.access(openapiPath);
|
||||
} catch {
|
||||
console.error(`❌ OpenAPI spec not found at: ${openapiPath}`);
|
||||
console.error('Run "npm run api:generate-spec" first to generate the OpenAPI spec from NestJS');
|
||||
console.error('Restore the committed OpenAPI contract or run "npm run api:generate-spec" to regenerate it.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -39,33 +43,24 @@ async function generateTypes() {
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
try {
|
||||
// Skip generating monolithic api.ts file
|
||||
// Use openapi-typescript to generate types
|
||||
// console.log('📝 Running openapi-typescript...');
|
||||
// execSync(`npx openapi-typescript "${openapiPath}" -o "${outputFile}"`, {
|
||||
// stdio: 'inherit',
|
||||
// cwd: path.join(__dirname, '..')
|
||||
// });
|
||||
|
||||
// console.log(`✅ TypeScript types generated at: ${outputFile}`);
|
||||
|
||||
// Generate individual DTO files
|
||||
await generateIndividualDtoFiles(openapiPath, outputDir);
|
||||
const specSha256 = await sha256OfFile(openapiPath);
|
||||
|
||||
// Generate individual DTO files + barrel index for deterministic imports
|
||||
await generateIndividualDtoFiles(openapiPath, outputDir, specSha256);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to generate types:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateIndividualDtoFiles(openapiPath: string, outputDir: string) {
|
||||
async function generateIndividualDtoFiles(openapiPath: string, outputDir: string, specSha256: string) {
|
||||
console.log('📝 Generating individual DTO files...');
|
||||
|
||||
|
||||
const specContent = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec = JSON.parse(specContent);
|
||||
const schemas = spec.components?.schemas || {};
|
||||
|
||||
const schemaNames = Object.keys(schemas);
|
||||
|
||||
const schemaNames = Object.keys(schemas).sort((a: string, b: string) => a.localeCompare(b));
|
||||
|
||||
// Get existing files in output directory
|
||||
let existingFiles: string[] = [];
|
||||
@@ -80,17 +75,24 @@ async function generateIndividualDtoFiles(openapiPath: string, outputDir: string
|
||||
const generatedFileNames: string[] = [];
|
||||
for (const schemaName of schemaNames) {
|
||||
const schema = schemas[schemaName];
|
||||
|
||||
|
||||
// File name should match the schema name exactly
|
||||
const fileName = `${schemaName}.ts`;
|
||||
const filePath = path.join(outputDir, fileName);
|
||||
|
||||
const fileContent = generateDtoFileContent(schemaName, schema, schemas);
|
||||
const fileContent = generateDtoFileContent(schemaName, schema, schemas, specSha256);
|
||||
await fs.writeFile(filePath, fileContent);
|
||||
console.log(` ✅ Generated ${fileName}`);
|
||||
generatedFileNames.push(fileName);
|
||||
}
|
||||
|
||||
const indexFileName = 'index.ts';
|
||||
const indexFilePath = path.join(outputDir, indexFileName);
|
||||
const indexFileContent = generateIndexFileContent(schemaNames, specSha256);
|
||||
await fs.writeFile(indexFilePath, indexFileContent);
|
||||
console.log(` ✅ Generated ${indexFileName}`);
|
||||
generatedFileNames.push(indexFileName);
|
||||
|
||||
// Clean up files that are no longer in the spec
|
||||
const filesToRemove = existingFiles.filter(f => !generatedFileNames.includes(f));
|
||||
for (const file of filesToRemove) {
|
||||
@@ -105,26 +107,44 @@ async function generateIndividualDtoFiles(openapiPath: string, outputDir: string
|
||||
}
|
||||
}
|
||||
|
||||
function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record<string, any>): string {
|
||||
function generateIndexFileContent(schemaNames: string[], specSha256: string): string {
|
||||
let content = `/**
|
||||
* Auto-generated barrel for API DTO types.
|
||||
* Spec SHA256: ${specSha256}
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
`;
|
||||
for (const schemaName of schemaNames) {
|
||||
content += `\nexport type { ${schemaName} } from './${schemaName}';`;
|
||||
}
|
||||
content += '\n';
|
||||
return content;
|
||||
}
|
||||
|
||||
function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record<string, any>, specSha256: string): string {
|
||||
// Collect dependencies (referenced DTOs)
|
||||
const dependencies = new Set<string>();
|
||||
collectDependencies(schema, dependencies, allSchemas);
|
||||
dependencies.delete(schemaName); // Remove self-reference
|
||||
|
||||
|
||||
const sortedDependencies = Array.from(dependencies).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
let content = `/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: ${specSha256}
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
`;
|
||||
|
||||
// Add imports for dependencies
|
||||
for (const dep of dependencies) {
|
||||
// Add imports for dependencies (sorted for deterministic output)
|
||||
for (const dep of sortedDependencies) {
|
||||
content += `import type { ${dep} } from './${dep}';\n`;
|
||||
}
|
||||
|
||||
if (dependencies.size > 0) {
|
||||
|
||||
if (sortedDependencies.length > 0) {
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
@@ -137,7 +157,7 @@ function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Rec
|
||||
for (const [propName, propSchema] of Object.entries(properties)) {
|
||||
const isRequired = required.has(propName);
|
||||
const optionalMark = isRequired ? '' : '?';
|
||||
const typeStr = schemaToTypeString(propSchema as any);
|
||||
const typeStr = schemaToTypeString(propSchema as any, allSchemas);
|
||||
|
||||
// Add JSDoc comment for format
|
||||
if ((propSchema as any).format) {
|
||||
@@ -197,42 +217,44 @@ function collectDependencies(schema: any, deps: Set<string>, allSchemas: Record<
|
||||
}
|
||||
}
|
||||
|
||||
function schemaToTypeString(schema: any): string {
|
||||
function schemaToTypeString(schema: any, allSchemas: Record<string, any>): string {
|
||||
if (!schema) return 'unknown';
|
||||
|
||||
|
||||
if (schema.$ref) {
|
||||
return schema.$ref.split('/').pop() || 'unknown';
|
||||
const refName = schema.$ref.split('/').pop();
|
||||
if (!refName) return 'unknown';
|
||||
return allSchemas[refName] ? refName : 'unknown';
|
||||
}
|
||||
|
||||
|
||||
if (schema.type === 'array') {
|
||||
const itemType = schemaToTypeString(schema.items);
|
||||
const itemType = schemaToTypeString(schema.items, allSchemas);
|
||||
return `${itemType}[]`;
|
||||
}
|
||||
|
||||
|
||||
if (schema.type === 'object') {
|
||||
if (schema.additionalProperties) {
|
||||
const valueType = schemaToTypeString(schema.additionalProperties);
|
||||
const valueType = schemaToTypeString(schema.additionalProperties, allSchemas);
|
||||
return `Record<string, ${valueType}>`;
|
||||
}
|
||||
|
||||
if (schema.properties) {
|
||||
// Inline object type
|
||||
const props = Object.entries(schema.properties)
|
||||
.map(([key, val]) => `${key}: ${schemaToTypeString(val as any)}`)
|
||||
.map(([key, val]) => `${key}: ${schemaToTypeString(val as any, allSchemas)}`)
|
||||
.join('; ');
|
||||
return `{ ${props} }`;
|
||||
}
|
||||
return 'Record<string, unknown>';
|
||||
}
|
||||
|
||||
|
||||
if (schema.oneOf) {
|
||||
return schema.oneOf.map((s: any) => schemaToTypeString(s)).join(' | ');
|
||||
return schema.oneOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | ');
|
||||
}
|
||||
|
||||
|
||||
if (schema.anyOf) {
|
||||
return schema.anyOf.map((s: any) => schemaToTypeString(s)).join(' | ');
|
||||
return schema.anyOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | ');
|
||||
}
|
||||
|
||||
|
||||
if (schema.enum) {
|
||||
return schema.enum.map((v: any) => JSON.stringify(v)).join(' | ');
|
||||
}
|
||||
|
||||
@@ -38,15 +38,34 @@ interface OpenAPISpec {
|
||||
};
|
||||
}
|
||||
|
||||
function getCliArgValue(flag: string): string | undefined {
|
||||
const index = process.argv.indexOf(flag);
|
||||
if (index === -1) return undefined;
|
||||
return process.argv[index + 1];
|
||||
}
|
||||
|
||||
function resolveOutputPath(): string {
|
||||
const cliOutput = getCliArgValue('--output') ?? getCliArgValue('-o');
|
||||
const envOutput = process.env.OPENAPI_OUTPUT_PATH;
|
||||
const configured = cliOutput ?? envOutput;
|
||||
|
||||
if (!configured) {
|
||||
return path.join(process.cwd(), 'apps/api/openapi.json');
|
||||
}
|
||||
|
||||
return path.resolve(process.cwd(), configured);
|
||||
}
|
||||
|
||||
async function generateSpec() {
|
||||
console.log('🔄 Generating OpenAPI spec from DTO files...');
|
||||
|
||||
const schemas: Record<string, OpenAPISchema> = {};
|
||||
|
||||
// Find all DTO files
|
||||
// Find all DTO files (sorted for deterministic output)
|
||||
const dtoFiles = await glob('apps/api/src/domain/*/dtos/**/*.ts', {
|
||||
cwd: process.cwd()
|
||||
});
|
||||
dtoFiles.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
console.log(`📁 Found ${dtoFiles.length} DTO files to process`);
|
||||
|
||||
@@ -58,6 +77,8 @@ async function generateSpec() {
|
||||
}
|
||||
}
|
||||
|
||||
const paths = await extractPathsFromControllers();
|
||||
|
||||
const spec: OpenAPISpec = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
@@ -65,17 +86,73 @@ async function generateSpec() {
|
||||
description: 'GridPilot API documentation',
|
||||
version: '1.0.0'
|
||||
},
|
||||
paths: {},
|
||||
paths,
|
||||
components: {
|
||||
schemas
|
||||
schemas: sortRecordKeys(schemas)
|
||||
}
|
||||
};
|
||||
|
||||
const outputPath = path.join(process.cwd(), 'apps/api/openapi.json');
|
||||
const outputPath = resolveOutputPath();
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, JSON.stringify(spec, null, 2));
|
||||
console.log(`✅ OpenAPI spec generated with ${Object.keys(schemas).length} schemas at: ${outputPath}`);
|
||||
}
|
||||
|
||||
function sortRecordKeys<T>(record: Record<string, T>): Record<string, T> {
|
||||
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
||||
}
|
||||
|
||||
function joinRouteParts(controllerPath: string, methodPath: string): string {
|
||||
const base = controllerPath.replace(/^\/+|\/+$/g, '');
|
||||
const sub = methodPath.replace(/^\/+|\/+$/g, '');
|
||||
const joined = [base, sub].filter(Boolean).join('/');
|
||||
return `/${joined}`;
|
||||
}
|
||||
|
||||
function toOpenApiPath(route: string): string {
|
||||
// Convert Nest-style ":param" to OpenAPI "{param}"
|
||||
return route.replace(/(^|\/):([^/]+)/g, '$1{$2}');
|
||||
}
|
||||
|
||||
async function extractPathsFromControllers(): Promise<Record<string, any>> {
|
||||
const controllerFiles = await glob('apps/api/src/domain/**/*Controller.ts', {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
controllerFiles.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const paths: Record<string, any> = {};
|
||||
|
||||
for (const controllerFile of controllerFiles) {
|
||||
const filePath = path.join(process.cwd(), controllerFile);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
const controllerMatch = content.match(/@Controller\(\s*['"]([^'"]+)['"]\s*\)/);
|
||||
if (!controllerMatch) continue;
|
||||
|
||||
const controllerPath = controllerMatch[1] ?? '';
|
||||
const methodRegex = /@(Get|Post|Put|Patch|Delete)\(\s*(?:['"]([^'"]*)['"])?\s*\)/g;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = methodRegex.exec(content)) !== null) {
|
||||
const httpMethod = match[1]?.toLowerCase();
|
||||
if (!httpMethod) continue;
|
||||
|
||||
const methodPath = match[2] ?? '';
|
||||
const route = joinRouteParts(controllerPath, methodPath);
|
||||
const openapiPath = toOpenApiPath(route);
|
||||
|
||||
paths[openapiPath] ??= {};
|
||||
paths[openapiPath][httpMethod] ??= {
|
||||
responses: {
|
||||
'200': { description: 'OK' },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return sortRecordKeys(paths);
|
||||
}
|
||||
|
||||
async function processDTOFile(filePath: string, schemas: Record<string, OpenAPISchema>) {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
@@ -160,9 +237,17 @@ function extractSchemaFromClassBody(classBody: string, fullContent: string): Ope
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect decorators
|
||||
// Collect decorators (support multi-line decorator calls like @ApiProperty({ ... }))
|
||||
if (line.startsWith('@')) {
|
||||
currentDecorators.push(line);
|
||||
let decorator = line;
|
||||
|
||||
while (!decorator.includes(')') && i + 1 < lines.length) {
|
||||
const nextLine = lines[i + 1]?.trim() ?? '';
|
||||
decorator = `${decorator} ${nextLine}`.trim();
|
||||
i++;
|
||||
}
|
||||
|
||||
currentDecorators.push(decorator);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -183,10 +268,11 @@ function extractSchemaFromClassBody(classBody: string, fullContent: string): Ope
|
||||
}
|
||||
|
||||
// Determine if required
|
||||
const hasApiProperty = currentDecorators.some(d => d.includes('@ApiProperty'));
|
||||
const isOptional = !!optional ||
|
||||
currentDecorators.some(d => d.includes('required: false') || d.includes('@IsOptional'));
|
||||
const isNullable = currentDecorators.some(d => d.includes('nullable: true'));
|
||||
const isOptional =
|
||||
!!optional || currentDecorators.some(d => d.includes('required: false') || d.includes('@IsOptional'));
|
||||
const isNullableFromDecorator = currentDecorators.some(d => d.includes('nullable: true'));
|
||||
const isNullableFromType = cleanedType.includes('| null') || cleanedType.includes('null |');
|
||||
const isNullable = isNullableFromDecorator || isNullableFromType;
|
||||
|
||||
if (!isOptional && !isNullable) {
|
||||
required.push(propName);
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { execSync } from 'child_process';
|
||||
import { createHash } from 'crypto';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
|
||||
describe('Type Generation Script', () => {
|
||||
const apiRoot = path.join(__dirname, '../../apps/api');
|
||||
@@ -16,6 +16,11 @@ describe('Type Generation Script', () => {
|
||||
const generatedTypesDir = path.join(websiteRoot, 'lib/types/generated');
|
||||
const backupDir = path.join(__dirname, '../../.backup/type-gen-test');
|
||||
|
||||
async function sha256OfFile(filePath: string): Promise<string> {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
return createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// Backup existing generated types
|
||||
await fs.mkdir(backupDir, { recursive: true });
|
||||
@@ -50,14 +55,7 @@ describe('Type Generation Script', () => {
|
||||
});
|
||||
|
||||
describe('OpenAPI Spec Generation', () => {
|
||||
it('should generate valid OpenAPI spec', async () => {
|
||||
// Run the spec generation
|
||||
execSync('npm run api:generate-spec', {
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
// Check that spec exists and is valid JSON
|
||||
it('should have a valid committed OpenAPI spec', async () => {
|
||||
const specContent = await fs.readFile(openapiPath, 'utf-8');
|
||||
expect(() => JSON.parse(specContent)).not.toThrow();
|
||||
|
||||
@@ -67,6 +65,32 @@ describe('Type Generation Script', () => {
|
||||
expect(spec.components.schemas).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include league schedule route and schema', async () => {
|
||||
const specContent = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec = JSON.parse(specContent);
|
||||
|
||||
// Route should exist (controller route extraction)
|
||||
expect(spec.paths?.['/leagues/{leagueId}/schedule']).toBeDefined();
|
||||
|
||||
// Schema should exist (DTO scanning)
|
||||
const scheduleSchema = spec.components?.schemas?.['LeagueScheduleDTO'];
|
||||
expect(scheduleSchema).toBeDefined();
|
||||
|
||||
// Contract requirements: season-aware schedule DTO
|
||||
expect(scheduleSchema.required ?? []).toContain('seasonId');
|
||||
expect(scheduleSchema.properties?.seasonId).toEqual({ type: 'string' });
|
||||
|
||||
// Races must be typed and use RaceDTO items
|
||||
expect(scheduleSchema.required ?? []).toContain('races');
|
||||
expect(scheduleSchema.properties?.races?.type).toBe('array');
|
||||
expect(scheduleSchema.properties?.races?.items).toEqual({ $ref: '#/components/schemas/RaceDTO' });
|
||||
|
||||
// RaceDTO.date must be ISO-safe string (OpenAPI generator maps Date->date-time, but DTO uses string)
|
||||
const raceSchema = spec.components?.schemas?.['RaceDTO'];
|
||||
expect(raceSchema).toBeDefined();
|
||||
expect(raceSchema.properties?.date).toEqual({ type: 'string' });
|
||||
});
|
||||
|
||||
it('should not have duplicate schema names with different casing', async () => {
|
||||
const specContent = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec = JSON.parse(specContent);
|
||||
@@ -100,6 +124,26 @@ describe('Type Generation Script', () => {
|
||||
});
|
||||
|
||||
describe('Type Generation', () => {
|
||||
it('should stamp generated output with the committed OpenAPI SHA256', async () => {
|
||||
execSync('npm run api:generate-types', {
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
const expectedHash = await sha256OfFile(openapiPath);
|
||||
|
||||
const barrelPath = path.join(generatedTypesDir, 'index.ts');
|
||||
const barrelContent = await fs.readFile(barrelPath, 'utf-8');
|
||||
|
||||
expect(barrelContent).toContain(`Spec SHA256: ${expectedHash}`);
|
||||
expect(barrelContent).toContain(`export type { RaceDTO } from './RaceDTO';`);
|
||||
expect(barrelContent).toContain(`export type { DriverDTO } from './DriverDTO';`);
|
||||
|
||||
const sampleDtoPath = path.join(generatedTypesDir, 'RaceDTO.ts');
|
||||
const sampleDtoContent = await fs.readFile(sampleDtoPath, 'utf-8');
|
||||
expect(sampleDtoContent).toContain(`Spec SHA256: ${expectedHash}`);
|
||||
});
|
||||
|
||||
it('should generate TypeScript files for all schemas', async () => {
|
||||
// Generate types
|
||||
execSync('npm run api:generate-types', {
|
||||
@@ -121,7 +165,7 @@ describe('Type Generation Script', () => {
|
||||
// Most schemas should have corresponding generated files
|
||||
// (allowing for some duplicates/conflicts that are intentionally skipped)
|
||||
const missingFiles = schemas.filter(schema => !generatedDTOs.includes(schema));
|
||||
|
||||
|
||||
// Should have at least 95% coverage
|
||||
const coverage = (schemas.length - missingFiles.length) / schemas.length;
|
||||
expect(coverage).toBeGreaterThan(0.95);
|
||||
@@ -129,15 +173,14 @@ describe('Type Generation Script', () => {
|
||||
|
||||
it('should generate files with correct interface names', async () => {
|
||||
const files = await fs.readdir(generatedTypesDir);
|
||||
const dtos = files.filter(f => f.endsWith('.ts'));
|
||||
const dtos = files.filter(f => f.endsWith('.ts') && f !== 'index.ts');
|
||||
|
||||
for (const file of dtos) {
|
||||
const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
|
||||
const interfaceName = file.replace('.ts', '');
|
||||
|
||||
|
||||
// File should contain an interface (name might be normalized)
|
||||
expect(content).toMatch(/export interface \w+\s*{/);
|
||||
|
||||
|
||||
// Should not have duplicate interface names in the same file
|
||||
const interfaceMatches = content.match(/export interface (\w+)/g);
|
||||
expect(interfaceMatches?.length).toBe(1);
|
||||
@@ -146,17 +189,23 @@ describe('Type Generation Script', () => {
|
||||
|
||||
it('should generate valid TypeScript syntax', async () => {
|
||||
const files = await fs.readdir(generatedTypesDir);
|
||||
const dtos = files.filter(f => f.endsWith('.ts'));
|
||||
const tsFiles = files.filter(f => f.endsWith('.ts'));
|
||||
|
||||
for (const file of dtos) {
|
||||
for (const file of tsFiles) {
|
||||
const content = await fs.readFile(path.join(generatedTypesDir, file), 'utf-8');
|
||||
|
||||
|
||||
if (file === 'index.ts') {
|
||||
expect(content).toContain('Auto-generated barrel');
|
||||
expect(content).toContain('export type { RaceDTO } from');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Basic syntax checks
|
||||
expect(content).toContain('export interface');
|
||||
expect(content).toContain('{');
|
||||
expect(content).toContain('}');
|
||||
expect(content).toContain('Auto-generated DTO');
|
||||
|
||||
|
||||
// Should not have syntax errors
|
||||
expect(content).not.toMatch(/interface\s+\w+\s*\{\s*\}/); // Empty interfaces
|
||||
expect(content).not.toContain('undefined;');
|
||||
|
||||
Reference in New Issue
Block a user