@popina/cli 5.0.0-20260203082304Z → 5.0.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @popina/cli
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Rename @fveauvy/cli to @popina/cli.
8
+
9
+ We now manage entirely publishing of our CLI.
10
+
11
+ ### Minor Changes
12
+
13
+ - Migrate from @pragma/cli to @popina/cli package.
14
+
15
+ ### Patch Changes
16
+
17
+ - Popina CLI generate command now uses turbo generators from packages/cli/turbo/ instead of turbo/.
18
+ - Bump vitest config from 0.x.x to 4.0.17
19
+ - Add metadata to Popina CLI package
20
+ - Change package name to Popina CLI
21
+
3
22
  ## 4.5.0
4
23
 
5
24
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popina/cli",
3
- "version": "5.0.0-20260203082304Z",
3
+ "version": "5.0.0",
4
4
  "description": "Popina CLI",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,8 +20,8 @@
20
20
  "author": "François Veauvy",
21
21
  "devDependencies": {
22
22
  "eslint": "^9.19.0",
23
- "@pragma/eslint": "1.3.4",
24
- "@pragma/config": "2.21.0",
23
+ "@pragma/config": "3.0.0",
24
+ "@pragma/eslint": "1.3.5",
25
25
  "@pragma/tsconfig": "1.0.2"
26
26
  },
