@@ -14,32 +14,77 @@ import gradient from 'gradient-string'
1414import { fileURLToPath } from 'node:url'
1515import detectPackageManager from 'which-pm-runs'
1616import { installPackage } from '@antfu/install-pkg'
17- import { basename , dirname , join , relative } from 'node:path'
1817import { BaseCommand , args , flags } from '@adonisjs/ace'
1918import { mkdir , readFile , writeFile } from 'node:fs/promises'
19+ import { basename , dirname , join , relative } from 'node:path'
2020
21- import { ADDITIONAL_PLUGINS , ASSERTION_CHOICES , PROJECT_TYPES } from './constants.js'
2221import type { PluginChoice } from './types.js'
22+ import { ADDITIONAL_PLUGINS , ASSERTION_CHOICES , PROJECT_TYPES } from './constants.js'
23+
24+ const TEMPLATES_DIR = join ( dirname ( fileURLToPath ( import . meta. url ) ) , '../templates' )
2325
2426export class InstallJapa extends BaseCommand {
27+ static #isPackageInstallFaked = false
2528 static commandName = 'create-japa'
26- static description = 'Install Japa testing framework'
29+ static description = 'Configure Japa inside a fresh or an existing Node.js project'
30+
31+ /**
32+ * Fake package install for testing
33+ */
34+ static fakePackageInstall ( ) {
35+ this . #isPackageInstallFaked = true
36+ }
37+
38+ /**
39+ * Restore package install
40+ */
41+ static restorePackageInstall ( ) {
42+ this . #isPackageInstallFaked = false
43+ }
2744
2845 /**
2946 * Destination directory
3047 */
31- @args . string ( { description : 'Destination' , default : cwd ( ) } )
48+ @args . string ( { description : 'Destination' , default : 'process. cwd()' } )
3249 declare destination : string
3350
3451 /**
3552 * Package manager to use
3653 */
37- @flags . string ( { description : 'Force a package manager to be used' , name : 'package-manager' } )
54+ @flags . string ( {
55+ name : 'package-manager' ,
56+ description : 'Define the package manager to use for installing dependencies' ,
57+ } )
3858 declare packageManager : string
3959
40- static #isPackageInstallFaked = false
60+ /**
61+ * An array of plugins to configure
62+ */
63+ @flags . array ( {
64+ description : 'Define a collection of plugins to install and configure' ,
65+ } )
66+ declare plugins : string [ ]
67+
68+ /**
69+ * Project type
70+ */
71+ @flags . string ( {
72+ description : 'Define the project type for which you want to configure Japa' ,
73+ } )
74+ declare projectType : 'typescript' | 'javascript'
75+
76+ /**
77+ * Whether or not to create the sample test file
78+ */
79+ @flags . boolean ( {
80+ description : 'Enable to create a sample test file' ,
81+ } )
82+ declare sampleTestFile : boolean
4183
42- #edge! : Edge
84+ /**
85+ * Edge is used for evaluating templates
86+ */
87+ #edge = new Edge ( { cache : false } ) . mount ( TEMPLATES_DIR )
4388
4489 /**
4590 * Config file bindings
@@ -59,47 +104,11 @@ export class InstallJapa extends BaseCommand {
59104 */
60105 #hasBrowserPlugin = false
61106
62- /**
63- * The project type. TypeScript or JavaScript
64- */
65- #projectType! : 'TypeScript' | 'JavaScript'
66-
67- /**
68- * Selected assertion library
69- */
70- #assertionLibrary! : PluginChoice
71-
72107 /**
73108 * Packages that should be installed
74109 */
75110 #packageToInstall: string [ ] = [ '@japa/runner' ]
76111
77- /**
78- * Fake package install for testing
79- */
80- static fakePackageInstall ( ) {
81- this . #isPackageInstallFaked = true
82- }
83-
84- /**
85- * Restore package install
86- */
87- static restorePackageInstall ( ) {
88- this . #isPackageInstallFaked = false
89- }
90-
91- /**
92- * Setup edge and detect package manager
93- */
94- #setup( ) {
95- if ( ! this . packageManager ) {
96- this . packageManager = detectPackageManager ( ) ?. name || 'npm'
97- }
98-
99- const templatesDir = join ( dirname ( fileURLToPath ( import . meta. url ) ) , '../templates' )
100- this . #edge = new Edge ( { cache : false } ) . mount ( templatesDir )
101- }
102-
103112 /**
104113 * Print Title in Ascii art
105114 */
@@ -114,72 +123,29 @@ export class InstallJapa extends BaseCommand {
114123 this . logger . log ( '' )
115124 }
116125
117- /**
118- * Given selections and list of choices, this method
119- * returns the original choices object for the selections
120- */
121- #findSelectionByName< Choices extends Record < string , any > [ ] , Selections extends string | string [ ] > (
122- choices : Choices ,
123- selections : Selections
124- ) : Selections extends string ? Choices [ number ] : Choices [ number ] [ ] {
125- if ( Array . isArray ( selections ) ) {
126- return selections . map ( ( selection ) => choices . find ( ( r ) => r . name === selection ) ! ) as any
127- }
128-
129- return choices . find ( ( r ) => r . name === selections ) as any
130- }
131-
132126 /**
133127 * Prompt to select an assertion library. Assertion library
134128 * is optional
135129 */
136130 async #promptAssertionLibrary( ) {
137- this . #assertionLibrary = await this . prompt . choice (
131+ const assertionPlugin = await this . prompt . choice (
138132 'Select the assertion library' ,
139133 ASSERTION_CHOICES ,
140- { result : ( selection ) => this . #findSelectionByName( ASSERTION_CHOICES , selection ) }
134+ {
135+ validate : ( value ) => ! ! value ,
136+ }
141137 )
142-
143- this . #prepareAssertionLibraryConfig( this . #assertionLibrary)
144- this . #packageToInstall. push ( ...( this . #assertionLibrary. packagesToInstall || [ ] ) )
138+ this . plugins . push ( assertionPlugin )
145139 }
146140
147141 /**
148142 * Prompt to select additional plugins
149143 */
150144 async #promptAdditionalPlugins( ) {
151145 const plugins = await this . prompt . multiple ( 'Select additional plugins' , ADDITIONAL_PLUGINS , {
152- result : ( selections ) => this . #findSelectionByName( ADDITIONAL_PLUGINS , selections ) ,
153146 default : [ ] ,
154147 } )
155-
156- this . #preparePluginsConfig( plugins )
157- this . #hasBrowserPlugin = this . #hasPickedPlugin( plugins , '@japa/browser-client' )
158- this . #packageToInstall. push ( ...plugins . map ( ( plugin ) => plugin . packagesToInstall || [ ] ) . flat ( ) )
159-
160- return plugins
161- }
162-
163- /**
164- * Ask if the project a JS or TS project
165- */
166- async #promptProjectType( ) {
167- this . #projectType = await this . prompt . choice ( 'Select the project type' , PROJECT_TYPES )
168- return this . #projectType
169- }
170-
171- /**
172- * Should we create a sample test file?
173- */
174- #promptSampleTestFile( ) {
175- return this . prompt . confirm ( 'Want us to create a sample test?' )
176- }
177-
178- /**
179- * Check if given plugin has been selected by the user
180- */
181- #hasPickedPlugin( plugins : PluginChoice [ ] , name : string ) {
182- return ! ! plugins . find ( ( plugin ) => plugin . name === name )
148+ this . plugins = this . plugins . concat ( plugins )
183149 }
184150
185151 /**
@@ -204,13 +170,40 @@ export class InstallJapa extends BaseCommand {
204170 }
205171 }
206172
173+ /**
174+ * Processing selected plugins
175+ */
176+ #processSelectedPlugins( ) {
177+ /**
178+ * Configuring assertion library
179+ */
180+ const assertionPlugin = ASSERTION_CHOICES . find ( ( choice ) => this . plugins . includes ( choice . name ) )
181+ if ( assertionPlugin ) {
182+ this . #prepareAssertionLibraryConfig( assertionPlugin )
183+ this . #packageToInstall. push ( ...( assertionPlugin . packagesToInstall || [ ] ) )
184+ }
185+
186+ /**
187+ * Configuring rest of the plugins
188+ */
189+ const additionalPlugins = ADDITIONAL_PLUGINS . filter ( ( choice ) =>
190+ this . plugins . includes ( choice . name )
191+ )
192+ this . #preparePluginsConfig( additionalPlugins )
193+ this . #hasBrowserPlugin = this . plugins . includes ( '@japa/browser-client' )
194+ this . #packageToInstall. push (
195+ ...additionalPlugins . map ( ( plugin ) => plugin . packagesToInstall || [ ] ) . flat ( )
196+ )
197+ }
198+
207199 /**
208200 * Create sample test files.
209201 * May also create a browser test file if browser plugin is selected
210202 */
211203 async #createSampleTestFiles( ) {
204+ const assertionPlugin = ASSERTION_CHOICES . find ( ( choice ) => this . plugins . includes ( choice . name ) )
212205 const content = await this . #edge. render ( 'test-sample' , {
213- assertPlugin : this . #assertionLibrary . namedImport ,
206+ assertPlugin : assertionPlugin ? .namedImport ,
214207 } )
215208
216209 const destination = this . #hasBrowserPlugin ? `tests/unit/maths.spec` : 'tests/maths.spec'
@@ -222,8 +215,12 @@ export class InstallJapa extends BaseCommand {
222215 }
223216 }
224217
218+ /**
219+ * Returns the file extensions based upon the selected
220+ * project type
221+ */
225222 #getExtension( ) {
226- return this . # projectType === 'TypeScript ' ? 'ts' : 'js'
223+ return this . projectType === 'typescript ' ? 'ts' : 'js'
227224 }
228225
229226 /**
@@ -282,7 +279,7 @@ export class InstallJapa extends BaseCommand {
282279 const pkgJsonPath = join ( this . destination , 'package.json' )
283280
284281 const testScript =
285- this . # projectType === 'TypeScript '
282+ this . projectType === 'typescript '
286283 ? 'node --loader ts-node/esm --enable-source-maps bin/test.ts'
287284 : 'node bin/test.js'
288285
@@ -329,17 +326,76 @@ export class InstallJapa extends BaseCommand {
329326 sticker . render ( )
330327 }
331328
332- async run ( ) {
333- this . #setup( )
329+ /**
330+ * The prepare lifecycle hook to setup the initial state
331+ */
332+ async prepare ( ) {
334333 this . #printTitle( )
334+ if ( ! this . packageManager ) {
335+ this . packageManager = detectPackageManager ( ) ?. name || 'npm'
336+ }
337+ if ( ! this . destination ) {
338+ this . destination = cwd ( )
339+ }
340+ }
341+
342+ /**
343+ * The interact lifecycle hook to trigger prompts
344+ */
345+ async interact ( ) {
346+ if ( ! this . projectType ) {
347+ this . projectType = await this . prompt . choice ( 'Select the project type' , PROJECT_TYPES , {
348+ validate : ( value ) => ! ! value ,
349+ } )
350+ }
351+
352+ if ( ! this . plugins ) {
353+ this . plugins = [ ]
354+ await this . #promptAssertionLibrary( )
355+ await this . #promptAdditionalPlugins( )
356+ }
357+
358+ if ( this . sampleTestFile === undefined ) {
359+ this . sampleTestFile = await this . prompt . confirm ( 'Want us to create a sample test?' )
360+ }
361+ }
362+
363+ /**
364+ * The run method is invoked by ace after the prepare
365+ * and the interact methods
366+ */
367+ async run ( ) {
368+ await this . prepare ( )
369+ await this . interact ( )
370+
371+ /**
372+ * If no plugins were selected
373+ */
374+ if ( ! this . plugins ) {
375+ this . plugins = [ ]
376+ }
335377
336378 /**
337- * Prompt user for selections
379+ * Ensure the project type was defined
338380 */
339- await this . #promptAssertionLibrary( )
340- await this . #promptAdditionalPlugins( )
341- await this . #promptProjectType( )
342- const createSampleTest = await this . #promptSampleTestFile( )
381+ if ( ! this . projectType ) {
382+ this . exitCode = 1
383+ this . logger . error (
384+ 'Missing project type. Make sure to define it using the "--project-type" flag'
385+ )
386+ return
387+ }
388+
389+ /**
390+ * Invalid mentioned project type
391+ */
392+ if ( ! PROJECT_TYPES . find ( ( { name } ) => this . projectType === name ) ) {
393+ this . exitCode = 1
394+ this . logger . error ( 'Invalid project type. It must be either "javascript" or "typescript"' )
395+ return
396+ }
397+
398+ this . #processSelectedPlugins( )
343399
344400 /**
345401 * Create the japa configuration file
@@ -355,7 +411,7 @@ export class InstallJapa extends BaseCommand {
355411 /**
356412 * Create the sample test files
357413 */
358- if ( createSampleTest ) {
414+ if ( this . sampleTestFile ) {
359415 await this . #createSampleTestFiles( )
360416 }
361417
0 commit comments