nestgen-cli 0.1.0 → 0.2.1
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/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 +450 -71
- package/nestjs-generator/features/configure_bootstrap.mjs +5 -2
- package/nestjs-generator/features/operational_foundation.mjs +210 -0
- package/nestjs-generator/features/operations.sh +16 -0
- package/nestjs-generator/features/preflight.mjs +2 -44
- package/nestjs-generator/generate_project.sh +7 -1
- package/package.json +11 -4
- package/readme.md +234 -13
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
|
|
3
|
-
const [mainPath, swaggerEnabled] = process.argv.slice(2);
|
|
3
|
+
const [mainPath, swaggerEnabled, operationsEnabled] = process.argv.slice(2);
|
|
4
4
|
|
|
5
5
|
if (!mainPath || !fs.existsSync(mainPath)) {
|
|
6
6
|
console.error(`❌ Bootstrap Nest introuvable : ${mainPath ?? 'src/main.ts'}`);
|
|
7
7
|
process.exitCode = 1;
|
|
8
8
|
} else {
|
|
9
9
|
const swagger = swaggerEnabled === 'y';
|
|
10
|
+
const operations = operationsEnabled === 'y';
|
|
10
11
|
const source = `import { ValidationPipe } from '@nestjs/common';
|
|
11
12
|
import { NestFactory } from '@nestjs/core';
|
|
12
13
|
${swagger ? "import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';\n" : ''}import { AppModule } from './app.module';
|
|
14
|
+
${operations ? "import { StructuredLogger } from './operations/structured-logger.service';\n" : ''}
|
|
13
15
|
|
|
14
16
|
async function bootstrap() {
|
|
15
|
-
const app = await NestFactory.create(AppModule);
|
|
17
|
+
const app = await NestFactory.create(AppModule${operations ? ', { bufferLogs: true }' : ''});
|
|
18
|
+
${operations ? ' app.useLogger(app.get(StructuredLogger));\n app.enableShutdownHooks();\n' : ''}
|
|
16
19
|
app.useGlobalPipes(new ValidationPipe({
|
|
17
20
|
whitelist: true,
|
|
18
21
|
forbidNonWhitelisted: true,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const [sourceDirectory, orm] = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (!sourceDirectory || !['typeorm', 'prisma'].includes(orm)) {
|
|
7
|
+
console.error('❌ Usage : operational_foundation.mjs <source-directory> <typeorm|prisma>');
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
} else {
|
|
10
|
+
const operationsDirectory = path.join(sourceDirectory, 'operations');
|
|
11
|
+
const appModule = path.join(sourceDirectory, 'app.module.ts');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(appModule)) {
|
|
14
|
+
console.error(`❌ AppModule introuvable : ${appModule}`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
} else if (fs.existsSync(operationsDirectory)) {
|
|
17
|
+
console.error(`❌ ${operationsDirectory} existe déjà. Le socle d’exploitation n’a pas été remplacé.`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
} else {
|
|
20
|
+
const databaseHealth =
|
|
21
|
+
orm === 'typeorm'
|
|
22
|
+
? `import { Injectable } from '@nestjs/common';
|
|
23
|
+
import { InjectDataSource } from '@nestjs/typeorm';
|
|
24
|
+
import type { DataSource } from 'typeorm';
|
|
25
|
+
|
|
26
|
+
@Injectable()
|
|
27
|
+
export class DatabaseHealthService {
|
|
28
|
+
constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
|
|
29
|
+
|
|
30
|
+
async check(): Promise<{ status: 'up' | 'down' }> {
|
|
31
|
+
try {
|
|
32
|
+
if (!this.dataSource.isInitialized) return { status: 'down' };
|
|
33
|
+
await this.dataSource.query('SELECT 1');
|
|
34
|
+
return { status: 'up' };
|
|
35
|
+
} catch {
|
|
36
|
+
return { status: 'down' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
: `import { Injectable } from '@nestjs/common';
|
|
42
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
43
|
+
|
|
44
|
+
@Injectable()
|
|
45
|
+
export class DatabaseHealthService {
|
|
46
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
47
|
+
|
|
48
|
+
async check(): Promise<{ status: 'up' | 'down' }> {
|
|
49
|
+
try {
|
|
50
|
+
await this.prisma.$queryRawUnsafe('SELECT 1');
|
|
51
|
+
return { status: 'up' };
|
|
52
|
+
} catch {
|
|
53
|
+
return { status: 'down' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const files = new Map([
|
|
60
|
+
[
|
|
61
|
+
'request-context.ts',
|
|
62
|
+
`import { AsyncLocalStorage } from 'node:async_hooks';
|
|
63
|
+
|
|
64
|
+
export const requestContext = new AsyncLocalStorage<{ requestId: string }>();
|
|
65
|
+
`,
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'request-id.middleware.ts',
|
|
69
|
+
`import { Injectable, type NestMiddleware } from '@nestjs/common';
|
|
70
|
+
import { randomUUID } from 'node:crypto';
|
|
71
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
72
|
+
import { requestContext } from './request-context';
|
|
73
|
+
|
|
74
|
+
const REQUEST_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
75
|
+
|
|
76
|
+
@Injectable()
|
|
77
|
+
export class RequestIdMiddleware implements NestMiddleware {
|
|
78
|
+
use(request: Request, response: Response, next: NextFunction): void {
|
|
79
|
+
const candidate = request.header('x-request-id');
|
|
80
|
+
const requestId = candidate && REQUEST_ID.test(candidate) ? candidate : randomUUID();
|
|
81
|
+
response.setHeader('x-request-id', requestId);
|
|
82
|
+
requestContext.run({ requestId }, next);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`,
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
'structured-logger.service.ts',
|
|
89
|
+
`import { Injectable, type LoggerService } from '@nestjs/common';
|
|
90
|
+
import { requestContext } from './request-context';
|
|
91
|
+
|
|
92
|
+
const SENSITIVE_KEY = /password|secret|token|authorization|cookie|api[-_]?key/i;
|
|
93
|
+
|
|
94
|
+
export function redact(value: unknown): unknown {
|
|
95
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
96
|
+
if (!value || typeof value !== 'object') return value;
|
|
97
|
+
if (value instanceof Error) return { name: value.name, message: value.message };
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(value).map(([key, nested]) => [key, SENSITIVE_KEY.test(key) ? '[REDACTED]' : redact(nested)]),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
export class StructuredLogger implements LoggerService {
|
|
105
|
+
log(message: unknown, ...optional: unknown[]): void {
|
|
106
|
+
this.write('log', message, optional);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
error(message: unknown, ...optional: unknown[]): void {
|
|
110
|
+
this.write('error', message, optional);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
warn(message: unknown, ...optional: unknown[]): void {
|
|
114
|
+
this.write('warn', message, optional);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
debug(message: unknown, ...optional: unknown[]): void {
|
|
118
|
+
this.write('debug', message, optional);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
verbose(message: unknown, ...optional: unknown[]): void {
|
|
122
|
+
this.write('verbose', message, optional);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private write(level: string, message: unknown, optional: unknown[]): void {
|
|
126
|
+
const context = requestContext.getStore();
|
|
127
|
+
const record = {
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
level,
|
|
130
|
+
message: typeof message === 'string' ? message : redact(message),
|
|
131
|
+
...(context ? { requestId: context.requestId } : {}),
|
|
132
|
+
...(optional.length > 0 ? { details: redact(optional) } : {}),
|
|
133
|
+
};
|
|
134
|
+
process[level === 'error' ? 'stderr' : 'stdout'].write(JSON.stringify(record) + '\\n');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`,
|
|
138
|
+
],
|
|
139
|
+
['database-health.service.ts', databaseHealth],
|
|
140
|
+
[
|
|
141
|
+
'health.controller.ts',
|
|
142
|
+
`import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
|
|
143
|
+
import { DatabaseHealthService } from './database-health.service';
|
|
144
|
+
|
|
145
|
+
@Controller('health')
|
|
146
|
+
export class HealthController {
|
|
147
|
+
constructor(private readonly databaseHealth: DatabaseHealthService) {}
|
|
148
|
+
|
|
149
|
+
@Get('live')
|
|
150
|
+
live(): { status: 'ok'; checks: { application: 'up' } } {
|
|
151
|
+
return { status: 'ok', checks: { application: 'up' } };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Get('ready')
|
|
155
|
+
async ready(): Promise<{ status: 'ok'; checks: { database: 'up' } }> {
|
|
156
|
+
const database = await this.databaseHealth.check();
|
|
157
|
+
if (database.status === 'down') {
|
|
158
|
+
throw new ServiceUnavailableException({ status: 'error', checks: { database: 'down' } });
|
|
159
|
+
}
|
|
160
|
+
return { status: 'ok', checks: { database: 'up' } };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
'operations.module.ts',
|
|
167
|
+
`import { MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
|
|
168
|
+
${orm === 'prisma' ? "import { PrismaModule } from '../prisma/prisma.module';\n" : ''}import { DatabaseHealthService } from './database-health.service';
|
|
169
|
+
import { HealthController } from './health.controller';
|
|
170
|
+
import { RequestIdMiddleware } from './request-id.middleware';
|
|
171
|
+
import { StructuredLogger } from './structured-logger.service';
|
|
172
|
+
|
|
173
|
+
@Module({
|
|
174
|
+
imports: [${orm === 'prisma' ? 'PrismaModule' : ''}],
|
|
175
|
+
controllers: [HealthController],
|
|
176
|
+
providers: [DatabaseHealthService, RequestIdMiddleware, StructuredLogger],
|
|
177
|
+
exports: [StructuredLogger],
|
|
178
|
+
})
|
|
179
|
+
export class OperationsModule implements NestModule {
|
|
180
|
+
configure(consumer: MiddlewareConsumer): void {
|
|
181
|
+
consumer.apply(RequestIdMiddleware).forRoutes('*');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`,
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
'README.md',
|
|
188
|
+
`# Socle d’exploitation
|
|
189
|
+
|
|
190
|
+
Cette option est locale au projet et ne requiert aucun service externe. Elle expose :
|
|
191
|
+
|
|
192
|
+
- \`GET /health/live\` : le processus Nest peut répondre ; aucun appel de dépendance n’est fait.
|
|
193
|
+
- \`GET /health/ready\` : le processus et la base PostgreSQL sont prêts. Une base indisponible retourne \`503\` avec \`{ status: 'error', checks: { database: 'down' } }\`.
|
|
194
|
+
|
|
195
|
+
Chaque réponse reçoit \`x-request-id\`. Une valeur cliente sûre est conservée ; sinon NestGen en crée une. Le logger Nest produit un JSON par ligne avec cet identifiant, et masque récursivement les clés contenant \`password\`, \`secret\`, \`token\`, \`authorization\`, \`cookie\` ou \`apiKey\`.
|
|
196
|
+
|
|
197
|
+
\`main.ts\` active \`enableShutdownHooks()\`. Ainsi, \`SIGTERM\` et \`SIGINT\` ferment le contexte Nest ; TypeORM ferme sa connexion et Prisma exécute son \`onModuleDestroy\` existant. Les migrations restent explicites et ne sont jamais appliquées par ces endpoints.
|
|
198
|
+
`,
|
|
199
|
+
],
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
fs.mkdirSync(operationsDirectory, { recursive: true });
|
|
203
|
+
try {
|
|
204
|
+
for (const [name, content] of files) fs.writeFileSync(path.join(operationsDirectory, name), content);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
fs.rmSync(operationsDirectory, { recursive: true, force: true });
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ORM=${1:?ORM requis}
|
|
6
|
+
FEATURES_PATH="$(dirname "$0")"
|
|
7
|
+
|
|
8
|
+
if [[ "$ORM" != "typeorm" && "$ORM" != "prisma" ]]; then
|
|
9
|
+
echo "❌ ORM non supporté : $ORM" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
node "$FEATURES_PATH/operational_foundation.mjs" src "$ORM"
|
|
14
|
+
node "$FEATURES_PATH/update_app_module.mjs" src/app.module.ts OperationsModule './operations/operations.module' "$ORM"
|
|
15
|
+
|
|
16
|
+
echo "✅ Socle d’exploitation configuré : /health/live et /health/ready."
|
|
@@ -1,48 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'node:path';
|
|
1
|
+
import { inspectProject } from '../../dist/engine/project-preflight.js';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
throw new Error(`Préflight échoué : ${message}`);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function inspectProject(projectRoot, orm) {
|
|
9
|
-
const packagePath = path.join(projectRoot, 'package.json');
|
|
10
|
-
const appModulePath = path.join(projectRoot, 'src', 'app.module.ts');
|
|
11
|
-
const nestCliPath = path.join(projectRoot, 'nest-cli.json');
|
|
12
|
-
|
|
13
|
-
if (!fs.existsSync(packagePath))
|
|
14
|
-
error('package.json introuvable. Exécute la commande à la racine d’un projet NestJS.');
|
|
15
|
-
if (!fs.existsSync(appModulePath))
|
|
16
|
-
error(
|
|
17
|
-
'src/app.module.ts introuvable. Les workspaces et structures personnalisées ne sont pas encore supportés.',
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
21
|
-
const nestCli = fs.existsSync(nestCliPath) ? JSON.parse(fs.readFileSync(nestCliPath, 'utf8')) : null;
|
|
22
|
-
if (nestCli?.monorepo) error('les workspaces Nest ne sont pas encore supportés. Cible une application autonome.');
|
|
23
|
-
|
|
24
|
-
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
25
|
-
const missing = ['@nestjs/common', '@nestjs/core', '@nestjs/cqrs'];
|
|
26
|
-
if (orm === 'typeorm') missing.push('@nestjs/typeorm', 'typeorm');
|
|
27
|
-
if (orm === 'prisma') missing.push('@prisma/client');
|
|
28
|
-
const unavailable = missing.filter((dependency) => !dependencies[dependency]);
|
|
29
|
-
if (unavailable.length) error(`dépendances manquantes : ${unavailable.join(', ')}.`);
|
|
30
|
-
|
|
31
|
-
const appModule = fs.readFileSync(appModulePath, 'utf8');
|
|
32
|
-
if (!/@Module\s*\(/.test(appModule)) error('le décorateur @Module est introuvable dans src/app.module.ts.');
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
appModulePath,
|
|
36
|
-
packageManager: fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))
|
|
37
|
-
? 'pnpm'
|
|
38
|
-
: fs.existsSync(path.join(projectRoot, 'yarn.lock'))
|
|
39
|
-
? 'yarn'
|
|
40
|
-
: fs.existsSync(path.join(projectRoot, 'package-lock.json'))
|
|
41
|
-
? 'npm'
|
|
42
|
-
: null,
|
|
43
|
-
hasRootTypeOrmConnection: /\bTypeOrmModule\.forRoot\s*\(/.test(appModule),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
3
|
+
export { inspectProject };
|
|
46
4
|
|
|
47
5
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
48
6
|
try {
|
|
@@ -87,7 +87,12 @@ if [ "$WITH_SWAGGER" = "y" ]; then
|
|
|
87
87
|
run_step "Configuration Swagger" bash "$FEATURES_PATH/swagger.sh" "$PM"
|
|
88
88
|
fi
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
WITH_OPERATIONS=${WITH_OPERATIONS:-$(read -p "🩺 Activer le socle d’exploitation ? (y/n) : " tmp && echo "$tmp")}
|
|
91
|
+
if [ "$WITH_OPERATIONS" = "y" ]; then
|
|
92
|
+
run_step "Configuration du socle d’exploitation" bash "$FEATURES_PATH/operations.sh" "$ORM"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
run_step "Configuration du bootstrap Nest" node "$FEATURES_PATH/configure_bootstrap.mjs" src/main.ts "$WITH_SWAGGER" "$WITH_OPERATIONS"
|
|
91
96
|
|
|
92
97
|
WITH_GIT=${WITH_GIT:-$(read -p "🔃 Initialiser Git ? (y/n) : " tmp && echo "$tmp")}
|
|
93
98
|
if [ "$WITH_GIT" = "y" ]; then
|
|
@@ -109,5 +114,6 @@ echo "📦 Package manager : $PM"
|
|
|
109
114
|
echo "🧠 ORM : $ORM"
|
|
110
115
|
if [ "$WITH_DOCKER" = "y" ]; then echo "🐳 Docker activé"; fi
|
|
111
116
|
if [ "$WITH_SWAGGER" = "y" ]; then echo "📚 Swagger activé"; fi
|
|
117
|
+
if [ "$WITH_OPERATIONS" = "y" ]; then echo "🩺 Socle d’exploitation activé"; fi
|
|
112
118
|
if [ "$WITH_GIT" = "y" ]; then echo "🔃 Git initialisé"; fi
|
|
113
119
|
if [ -n "$MODULES" ]; then echo "📦 Modules générés : $MODULES"; fi
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestgen-cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "NestGen CLI — Générateur modulaire NestJS (DDD, CQRS, Hexagonal)",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
|
+
"examples",
|
|
7
8
|
"nestgen.js",
|
|
8
9
|
"nestjs-generator"
|
|
9
10
|
],
|
|
@@ -35,24 +36,30 @@
|
|
|
35
36
|
"check": "npm run typecheck && eslint .",
|
|
36
37
|
"format": "prettier --write .",
|
|
37
38
|
"format:check": "prettier --check .",
|
|
38
|
-
"prepack": "npm run build && npm run check && npm run format:check && npm test",
|
|
39
|
+
"prepack": "npm run build && npm run check && npm run docs:check && npm run landing:check && npm run format:check && npm test && npm run test:example",
|
|
39
40
|
"test": "node --test",
|
|
40
41
|
"test:package": "node scripts/package-smoke.mjs",
|
|
42
|
+
"test:package-manager": "node scripts/package-manager-lockfile-integration.mjs",
|
|
41
43
|
"test:prisma": "node scripts/prisma-integration.mjs",
|
|
42
44
|
"test:docker": "node scripts/docker-config-integration.mjs",
|
|
45
|
+
"test:example": "node scripts/example-store-api-integration.mjs",
|
|
43
46
|
"test:typeorm": "node scripts/typeorm-http-integration.mjs",
|
|
47
|
+
"release:verify": "node scripts/release-verify.mjs",
|
|
48
|
+
"release:verify-remote": "node scripts/release-verify-remote.mjs",
|
|
49
|
+
"docs:check": "node scripts/docs-contract.mjs",
|
|
50
|
+
"landing:check": "node scripts/landing-contract.mjs",
|
|
44
51
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
45
52
|
},
|
|
46
53
|
"licenses": "MIT",
|
|
47
54
|
"dependencies": {
|
|
48
55
|
"chalk": "^5.3.0",
|
|
49
56
|
"inquirer": "^9.0.0",
|
|
57
|
+
"prettier": "^3.9.9",
|
|
50
58
|
"typescript": "^5.9.3"
|
|
51
59
|
},
|
|
52
60
|
"devDependencies": {
|
|
53
61
|
"@eslint/js": "^9.39.5",
|
|
54
62
|
"@types/node": "^24.19.0",
|
|
55
|
-
"eslint": "^9.39.5"
|
|
56
|
-
"prettier": "^3.9.9"
|
|
63
|
+
"eslint": "^9.39.5"
|
|
57
64
|
}
|
|
58
65
|
}
|