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.
Files changed (33) hide show
  1. package/dist/cli/doctor-output.js +45 -0
  2. package/dist/cli/output.js +65 -0
  3. package/dist/engine/file-transaction.js +166 -0
  4. package/dist/engine/generated-code.js +26 -0
  5. package/dist/engine/generation-manifest.js +118 -0
  6. package/dist/engine/migration-plan.js +78 -0
  7. package/dist/engine/module-generator.js +80 -65
  8. package/dist/engine/project-config.js +5 -2
  9. package/dist/engine/project-path.js +25 -0
  10. package/dist/engine/project-preflight.js +96 -0
  11. package/dist/engine/resource-generator.js +1104 -68
  12. package/dist/engine/resource-spec.js +254 -11
  13. package/examples/store-api/README.md +76 -0
  14. package/examples/store-api/definitions/customers.resource.json +14 -0
  15. package/examples/store-api/definitions/order-items.resource.json +26 -0
  16. package/examples/store-api/definitions/orders.resource.json +22 -0
  17. package/examples/store-api/definitions/products.resource.json +19 -0
  18. package/examples/store-api/nestgen.example.json +7 -0
  19. package/nestgen.js +488 -84
  20. package/nestjs-generator/features/configure_bootstrap.mjs +5 -2
  21. package/nestjs-generator/features/docker.sh +6 -4
  22. package/nestjs-generator/features/git.sh +1 -1
  23. package/nestjs-generator/features/inject_module_to_app.sh +8 -6
  24. package/nestjs-generator/features/logger.sh +8 -4
  25. package/nestjs-generator/features/operational_foundation.mjs +210 -0
  26. package/nestjs-generator/features/operations.sh +18 -0
  27. package/nestjs-generator/features/preflight.mjs +2 -44
  28. package/nestjs-generator/features/prisma.sh +3 -3
  29. package/nestjs-generator/features/typeorm.sh +3 -3
  30. package/nestjs-generator/features/utils.sh +5 -3
  31. package/nestjs-generator/generate_project.sh +15 -9
  32. package/package.json +11 -4
  33. 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
- // The legacy preflight is JavaScript while the migration is incremental.
6
- // @ts-expect-error declaration added when preflight moves into this engine.
7
- import { inspectProject } from '../../nestjs-generator/features/preflight.mjs';
8
- const stateFile = '.nestgen-transaction.json';
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 imports = decorator.expression.arguments[0].properties.find((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'imports');
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 ensurePrismaRuntime(projectRoot) {
63
- const prismaDirectory = path.join(projectRoot, 'src', 'prisma');
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
- return;
68
- fs.mkdirSync(prismaDirectory, { recursive: true });
69
- fs.writeFileSync(servicePath, "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");
70
- fs.writeFileSync(modulePath, "import { Global, Module } from '@nestjs/common';\nimport { PrismaService } from './prisma.service';\n\n@Global()\n@Module({ providers: [PrismaService], exports: [PrismaService] })\nexport class PrismaModule {}\n");
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 generateModule(projectRoot, rawName, orm) {
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 pending = path.join(projectRoot, stateFile);
98
- const moduleDirectory = path.join(projectRoot, 'src/app', resource.name);
99
- const stage = path.join(projectRoot, `.nestgen-stage-${crypto.randomUUID()}`);
100
- const appModule = path.join(projectRoot, 'src/app.module.ts');
101
- const prismaSchema = path.join(projectRoot, 'prisma/schema.prisma');
102
- if (fs.existsSync(pending))
103
- throw new Error(`Transaction interrompue détectée : ${stateFile}.`);
104
- if (fs.existsSync(moduleDirectory))
105
- throw new Error(`Le module ${resource.name} existe déjà.`);
106
- const originalAppModule = fs.readFileSync(appModule, 'utf8');
107
- const originalSchema = orm === 'prisma' ? fs.readFileSync(prismaSchema, 'utf8') : undefined;
108
- const state = { status: 'prepared', stage, moduleDirectory, appModule };
109
- try {
110
- for (const [relative, content] of files(resource, orm)) {
111
- const target = path.join(stage, relative);
112
- fs.mkdirSync(path.dirname(target), { recursive: true });
113
- fs.writeFileSync(target, content);
114
- }
115
- const updated = updateAppModule(originalAppModule, `${resource.pascal}Module`, `./app/${resource.name}/${resource.name}.module`);
116
- fs.writeFileSync(pending, JSON.stringify(state, null, 2));
117
- fs.mkdirSync(path.dirname(moduleDirectory), { recursive: true });
118
- fs.renameSync(stage, moduleDirectory);
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 !== 'src')
24
- throw new Error('Seul sourceRoot "src" est supporté.');
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
+ }