11#!/usr/bin/env node
22
3- import { mkdir } from 'node:fs/promises' ;
4- import { relative } from 'node:path' ;
3+ import { mkdir , access , stat , readdir } from 'node:fs/promises' ;
4+ import { join , relative , resolve } from 'node:path' ;
55import { parseArgs , ParseArgsConfig } from 'node:util' ;
66import { generate , GenerationTask , logger } from './generate' ;
77import { argname , description } from './lib' ;
88import { parseInitCommand } from './subcommands/init' ;
99import { modelCliArguments , parseModelCommand } from './subcommands/model' ;
1010import { parsePathCommand , pathCliArguments } from './subcommands/path' ;
1111import { getBoatsRc , getIndex } from './templates/init' ;
12+ import { getComponentIndex , getModel , getModels , getPaginationModel , getParam } from './templates/model' ;
13+ import { getCreate , getDelete , getList , getPathIndex , getReplace , getShow , getUpdate } from './templates/path' ;
1214
1315export type ParseArgsOptionConfig = NonNullable < ParseArgsConfig [ 'options' ] > [ string ] ;
1416export type CliArg = ParseArgsOptionConfig & { [ description ] : string ; [ argname ] ? : string } ;
@@ -22,8 +24,27 @@ export type GlobalOptions = {
2224 quiet ? : boolean ;
2325 verbose ? : boolean ;
2426 'root-ref' ? : string ;
27+ customTemplates ?: {
28+ getBoatsRc ?: typeof getBoatsRc ;
29+ getIndex ?: typeof getIndex ;
30+
31+ getComponentIndex ?: typeof getComponentIndex ;
32+ getModel ?: typeof getModel ;
33+ getModels ?: typeof getModels ;
34+ getParam ?: typeof getParam ;
35+ getPaginationModel ?: typeof getPaginationModel ;
36+
37+ getPathIndex ?: typeof getPathIndex ;
38+ getList ?: typeof getList ;
39+ getCreate ?: typeof getCreate ;
40+ getShow ?: typeof getShow ;
41+ getDelete ?: typeof getDelete ;
42+ getUpdate ?: typeof getUpdate ;
43+ getReplace ?: typeof getReplace ;
44+ } ;
2545} ;
2646export type SubcommandGenerator = ( args : string [ ] , options : GlobalOptions ) => GenerationTask [ ] | null ;
47+ export type CustomTemplates = Exclude < GlobalOptions [ 'customTemplates' ] , undefined > ;
2748
2849/**
2950 * Custom error to immediately exit on errors.
@@ -45,8 +66,35 @@ const subcommands: Record<string, SubcommandGenerator> = {
4566 init : parseInitCommand ,
4667} ;
4768
69+ const templateFileMapping = {
70+ 'boats-rc.js' : 'getBoatsRc' ,
71+ 'component-index.js' : 'getComponentIndex' ,
72+ 'create.js' : 'getCreate' ,
73+ 'delete.js' : 'getDelete' ,
74+ 'index.js' : 'getIndex' ,
75+ 'list.js' : 'getList' ,
76+ 'model.js' : 'getModel' ,
77+ 'models.js' : 'getModels' ,
78+ 'pagination-model.js' : 'getPaginationModel' ,
79+ 'param.js' : 'getParam' ,
80+ 'path-index.js' : 'getPathIndex' ,
81+ 'replace.js' : 'getReplace' ,
82+ 'show.js' : 'getShow' ,
83+ 'update.js' : 'getUpdate' ,
84+ } as const satisfies Record < string , keyof Exclude < GlobalOptions [ 'customTemplates' ] , undefined >> ;
85+
4886export const cliArguments : Record < string , CliArg > = {
87+ /*
88+ -t --templates
89+ create.js delete.js list.js model.js models.js pagination.js param.js replace.js show.js update.js
90+ */
4991 'dry-run' : { type : 'boolean' , short : 'D' , [ description ] : 'Print the changes to be made' } ,
92+ templates : {
93+ type : 'string' ,
94+ short : 'T' ,
95+ [ argname ] : 'TEMPLATES' ,
96+ [ description ] : 'Folder or module containing template overrides' ,
97+ } ,
5098 force : { type : 'boolean' , short : 'f' , [ description ] : 'Overwrite existing files' } ,
5199 'no-index' : { type : 'boolean' , short : 'I' , [ description ] : 'Skip auto-creating index files, only models' } ,
52100 'no-init' : { type : 'boolean' , short : 'N' , [ description ] : 'Skip auto-creating init files' } ,
@@ -114,6 +162,11 @@ Subcommands:
114162 model SINGULAR_NAME [...OPTIONS]
115163 init [-a]
116164
165+ If templates are used (-T, --templates), the following filenames / exports are used (files when path is a folder, export function name if is a module):
166+ - ${ Object . entries ( templateFileMapping )
167+ . map ( ( [ file , fn ] ) => `${ file . padEnd ( 19 , ' ' ) } - ${ fn } ` )
168+ . join ( '\n - ' ) }
169+
117170Examples:
118171 npx bc path users/:id --list --get --delete --patch --put
119172
@@ -153,6 +206,90 @@ export const parseCliArgs = (
153206 }
154207} ;
155208
209+ const tryRequire = ( path : string ) : GlobalOptions [ 'customTemplates' ] | null = > {
210+ const overrides : Exclude < GlobalOptions [ 'customTemplates' ] , undefined> = { } ;
211+ let lib : Exclude < GlobalOptions [ 'customTemplates' ] , undefined > | null = null ;
212+ try {
213+ // eslint-disable-next-line @typescript-eslint/no-require-imports
214+ lib = require ( path ) as Exclude < GlobalOptions [ 'customTemplates' ] , undefined > ;
215+ } catch ( _ ) { }
216+ try {
217+ // eslint-disable-next-line @typescript-eslint/no-require-imports
218+ lib = require ( resolve ( path ) ) as Exclude < GlobalOptions [ 'customTemplates' ] , undefined > ;
219+ } catch ( _ ) { }
220+
221+ if ( lib ) {
222+ overrides . getBoatsRc = lib . getBoatsRc ;
223+ overrides . getIndex = lib . getIndex ;
224+ overrides . getComponentIndex = lib . getComponentIndex ;
225+ overrides . getModel = lib . getModel ;
226+ overrides . getModels = lib . getModels ;
227+ overrides . getParam = lib . getParam ;
228+ overrides . getPaginationModel = lib . getPaginationModel ;
229+ overrides . getPathIndex = lib . getPathIndex ;
230+ overrides . getList = lib . getList ;
231+ overrides . getCreate = lib . getCreate ;
232+ overrides . getShow = lib . getShow ;
233+ overrides . getDelete = lib . getDelete ;
234+ overrides . getUpdate = lib . getUpdate ;
235+ overrides . getReplace = lib . getReplace ;
236+
237+ if ( ! Object . values ( overrides ) . find ( ( v ) => typeof v !== 'undefined' ) ) {
238+ logger . console . error ( `cannot load templates "${ path } ": module has no override exports\n` ) ;
239+
240+ return help ( 1 ) ;
241+ }
242+
243+ return overrides ;
244+ }
245+
246+ logger . console . error ( `cannot load templates "${ path } ": not a module\n` ) ;
247+
248+ return help ( 1 ) ;
249+ } ;
250+
251+ const getTemplates = async ( path : string ) : Promise < GlobalOptions [ 'customTemplates' ] | null > => {
252+ const overrides : Exclude < GlobalOptions [ 'customTemplates' ] , undefined> = { } ;
253+ const fullPath = resolve ( path ) ;
254+
255+ const accessible = await access ( fullPath )
256+ . then ( ( ) => true )
257+ . catch ( ( ) => false ) ;
258+ if ( ! accessible ) {
259+ return tryRequire ( path ) ;
260+ }
261+
262+ const folder = await stat ( fullPath ) . catch ( ( ) => null ) ;
263+ if ( folder === null ) {
264+ return tryRequire ( path ) ;
265+ }
266+
267+ if ( ! folder . isDirectory ( ) ) {
268+ return tryRequire ( path ) ;
269+ }
270+
271+ const files = await readdir ( fullPath ) . catch ( ( ) => null ) ;
272+ if ( files === null ) {
273+ logger . console . error ( `cannot load templates "${ path } ": could not read template folder contents\n` ) ;
274+
275+ return help ( 1 ) ;
276+ }
277+
278+ const matchingFiles = files . filter ( ( file ) => file in templateFileMapping ) as ( keyof typeof templateFileMapping ) [ ] ;
279+ if ( matchingFiles . length === 0 ) {
280+ logger . console . error ( `cannot load templates "${ path } ": template folder has no override files\n` ) ;
281+
282+ return help ( 1 ) ;
283+ }
284+
285+ for ( const file of matchingFiles ) {
286+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
287+ overrides [ templateFileMapping [ file ] ] = require ( join ( fullPath , file ) ) ;
288+ }
289+
290+ return overrides ;
291+ } ;
292+
156293export const cli = async ( args : string [ ] ) : Promise < Record < string , GenerationTask > > => {
157294 const processed = parseCliArgs (
158295 {
@@ -176,6 +313,7 @@ export const cli = async (args: string[]): Promise<Record<string, GenerationTask
176313 const globalOptions : GlobalOptions = { } ;
177314 const todo : string [ ] [ ] = [ ] ;
178315 const subcommand : string [ ] = [ ] ;
316+ let processTemplates : string | null = null ;
179317
180318 let done = false ;
181319 let hasSubCommand = false ;
@@ -194,13 +332,24 @@ export const cli = async (args: string[]): Promise<Record<string, GenerationTask
194332 }
195333
196334 if ( arg . name in cliArguments ) {
197- if ( arg . name === 'output' || arg . name === 'root-ref' ) {
335+ if ( arg . name === 'templates' ) {
336+ if ( ! arg . value ) {
337+ help ( 1 , `Parameter '--${ arg . name } ' requires a value` ) ;
338+
339+ return { } ;
340+ }
341+ processTemplates = arg . inlineValue ? arg . value . slice ( 1 ) : arg . value ;
342+ } else if ( arg . name === 'output' || arg . name === 'root-ref' ) {
198343 if ( ! arg . value ) {
199344 help ( 1 , `Parameter '--${ arg . name } ' requires a value` ) ;
200345
201346 return { } ;
202347 }
203348 globalOptions [ arg . name ] = arg . value ;
349+ } else if ( arg . value ) {
350+ help ( 1 , `Parameter '--${ arg . name } ' is not expecting a value` ) ;
351+
352+ return { } ;
204353 } else {
205354 globalOptions [ arg . name as Exclude < keyof typeof globalOptions , 'output' | 'root-ref' > ] = true ;
206355 }
@@ -266,8 +415,18 @@ export const cli = async (args: string[]): Promise<Record<string, GenerationTask
266415 globalOptions . output = relative ( '.' , globalOptions . output ) ;
267416 }
268417
418+ if ( processTemplates ) {
419+ const templates = await getTemplates ( processTemplates ) ;
420+ if ( templates !== null ) {
421+ globalOptions . customTemplates = templates ;
422+ }
423+ }
424+
269425 if ( ! globalOptions [ 'no-init' ] ) {
270- tasks . push ( { contents : ( ) => getIndex ( globalOptions ) , filename : 'src/index.yml' } , { contents : getBoatsRc , filename : '.boatsrc' } ) ;
426+ tasks . push (
427+ { contents : ( ) => getIndex ( globalOptions , 'src/index.yml' ) , filename : 'src/index.yml' } ,
428+ { contents : ( ) => getBoatsRc ( globalOptions , '.boatsrc' ) , filename : '.boatsrc' } ,
429+ ) ;
271430 }
272431
273432 if ( ! tasks . length ) {
@@ -291,6 +450,8 @@ export const cli = async (args: string[]): Promise<Record<string, GenerationTask
291450 }
292451 }
293452
453+ // if custom templates - find all and import
454+
294455 return await generate ( tasks , globalOptions ) ;
295456} ;
296457
0 commit comments