springnext 0.0.1 → 0.0.3

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/bin/cli.js ADDED
@@ -0,0 +1,1537 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ var args = process.argv.slice(2);
6
+
7
+ var [command, entityName, ...options] = args;
8
+
9
+ function insertAfterLineInFile(filePath, targetLine, newLine) {
10
+ let content = fs.readFileSync(filePath, 'utf8');
11
+
12
+ const lines = content.split('\n');
13
+ const index = lines.findIndex(line => line.includes(targetLine));
14
+
15
+ if (index !== -1) {
16
+ lines.splice(index + 1, 0, newLine);
17
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
18
+ }
19
+ }
20
+
21
+ function insertBeforeLineInFile(filePath, targetLine, newLine, before = true) {
22
+ const content = fs.readFileSync(filePath, 'utf8');
23
+ const lines = content.split('\n');
24
+
25
+ const index = lines.findIndex(line => line.includes(targetLine));
26
+
27
+ if (index !== -1) {
28
+ lines.splice(before ? index - 1 : index, 0, newLine);
29
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
30
+ }
31
+ }
32
+
33
+ function camelizeVariants(str) {
34
+ if (!str.includes('-')) {
35
+ return [str, str.substring(0, 1).toUpperCase() + str.substring(1)]
36
+ }
37
+
38
+ const words = str.split("-");
39
+
40
+ const lowerCamel = words
41
+ .map((word, index) =>
42
+ index === 0 ? word.toLowerCase() : word[0].toUpperCase() + word.slice(1).toLowerCase()
43
+ )
44
+ .join("");
45
+
46
+ const upperCamel = words
47
+ .map(word => word[0].toUpperCase() + word.slice(1).toLowerCase())
48
+ .join("");
49
+
50
+ return [lowerCamel, upperCamel];
51
+ }
52
+
53
+ function findProjectRoot(startDir = process.cwd()) {
54
+ let dir = startDir;
55
+ while (dir !== path.parse(dir).root) {
56
+ if (fs.existsSync(path.join(dir, "package.json"))) {
57
+ return dir;
58
+ }
59
+ dir = path.dirname(dir);
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function loadConfig() {
65
+ const projectRoot = findProjectRoot();
66
+ if (!projectRoot) {
67
+ return null;
68
+ }
69
+
70
+ const configPath = path.join(projectRoot, "nzmt.config.json");
71
+
72
+ if (!fs.existsSync(configPath)) {
73
+ return null;
74
+ }
75
+
76
+ try {
77
+ const rawData = fs.readFileSync(configPath, "utf-8");
78
+ const config = JSON.parse(rawData);
79
+ return config;
80
+ } catch (err) {
81
+ throw err;
82
+ }
83
+ }
84
+
85
+ const config = loadConfig();
86
+
87
+ function createDefaultConfig() {
88
+ if (!config) {
89
+ const projectRoot = findProjectRoot()
90
+ if (!projectRoot) {
91
+ throw 'No package.json was found'
92
+ }
93
+
94
+ const folders = fs.readdirSync(projectRoot, { withFileTypes: true }).filter(x => x.isDirectory).map(x => x.name)
95
+ const withSrcFolder = folders.includes('src')
96
+ const coreFolder = withSrcFolder ? './src' : '.'
97
+
98
+ const prismaClientPathOption = [entityName, ...options].filter(x => typeof x === 'string').find(x => x.startsWith('prismaClientPath:'))
99
+
100
+ let prismaClientPath = prismaClientPathOption ? prismaClientPathOption.split(':')[1] : undefined
101
+
102
+ fs.writeFileSync(path.resolve(projectRoot, 'nzmt.config.json'), JSON.stringify(prismaClientPath ? {
103
+ coreFolder,
104
+ paths: {
105
+ di: '/server/di',
106
+ stores: '/server/stores',
107
+ services: '/server/services',
108
+ providers: '/server/providers',
109
+ controllers: '/server/controllers',
110
+ infrastructure: '/server/infrastructure',
111
+ entities: '/domain/entities',
112
+ valueObjects: '/domain/value-objects',
113
+ sharedErrors: '/domain/errors',
114
+ queries: '/ui/shared/queries',
115
+ clientUtils: '/ui/shared/utils',
116
+ widgets: '/ui/widgets'
117
+ },
118
+ store: {
119
+ prisma: {
120
+ clientPath: prismaClientPath
121
+ },
122
+ },
123
+ services: {
124
+ defaultInjections: []
125
+ }
126
+ } : {
127
+ coreFolder,
128
+ paths: {
129
+ di: '/server/di',
130
+ stores: '/server/stores',
131
+ services: '/server/services',
132
+ providers: '/server/providers',
133
+ controllers: '/server/controllers',
134
+ infrastructure: '/server/infrastructure',
135
+ entities: '/domain/entities',
136
+ valueObjects: '/domain/value-objects',
137
+ sharedErrors: '/domain/errors',
138
+ queries: '/ui/shared/queries',
139
+ clientUtils: '/ui/shared/utils',
140
+ widgets: '/ui/widgets'
141
+ },
142
+ services: {
143
+ defaultInjections: []
144
+ }
145
+ }, null, '\t'))
146
+ }
147
+ }
148
+
149
+ function initSharedErrors() {
150
+ const config = loadConfig()
151
+ const folder = path.resolve(process.cwd(), `${config.coreFolder}${config.paths.sharedErrors}`)
152
+ fs.mkdirSync(folder, { recursive: true })
153
+
154
+ fs.writeFileSync(path.resolve(folder, 'shared-error-codes.ts'), [
155
+ "export enum CommonErrorCodes {",
156
+ "\tNO_DATA_WAS_FOUND = 'COMMON___NO_DATA_WAS_FOUND'",
157
+ "}"
158
+ ].join('\n'))
159
+
160
+ fs.writeFileSync(path.resolve(folder, 'index.ts'), "export * from './shared-error-codes'")
161
+ }
162
+
163
+ function initTSHelpers() {
164
+ const config = loadConfig()
165
+ const folder = path.resolve(process.cwd(), `${config.coreFolder}${config.paths.infrastructure}`)
166
+
167
+ fs.writeFileSync(path.resolve(folder, 'ts-helpers.ts'), [
168
+ "export type PublicFields<A> = { [k in keyof A]: A[k] }",
169
+ ].join('\n'))
170
+ }
171
+
172
+ function initVO() {
173
+ const config = loadConfig()
174
+ const voFolder = path.resolve(process.cwd(), `${config.coreFolder}${config.paths.valueObjects}`)
175
+ const identifierFolder = path.resolve(voFolder, 'identifier')
176
+
177
+ fs.mkdirSync(identifierFolder, { recursive: true })
178
+
179
+ fs.writeFileSync(path.resolve(identifierFolder, 'identifier.value-object.ts'), [
180
+ "import { randomUUID } from 'node:crypto'",
181
+ "import z from 'zod'",
182
+ "",
183
+ "export type IdentifierModel = z.infer<typeof Identifier.schema>",
184
+ "",
185
+ "export class Identifier {",
186
+ "\tstatic schema = z.string().nonempty()",
187
+ "\t",
188
+ "\tprivate constructor(private readonly data: string) {}",
189
+ "\t",
190
+ "\tstatic create = (data: IdentifierModel) => {",
191
+ "\t\treturn new Identifier(Identifier.schema.parse(data))",
192
+ "\t}",
193
+ "\t",
194
+ "\tstatic get randomUUID() {",
195
+ "\t\treturn Identifier.create(randomUUID())",
196
+ "\t}",
197
+ "\t",
198
+ "\tget model(): IdentifierModel {",
199
+ "\t\treturn this.data",
200
+ "\t}",
201
+ "}"
202
+ ].join('\n'))
203
+
204
+ fs.writeFileSync(path.resolve(identifierFolder, 'index.ts'), "export * from './identifier.value-object'")
205
+ }
206
+
207
+ function initDI() {
208
+ const config = loadConfig()
209
+ const diPath = config?.paths?.di
210
+
211
+ const folder = path.resolve(process.cwd(), `${config.coreFolder}${diPath}`)
212
+ fs.mkdirSync(folder, { recursive: true })
213
+
214
+ // Entries
215
+ fs.writeFileSync(path.resolve(folder, `entries.di.ts`), [
216
+ "import type { BindInWhenOnFluentSyntax } from 'inversify'",
217
+ "",
218
+ "type DIEntries = Record<",
219
+ "\tstring,",
220
+ "\t| { constantValue: object }",
221
+ "\t| (new (...args: any[]) => any)",
222
+ "\t| Record<'test' | 'dev' | 'prod',",
223
+ "\t\t| [new (...args: any[]) => any, (x: BindInWhenOnFluentSyntax<unknown>) => any]",
224
+ "\t\t| (new (...args: any[]) => any)",
225
+ "\t\t| { constantValue: object }",
226
+ "\t>",
227
+ ">",
228
+ "",
229
+ "export const diEntries = {",
230
+ "\t// Stores",
231
+ "\t// Providers",
232
+ "\t// Services",
233
+ "\t// Controllers",
234
+ "\t// Infrastructure",
235
+ "} satisfies DIEntries",
236
+ "",
237
+ "export type DITokens = keyof typeof diEntries",
238
+ ].join('\n'))
239
+
240
+ // Containers
241
+ fs.writeFileSync(path.resolve(folder, `container.dev.di.ts`), [
242
+ "import { Container } from 'inversify'",
243
+ "import { diEntries } from './entries.di'",
244
+ "",
245
+ "const container = new Container()",
246
+ "",
247
+ "for (const rule in diEntries) {",
248
+ "\tconst ruleContentRaw = diEntries[rule as keyof typeof diEntries]",
249
+ "\tif ('constantValue' in ruleContentRaw) {",
250
+ "\t\tcontainer.bind(rule).toConstantValue(ruleContentRaw.constantValue)",
251
+ "\t\tcontinue",
252
+ "\t}",
253
+ "\tconst ruleContent =",
254
+ "\t\ttypeof ruleContentRaw === 'object'",
255
+ "\t\t\t? ruleContentRaw.dev",
256
+ "\t\t\t: ruleContentRaw",
257
+ "\tif (Array.isArray(ruleContent)) {",
258
+ "\t\tconst [Entry, builder] = ruleContent",
259
+ "\t\tbuilder(container.bind(rule).to(Entry))",
260
+ "\t\tcontinue",
261
+ "\t}",
262
+ "\tif ('constantValue' in ruleContent) {",
263
+ "\t\tcontainer.bind(rule).toConstantValue(ruleContent.constantValue)",
264
+ "\t\tcontinue",
265
+ "\t}",
266
+ "\tcontainer.bind(rule).to(ruleContent)",
267
+ "}",
268
+ "",
269
+ "export { container as devContainer }"
270
+ ].join('\n'))
271
+
272
+ fs.writeFileSync(path.resolve(folder, `container.test.di.ts`), [
273
+ "import { Container } from 'inversify'",
274
+ "import { diEntries } from './entries.di'",
275
+ "",
276
+ "const container = new Container()",
277
+ "",
278
+ "for (const rule in diEntries) {",
279
+ "\tconst ruleContentRaw = diEntries[rule as keyof typeof diEntries]",
280
+ "\tif ('constantValue' in ruleContentRaw) {",
281
+ "\t\tcontainer.bind(rule).toConstantValue(ruleContentRaw.constantValue)",
282
+ "\t\tcontinue",
283
+ "\t}",
284
+ "\tconst ruleContent =",
285
+ "\t\ttypeof ruleContentRaw === 'object'",
286
+ "\t\t\t? ruleContentRaw.test",
287
+ "\t\t\t: ruleContentRaw",
288
+ "\tif (Array.isArray(ruleContent)) {",
289
+ "\t\tconst [Entry, builder] = ruleContent",
290
+ "\t\tbuilder(container.bind(rule).to(Entry))",
291
+ "\t\tcontinue",
292
+ "\t}",
293
+ "\tif ('constantValue' in ruleContent) {",
294
+ "\t\tcontainer.bind(rule).toConstantValue(ruleContent.constantValue)",
295
+ "\t\tcontinue",
296
+ "\t}",
297
+ "\tcontainer.bind(rule).to(ruleContent)",
298
+ "}",
299
+ "",
300
+ "export { container as testContainer }"
301
+ ].join('\n'))
302
+
303
+ fs.writeFileSync(path.resolve(folder, `container.prod.di.ts`), [
304
+ "import { Container } from 'inversify'",
305
+ "import { diEntries } from './entries.di'",
306
+ "",
307
+ "const container = new Container()",
308
+ "",
309
+ "for (const rule in diEntries) {",
310
+ "\tconst ruleContentRaw = diEntries[rule as keyof typeof diEntries]",
311
+ "\tif ('constantValue' in ruleContentRaw) {",
312
+ "\t\tcontainer.bind(rule).toConstantValue(ruleContentRaw.constantValue)",
313
+ "\t\tcontinue",
314
+ "\t}",
315
+ "\tconst ruleContent =",
316
+ "\t\ttypeof ruleContentRaw === 'object'",
317
+ "\t\t\t? ruleContentRaw.prod",
318
+ "\t\t\t: ruleContentRaw",
319
+ "\tif (Array.isArray(ruleContent)) {",
320
+ "\t\tconst [Entry, builder] = ruleContent",
321
+ "\t\tbuilder(container.bind(rule).to(Entry))",
322
+ "\t\tcontinue",
323
+ "\t}",
324
+ "\tif ('constantValue' in ruleContent) {",
325
+ "\t\tcontainer.bind(rule).toConstantValue(ruleContent.constantValue)",
326
+ "\t\tcontinue",
327
+ "\t}",
328
+ "\tcontainer.bind(rule).to(ruleContent)",
329
+ "}",
330
+ "",
331
+ "export { container as prodContainer }"
332
+ ].join('\n'))
333
+
334
+ // Index
335
+ fs.writeFileSync(path.resolve(folder, `index.ts`), [
336
+ "import 'reflect-metadata'",
337
+ "import { devContainer } from './container.dev.di'",
338
+ "import { prodContainer } from './container.prod.di'",
339
+ "import { testContainer } from './container.test.di'",
340
+ "import type { DITokens } from './entries.di'",
341
+ "",
342
+ "const getActiveContainer = () => {",
343
+ "\tconst environment = process.env.NODE_ENV",
344
+ "\tif (environment === 'test') {",
345
+ "\t\treturn testContainer",
346
+ "\t}",
347
+ "\tif (environment === 'development') {",
348
+ "\t\treturn devContainer",
349
+ "\t}",
350
+ "\treturn prodContainer",
351
+ "}",
352
+ "",
353
+ "export const fromDI = <Result>(key: DITokens) => {",
354
+ "\tconst container = getActiveContainer()",
355
+ "\treturn container.get<Result>(key)",
356
+ "}",
357
+ "",
358
+ "export { DITokens }"
359
+ ].join('\n'))
360
+ };
361
+
362
+ function initClientUtils() {
363
+ const config = loadConfig()
364
+ const root = findProjectRoot()
365
+ const folder = path.resolve(root, `${config.coreFolder}${config.paths.clientUtils}`)
366
+ fs.mkdirSync(folder, { recursive: true })
367
+
368
+ fs.writeFileSync(path.resolve(folder, 'api-request.ts'), [
369
+ "export const apiRequest = (url: string, method: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT') =>",
370
+ "\tasync (payload: { body?: Object; query?: Object }) => {",
371
+ "\t\tconst query = payload?.query ? '?' + new URLSearchParams(Object.entries(payload.query).filter(([, v]) => v != null && v !== '')) : ''",
372
+ "\t\tconst res = await fetch(url + query, { method, headers: { 'Content-Type': 'application/json' }, body: payload?.body && JSON.stringify(payload.body) })",
373
+ "\t\tif (!res.ok) throw await res.json()",
374
+ "\t\treturn res.json()",
375
+ "\t}"
376
+ ].join('\n'))
377
+
378
+ fs.writeFileSync(path.resolve(folder, 'normalize-query-key-payload.ts'), [
379
+ 'export const normalizeObjectKeysOrder = (input: any): any => {',
380
+ '\tif (Array.isArray(input)) {',
381
+ '\t\treturn input.map(item => normalizeObjectKeysOrder(item))',
382
+ '\t}',
383
+ '',
384
+ '\tif (input !== null && typeof input === "object") {',
385
+ '\t\tconst sortedKeys = Object.keys(input).sort((a, b) => a.localeCompare(b))',
386
+ '\t\tconst result: Record<string, any> = {}',
387
+ '\t\tfor (const key of sortedKeys) {',
388
+ '\t\t\tresult[key] = normalizeObjectKeysOrder(input[key])',
389
+ '\t\t}',
390
+ '\t\treturn result',
391
+ '\t}',
392
+ '',
393
+ '\treturn input',
394
+ '}'
395
+ ].join('\n'))
396
+
397
+ fs.writeFileSync(path.resolve(folder, 'index.ts'), [
398
+ `export * from './api-request'`,
399
+ `export * from './normalize-query-key-payload'`
400
+ ].join('\n'))
401
+ }
402
+
403
+ function initPrisma() {
404
+ const config = loadConfig()
405
+ const prismaClientPath = config?.store?.prisma?.clientPath
406
+ if (!prismaClientPath) {
407
+ return
408
+ }
409
+ const prismaFolder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, 'prisma')
410
+ fs.mkdirSync(prismaFolder, { recursive: true })
411
+
412
+ fs.writeFileSync(path.resolve(prismaFolder, 'client.ts'), [
413
+ '/** ! import required Prisma adapter */',
414
+ `import { PrismaClient } from '${prismaClientPath}'`,
415
+ ``,
416
+ 'const connectionString = `${process.env.DATABASE_URL}`',
417
+ `const adapter = /** ! instanse of the adapter */`,
418
+ ``,
419
+ `export const prismaClient = new PrismaClient({ adapter })`
420
+ ].join('\n'))
421
+
422
+ fs.writeFileSync(path.resolve(prismaFolder, 'index.ts'), [
423
+ `export * from './client'`
424
+ ].join('\n'))
425
+
426
+ // Update DI
427
+
428
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
429
+
430
+ insertBeforeLineInFile(
431
+ diEntriesPath,
432
+ 'type DIEntries =',
433
+ `import { prismaClient } from '@${config?.paths?.infrastructure}/prisma'`
434
+ )
435
+
436
+ insertAfterLineInFile(
437
+ diEntriesPath,
438
+ '// Infrastructure',
439
+ `\tPrismaClient: { constantValue: prismaClient },`,
440
+ )
441
+ }
442
+
443
+ function generateInfrastructure(upperCase, lowerCase) {
444
+ const config = loadConfig()
445
+ const folder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, entityName)
446
+ fs.mkdirSync(folder, { recursive: true })
447
+
448
+ fs.writeFileSync(path.resolve(folder, `${entityName}.ts`), [
449
+ `import { Module } from '@alevnyacow/nzmt'`,
450
+ '',
451
+ `export const ${lowerCase}InfrastructureMetadata = {`,
452
+ `\tname: '${upperCase}Infrastructure',`,
453
+ `\tschemas: {}`,
454
+ `} satisfies Module.Metadata`,
455
+ '',
456
+ `export class ${upperCase} {`,
457
+ `\tprivate methods = Module.methods(${lowerCase}InfrastructureMetadata)`,
458
+ `}`
459
+ ].join('\n'))
460
+
461
+ fs.writeFileSync(path.resolve(folder, `${entityName}.mock.ts`), [
462
+ `import { PublicFields } from '@/${config.paths.infrastructure}/ts-helpers'`,
463
+ `import { ${upperCase} } from './${entityName}'`,
464
+ '',
465
+ `export class Mock${upperCase} implements PublicFields<${upperCase}> {`,
466
+ `\t`,
467
+ `}`
468
+ ].join('\n'))
469
+
470
+ fs.writeFileSync(path.resolve(folder, `index.ts`), [
471
+ `export * from './${entityName}'`,
472
+ `export * from './${entityName}.mock'`
473
+ ].join('\n'))
474
+
475
+ // Update DI
476
+
477
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
478
+
479
+ insertBeforeLineInFile(
480
+ diEntriesPath,
481
+ 'type DIEntries =',
482
+ `import { ${upperCase}, Mock${upperCase} } from '@${config?.paths?.infrastructure}/${entityName}'`
483
+ )
484
+
485
+ insertAfterLineInFile(
486
+ diEntriesPath,
487
+ '// Infrastructure',
488
+ `\t${upperCase}: { test: Mock${upperCase}, dev: ${upperCase}, prod: ${upperCase} },`,
489
+ )
490
+ }
491
+
492
+
493
+ function initGuards() {
494
+ const config = loadConfig()
495
+ const endpointGuardsFolder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, 'guards')
496
+ fs.mkdirSync(endpointGuardsFolder, { recursive: true })
497
+
498
+ fs.writeFileSync(path.resolve(endpointGuardsFolder, 'guards.ts'), [
499
+ `import type { Controller } from '@alevnyacow/nzmt'`,
500
+ '',
501
+ `export class Guards {`,
502
+ `\tdummyGuard: Controller.Guard = async () => { return undefined }`,
503
+ `}`
504
+ ].join('\n'))
505
+
506
+ fs.writeFileSync(path.resolve(endpointGuardsFolder, 'index.ts'), [
507
+ `export * from './guards'`
508
+ ].join('\n'))
509
+
510
+ // Update DI
511
+
512
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
513
+
514
+ insertBeforeLineInFile(
515
+ diEntriesPath,
516
+ 'type DIEntries =',
517
+ `import { Guards } from '@${config?.paths?.infrastructure}/guards'`
518
+ )
519
+
520
+ insertAfterLineInFile(
521
+ diEntriesPath,
522
+ '// Infrastructure',
523
+ `\tGuards,`,
524
+ )
525
+ }
526
+
527
+ function initLogger() {
528
+ const config = loadConfig()
529
+ const loggerFolder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, 'logger')
530
+ fs.mkdirSync(loggerFolder, { recursive: true })
531
+
532
+ fs.writeFileSync(path.resolve(loggerFolder, 'logger.ts'), [
533
+ `export abstract class Logger {`,
534
+ `\tabstract error: (payload: Record<string, unknown>) => Promise<void>`,
535
+ `}`
536
+ ].join('\n'))
537
+
538
+ fs.writeFileSync(path.resolve(loggerFolder, 'logger.console.ts'), [
539
+ `import { injectable } from 'inversify'`,
540
+ `import { Logger } from './logger'`,
541
+ '',
542
+ '@injectable()',
543
+ `export class ConsoleLogger extends Logger {`,
544
+ `\terror: Logger['error'] = async (payload) => console.error(payload)`,
545
+ `}`
546
+ ].join('\n'))
547
+
548
+
549
+ fs.writeFileSync(path.resolve(loggerFolder, 'index.ts'), [
550
+ `export * from './logger'`,
551
+ `export * from './logger.console'`
552
+ ].join('\n'))
553
+
554
+ // Update DI
555
+
556
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
557
+
558
+ insertBeforeLineInFile(
559
+ diEntriesPath,
560
+ 'type DIEntries =',
561
+ `import { ConsoleLogger } from '@${config?.paths?.infrastructure}/logger'`
562
+ )
563
+
564
+ insertAfterLineInFile(
565
+ diEntriesPath,
566
+ '// Infrastructure',
567
+ `\tLogger: ConsoleLogger,`,
568
+ )
569
+ }
570
+
571
+ if (command.toLowerCase() === 'init') {
572
+ createDefaultConfig()
573
+ initDI()
574
+ initClientUtils()
575
+ initVO()
576
+ initSharedErrors()
577
+ initPrisma()
578
+ initLogger()
579
+ initGuards()
580
+ initTSHelpers()
581
+
582
+ process.exit(0)
583
+ }
584
+
585
+ function generateStores(lowerCase, upperCase, withEntityPreset) {
586
+ const folder = config?.paths?.stores ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.stores}`, entityName) : path.resolve(process.cwd(), entityName);
587
+
588
+ fs.mkdirSync(folder, { recursive: true })
589
+
590
+ const withEntity = withEntityPreset || (options ?? []).includes('import-entity')
591
+
592
+ // Contract
593
+
594
+ fs.writeFileSync(path.resolve(folder, `${entityName}.store.ts`), [
595
+ "import { Store } from '@alevnyacow/nzmt'",
596
+ withEntity ? `import { ${upperCase} } from '@${config?.paths?.entities}/${entityName}'` : undefined,
597
+ "",
598
+ `export const ${lowerCase}StoreMetadata = {`,
599
+ "\tmodels: {",
600
+ withEntity ? `\t\tlist: ${upperCase}.schema,` : "\t\tlist: z.object({ }),",
601
+ withEntity ? `\t\tdetails: ${upperCase}.schema,` : "\t\tdetails: z.object({ }),",
602
+ "\t},",
603
+ "",
604
+ "\tsearchPayload: {",
605
+ withEntity ? `\t\tlist: ${upperCase}.schema.omit({ id: true }).partial(),` : "\t\tlist: z.object({ }),",
606
+ withEntity ? `\t\tspecific: ${upperCase}.schema.pick({ id: true }),` : "\t\tspecific: z.object({ }),",
607
+ "\t},",
608
+ "",
609
+ "\tactionsPayload: {",
610
+ withEntity ? `\t\tcreate: ${upperCase}.schema.omit({ id: true }),` : "\t\tcreate: z.object({ }),",
611
+ withEntity ? `\t\tupdate: ${upperCase}.schema.omit({ id: true }).partial(),` : "\t\tupdate: z.object({ }),",
612
+ "\t},",
613
+ "",
614
+ `\tname: '${upperCase}Store'`,
615
+ "} satisfies Store.Metadata",
616
+ "",
617
+ `export const { schemas: ${lowerCase}StoreSchemas } = Store.toModuleMetadata(${lowerCase}StoreMetadata)`,
618
+ "",
619
+ `export type ${upperCase}Store = Store.Contract<typeof ${lowerCase}StoreMetadata>`
620
+ ].filter(x => typeof x === 'string').join('\n'))
621
+
622
+ // RAM
623
+
624
+ fs.writeFileSync(path.resolve(folder, `${entityName}.store.ram.ts`), [
625
+ "import { injectable } from 'inversify'",
626
+ "import { Store } from '@alevnyacow/nzmt'",
627
+ `import { type ${upperCase}Store, ${lowerCase}StoreMetadata } from './${entityName}.store'`,
628
+ "",
629
+ `const CRUDInRAM = Store.InRAM(${lowerCase}StoreMetadata)`,
630
+ "",
631
+ "@injectable()",
632
+ `export class ${upperCase}RAMStore extends CRUDInRAM implements ${upperCase}Store {`,
633
+ "\t",
634
+ "}"
635
+ ].filter(x => typeof x === 'string').join('\n'))
636
+
637
+ // Prisma
638
+ const prismaPath = config.store?.prisma?.clientPath
639
+ if (prismaPath) {
640
+ fs.writeFileSync(path.resolve(folder, `${entityName}.store.prisma.ts`), [
641
+ `import type { Prisma, PrismaClient } from '${prismaPath}'`,
642
+ `import { DITokens } from '@${config?.paths?.di}'`,
643
+ "import { injectable, inject } from 'inversify'",
644
+ "import { Store } from '@alevnyacow/nzmt'",
645
+ `import { type ${upperCase}Store, ${lowerCase}StoreMetadata } from './${entityName}.store'`,
646
+ "",
647
+ `type Types = Store.Types<${upperCase}Store>`,
648
+ "",
649
+ "const mappers = {",
650
+ `\ttoFindOnePayload: (source: Types['findOnePayload']): Prisma.${upperCase}WhereUniqueInput => {`,
651
+ "\t\treturn {",
652
+ "\t\t\t",
653
+ "\t\t};",
654
+ "\t},",
655
+ `\ttoFindListPayload: (source: Types['findListPayload']): Prisma.${upperCase}WhereInput => {`,
656
+ "\t\treturn {",
657
+ "\t\t\t",
658
+ "\t\t};",
659
+ "\t},",
660
+ `\ttoListModel: (source: Prisma.${upperCase}GetPayload<{}>): Types['listModel'] => {`,
661
+ "\t\treturn {",
662
+ "\t\t\t",
663
+ "\t\t};",
664
+ "\t},",
665
+ `\ttoDetails: (source: Prisma.${upperCase}GetPayload<{}>): Types['details'] => {`,
666
+ "\t\treturn {",
667
+ "\t\t\t",
668
+ "\t\t};",
669
+ "\t},",
670
+ `\ttoCreatePayload: (source: Types['createPayload']): Prisma.${upperCase}CreateInput => {`,
671
+ "\t\treturn {",
672
+ "\t\t\t",
673
+ "\t\t};",
674
+ "\t},",
675
+ `\ttoUpdatePayload: (source: Types['updatePayload']): Prisma.${upperCase}UpdateInput => {`,
676
+ "\t\treturn {",
677
+ "\t\t\t",
678
+ "\t\t};",
679
+ "\t}",
680
+ "}",
681
+ "",
682
+ "@injectable()",
683
+ `export class ${upperCase}PrismaStore implements ${upperCase}Store {`,
684
+ `\tprivate method = Store.methods(${lowerCase}StoreMetadata);`,
685
+ "",
686
+ "\tconstructor(@inject('PrismaClient' satisfies DITokens) private readonly prismaClient: PrismaClient) {}",
687
+ "",
688
+ "\tlist = this.method('list', async ({ filter, pagination: { pageSize, zeroBasedIndex } = { pageSize: 1000, zeroBasedIndex: 0 }}) => {",
689
+ `\t\tconst list = await this.prismaClient.${lowerCase}.findMany({`,
690
+ "\t\t\twhere: mappers.toFindListPayload(filter),",
691
+ "\t\t\tskip: zeroBasedIndex * pageSize,",
692
+ "\t\t\ttake: pageSize",
693
+ "\t\t})",
694
+ "\t\t",
695
+ "\t\treturn list.map(mappers.toListModel)",
696
+ "\t});",
697
+ "",
698
+ "\tdetails = this.method('details', async ({ filter }) => {",
699
+ `\t\tconst details = await this.prismaClient.${lowerCase}.findUnique({`,
700
+ "\t\t\twhere: mappers.toFindOnePayload(filter)",
701
+ "\t\t})",
702
+ "",
703
+ "\t\tif (!details) {",
704
+ "\t\t\treturn null",
705
+ "\t\t}",
706
+ "",
707
+ "\t\treturn mappers.toDetails(details)",
708
+ "\t});",
709
+ "",
710
+ "\tcreate = this.method('create', async ({ payload }) => {",
711
+ `\t\tconst { id } = await this.prismaClient.${lowerCase}.create({`,
712
+ "\t\t\tdata: mappers.toCreatePayload(payload),",
713
+ "\t\t\tselect: { id: true }",
714
+ "\t\t})",
715
+ "",
716
+ "\t\treturn { id }",
717
+ "\t});",
718
+ "",
719
+ "\tupdateOne = this.method('updateOne', async ({ filter, payload }) => {",
720
+ "\t\ttry {",
721
+ `\t\t\tawait this.prismaClient.${lowerCase}.update({`,
722
+ "\t\t\t\twhere: mappers.toFindOnePayload(filter),",
723
+ "\t\t\t\tdata: mappers.toUpdatePayload(payload),",
724
+ "\t\t\t})",
725
+ "",
726
+ "\t\t\treturn { success: true }",
727
+ "\t\t}",
728
+ "\t\tcatch {",
729
+ "\t\t\treturn { success: false }",
730
+ "\t\t}",
731
+ "\t});",
732
+ "",
733
+ "\tdeleteOne = this.method('deleteOne', async ({ filter }) => {",
734
+ "\t\ttry {",
735
+ `\t\t\tawait this.prismaClient.${lowerCase}.delete({`,
736
+ "\t\t\t\twhere: mappers.toFindOnePayload(filter),",
737
+ "\t\t\t})",
738
+ "",
739
+ "\t\t\treturn { success: true }",
740
+ "\t\t}",
741
+ "\t\tcatch {",
742
+ "\t\t\treturn { success: false }",
743
+ "\t\t}",
744
+ "\t});",
745
+ "};"
746
+ ].filter(x => typeof x === 'string').join('\n'))
747
+ }
748
+
749
+
750
+ // barrel
751
+
752
+ fs.writeFileSync(path.resolve(folder, 'index.ts'), [
753
+ `export * from './${entityName}.store.ts'`,
754
+ prismaPath ? `export * from './${entityName}.store.prisma.ts'` : undefined,
755
+ `export * from './${entityName}.store.ram.ts'`
756
+ ].filter(x => typeof x === 'string').join('\n'))
757
+
758
+ // update DI
759
+
760
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
761
+
762
+ insertBeforeLineInFile(
763
+ diEntriesPath,
764
+ 'type DIEntries =',
765
+ prismaPath ? `import { ${upperCase}PrismaStore, ${upperCase}RAMStore } from '@${config?.paths?.stores}/${entityName}'` : `import { ${upperCase}RAMStore } from '@${config?.paths?.stores}/${entityName}'`
766
+ )
767
+
768
+ insertAfterLineInFile(
769
+ diEntriesPath,
770
+ '// Stores',
771
+ prismaPath ? `\t${upperCase}Store: { test: [${upperCase}RAMStore, (x) => x.inSingletonScope()], prod: ${upperCase}PrismaStore, dev: ${upperCase}PrismaStore },` : `\t${upperCase}Store: { test: [${upperCase}RAMStore, (x) => x.inSingletonScope()], prod: ${upperCase}RAMStore, dev: ${upperCase}RAMStore },`,
772
+ )
773
+
774
+ }
775
+
776
+ if (command.toLowerCase() === 'store' || command === 'cs') {
777
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
778
+ generateStores(lowerCase, upperCase)
779
+ process.exit(0);
780
+ }
781
+
782
+ function generateEntity(upperCase) {
783
+ const folder = config?.paths?.entities ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.entities}`, entityName) : path.resolve(process.cwd(), entityName);
784
+ const fields = options.filter(x => x.startsWith('f:')).flatMap(x => x.split(':')[1]).join(',').split(',').map(x => x.split('-')).filter(x => x.length === 2)
785
+
786
+ fs.mkdirSync(folder, { recursive: true })
787
+
788
+ const body = [
789
+ "import z from 'zod'",
790
+ `import { Identifier } from '@${config?.paths?.valueObjects}/identifier'`,
791
+ "",
792
+ `export type ${upperCase}Model = z.infer<typeof ${upperCase}.schema>`,
793
+ "",
794
+ `export class ${upperCase} {`,
795
+ "\tstatic schema = z.object({",
796
+ "\t\tid: Identifier.schema,",
797
+ fields.length ?
798
+ fields.map(([fieldName, description]) => {
799
+ return `\t\t${fieldName}: z.${description.split('.').join('().')}(),`
800
+ }).join('\n')
801
+ : "\t\t",
802
+ "\t})",
803
+ "\t",
804
+ `\tprivate constructor(private readonly data: ${upperCase}Model) {}`,
805
+ "\t",
806
+ `\tstatic create = (data: ${upperCase}Model) => {`,
807
+ `\t\tconst parsedModel = ${upperCase}.schema.parse(data)`,
808
+ `\t\treturn new ${upperCase}(parsedModel)`,
809
+ "\t}",
810
+ "\t",
811
+ `\tget model(): ${upperCase}Model {`,
812
+ "\t\treturn this.data",
813
+ "\t}",
814
+ "}"
815
+ ].join('\n')
816
+
817
+ fs.writeFileSync(path.resolve(folder, `${entityName}.entity.ts`), body)
818
+ fs.writeFileSync(path.resolve(folder, 'index.ts'), `export * from './${entityName}.entity'`)
819
+ }
820
+
821
+ if (command.toLowerCase() === 'entity' || command === 'e') {
822
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
823
+ generateEntity(upperCase)
824
+ process.exit(0)
825
+ }
826
+
827
+ function generateValueObject(upperCase) {
828
+ const folder = config?.paths?.valueObjects ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.valueObjects}`, entityName) : path.resolve(process.cwd(), entityName);
829
+ const fields = options.filter(x => x.startsWith('f:')).flatMap(x => x.split(':')[1]).join(',').split(',').map(x => x.split('-')).filter(x => x.length === 2)
830
+
831
+ fs.mkdirSync(folder, { recursive: true })
832
+
833
+ const body = [
834
+ "import z from 'zod'",
835
+ "",
836
+ `export type ${upperCase}Model = z.infer<typeof ${upperCase}.schema>`,
837
+ "",
838
+ `export class ${upperCase} {`,
839
+ "\tstatic schema = z.object({",
840
+ fields.length ?
841
+ fields.map(([fieldName, description]) => {
842
+ return `\t\t${fieldName}: z.${description.split('.').join('().')}(),`
843
+ }).join('\n')
844
+ : "\t\t",
845
+ "\t})",
846
+ "\t",
847
+ `\tprivate constructor(private readonly data: ${upperCase}Model) {}`,
848
+ "\t",
849
+ `\tstatic create = (data: ${upperCase}Model) => {`,
850
+ `\t\tconst parsedModel = ${upperCase}.schema.parse(data)`,
851
+ `\t\treturn new ${upperCase}(parsedModel)`,
852
+ "\t}",
853
+ "\t",
854
+ `\tget model(): ${upperCase}Model {`,
855
+ "\t\treturn this.data",
856
+ "\t}",
857
+ "}"
858
+ ].join('\n')
859
+
860
+ fs.writeFileSync(path.resolve(folder, `${entityName}.value-object.ts`), body)
861
+ fs.writeFileSync(path.resolve(folder, 'index.ts'), `export * from './${entityName}.value-object'`)
862
+ }
863
+
864
+ if (command.toLowerCase() === 'value-object' || command === 'vo') {
865
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
866
+ generateValueObject(upperCase)
867
+ process.exit(0)
868
+ }
869
+
870
+ function generateProvider(lowerCase, upperCase) {
871
+ const folder = config?.paths?.providers ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.providers}`, entityName) : path.resolve(process.cwd(), entityName);
872
+
873
+ fs.mkdirSync(folder, { recursive: true })
874
+
875
+ // Provider
876
+ fs.writeFileSync(path.resolve(folder, `${entityName}.provider.ts`), [
877
+ `import { Module } from '@alevnyacow/nzmt'`,
878
+ '',
879
+ `export const ${lowerCase}ProviderMetadata = {`,
880
+ `\tname: '${upperCase}Provider',`,
881
+ `\tschemas: {}`,
882
+ `} satisfies Module.Metadata`,
883
+ '',
884
+ `export class ${upperCase}Provider {`,
885
+ `\tprivate methods = Module.methods(${lowerCase}ProviderMetadata)`,
886
+ `}`
887
+ ].join('\n'))
888
+
889
+ // Mock
890
+ fs.writeFileSync(path.resolve(folder, `${entityName}.provider.mock.ts`), [
891
+ `import { PublicFields } from '@/${config.paths.infrastructure}/ts-helpers'`,
892
+ `import { ${upperCase}Provider } from './${entityName}.provider'`,
893
+ '',
894
+ `export class ${upperCase}MockProvider implements PublicFields<${upperCase}Provider> {`,
895
+ `\t`,
896
+ `}`
897
+ ].join('\n'))
898
+
899
+ // Barrel
900
+ fs.writeFileSync(path.resolve(folder, `index.ts`), [
901
+ `export * from './${entityName}.provider'`,
902
+ `export * from './${entityName}.provider.mock'`
903
+ ].join('\n'))
904
+
905
+ // Update DI
906
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
907
+
908
+ insertBeforeLineInFile(
909
+ diEntriesPath,
910
+ 'type DIEntries =',
911
+ `import { ${upperCase}MockProvider, ${upperCase}Provider } from '@${config?.paths?.providers}/${entityName}'`
912
+ )
913
+
914
+ insertAfterLineInFile(
915
+ diEntriesPath,
916
+ '// Providers',
917
+ `\t${upperCase}Provider: { test: ${upperCase}MockProvider, prod: ${upperCase}Provider, dev: ${upperCase}Provider },`,
918
+ )
919
+ }
920
+
921
+ if (command.toLowerCase() === 'provider' || command === 'p') {
922
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
923
+ generateProvider(lowerCase, upperCase)
924
+ process.exit(0)
925
+ }
926
+
927
+ function toKebabFromPascal(str) {
928
+ return str
929
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
930
+ .toLowerCase()
931
+ }
932
+
933
+ function generateService(lowerCase, upperCase, store) {
934
+ const folder = config?.paths?.services ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.services}`, entityName) : path.resolve(process.cwd(), entityName);
935
+ const defaultInjections = config?.services?.defaultInjections
936
+
937
+ const proxiedStore = store || options.find(x => x.startsWith('p:'))?.split(':')?.at(-1)
938
+ if (proxiedStore && !proxiedStore.endsWith('Store')) {
939
+ throw 'Only stores can be proxied in services!'
940
+ }
941
+
942
+ let injections = options.filter(x => x.startsWith('i:')).flatMap(x => x.split(':')[1]).join(',').split(',').filter(x => !!x.length)
943
+ if (proxiedStore && !injections.includes(proxiedStore)) {
944
+ injections = injections.concat(proxiedStore)
945
+ }
946
+
947
+ injections = [...defaultInjections, ...injections.filter(x => !defaultInjections.includes(x))]
948
+
949
+ const importInjections = injections.map((i) => {
950
+ if (i.endsWith('Service') || i.endsWith('Controller')) {
951
+ throw 'Only stores and infrastructure can be injected in services'
952
+ }
953
+
954
+ if (i.endsWith('Store')) {
955
+ return `import type { ${i} } from '@${config?.paths?.stores}/${toKebabFromPascal(i).slice(0, -'-store'.length)}'`
956
+ }
957
+
958
+ return `import type { ${i} } from '@${config?.paths?.infrastructure}/${toKebabFromPascal(i)}'`
959
+ })
960
+
961
+ fs.mkdirSync(folder, { recursive: true })
962
+
963
+ // Metadata
964
+
965
+ fs.writeFileSync(path.resolve(folder, `${entityName}.service.metadata.ts`), [
966
+ "import type { Module } from '@alevnyacow/nzmt'",
967
+ proxiedStore ? `import { ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas } from '@${config?.paths?.stores}/${toKebabFromPascal(proxiedStore).slice(0, -'-store'.length)}'` : undefined,
968
+ "",
969
+ `export const ${lowerCase}ServiceMetadata = {`,
970
+ `\tname: '${upperCase}Service',`,
971
+ proxiedStore ? [
972
+ `\tschemas: {`,
973
+ `\t\tgetList: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.list,`,
974
+ `\t\tgetDetails: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.details,`,
975
+ `\t\tcreate: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.create,`,
976
+ `\t\tupdate: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.updateOne,`,
977
+ `\t\tdelete: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.deleteOne,`,
978
+ `\t}`,
979
+ ].join('\n') : "\tschemas: {}",
980
+ "} satisfies Module.Metadata",
981
+ "",
982
+ `export type ${upperCase}ServiceDTOs = Module.DTOs<typeof ${lowerCase}ServiceMetadata>`
983
+ ].filter(x => typeof x === 'string').join('\n'))
984
+
985
+
986
+ // Service body
987
+
988
+ fs.writeFileSync(path.resolve(folder, `${entityName}.service.ts`), [
989
+ "import { injectable, inject } from 'inversify'",
990
+ injections.length ? `import { DITokens } from '@${config?.paths?.di}'` : undefined,
991
+ `import { ${lowerCase}ServiceMetadata } from './${entityName}.service.metadata'`,
992
+ "import { Module } from '@alevnyacow/nzmt'",
993
+ ...importInjections,
994
+ "",
995
+ `type Methods = Module.Methods<typeof ${lowerCase}ServiceMetadata>`,
996
+ "",
997
+ "@injectable()",
998
+ `export class ${upperCase}Service implements Methods {`,
999
+ `\tprivate methods = Module.methods(${lowerCase}ServiceMetadata)`,
1000
+ ``,
1001
+ `\tconstructor(`,
1002
+ ...injections.map(x => `\t\t@inject('${x}' satisfies DITokens) private readonly ${x.charAt(0).toLowerCase() + x.slice(1)}: ${x},`),
1003
+ `\t) {}`,
1004
+ ``,
1005
+ proxiedStore ? `\tgetList = this.methods('getList', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.list)` : undefined,
1006
+ proxiedStore ? `\tgetDetails = this.methods('getDetails', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.details)` : undefined,
1007
+ proxiedStore ? `\tcreate = this.methods('create', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.create)` : undefined,
1008
+ proxiedStore ? `\tupdate = this.methods('update', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.updateOne)` : undefined,
1009
+ proxiedStore ? `\tdelete = this.methods('delete', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.deleteOne)` : undefined,
1010
+ "}"
1011
+ ].filter(x => typeof x === 'string').join('\n'))
1012
+
1013
+
1014
+ // Barrel
1015
+
1016
+ fs.writeFileSync(path.resolve(folder, 'index.ts'), [
1017
+ `export * from './${entityName}.service.metadata'`,
1018
+ `export * from './${entityName}.service'`
1019
+ ].join('\n'))
1020
+
1021
+ // Update DI
1022
+
1023
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
1024
+
1025
+ insertBeforeLineInFile(
1026
+ diEntriesPath,
1027
+ 'type DIEntries =',
1028
+ `import { ${upperCase}Service } from '@${config?.paths?.services}/${entityName}'`
1029
+ )
1030
+
1031
+ insertAfterLineInFile(
1032
+ diEntriesPath,
1033
+ '// Services',
1034
+ `\t${upperCase}Service,`,
1035
+ )
1036
+ }
1037
+
1038
+ if (command.toLowerCase() === 'service' || command === 's') {
1039
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1040
+ generateService(lowerCase, upperCase, false)
1041
+ process.exit(0)
1042
+ }
1043
+
1044
+ function generateController(upperCase, lowerCase, crudService) {
1045
+ if (crudService && !crudService.endsWith('Service')) {
1046
+ throw 'Incorrect crudService'
1047
+ }
1048
+
1049
+ const folder = config?.paths?.controllers ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.controllers}`, entityName) : path.resolve(process.cwd(), entityName);
1050
+
1051
+ let injections = options.filter(x => x.startsWith('i:')).flatMap(x => x.split(':')[1]).join(',').split(',').filter(x => !!x.length)
1052
+ if (crudService && !injections.includes(crudService)) {
1053
+ injections = injections.concat(crudService)
1054
+ }
1055
+
1056
+ injections = ['Logger', 'Guards', ...injections.filter(x => x !== 'Logger' && x !== 'Guards')]
1057
+
1058
+ const crudServiceLowercase = crudService ? crudService.substring(0, 1).toLowerCase() + crudService.substring(1) : undefined
1059
+
1060
+ const importInjections = injections.map((i) => {
1061
+ if (i.endsWith('Controller')) {
1062
+ throw 'Only stores, services and infrastructure can be injected in controllers'
1063
+ }
1064
+
1065
+ if (i.endsWith('Store')) {
1066
+ return `import type { ${i} } from '@${config?.paths?.stores}/${toKebabFromPascal(i).slice(0, -'-store'.length)}'`
1067
+ }
1068
+
1069
+ if (i.endsWith('Service')) {
1070
+ return `import type { ${i} } from '@${config?.paths?.services}/${toKebabFromPascal(i).slice(0, -'-service'.length)}'`
1071
+ }
1072
+
1073
+ return `import type { ${i} } from '@${config?.paths?.infrastructure}/${toKebabFromPascal(i)}'`
1074
+ })
1075
+
1076
+ fs.mkdirSync(folder, { recursive: true })
1077
+
1078
+ // Metadata
1079
+
1080
+ fs.writeFileSync(path.resolve(folder, `${entityName}.controller.metadata.ts`), [
1081
+ crudService ? `import z from 'zod'` : undefined,
1082
+ `import { Controller, ValueObjects } from '@alevnyacow/nzmt'`,
1083
+ crudService ? `import { ${crudServiceLowercase}Metadata } from '@${config.paths.services}/${toKebabFromPascal(crudService).slice(0, -'-service'.length)}'` : undefined,
1084
+ ``,
1085
+ `export const ${lowerCase}ControllerMetadata = {`,
1086
+ `\tname: '${upperCase}Controller',`,
1087
+ crudService ? [
1088
+ `\tschemas: {`,
1089
+ `\t\tGET: {`,
1090
+ `\t\t\tquery: z.union([`,
1091
+ `\t\t\t\t// Request all items`,
1092
+ `\t\t\t\t${crudServiceLowercase}Metadata.schemas.getList.payload.shape.filter,`,
1093
+ '\t\t\t\t// Request specific page',
1094
+ `\t\t\t\tValueObjects.Pagination.schema.extend(${crudServiceLowercase}Metadata.schemas.getList.payload.shape.filter.shape),`,
1095
+ `\t\t\t]),`,
1096
+ `\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.getList.response`,
1097
+ `\t\t},`,
1098
+ `\t\tdetails_GET: {`,
1099
+ `\t\t\tquery: ${crudServiceLowercase}Metadata.schemas.getDetails.payload,`,
1100
+ '\t\t\t// unwrap() to remove null',
1101
+ `\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.getDetails.response.unwrap()`,
1102
+ `\t\t},`,
1103
+ `\t\tPOST: {`,
1104
+ `\t\t\tbody: ${crudServiceLowercase}Metadata.schemas.create.payload,`,
1105
+ `\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.create.response`,
1106
+ `\t\t},`,
1107
+ `\t\tPATCH: {`,
1108
+ `\t\t\tbody: ${crudServiceLowercase}Metadata.schemas.update.payload,`,
1109
+ `\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.update.response`,
1110
+ `\t\t},`,
1111
+ `\t\tDELETE: {`,
1112
+ `\t\t\tquery: ${crudServiceLowercase}Metadata.schemas.delete.payload.shape.filter,`,
1113
+ `\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.delete.response`,
1114
+ `\t\t},`,
1115
+ `\t}`,
1116
+ ].join('\n') : `\tschemas: {}`,
1117
+ `} satisfies Controller.Metadata`,
1118
+ ``,
1119
+ `export type ${upperCase}API = Controller.Contract<typeof ${lowerCase}ControllerMetadata>`
1120
+ ].filter(x => typeof x === 'string').join('\n'))
1121
+
1122
+ // Body
1123
+
1124
+ fs.writeFileSync(path.resolve(folder, `${entityName}.controller.ts`), [
1125
+ `import { injectable, inject } from 'inversify'`,
1126
+ `import { Controller } from '@alevnyacow/nzmt'`,
1127
+ `import { DITokens } from '@${config?.paths?.di}'`,
1128
+ crudService ? `import { CommonErrorCodes } from '@${config?.paths?.sharedErrors}'` : undefined,
1129
+ `import { ${lowerCase}ControllerMetadata } from './${entityName}.controller.metadata'`,
1130
+ ...importInjections,
1131
+ ``,
1132
+ `type Endpoints = Controller.EndpointList<typeof ${lowerCase}ControllerMetadata>`,
1133
+ ``,
1134
+ `@injectable()`,
1135
+ `export class ${upperCase}Controller implements Endpoints {`,
1136
+ `\tconstructor(`,
1137
+ ...injections.map(x => `\t\t@inject('${x}' satisfies DITokens) private readonly ${x.charAt(0).toLowerCase() + x.slice(1)}: ${x},`),
1138
+ `\t) {}`,
1139
+ ``,
1140
+ `\tprivate readonly endpoints = Controller.endpoints(`,
1141
+ `\t\t${lowerCase}ControllerMetadata,`,
1142
+ '\t\t{ onErrorHandlers: [this.logger.error], guards: [] }',
1143
+ '\t)',
1144
+ ``,
1145
+ crudService ? [
1146
+ `\tGET = this.endpoints('GET', async (x) => {`,
1147
+ `\t\tif ('pageSize' in x && 'zeroBasedIndex' in x) {`,
1148
+ `\t\t\tconst { pageSize, zeroBasedIndex, ...filter } = x`,
1149
+ `\t\t\treturn await this.${crudServiceLowercase}.getList({ filter, pagination: { pageSize, zeroBasedIndex } })`,
1150
+ `\t\t}`,
1151
+ `\t\treturn await this.${crudServiceLowercase}.getList({ filter: x })`,
1152
+ `\t})`,
1153
+ ``,
1154
+ `\tdetails_GET = this.endpoints('details_GET', async (payload, { endpointError }) => {`,
1155
+ `\t\tconst details = await this.${crudServiceLowercase}.getDetails(payload)`,
1156
+ `\t\tif (!details) { throw endpointError(CommonErrorCodes.NO_DATA_WAS_FOUND, 404) }`,
1157
+ `\t\treturn details`,
1158
+ `\t})`,
1159
+ ``,
1160
+ `\tPOST = this.endpoints('POST', this.${crudServiceLowercase}.create)`,
1161
+ `\tPATCH = this.endpoints('PATCH', this.${crudServiceLowercase}.update)`,
1162
+ `\tDELETE = this.endpoints('DELETE', (filter) => this.${crudServiceLowercase}.delete({ filter }))`,
1163
+ ].join('\n') : undefined,
1164
+ `}`
1165
+ ].filter(x => typeof x === 'string').join('\n'))
1166
+
1167
+ // Barrel
1168
+
1169
+ fs.writeFileSync(path.resolve(folder, `index.ts`), [
1170
+ `export * from './${entityName}.controller'`,
1171
+ `export * from './${entityName}.controller.metadata'`
1172
+ ].filter(x => typeof x === 'string').join('\n'))
1173
+
1174
+ // Update DI
1175
+
1176
+ const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
1177
+
1178
+ insertBeforeLineInFile(
1179
+ diEntriesPath,
1180
+ 'type DIEntries =',
1181
+ `import { ${upperCase}Controller } from '@${config?.paths?.controllers}/${entityName}'`
1182
+ )
1183
+
1184
+ insertAfterLineInFile(
1185
+ diEntriesPath,
1186
+ '// Controllers',
1187
+ `\t${upperCase}Controller,`,
1188
+ )
1189
+ }
1190
+
1191
+ function generateAPIRoutes(lowerCase, upperCase, entity) {
1192
+ const requiredEntity = entity || entityName
1193
+ const projectRoot = findProjectRoot()
1194
+ const fileText = fs.readFileSync(
1195
+ path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`, requiredEntity, `${requiredEntity}.controller.ts`),
1196
+ 'utf-8'
1197
+ )
1198
+
1199
+ const regex = /^\s*(\w+)\s*=\s*this\.endpoints/mg
1200
+ const methods = Array.from(fileText.matchAll(regex), m => m[1])
1201
+
1202
+ const methodInfo = methods.map(method => ({method: method.split('_').pop(), path: method.split('_').slice(0, -1).join('/')}))
1203
+
1204
+ const rootMethods = methodInfo.filter(x => !x.path.length).map(x => x.method)
1205
+ const nestedMethods = methodInfo.filter(x => !!x.path.length).reduce((acc, cur) => {
1206
+ if (!acc[cur.path]) {
1207
+ acc[cur.path] = []
1208
+ }
1209
+ acc[cur.path].push(cur.method)
1210
+ return acc
1211
+ }, {})
1212
+
1213
+ const controllerHandlersRootPath = path.resolve(projectRoot, config.coreFolder, 'app', 'api', `${requiredEntity}-controller`)
1214
+
1215
+ if (fs.existsSync(controllerHandlersRootPath)) {
1216
+ fs.rmSync(controllerHandlersRootPath, { force: true, recursive: true })
1217
+ }
1218
+
1219
+ fs.mkdirSync(controllerHandlersRootPath, { recursive: true })
1220
+
1221
+ if (rootMethods.length) {
1222
+ fs.writeFileSync(path.resolve(controllerHandlersRootPath, 'route.ts'), [
1223
+ `import type { ${upperCase}Controller } from '@${config.paths.controllers}/${requiredEntity}'`,
1224
+ `import { fromDI } from '@${config.paths.di}'`,
1225
+ '',
1226
+ `const controller = fromDI<${upperCase}Controller>('${upperCase}Controller')`,
1227
+ '',
1228
+ rootMethods.map(x => `export const ${x} = controller.${x}`).join('\n')
1229
+ ].join('\n'))
1230
+ }
1231
+
1232
+ for (const [currentPath, methods] of Object.entries(nestedMethods)) {
1233
+ const nestedFolder = path.resolve(controllerHandlersRootPath, currentPath)
1234
+ fs.mkdirSync(nestedFolder, { recursive: true })
1235
+ fs.writeFileSync(path.resolve(nestedFolder, 'route.ts'), [
1236
+ `import type { ${upperCase}Controller } from '@${config.paths.controllers}/${requiredEntity}'`,
1237
+ `import { fromDI } from '@${config.paths.di}'`,
1238
+ '',
1239
+ `const controller = fromDI<${upperCase}Controller>('${upperCase}Controller')`,
1240
+ '',
1241
+ methods.map(x => `export const ${x} = controller.${currentPath.replaceAll('/', '_')}_${x}`).join('\n')
1242
+ ].join('\n'))
1243
+ }
1244
+ }
1245
+
1246
+ function generateQueries(lowerCase, upperCase, entity) {
1247
+ const requiredEntity = entity || entityName
1248
+ const projectRoot = findProjectRoot()
1249
+
1250
+ const fileText = fs.readFileSync(
1251
+ path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`, requiredEntity, `${requiredEntity}.controller.ts`),
1252
+ 'utf-8'
1253
+ )
1254
+
1255
+ const regex = /^\s*(\w+)\s*=\s*this\.endpoints/mg
1256
+ const methods = Array.from(fileText.matchAll(regex), m => m[1])
1257
+
1258
+ const methodInfo = methods.map(method => ({method: method.split('_').pop(), path: method.split('_').slice(0, -1).join('/')}))
1259
+
1260
+ const rootMethods = methodInfo.filter(x => !x.path.length).map(x => x.method)
1261
+ const nestedMethods = methodInfo.filter(x => !!x.path.length).reduce((acc, cur) => {
1262
+ if (!acc[cur.path]) {
1263
+ acc[cur.path] = []
1264
+ }
1265
+ acc[cur.path].push(cur.method)
1266
+ return acc
1267
+ }, {})
1268
+
1269
+ const controllerQueriesPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${requiredEntity}`, 'endpoints')
1270
+ let scaffoldedMethods = []
1271
+
1272
+ fs.mkdirSync(controllerQueriesPath, { recursive: true })
1273
+
1274
+ for (const rootMethod of rootMethods) {
1275
+ scaffoldedMethods.push(rootMethod)
1276
+ if (!rootMethod) {
1277
+ continue
1278
+ }
1279
+ const fileName = path.resolve(controllerQueriesPath, `${rootMethod}.ts` )
1280
+ const alreadyExists = fs.existsSync(fileName)
1281
+ if (alreadyExists) {
1282
+ continue
1283
+ }
1284
+ fs.writeFileSync(fileName, [
1285
+ `import { ${rootMethod === 'GET' ? 'useQuery' : 'useMutation, useQueryClient'} } from '@tanstack/react-query'`,
1286
+ `import type { ${upperCase}API } from '@${config.paths.controllers}/${requiredEntity}'`,
1287
+ `import { apiRequest${rootMethod === 'GET' ? ', normalizeObjectKeysOrder' : ''} } from '@${config.paths.clientUtils}'`,
1288
+ '',
1289
+ `type Method = ${upperCase}API['endpoints']['${rootMethod}']`,
1290
+ ``,
1291
+ `const endpoint = '/api/${requiredEntity}-controller'`,
1292
+ ``,
1293
+ rootMethod === 'GET'
1294
+ ? [
1295
+ `export const use${rootMethod} = (payload: Method['payload']) => {`,
1296
+ `\treturn useQuery<Method['response'], Method['error']>({`,
1297
+ `\t\tqueryKey: ['${requiredEntity}', '${rootMethod}', normalizeObjectKeysOrder(payload)],`,
1298
+ `\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
1299
+ `\t})`,
1300
+ `}`
1301
+ ].join('\n')
1302
+ : [
1303
+ `export const use${rootMethod} = () => {`,
1304
+ `\tconst queryClient = useQueryClient()`,
1305
+ `\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
1306
+ `\t\tmutationFn: apiRequest(endpoint, '${rootMethod}'),`,
1307
+ `\t\tonSuccess: () => { queryClient.invalidateQueries({ queryKey: ['${requiredEntity}'], exact: false }) }`,
1308
+ `\t})`,
1309
+ `}`
1310
+ ].join('\n')
1311
+ ].join('\n'))
1312
+ }
1313
+
1314
+ for (const [currentPath, methods] of Object.entries(nestedMethods)) {
1315
+ for (const method of methods) {
1316
+ const fullMethodName = `${currentPath.replaceAll('/', '_')}_${method}`
1317
+ scaffoldedMethods.push(fullMethodName)
1318
+ const fileName = path.resolve(controllerQueriesPath, `${fullMethodName}.ts`)
1319
+ const alreadyExists = fs.existsSync(fileName)
1320
+ if (alreadyExists) {
1321
+ continue
1322
+ }
1323
+
1324
+ const nameForHook = (fullMethodName.charAt(0).toUpperCase() + fullMethodName.slice(1)).replaceAll('_', '');
1325
+
1326
+ fs.writeFileSync(fileName, [
1327
+ `import { ${method === 'GET' ? 'useQuery' : 'useMutation, useQueryClient' } } from '@tanstack/react-query'`,
1328
+ `import type { ${upperCase}API } from '@${config.paths.controllers}/${requiredEntity}'`,
1329
+ `import { apiRequest${method === 'GET' ? ', normalizeObjectKeysOrder' : ''} } from '@${config.paths.clientUtils}'`,
1330
+ '',
1331
+ `type Method = ${upperCase}API['endpoints']['${fullMethodName}']`,
1332
+ ``,
1333
+ `const endpoint = '/api/${requiredEntity}-controller/${currentPath}'`,
1334
+ ``,
1335
+ method === 'GET'
1336
+ ? [
1337
+ `export const use${nameForHook} = (payload: Method['payload']) => {`,
1338
+ `\treturn useQuery<Method['response'], Method['error']>({`,
1339
+ `\t\tqueryKey: ['${requiredEntity}', ${currentPath.split('/').map(x => `'${x}'`).join(', ')}, normalizeObjectKeysOrder(payload)],`,
1340
+ `\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
1341
+ `\t})`,
1342
+ `}`
1343
+ ].join('\n')
1344
+ : [
1345
+ `export const use${nameForHook} = () => {`,
1346
+ `\tconst queryClient = useQueryClient()`,
1347
+ `\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
1348
+ `\t\tmutationFn: apiRequest(endpoint, '${method}'),`,
1349
+ `\t\tonSuccess: () => { queryClient.invalidateQueries({ queryKey: ['${requiredEntity}'], exact: false }) }`,
1350
+ `\t})`,
1351
+ `}`
1352
+ ].join('\n')
1353
+ ].join('\n'))
1354
+
1355
+ }
1356
+ }
1357
+
1358
+ const allQueryFiles = fs.readdirSync(controllerQueriesPath, { withFileTypes: true }).filter(x => x.isFile())
1359
+ const deprecatedQueries = allQueryFiles.filter(x => scaffoldedMethods.every(scaffolded => !x.name.startsWith(scaffolded))).map(x => x.name)
1360
+
1361
+ for (const deprecated of deprecatedQueries) {
1362
+ fs.rmSync(path.resolve(controllerQueriesPath, deprecated))
1363
+ }
1364
+
1365
+ fs.writeFileSync(path.resolve(controllerQueriesPath, 'index.ts'), scaffoldedMethods.map(x => `export * from './${x}'`).join('\n'))
1366
+
1367
+ const indexPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${requiredEntity}`)
1368
+ fs.writeFileSync(path.resolve(indexPath, 'index.ts'), `export * as ${upperCase}Queries from './endpoints'`)
1369
+ }
1370
+
1371
+
1372
+
1373
+ if (command === 'api-routes') {
1374
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1375
+
1376
+ generateAPIRoutes(lowerCase, upperCase)
1377
+ }
1378
+
1379
+ if (command === 'queries') {
1380
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1381
+
1382
+ generateQueries(lowerCase, upperCase)
1383
+ }
1384
+
1385
+ if (command.toLowerCase() === 'routes-with-queries' || command === 'rq') {
1386
+ const projectRoot = findProjectRoot()
1387
+ const controllersFolder = path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`)
1388
+ const controllerEntities = fs.readdirSync(controllersFolder, { withFileTypes: true }).filter(x => x.isDirectory()).map(x => x.name)
1389
+ for (const entity of controllerEntities) {
1390
+ var [lowerCase, upperCase] = camelizeVariants(entity)
1391
+ generateAPIRoutes(lowerCase, upperCase, entity)
1392
+ generateQueries(lowerCase, upperCase, entity)
1393
+ }
1394
+ }
1395
+
1396
+ if (command.toLowerCase() === 'controller' || command === 'c') {
1397
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1398
+ generateController(upperCase, lowerCase)
1399
+ process.exit(0)
1400
+ }
1401
+
1402
+ if (command.toLowerCase() === 'infrastructure' || command === 'i') {
1403
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1404
+ generateInfrastructure(upperCase, lowerCase)
1405
+ process.exit(0)
1406
+
1407
+ }
1408
+
1409
+ if (command.toLowerCase() === 'stored-entity' || command === 'se') {
1410
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1411
+ generateEntity(upperCase)
1412
+ generateStores(lowerCase, upperCase, true)
1413
+ process.exit(0)
1414
+ }
1415
+
1416
+ if (command.toLowerCase() === 'crud-service') {
1417
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1418
+ generateEntity(upperCase)
1419
+ generateStores(lowerCase, upperCase, true)
1420
+ generateService(lowerCase, upperCase, upperCase + 'Store')
1421
+ process.exit(0)
1422
+ }
1423
+
1424
+ if (command.toLowerCase() === 'crud-api') {
1425
+ var [lowerCase, upperCase] = camelizeVariants(entityName)
1426
+ generateEntity(upperCase)
1427
+ generateStores(lowerCase, upperCase, true)
1428
+ generateService(lowerCase, upperCase, upperCase + 'Store')
1429
+ generateController(upperCase, lowerCase, upperCase + 'Service')
1430
+ generateAPIRoutes(lowerCase, upperCase)
1431
+ generateQueries(lowerCase, upperCase)
1432
+ process.exit(0)
1433
+ }
1434
+
1435
+ function generateWidget(name, rootPath) {
1436
+ const [lowerCase, upperCase] = camelizeVariants(name)
1437
+
1438
+ const widgetsPath = config.paths.widgets
1439
+ const root = findProjectRoot()
1440
+
1441
+ const folder = path.resolve(root, `${config.coreFolder}${widgetsPath}`, rootPath ?? '.', name)
1442
+ fs.mkdirSync(folder, { recursive: true })
1443
+
1444
+ fs.writeFileSync(path.resolve(folder, `${name}.widget.tsx`), [
1445
+ `import { FC } from 'react'`,
1446
+ `import styles from './${lowerCase}.widget.module.css'`,
1447
+ ``,
1448
+ `export type ${upperCase}WidgetProps = {}`,
1449
+ ``,
1450
+ `export const ${upperCase}Widget: FC<${upperCase}WidgetProps> = ({ }) => {`,
1451
+ `\treturn undefined`,
1452
+ `}`
1453
+ ].join('\n'))
1454
+
1455
+ fs.writeFileSync(path.resolve(folder, `${name}.widget.module.css`), [
1456
+ ''
1457
+ ].join('\n'))
1458
+
1459
+ fs.writeFileSync(path.resolve(folder, `index.ts`), [
1460
+ `export * from './${name}.widget'`
1461
+ ].join('\n'))
1462
+ }
1463
+
1464
+ if (command === 'w') {
1465
+ let rootPath = undefined
1466
+ if (entityName.includes('/')) {
1467
+ const splitData = entityName.split('/')
1468
+ entityName = splitData.pop()
1469
+ rootPath = splitData.join('/')
1470
+ }
1471
+ generateWidget(entityName, rootPath)
1472
+ }
1473
+
1474
+ function generateLayoutedWidget(name, rootPath) {
1475
+ const [lowerCase, upperCase] = camelizeVariants(name)
1476
+
1477
+ const widgetsPath = config.paths.widgets
1478
+ const root = findProjectRoot()
1479
+
1480
+ const folder = path.resolve(root, `${config.coreFolder}${widgetsPath}`, rootPath ?? '.', name)
1481
+ fs.mkdirSync(folder, { recursive: true })
1482
+
1483
+ fs.writeFileSync(path.resolve(folder, `${name}.headless-widget.tsx`), [
1484
+ `import { FC } from 'react'`,
1485
+ `import { ${upperCase}WidgetLayoutProps } from './${name}.widget-layout'`,
1486
+ ``,
1487
+ `export type ${upperCase}HeadlessWidgetProps = {`,
1488
+ `\tLayout: FC<${upperCase}WidgetLayoutProps>,`,
1489
+ `}`,
1490
+ ``,
1491
+ `export const ${upperCase}HeadlessWidget: FC<${upperCase}HeadlessWidgetProps> = ({ Layout }) => {`,
1492
+ `\treturn <Layout />`,
1493
+ `}`
1494
+ ].join('\n'))
1495
+
1496
+ fs.writeFileSync(path.resolve(folder, `${name}.widget-layout.module.css`), [
1497
+ ''
1498
+ ].join('\n'))
1499
+
1500
+ fs.writeFileSync(path.resolve(folder, `${name}.widget-layout.tsx`), [
1501
+ `import { FC } from 'react'`,
1502
+ `import styles from './${name}.widget-layout.module.css'`,
1503
+ ``,
1504
+ `export type ${upperCase}WidgetLayoutProps = {}`,
1505
+ ``,
1506
+ `export const ${upperCase}WidgetLayout: FC<${upperCase}WidgetLayoutProps> = ({}) => {`,
1507
+ `\treturn undefined`,
1508
+ `}`
1509
+ ].join('\n'))
1510
+
1511
+ fs.writeFileSync(path.resolve(folder, `${name}.widget.tsx`), [
1512
+ `import { FC } from 'react'`,
1513
+ `import { ${upperCase}HeadlessWidget, ${upperCase}HeadlessWidgetProps } from './${name}.headless-widget'`,
1514
+ `import { ${upperCase}WidgetLayout } from './${name}.widget-layout'`,
1515
+ ``,
1516
+ `export type ${upperCase}WidgetProps = Omit<${upperCase}HeadlessWidgetProps, 'Layout'>`,
1517
+ ``,
1518
+ `export const ${upperCase}Widget: FC<${upperCase}WidgetProps> = (props) => {`,
1519
+ `\treturn <${upperCase}HeadlessWidget Layout={${upperCase}WidgetLayout} {...props}/>`,
1520
+ `}`
1521
+ ].join('\n'))
1522
+
1523
+ fs.writeFileSync(path.resolve(folder, `index.ts`), [
1524
+ `export * from './${name}.widget'`
1525
+ ].join('\n'))
1526
+ }
1527
+
1528
+
1529
+ if (command === 'lw') {
1530
+ let rootPath = undefined
1531
+ if (entityName.includes('/')) {
1532
+ const splitData = entityName.split('/')
1533
+ entityName = splitData.pop()
1534
+ rootPath = splitData.join('/')
1535
+ }
1536
+ generateLayoutedWidget(entityName, rootPath)
1537
+ }