27
27
  "dependencies": {
@@ -0,0 +1,11 @@
1
+ import type { PlopTypes } from '@turbo/gen'
2
+
3
+ import { generatorApi } from './plopGenerator/api'
4
+ import { generatorComponent } from './plopGenerator/component'
5
+ import { generatorIcon } from './plopGenerator/icon'
6
+
7
+ export default function generator(plop: PlopTypes.NodePlopAPI): void {
8
+ generatorComponent(plop)
9
+ generatorIcon(plop)
10
+ generatorApi(plop)
11
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,456 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import type { PlopTypes } from '@turbo/gen'
4
+ import pluralize from 'pluralize'
5
+
6
+ type EndpointActionValues = {
7
+ actionName: string
8
+ entityName: string
9
+ zodElementarySchemaDefinition: string
10
+ zodSchemaDefinitionName: string
11
+ typeName: string
12
+ fastifySchemaName: string
13
+ fastifyDescription: string
14
+ fastifySummary: string
15
+ fastifyOperationId: string
16
+ verb: string
17
+ responseCode: number
18
+ route: string
19
+ isAParamRoute?: boolean
20
+ }
21
+
22
+ export const generatorApi = (plop: PlopTypes.NodePlopAPI) => {
23
+ plop.setGenerator('api', {
24
+ description: 'Create a new API endpoint',
25
+ prompts: [
26
+ {
27
+ type: 'list',
28
+ name: 'apiDirectory',
29
+ message:
30
+ 'What is the new API directory in which you want to create the endpoint(s) ?',
31
+ choices: () => {
32
+ return fs
33
+ .readdirSync(path.join(plop.getDestBasePath(), '/apps/'))
34
+ .filter(
35
+ (f) =>
36
+ fs
37
+ .statSync(path.join(plop.getDestBasePath() + '/apps/', f))
38
+ .isDirectory() && f.startsWith('api'),
39
+ )
40
+ .map((f) => ({
41
+ name: f,
42
+ value: f,
43
+ }))
44
+ },
45
+ },
46
+ {
47
+ type: 'input',
48
+ name: 'modulePath',
49
+ message:
50
+ "What is the name of the new module to create the endpoints in ? (e.g: 'auth', 'v1/auth')",
51
+ validate: (input: string) => {
52
+ if (!input) {
53
+ return 'module name is required'
54
+ }
55
+
56
+ if (input.match(/^([a-zA-Z0-9]+)(\/[a-zA-Z0-9]+)*$/)) {
57
+ return true
58
+ } else {
59
+ return 'Malformed module name must only contain alphanumeric characters [a-zA-Z0-9] and slashes (no slashes at the beginning or end & at least one character between slashes)'
60
+ }
61
+ },
62
+ },
63
+ {
64
+ type: 'checkbox',
65
+ name: 'endpointActions',
66
+ message: 'What is the action(s) of the module ?',
67
+ choices: (inputs) => {
68
+ const entity = pluralize.singular(inputs.modulePath.split('/').pop())
69
+ const pluralEntity = pluralize.plural(entity)
70
+ const capitalizedEntity = entity[0].toUpperCase() + entity.slice(1)
71
+ const capitalizedPluralEntity =
72
+ pluralEntity[0].toUpperCase() + pluralEntity.slice(1)
73
+
74
+ return [
75
+ {
76
+ name: `getOne${capitalizedEntity}`,
77
+ value: {
78
+ actionName: `getOne${capitalizedEntity}`,
79
+ entityName: entity,
80
+ zodElementarySchemaDefinition: `fetch${capitalizedEntity}Output`,
81
+ zodSchemaDefinitionName: `fetch${capitalizedEntity}ReplySchema`,
82
+ typeName: `${capitalizedEntity}OutputType`,
83
+ fastifySchemaName: `fetch${capitalizedEntity}Schema`,
84
+ fastifyDescription: `Fetch one ${entity} based on the ${entity} id`,
85
+ fastifySummary: `fetch a ${entity}`,
86
+ fastifyOperationId: `fetch-${entity}`,
87
+ verb: 'get',
88
+ responseCode: 200,
89
+ route: '/:id',
90
+ isAParamRoute: true,
91
+ },
92
+ },
93
+ {
94
+ name: `getMany${capitalizedPluralEntity}`,
95
+ value: {
96
+ actionName: `getMany${capitalizedPluralEntity}`,
97
+ entityName: entity,
98
+ zodElementarySchemaDefinition: `fetch${capitalizedPluralEntity}ListOutput`,
99
+ zodSchemaDefinitionName: `fetch${capitalizedPluralEntity}ListReplySchema`,
100
+ typeName: `${capitalizedPluralEntity}ListOutputType`,
101
+ fastifySchemaName: `fetch${capitalizedPluralEntity}ListSchema`,
102
+ fastifyDescription: `Fetch many ${pluralEntity}`,
103
+ fastifySummary: `fetch a list of ${pluralEntity}`,
104
+ fastifyOperationId: `fetch-many-${pluralEntity}`,
105
+ verb: 'get',
106
+ responseCode: 200,
107
+ route: '/',
108
+ },
109
+ },
110
+ {
111
+ name: 'custom',
112
+ value: {
113
+ entityName: 'custom',
114
+ },
115
+ },
116
+ ]
117
+ },
118
+ },
119
+ {
120
+ when: (inputs) => {
121
+ return inputs.endpointActions.some(
122
+ (action: EndpointActionValues) => action.entityName === 'custom',
123
+ )
124
+ },
125
+ type: 'input',
126
+ name: 'customName',
127
+ message: 'What is the name of the custom endpoint ?',
128
+ validate(input, answers) {
129
+ if (!input) {
130
+ return 'custom endpoint name is required'
131
+ }
132
+
133
+ if (input.match(/^([a-zA-Z0-9]+)(\/[a-zA-Z0-9]+)*$/) && answers) {
134
+ answers.endpointActions.find((action: EndpointActionValues) => {
135
+ if (action.entityName === 'custom') {
136
+ action.actionName = input
137
+ action.entityName = answers.modulePath.split('/').pop()
138
+ action.zodElementarySchemaDefinition = input
139
+ action.zodSchemaDefinitionName = input + 'Schema'
140
+ action.typeName =
141
+ input[0].toUpperCase() + input.slice(1) + 'Type'
142
+ action.fastifySchemaName = input + 'FastifySchema'
143
+ action.fastifyDescription =
144
+ 'Please set the description of the custom endpoint'
145
+ action.fastifySummary =
146
+ 'Please set the summary of the custom endpoint'
147
+ action.fastifyOperationId = input
148
+ action.verb = 'get'
149
+ action.responseCode = 200
150
+ action.route = '/'
151
+ }
152
+ })
153
+
154
+ console.log(answers.endpointActions)
155
+
156
+ return true
157
+ } else {
158
+ return 'Malformed custom endpoint name must only contain alphanumeric characters [a-zA-Z0-9] and slashes (no slashes at the beginning or end & at least one character between slashes)'
159
+ }
160
+ },
161
+ },
162
+ ],
163
+ actions: function (data) {
164
+ const entity = data?.modulePath.split('/').pop()
165
+ const actions: PlopTypes.Actions = [
166
+ {
167
+ type: 'add',
168
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.schema.ts`,
169
+ templateFile: 'templates/api/api-schema.hbs',
170
+ abortOnFail: true,
171
+ },
172
+ {
173
+ type: 'add',
174
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.router.ts`,
175
+ templateFile: 'templates/api/api-router.hbs',
176
+ },
177
+ {
178
+ type: 'add',
179
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.controller.ts`,
180
+ templateFile: 'templates/api/api-controller.hbs',
181
+ },
182
+ {
183
+ type: 'add',
184
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.service.ts`,
185
+ templateFile: 'templates/api/api-service.hbs',
186
+ },
187
+ {
188
+ type: 'modify',
189
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
190
+ pattern: /(?<=(import.*'\.\/modules.*schema')).*(?=(\n\n))/gm,
191
+ template: `\nimport { ${entity}Schemas } from './modules/{{modulePath}}/${entity}.schema'`,
192
+ },
193
+ {
194
+ type: 'modify',
195
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
196
+ pattern: /(?<=(for .* \[\n?))(((\s+)?\.{3}\w+(,?( |\r|)))+)/gm,
197
+ template: `\n ...${entity}Schemas,\r$2`,
198
+ },
199
+ ]
200
+
201
+ data?.endpointActions.forEach((action: EndpointActionValues) => {
202
+ if (action.isAParamRoute) {
203
+ actions.push(
204
+ {
205
+ type: 'add',
206
+ path: `{{ turbo.paths.root }}/packages/common/server/schemas/params/${entity}.schema.ts`,
207
+ templateFile: 'templates/api/api-param-schema.hbs',
208
+ },
209
+ {
210
+ type: 'append',
211
+ path: `{{ turbo.paths.root }}/packages/common/server/schemas/params/index.ts`,
212
+ template: `export * from './${entity}.schema'`,
213
+ },
214
+ {
215
+ type: 'modify',
216
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
217
+ // ----- Append entityParamId schema to the common/server import block ----- //
218
+ pattern:
219
+ /(?<=(import {\n?))(.*)(?=((.*\n?( |,\n))*} from '@pragma\/common\/server'))/gm,
220
+ template: `\n ${entity}IdParamSchema,\n$2`,
221
+ },
222
+ {
223
+ type: 'modify',
224
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
225
+ // ----- Export entityParamId schema to the common/server export block ----- //
226
+ pattern: /(?<=(for .* \[\n?))(((\s+)?\.{3}\w+(,?( |\r|)))+)/gm,
227
+ template: `\n ...${entity}IdParamSchema,\r$2`,
228
+ },
229
+ )
230
+ }
231
+ })
232
+
233
+ return actions
234
+ },
235
+ })
236
+
237
+ plop.addHelper('getEntityFromPath', (path: string) => {
238
+ if (path.includes('/')) {
239
+ return path.split('/').pop()
240
+ }
241
+
242
+ return path
243
+ })
244
+
245
+ plop.setGenerator('api', {
246
+ description: 'Create a new API endpoint',
247
+ prompts: [
248
+ {
249
+ type: 'list',
250
+ name: 'apiDirectory',
251
+ message:
252
+ 'What is the new API directory in which you want to create the endpoint(s) ?',
253
+ choices: () => {
254
+ return fs
255
+ .readdirSync(path.join(plop.getDestBasePath(), '/apps/'))
256
+ .filter(
257
+ (f) =>
258
+ fs
259
+ .statSync(path.join(plop.getDestBasePath() + '/apps/', f))
260
+ .isDirectory() && f.startsWith('api'),
261
+ )
262
+ .map((f) => ({
263
+ name: f,
264
+ value: f,
265
+ }))
266
+ },
267
+ },
268
+ {
269
+ type: 'input',
270
+ name: 'modulePath',
271
+ message:
272
+ "What is the name of the new module to create the endpoints in ? (e.g: 'auth', 'v1/auth')",
273
+ validate: (input: string) => {
274
+ if (!input) {
275
+ return 'module name is required'
276
+ }
277
+
278
+ if (input.match(/^([a-zA-Z0-9]+)(\/[a-zA-Z0-9]+)*$/)) {
279
+ return true
280
+ } else {
281
+ return 'Malformed module name must only contain alphanumeric characters [a-zA-Z0-9] and slashes (no slashes at the beginning or end & at least one character between slashes)'
282
+ }
283
+ },
284
+ },
285
+ {
286
+ type: 'checkbox',
287
+ name: 'endpointActions',
288
+ message: 'What is the action(s) of the module ?',
289
+ choices: (inputs) => {
290
+ const entity = pluralize.singular(inputs.modulePath.split('/').pop())
291
+ const pluralEntity = pluralize.plural(entity)
292
+ const capitalizedEntity = entity[0].toUpperCase() + entity.slice(1)
293
+ const capitalizedPluralEntity =
294
+ pluralEntity[0].toUpperCase() + pluralEntity.slice(1)
295
+
296
+ return [
297
+ {
298
+ name: `getOne${capitalizedEntity}`,
299
+ value: {
300
+ actionName: `getOne${capitalizedEntity}`,
301
+ entityName: entity,
302
+ zodElementarySchemaDefinition: `fetch${capitalizedEntity}Output`,
303
+ zodSchemaDefinitionName: `fetch${capitalizedEntity}ReplySchema`,
304
+ typeName: `${capitalizedEntity}OutputType`,
305
+ fastifySchemaName: `fetch${capitalizedEntity}Schema`,
306
+ fastifyDescription: `Fetch one ${entity} based on the ${entity} id`,
307
+ fastifySummary: `fetch a ${entity}`,
308
+ fastifyOperationId: `fetch-${entity}`,
309
+ verb: 'get',
310
+ responseCode: 200,
311
+ route: '/:id',
312
+ isAParamRoute: true,
313
+ },
314
+ },
315
+ {
316
+ name: `getMany${capitalizedPluralEntity}`,
317
+ value: {
318
+ actionName: `getMany${capitalizedPluralEntity}`,
319
+ entityName: entity,
320
+ zodElementarySchemaDefinition: `fetch${capitalizedPluralEntity}ListOutput`,
321
+ zodSchemaDefinitionName: `fetch${capitalizedPluralEntity}ListReplySchema`,
322
+ typeName: `${capitalizedPluralEntity}ListOutputType`,
323
+ fastifySchemaName: `fetch${capitalizedPluralEntity}ListSchema`,
324
+ fastifyDescription: `Fetch many ${pluralEntity}`,
325
+ fastifySummary: `fetch a list of ${pluralEntity}`,
326
+ fastifyOperationId: `fetch-many-${pluralEntity}`,
327
+ verb: 'get',
328
+ responseCode: 200,
329
+ route: '/',
330
+ },
331
+ },
332
+ {
333
+ name: 'custom',
334
+ value: {
335
+ entityName: 'custom',
336
+ },
337
+ },
338
+ ]
339
+ },
340
+ },
341
+ {
342
+ when: (inputs) => {
343
+ return inputs.endpointActions.some(
344
+ (action: EndpointActionValues) => action.entityName === 'custom',
345
+ )
346
+ },
347
+ type: 'input',
348
+ name: 'customName',
349
+ message: 'What is the name of the custom endpoint ?',
350
+ validate(input, answers) {
351
+ if (!input) {
352
+ return 'custom endpoint name is required'
353
+ }
354
+
355
+ if (input.match(/^([a-zA-Z0-9]+)(\/[a-zA-Z0-9]+)*$/) && answers) {
356
+ answers.endpointActions.find((action: EndpointActionValues) => {
357
+ if (action.entityName === 'custom') {
358
+ action.actionName = input
359
+ action.entityName = answers.modulePath.split('/').pop()
360
+ action.zodElementarySchemaDefinition = input
361
+ action.zodSchemaDefinitionName = input + 'Schema'
362
+ action.typeName =
363
+ input[0].toUpperCase() + input.slice(1) + 'Type'
364
+ action.fastifySchemaName = input + 'FastifySchema'
365
+ action.fastifyDescription =
366
+ 'Please set the description of the custom endpoint'
367
+ action.fastifySummary =
368
+ 'Please set the summary of the custom endpoint'
369
+ action.fastifyOperationId = input
370
+ action.verb = 'get'
371
+ action.responseCode = 200
372
+ action.route = `/${input}`
373
+ }
374
+ })
375
+
376
+ return true
377
+ } else {
378
+ return 'Malformed custom endpoint name must only contain alphanumeric characters [a-zA-Z0-9] and slashes (no slashes at the beginning or end & at least one character between slashes)'
379
+ }
380
+ },
381
+ },
382
+ ],
383
+ actions: function (data) {
384
+ const entity = data?.modulePath.split('/').pop()
385
+ const actions: PlopTypes.Actions = [
386
+ {
387
+ type: 'add',
388
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.schema.ts`,
389
+ templateFile: 'templates/api/api-schema.hbs',
390
+ abortOnFail: true,
391
+ },
392
+ {
393
+ type: 'add',
394
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.router.ts`,
395
+ templateFile: 'templates/api/api-router.hbs',
396
+ },
397
+ {
398
+ type: 'add',
399
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.controller.ts`,
400
+ templateFile: 'templates/api/api-controller.hbs',
401
+ },
402
+ {
403
+ type: 'add',
404
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/modules/{{modulePath}}/${entity}.service.ts`,
405
+ templateFile: 'templates/api/api-service.hbs',
406
+ },
407
+ {
408
+ type: 'modify',
409
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
410
+ pattern: /(?<=(import.*'\.\/modules.*schema')).*(?=(\n\n))/gm,
411
+ template: `\nimport { ${entity}Schemas } from './modules/{{modulePath}}/${entity}.schema'`,
412
+ },
413
+ {
414
+ type: 'modify',
415
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
416
+ pattern: /(?<=(for .* \[\n?))(((\s+)?\.{3}\w+(,?( |\r|)))+)/gm,
417
+ template: `\n ...${entity}Schemas,\r$2`,
418
+ },
419
+ ]
420
+
421
+ data?.endpointActions.forEach((action: EndpointActionValues) => {
422
+ if (action.isAParamRoute) {
423
+ actions.push(
424
+ {
425
+ type: 'add',
426
+ path: `{{ turbo.paths.root }}/packages/common/server/schemas/params/${entity}.schema.ts`,
427
+ templateFile: 'templates/api/api-param-schema.hbs',
428
+ },
429
+ {
430
+ type: 'append',
431
+ path: `{{ turbo.paths.root }}/packages/common/server/schemas/params/index.ts`,
432
+ template: `export * from './${entity}.schema'`,
433
+ },
434
+ {
435
+ type: 'modify',
436
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
437
+ // ----- Append entityParamId schema to the common/server import block ----- //
438
+ pattern:
439
+ /(?<=(import {))(\n?.*)(?=((.*\n?( |,\n))*} from '@pragma\/common\/server'))/gm,
440
+ template: `\n ${entity}IdParamSchema,\n$2`,
441
+ },
442
+ {
443
+ type: 'modify',
444
+ path: `{{ turbo.paths.root }}/apps/{{apiDirectory}}/src/schemas.ts`,
445
+ // ----- Export entityParamId schema to the common/server export block ----- //
446
+ pattern: /(?<=(for .* \[\n?))(((\s+)?\.{3}\w+(,?( |\r|)))+)/gm,
447
+ template: `\n ...${entity}IdParamSchema,\r$2`,
448
+ },
449
+ )
450
+ }
451
+ })
452
+
453
+ return actions
454
+ },
455
+ })
456
+ }
@@ -0,0 +1,63 @@
1
+ import type { PlopTypes } from '@turbo/gen'
2
+
3
+ export const generatorComponent = (plop: PlopTypes.NodePlopAPI) => {
4
+ plop.setGenerator('component', {
5
+ description: 'Create a new component in @pragma/ui',
6
+ prompts: [
7
+ {
8
+ type: 'input',
9
+ name: 'componentName',
10
+ message: 'What is the name of the new component to create?',
11
+ validate: (input) => {
12
+ if (input.includes('.')) {
13
+ return 'File name cannot include an extension.'
14
+ }
15
+ if (!input) {
16
+ return 'File name is required.'
17
+ }
18
+ return true
19
+ },
20
+ },
21
+ ],
22
+ actions: (data) => {
23
+ const componentName = data?.componentName
24
+ const camelCaseComponentName = plop.getHelper('camelCase')(componentName)
25
+
26
+ const pascalCaseComponentName =
27
+ plop.getHelper('pascalCase')(componentName)
28
+
29
+ const actions: PlopTypes.Actions = []
30
+
31
+ actions.push(
32
+ {
33
+ type: 'add',
34
+ path: `{{ turbo.paths.root }}/packages/ui/src/components/${camelCaseComponentName}/index.ts`,
35
+ templateFile: 'templates/component/component-index.hbs',
36
+ },
37
+ {
38
+ type: 'add',
39
+ path: `{{ turbo.paths.root }}/packages/ui/src/components/${camelCaseComponentName}/${pascalCaseComponentName}.tsx`,
40
+ templateFile: 'templates/component/component-react.hbs',
41
+ },
42
+ {
43
+ type: 'add',
44
+ path: `{{ turbo.paths.root }}/packages/ui/src/components/${camelCaseComponentName}/${camelCaseComponentName}.stories.tsx`,
45
+ templateFile: 'templates/component/component-story.hbs',
46
+ },
47
+ {
48
+ type: 'add',
49
+ path: `{{ turbo.paths.root }}/packages/ui/src/components/${camelCaseComponentName}/${camelCaseComponentName}.test.tsx`,
50
+ templateFile: 'templates/component/component-test.hbs',
51
+ },
52
+ {
53
+ type: 'modify',
54
+ path: '{{ turbo.paths.root }}/packages/ui/index.ts',
55
+ pattern: /export \{/,
56
+ template: `import { ${pascalCaseComponentName} } from './src/components/${camelCaseComponentName}/index';\n\nexport {\n ${pascalCaseComponentName},`,
57
+ },
58
+ )
59
+
60
+ return actions
61
+ },
62
+ })
63
+ }