nestgen-cli 0.1.0 → 0.2.2
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.
- package/dist/cli/doctor-output.js +45 -0
- package/dist/cli/output.js +65 -0
- package/dist/engine/file-transaction.js +166 -0
- package/dist/engine/generated-code.js +26 -0
- package/dist/engine/generation-manifest.js +118 -0
- package/dist/engine/migration-plan.js +78 -0
- package/dist/engine/module-generator.js +80 -65
- package/dist/engine/project-config.js +5 -2
- package/dist/engine/project-path.js +25 -0
- package/dist/engine/project-preflight.js +96 -0
- package/dist/engine/resource-generator.js +1104 -68
- package/dist/engine/resource-spec.js +254 -11
- package/examples/store-api/README.md +76 -0
- package/examples/store-api/definitions/customers.resource.json +14 -0
- package/examples/store-api/definitions/order-items.resource.json +26 -0
- package/examples/store-api/definitions/orders.resource.json +22 -0
- package/examples/store-api/definitions/products.resource.json +19 -0
- package/examples/store-api/nestgen.example.json +7 -0
- package/nestgen.js +488 -84
- package/nestjs-generator/features/configure_bootstrap.mjs +5 -2
- package/nestjs-generator/features/docker.sh +6 -4
- package/nestjs-generator/features/git.sh +1 -1
- package/nestjs-generator/features/inject_module_to_app.sh +8 -6
- package/nestjs-generator/features/logger.sh +8 -4
- package/nestjs-generator/features/operational_foundation.mjs +210 -0
- package/nestjs-generator/features/operations.sh +18 -0
- package/nestjs-generator/features/preflight.mjs +2 -44
- package/nestjs-generator/features/prisma.sh +3 -3
- package/nestjs-generator/features/typeorm.sh +3 -3
- package/nestjs-generator/features/utils.sh +5 -3
- package/nestjs-generator/generate_project.sh +15 -9
- package/package.json +11 -4
- package/readme.md +234 -13
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
1
|
import fs from 'node:fs';
|
|
2
|
+
import { formatGeneratedCode } from './generated-code.js';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import ts from 'typescript';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { applyFileChanges, previewFileChanges } from './file-transaction.js';
|
|
6
|
+
import { inspectProject } from './project-preflight.js';
|
|
7
|
+
import { projectPath } from './project-path.js';
|
|
8
|
+
import { moduleGenerationDefinition, withGenerationManifest } from './generation-manifest.js';
|
|
9
9
|
function describe(rawName) {
|
|
10
10
|
const name = rawName.trim().toLowerCase().replace(/_/g, '-');
|
|
11
11
|
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name))
|
|
@@ -35,7 +35,15 @@ function updateAppModule(source, moduleClass, moduleImport, moduleReference = mo
|
|
|
35
35
|
!ts.isCallExpression(decorator.expression) ||
|
|
36
36
|
!ts.isObjectLiteralExpression(decorator.expression.arguments[0]))
|
|
37
37
|
throw new Error('AppModule doit utiliser @Module({ ... }).');
|
|
38
|
-
const
|
|
38
|
+
const metadata = decorator.expression.arguments[0];
|
|
39
|
+
if (metadata.properties.some((property) => ts.isSpreadAssignment(property) || (property.name && ts.isComputedPropertyName(property.name))))
|
|
40
|
+
throw new Error('AppModule : les métadonnées avec spread ou clés calculées ne sont pas supportées.');
|
|
41
|
+
const importProperties = metadata.properties.filter((property) => property.name &&
|
|
42
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
|
43
|
+
property.name.text === 'imports');
|
|
44
|
+
if (importProperties.length > 1)
|
|
45
|
+
throw new Error('AppModule : propriété imports dupliquée.');
|
|
46
|
+
const imports = importProperties[0] && ts.isPropertyAssignment(importProperties[0]) ? importProperties[0] : undefined;
|
|
39
47
|
if (!imports || !ts.isArrayLiteralExpression(imports.initializer))
|
|
40
48
|
throw new Error('AppModule doit définir imports: [] pour enregistrer un module.');
|
|
41
49
|
if (imports.initializer.elements.some((element) => element.getText(file) === moduleReference))
|
|
@@ -59,84 +67,91 @@ function updatePrismaSchema(source, resource) {
|
|
|
59
67
|
throw new Error(`Le modèle Prisma ${resource.pascal} existe déjà.`);
|
|
60
68
|
return `${source.trimEnd()}\n\nmodel ${resource.pascal} {\n id String @id @default(uuid())\n name String\n email String @unique\n\n @@map("${resource.table}")\n}\n`;
|
|
61
69
|
}
|
|
62
|
-
function
|
|
63
|
-
const prismaDirectory = path.join(projectRoot,
|
|
70
|
+
function prismaRuntimeChanges(projectRoot, sourceRoot) {
|
|
71
|
+
const prismaDirectory = path.join(projectRoot, sourceRoot, 'prisma');
|
|
64
72
|
const servicePath = path.join(prismaDirectory, 'prisma.service.ts');
|
|
65
73
|
const modulePath = path.join(prismaDirectory, 'prisma.module.ts');
|
|
74
|
+
if (fs.existsSync(servicePath) && fs.existsSync(modulePath))
|
|
75
|
+
return [];
|
|
66
76
|
if (fs.existsSync(servicePath) || fs.existsSync(modulePath))
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
throw new Error('Runtime Prisma incomplet.');
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
path: `${sourceRoot}/prisma/prisma.service.ts`,
|
|
81
|
+
operation: 'create',
|
|
82
|
+
content: "import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';\nimport { PrismaClient } from '@prisma/client';\n\n@Injectable()\nexport class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {\n async onModuleInit(): Promise<void> { await this.$connect(); }\n async onModuleDestroy(): Promise<void> { await this.$disconnect(); }\n}\n",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
path: `${sourceRoot}/prisma/prisma.module.ts`,
|
|
86
|
+
operation: 'create',
|
|
87
|
+
content: "import { Global, Module } from '@nestjs/common';\nimport { PrismaService } from './prisma.service.js';\n\n@Global()\n@Module({ providers: [PrismaService], exports: [PrismaService] })\nexport class PrismaModule {}\n",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
71
90
|
}
|
|
72
91
|
function files(resource, orm) {
|
|
73
92
|
const token = `${resource.pascal}RepositoryToken`;
|
|
74
93
|
const result = new Map();
|
|
75
94
|
result.set(`core/domain/entities/${resource.name}.entity.ts`, `export class ${resource.pascal} {\n constructor(public readonly id: string | undefined, public name: string, public email: string) {}\n}\n`);
|
|
76
|
-
result.set(`core/domain/ports/${resource.name}.repository.ts`, `import { ${resource.pascal} } from '../entities/${resource.name}.entity';\n\nexport const ${token} = Symbol('${resource.pascal}RepositoryPort');\nexport interface ${resource.pascal}RepositoryPort { save(value: ${resource.pascal}): Promise<${resource.pascal}>; findById(id: string): Promise<${resource.pascal} | null>; }\n`);
|
|
95
|
+
result.set(`core/domain/ports/${resource.name}.repository.ts`, `import { ${resource.pascal} } from '../entities/${resource.name}.entity.js';\n\nexport const ${token} = Symbol('${resource.pascal}RepositoryPort');\nexport interface ${resource.pascal}RepositoryPort { save(value: ${resource.pascal}): Promise<${resource.pascal}>; findById(id: string): Promise<${resource.pascal} | null>; }\n`);
|
|
77
96
|
result.set(`core/application/commands/create-${resource.name}.command.ts`, `export class Create${resource.pascal}Command { constructor(public readonly name: string, public readonly email: string) {} }\n`);
|
|
78
|
-
result.set(`core/application/commands/create-${resource.name}.handler.ts`, `import { Inject } from '@nestjs/common';\nimport { CommandHandler, ICommandHandler } from '@nestjs/cqrs';\nimport { Create${resource.pascal}Command } from './create-${resource.name}.command';\nimport { ${resource.pascal} } from '../../domain/entities/${resource.name}.entity';\nimport { ${resource.pascal}RepositoryPort, ${token} } from '../../domain/ports/${resource.name}.repository';\n@CommandHandler(Create${resource.pascal}Command)\nexport class Create${resource.pascal}Handler implements ICommandHandler<Create${resource.pascal}Command> { constructor(@Inject(${token}) private readonly repository: ${resource.pascal}RepositoryPort) {} async execute(command: Create${resource.pascal}Command): Promise<string> { return (await this.repository.save(new ${resource.pascal}(undefined, command.name, command.email))).id!; } }\n`);
|
|
97
|
+
result.set(`core/application/commands/create-${resource.name}.handler.ts`, `import { Inject } from '@nestjs/common';\nimport { CommandHandler, ICommandHandler } from '@nestjs/cqrs';\nimport { Create${resource.pascal}Command } from './create-${resource.name}.command.js';\nimport { ${resource.pascal} } from '../../domain/entities/${resource.name}.entity.js';\nimport { ${resource.pascal}RepositoryPort, ${token} } from '../../domain/ports/${resource.name}.repository.js';\n@CommandHandler(Create${resource.pascal}Command)\nexport class Create${resource.pascal}Handler implements ICommandHandler<Create${resource.pascal}Command> { constructor(@Inject(${token}) private readonly repository: ${resource.pascal}RepositoryPort) {} async execute(command: Create${resource.pascal}Command): Promise<string> { return (await this.repository.save(new ${resource.pascal}(undefined, command.name, command.email))).id!; } }\n`);
|
|
79
98
|
result.set(`interfaces/dtos/create-${resource.name}.dto.ts`, `import { IsEmail, IsString } from 'class-validator';\nexport class Create${resource.pascal}Dto { @IsString() name!: string; @IsEmail() email!: string; }\n`);
|
|
80
|
-
result.set(`interfaces/controllers/${resource.name}.controller.ts`, `import { Body, Controller, Post } from '@nestjs/common';\nimport { CommandBus } from '@nestjs/cqrs';\nimport { Create${resource.pascal}Command } from '../../core/application/commands/create-${resource.name}.command';\nimport { Create${resource.pascal}Dto } from '../dtos/create-${resource.name}.dto';\n@Controller('${resource.route}') export class ${resource.pascal}Controller { constructor(private readonly commandBus: CommandBus) {} @Post() async create(@Body() dto: Create${resource.pascal}Dto) { return { id: await this.commandBus.execute(new Create${resource.pascal}Command(dto.name, dto.email)) }; } }\n`);
|
|
99
|
+
result.set(`interfaces/controllers/${resource.name}.controller.ts`, `import { Body, Controller, Post } from '@nestjs/common';\nimport { CommandBus } from '@nestjs/cqrs';\nimport { Create${resource.pascal}Command } from '../../core/application/commands/create-${resource.name}.command.js';\nimport { Create${resource.pascal}Dto } from '../dtos/create-${resource.name}.dto.js';\n@Controller('${resource.route}') export class ${resource.pascal}Controller { constructor(private readonly commandBus: CommandBus) {} @Post() async create(@Body() dto: Create${resource.pascal}Dto) { return { id: await this.commandBus.execute(new Create${resource.pascal}Command(dto.name, dto.email)) }; } }\n`);
|
|
81
100
|
if (orm === 'typeorm') {
|
|
82
101
|
result.set(`infrastructure/persistences/repositories/${resource.name}.orm.ts`, `import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n@Entity('${resource.table}') export class ${resource.pascal}Entity { @PrimaryGeneratedColumn('uuid') id!: string; @Column() name!: string; @Column({ unique: true }) email!: string; }\n`);
|
|
83
|
-
result.set(`infrastructure/persistences/repositories/${resource.name}.typeorm.repository.ts`, `import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { ${resource.pascal} } from '../../../core/domain/entities/${resource.name}.entity';\nimport { ${resource.pascal}RepositoryPort } from '../../../core/domain/ports/${resource.name}.repository';\nimport { ${resource.pascal}Entity } from './${resource.name}.orm';\n@Injectable() export class ${resource.pascal}TypeOrmRepository implements ${resource.pascal}RepositoryPort { constructor(@InjectRepository(${resource.pascal}Entity) private readonly repository: Repository<${resource.pascal}Entity>) {} async save(value: ${resource.pascal}) { const saved = await this.repository.save(this.repository.create({ name: value.name, email: value.email })); return new ${resource.pascal}(saved.id, saved.name, saved.email); } async findById(id: string) { const found = await this.repository.findOneBy({ id }); return found ? new ${resource.pascal}(found.id, found.name, found.email) : null; } }\n`);
|
|
84
|
-
result.set(`${resource.name}.module.ts`, `import { Module } from '@nestjs/common';\nimport { CqrsModule } from '@nestjs/cqrs';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Create${resource.pascal}Handler } from './core/application/commands/create-${resource.name}.handler';\nimport { ${token} } from './core/domain/ports/${resource.name}.repository';\nimport { ${resource.pascal}Entity } from './infrastructure/persistences/repositories/${resource.name}.orm';\nimport { ${resource.pascal}TypeOrmRepository } from './infrastructure/persistences/repositories/${resource.name}.typeorm.repository';\nimport { ${resource.pascal}Controller } from './interfaces/controllers/${resource.name}.controller';\n@Module({ imports: [CqrsModule, TypeOrmModule.forFeature([${resource.pascal}Entity])], controllers: [${resource.pascal}Controller], providers: [Create${resource.pascal}Handler, { provide: ${token}, useClass: ${resource.pascal}TypeOrmRepository }] }) export class ${resource.pascal}Module {}\n`);
|
|
102
|
+
result.set(`infrastructure/persistences/repositories/${resource.name}.typeorm.repository.ts`, `import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { ${resource.pascal} } from '../../../core/domain/entities/${resource.name}.entity.js';\nimport { ${resource.pascal}RepositoryPort } from '../../../core/domain/ports/${resource.name}.repository.js';\nimport { ${resource.pascal}Entity } from './${resource.name}.orm.js';\n@Injectable() export class ${resource.pascal}TypeOrmRepository implements ${resource.pascal}RepositoryPort { constructor(@InjectRepository(${resource.pascal}Entity) private readonly repository: Repository<${resource.pascal}Entity>) {} async save(value: ${resource.pascal}) { const saved = await this.repository.save(this.repository.create({ name: value.name, email: value.email })); return new ${resource.pascal}(saved.id, saved.name, saved.email); } async findById(id: string) { const found = await this.repository.findOneBy({ id }); return found ? new ${resource.pascal}(found.id, found.name, found.email) : null; } }\n`);
|
|
103
|
+
result.set(`${resource.name}.module.ts`, `import { Module } from '@nestjs/common';\nimport { CqrsModule } from '@nestjs/cqrs';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Create${resource.pascal}Handler } from './core/application/commands/create-${resource.name}.handler.js';\nimport { ${token} } from './core/domain/ports/${resource.name}.repository.js';\nimport { ${resource.pascal}Entity } from './infrastructure/persistences/repositories/${resource.name}.orm.js';\nimport { ${resource.pascal}TypeOrmRepository } from './infrastructure/persistences/repositories/${resource.name}.typeorm.repository.js';\nimport { ${resource.pascal}Controller } from './interfaces/controllers/${resource.name}.controller.js';\n@Module({ imports: [CqrsModule, TypeOrmModule.forFeature([${resource.pascal}Entity])], controllers: [${resource.pascal}Controller], providers: [Create${resource.pascal}Handler, { provide: ${token}, useClass: ${resource.pascal}TypeOrmRepository }] }) export class ${resource.pascal}Module {}\n`);
|
|
85
104
|
}
|
|
86
105
|
else {
|
|
87
|
-
result.set(`infrastructure/persistences/repositories/${resource.name}.prisma.repository.ts`, `import { Injectable } from '@nestjs/common';\nimport { PrismaService } from '../../../../../prisma/prisma.service';\nimport { ${resource.pascal} } from '../../../core/domain/entities/${resource.name}.entity';\nimport { ${resource.pascal}RepositoryPort } from '../../../core/domain/ports/${resource.name}.repository';\n@Injectable() export class ${resource.pascal}PrismaRepository implements ${resource.pascal}RepositoryPort { constructor(private readonly prisma: PrismaService) {} async save(value: ${resource.pascal}) { const saved = await this.prisma.${resource.camel}.create({ data: { name: value.name, email: value.email } }); return new ${resource.pascal}(saved.id, saved.name, saved.email); } async findById(id: string) { const found = await this.prisma.${resource.camel}.findUnique({ where: { id } }); return found ? new ${resource.pascal}(found.id, found.name, found.email) : null; } }\n`);
|
|
88
|
-
result.set(`${resource.name}.module.ts`, `import { Module } from '@nestjs/common';\nimport { CqrsModule } from '@nestjs/cqrs';\nimport { PrismaModule } from '../../prisma/prisma.module';\nimport { Create${resource.pascal}Handler } from './core/application/commands/create-${resource.name}.handler';\nimport { ${token} } from './core/domain/ports/${resource.name}.repository';\nimport { ${resource.pascal}PrismaRepository } from './infrastructure/persistences/repositories/${resource.name}.prisma.repository';\nimport { ${resource.pascal}Controller } from './interfaces/controllers/${resource.name}.controller';\n@Module({ imports: [CqrsModule, PrismaModule], controllers: [${resource.pascal}Controller], providers: [Create${resource.pascal}Handler, { provide: ${token}, useClass: ${resource.pascal}PrismaRepository }] }) export class ${resource.pascal}Module {}\n`);
|
|
106
|
+
result.set(`infrastructure/persistences/repositories/${resource.name}.prisma.repository.ts`, `import { Injectable } from '@nestjs/common';\nimport { PrismaService } from '../../../../../prisma/prisma.service.js';\nimport { ${resource.pascal} } from '../../../core/domain/entities/${resource.name}.entity.js';\nimport { ${resource.pascal}RepositoryPort } from '../../../core/domain/ports/${resource.name}.repository.js';\n@Injectable() export class ${resource.pascal}PrismaRepository implements ${resource.pascal}RepositoryPort { constructor(private readonly prisma: PrismaService) {} async save(value: ${resource.pascal}) { const saved = await this.prisma.${resource.camel}.create({ data: { name: value.name, email: value.email } }); return new ${resource.pascal}(saved.id, saved.name, saved.email); } async findById(id: string) { const found = await this.prisma.${resource.camel}.findUnique({ where: { id } }); return found ? new ${resource.pascal}(found.id, found.name, found.email) : null; } }\n`);
|
|
107
|
+
result.set(`${resource.name}.module.ts`, `import { Module } from '@nestjs/common';\nimport { CqrsModule } from '@nestjs/cqrs';\nimport { PrismaModule } from '../../prisma/prisma.module.js';\nimport { Create${resource.pascal}Handler } from './core/application/commands/create-${resource.name}.handler.js';\nimport { ${token} } from './core/domain/ports/${resource.name}.repository.js';\nimport { ${resource.pascal}PrismaRepository } from './infrastructure/persistences/repositories/${resource.name}.prisma.repository.js';\nimport { ${resource.pascal}Controller } from './interfaces/controllers/${resource.name}.controller.js';\n@Module({ imports: [CqrsModule, PrismaModule], controllers: [${resource.pascal}Controller], providers: [Create${resource.pascal}Handler, { provide: ${token}, useClass: ${resource.pascal}PrismaRepository }] }) export class ${resource.pascal}Module {}\n`);
|
|
89
108
|
}
|
|
90
109
|
return result;
|
|
91
110
|
}
|
|
92
|
-
export function
|
|
111
|
+
export async function planModuleGeneration(projectRoot, rawName, orm, application) {
|
|
93
112
|
if (orm !== 'typeorm' && orm !== 'prisma')
|
|
94
113
|
throw new Error(`ORM non supporté : ${orm}.`);
|
|
95
|
-
inspectProject(projectRoot, orm);
|
|
96
114
|
const resource = describe(rawName);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const appModule =
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
state.status = 'module-applied';
|
|
120
|
-
fs.writeFileSync(pending, JSON.stringify(state, null, 2));
|
|
121
|
-
const temporary = `${appModule}.nestgen-${process.pid}`;
|
|
122
|
-
fs.writeFileSync(temporary, updated);
|
|
123
|
-
fs.renameSync(temporary, appModule);
|
|
124
|
-
if (originalSchema) {
|
|
125
|
-
fs.writeFileSync(prismaSchema, updatePrismaSchema(originalSchema, resource));
|
|
126
|
-
ensurePrismaRuntime(projectRoot);
|
|
127
|
-
}
|
|
128
|
-
fs.rmSync(pending);
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
if (state.status === 'module-applied')
|
|
132
|
-
fs.rmSync(moduleDirectory, { recursive: true, force: true });
|
|
133
|
-
if (state.status === 'module-applied')
|
|
134
|
-
fs.writeFileSync(appModule, originalAppModule);
|
|
135
|
-
if (originalSchema)
|
|
136
|
-
fs.writeFileSync(prismaSchema, originalSchema);
|
|
137
|
-
throw error;
|
|
138
|
-
}
|
|
139
|
-
finally {
|
|
140
|
-
fs.rmSync(stage, { recursive: true, force: true });
|
|
115
|
+
const project = inspectProject(projectRoot, orm, { application });
|
|
116
|
+
projectRoot = project.root;
|
|
117
|
+
const featureDirectory = projectPath(projectRoot, `${project.sourceRoot}/app/${resource.name}`);
|
|
118
|
+
const appModule = fs.readFileSync(project.appModulePath, 'utf8');
|
|
119
|
+
const changes = [...files(resource, orm)].map(([relative, content]) => ({
|
|
120
|
+
path: `${project.sourceRoot}/app/${resource.name}/${relative}`,
|
|
121
|
+
content,
|
|
122
|
+
operation: 'create',
|
|
123
|
+
}));
|
|
124
|
+
changes.push({
|
|
125
|
+
path: `${project.sourceRoot}/app.module.ts`,
|
|
126
|
+
operation: 'replace',
|
|
127
|
+
content: updateAppModule(appModule, `${resource.pascal}Module`, `./app/${resource.name}/${resource.name}.module.js`),
|
|
128
|
+
});
|
|
129
|
+
if (orm === 'prisma') {
|
|
130
|
+
const schema = fs.readFileSync(path.join(projectRoot, 'prisma/schema.prisma'), 'utf8');
|
|
131
|
+
changes.push({
|
|
132
|
+
path: 'prisma/schema.prisma',
|
|
133
|
+
operation: 'replace',
|
|
134
|
+
content: updatePrismaSchema(schema, resource),
|
|
135
|
+
});
|
|
136
|
+
changes.push(...prismaRuntimeChanges(projectRoot, project.sourceRoot));
|
|
141
137
|
}
|
|
138
|
+
const formattedChanges = await formatGeneratedCode(projectRoot, changes);
|
|
139
|
+
const changesWithManifest = withGenerationManifest(projectRoot, formattedChanges, moduleGenerationDefinition(resource.name, orm, project.sourceRoot));
|
|
140
|
+
return {
|
|
141
|
+
root: projectRoot,
|
|
142
|
+
changes: changesWithManifest,
|
|
143
|
+
preview: previewFileChanges(projectRoot, changesWithManifest),
|
|
144
|
+
featureConflict: fs.existsSync(featureDirectory)
|
|
145
|
+
? `Le module ou la ressource ${resource.name} existe déjà.`
|
|
146
|
+
: undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export async function generateModule(projectRoot, rawName, orm, application) {
|
|
150
|
+
const plan = await planModuleGeneration(projectRoot, rawName, orm, application);
|
|
151
|
+
if (plan.featureConflict)
|
|
152
|
+
throw new Error(plan.featureConflict);
|
|
153
|
+
const conflict = plan.preview.find((change) => change.status === 'conflict');
|
|
154
|
+
if (conflict)
|
|
155
|
+
throw new Error(conflict.reason);
|
|
156
|
+
applyFileChanges(plan.root, plan.changes);
|
|
142
157
|
}
|
|
@@ -20,8 +20,11 @@ export function loadConfig(projectRoot) {
|
|
|
20
20
|
throw new Error('Version de configuration ou de templates non supportée.');
|
|
21
21
|
if (config.orm !== 'typeorm' && config.orm !== 'prisma')
|
|
22
22
|
throw new Error('ORM de configuration invalide.');
|
|
23
|
-
if (config.sourceRoot !== '
|
|
24
|
-
|
|
23
|
+
if (typeof config.sourceRoot !== 'string' ||
|
|
24
|
+
!config.sourceRoot ||
|
|
25
|
+
path.isAbsolute(config.sourceRoot) ||
|
|
26
|
+
config.sourceRoot.split(/[\\/]/).includes('..'))
|
|
27
|
+
throw new Error('sourceRoot de configuration invalide.');
|
|
25
28
|
const packageManager = config.packageManager;
|
|
26
29
|
if (!packageManager || !['npm', 'pnpm', 'yarn'].includes(packageManager))
|
|
27
30
|
throw new Error('Package manager invalide.');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Resolve a path inside a canonical project root, without following child symlinks. */
|
|
4
|
+
export function projectPath(root, relativePath) {
|
|
5
|
+
const target = path.resolve(root, relativePath);
|
|
6
|
+
const relative = path.relative(root, target);
|
|
7
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
8
|
+
throw new Error(`Destination hors projet : ${relativePath}`);
|
|
9
|
+
let current = root;
|
|
10
|
+
for (const part of relative.split(path.sep)) {
|
|
11
|
+
current = path.join(current, part);
|
|
12
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
13
|
+
if (stat?.isSymbolicLink())
|
|
14
|
+
throw new Error(`Lien symbolique non supporté : ${current}`);
|
|
15
|
+
if (stat && current !== target && !stat.isDirectory())
|
|
16
|
+
throw new Error(`Répertoire attendu : ${current}`);
|
|
17
|
+
}
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
export function availableFeatureDirectory(root, name, sourceRoot = 'src') {
|
|
21
|
+
const directory = projectPath(root, `${sourceRoot}/app/${name}`);
|
|
22
|
+
if (fs.lstatSync(directory, { throwIfNoEntry: false }))
|
|
23
|
+
throw new Error(`Le module ou la ressource ${name} existe déjà.`);
|
|
24
|
+
return directory;
|
|
25
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { projectPath } from './project-path.js';
|
|
4
|
+
function error(message) {
|
|
5
|
+
throw new Error(`Préflight échoué : ${message}`);
|
|
6
|
+
}
|
|
7
|
+
function projectFile(root, relative, required, missingMessage) {
|
|
8
|
+
const target = projectPath(root, relative);
|
|
9
|
+
const stat = fs.lstatSync(target, { throwIfNoEntry: false });
|
|
10
|
+
if (!stat) {
|
|
11
|
+
if (required)
|
|
12
|
+
error(missingMessage ?? `${relative} introuvable.`);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (!stat.isFile())
|
|
16
|
+
error(`fichier régulier attendu : ${relative}.`);
|
|
17
|
+
return target;
|
|
18
|
+
}
|
|
19
|
+
function readObject(file) {
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
+
throw new Error('objet JSON attendu');
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
error(`JSON invalide dans ${file} : ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Read-only prerequisite checks shared by module, resource and legacy entry points. */
|
|
31
|
+
export function inspectProject(projectRoot, orm, options = {}) {
|
|
32
|
+
const root = fs.realpathSync(projectRoot);
|
|
33
|
+
const packagePath = projectFile(root, 'package.json', true, 'package.json introuvable. Exécute la commande à la racine d’un projet NestJS.');
|
|
34
|
+
const nestCliPath = projectFile(root, 'nest-cli.json', false);
|
|
35
|
+
const packageJson = readObject(packagePath);
|
|
36
|
+
const nestCli = nestCliPath ? readObject(nestCliPath) : null;
|
|
37
|
+
const projects = nestCli?.projects;
|
|
38
|
+
const applicationNames = Object.entries(projects ?? {})
|
|
39
|
+
.filter(([, project]) => project.type === undefined || project.type === 'application')
|
|
40
|
+
.map(([name]) => name);
|
|
41
|
+
if (nestCli?.monorepo && applicationNames.length === 0)
|
|
42
|
+
error('workspaces Nest : aucune application déclarée dans nest-cli.json.');
|
|
43
|
+
const application = options.application;
|
|
44
|
+
if (application && !applicationNames.includes(application))
|
|
45
|
+
error(`application inconnue : ${application}. Applications disponibles : ${applicationNames.join(', ') || 'aucune'}.`);
|
|
46
|
+
if (!application && applicationNames.length > 1)
|
|
47
|
+
error(`workspace ambigu : utilise --application. Applications disponibles : ${applicationNames.join(', ')}.`);
|
|
48
|
+
const selected = application
|
|
49
|
+
? projects[application]
|
|
50
|
+
: applicationNames.length === 1
|
|
51
|
+
? projects[applicationNames[0]]
|
|
52
|
+
: nestCli;
|
|
53
|
+
const sourceRoot = selected?.sourceRoot ?? 'src';
|
|
54
|
+
if (typeof sourceRoot !== 'string' || !sourceRoot || path.isAbsolute(sourceRoot) || sourceRoot.includes('..'))
|
|
55
|
+
error('sourceRoot invalide dans nest-cli.json.');
|
|
56
|
+
const appModulePath = projectFile(root, `${sourceRoot}/app.module.ts`, true, sourceRoot === 'src'
|
|
57
|
+
? `${sourceRoot}/app.module.ts introuvable.`
|
|
58
|
+
: `sourceRoot personnalisé : ${sourceRoot}/app.module.ts introuvable.`);
|
|
59
|
+
const dependencies = {
|
|
60
|
+
...packageJson.dependencies,
|
|
61
|
+
...packageJson.devDependencies,
|
|
62
|
+
};
|
|
63
|
+
const required = ['@nestjs/common', '@nestjs/core'];
|
|
64
|
+
if (options.cqrs !== false)
|
|
65
|
+
required.push('@nestjs/cqrs');
|
|
66
|
+
if (orm === 'typeorm')
|
|
67
|
+
required.push('@nestjs/typeorm', 'typeorm');
|
|
68
|
+
else if (orm === 'prisma')
|
|
69
|
+
required.push('@prisma/client');
|
|
70
|
+
else
|
|
71
|
+
error(`ORM non supporté : ${orm}.`);
|
|
72
|
+
const unavailable = required.filter((dependency) => !dependencies[dependency]);
|
|
73
|
+
if (unavailable.length)
|
|
74
|
+
error(`dépendances manquantes : ${unavailable.join(', ')}.`);
|
|
75
|
+
if (orm === 'prisma') {
|
|
76
|
+
projectFile(root, 'prisma/schema.prisma', true);
|
|
77
|
+
projectFile(root, `${sourceRoot}/prisma/prisma.service.ts`, false);
|
|
78
|
+
projectFile(root, `${sourceRoot}/prisma/prisma.module.ts`, false);
|
|
79
|
+
}
|
|
80
|
+
const appModule = fs.readFileSync(appModulePath, 'utf8');
|
|
81
|
+
if (!/@Module\s*\(/.test(appModule))
|
|
82
|
+
error(`le décorateur @Module est introuvable dans ${sourceRoot}/app.module.ts.`);
|
|
83
|
+
return {
|
|
84
|
+
root,
|
|
85
|
+
appModulePath,
|
|
86
|
+
sourceRoot,
|
|
87
|
+
packageManager: projectFile(root, 'pnpm-lock.yaml', false)
|
|
88
|
+
? 'pnpm'
|
|
89
|
+
: projectFile(root, 'yarn.lock', false)
|
|
90
|
+
? 'yarn'
|
|
91
|
+
: projectFile(root, 'package-lock.json', false)
|
|
92
|
+
? 'npm'
|
|
93
|
+
: null,
|
|
94
|
+
hasRootTypeOrmConnection: /\bTypeOrmModule\.forRoot\s*\(/.test(appModule),
|
|
95
|
+
};
|
|
96
|
+
}
|