11const assert = require ( "node:assert/strict" ) ;
2+ const { execFileSync } = require ( "node:child_process" ) ;
23const fs = require ( "node:fs" ) ;
34const os = require ( "node:os" ) ;
45const path = require ( "node:path" ) ;
@@ -8,6 +9,9 @@ const { createApp } = require("../src/core");
89const { askProjectDetails } = require ( "../src/core/promptService" ) ;
910const { validateProjectName } = require ( "../src/core/validators/projectValidator" ) ;
1011
12+ const cliPath = path . join ( __dirname , ".." , "bin" , "cli.js" ) ;
13+ let cliSpawningUnavailable = false ;
14+
1115async function withTempDir ( run ) {
1216 const tempRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "setup-node-api-" ) ) ;
1317 const previousCwd = process . cwd ( ) ;
@@ -21,6 +25,55 @@ async function withTempDir(run) {
2125 }
2226}
2327
28+ function runCli ( args , cwd ) {
29+ try {
30+ return execFileSync ( process . execPath , [ cliPath , ...args ] , {
31+ cwd,
32+ encoding : "utf8" ,
33+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
34+ } ) ;
35+ } catch ( error ) {
36+ if ( error . code === "EPERM" && ! error . stdout && ! error . stderr ) {
37+ cliSpawningUnavailable = true ;
38+ throw new Error ( "CLI spawning is unavailable in this environment." ) ;
39+ }
40+ throw error ;
41+ }
42+ }
43+
44+ function runCliExpectFailure ( args , cwd ) {
45+ try {
46+ runCli ( args , cwd ) ;
47+ throw new Error ( "Expected CLI command to fail" ) ;
48+ } catch ( error ) {
49+ if ( ! error . stdout && ! error . stderr ) {
50+ throw error ;
51+ }
52+
53+ return {
54+ stdout : error . stdout ?? "" ,
55+ stderr : error . stderr ?? "" ,
56+ status : error . status ,
57+ } ;
58+ }
59+ }
60+
61+ async function maybeRunCliTest ( testFn ) {
62+ if ( cliSpawningUnavailable ) {
63+ return "CLI spawning is unavailable in this environment." ;
64+ }
65+
66+ try {
67+ await testFn ( ) ;
68+ return null ;
69+ } catch ( error ) {
70+ if ( error . message === "CLI spawning is unavailable in this environment." ) {
71+ return error . message ;
72+ }
73+ throw error ;
74+ }
75+ }
76+
2477async function testJavaScriptTemplate ( ) {
2578 await withTempDir ( async ( tempRoot ) => {
2679 await createProject ( "sample-api" , {
@@ -90,6 +143,64 @@ async function testCreateAppRejectsExistingDirectoryAfterPromptResolution() {
90143 } ) ;
91144}
92145
146+ async function testCliHelpOutput ( ) {
147+ const output = runCli ( [ "--help" ] , process . cwd ( ) ) ;
148+
149+ assert . match ( output , / S c a f f o l d a N o d e \. j s \+ E x p r e s s A P I / ) ;
150+ assert . match ( output , / - - t y p e s c r i p t / ) ;
151+ assert . match ( output , / - - n o - i n s t a l l / ) ;
152+ }
153+
154+ async function testCliScaffoldsJavaScriptProject ( ) {
155+ await withTempDir ( async ( tempRoot ) => {
156+ const output = runCli ( [ "cli-js-app" , "--no-install" , "--port" , "5050" ] , tempRoot ) ;
157+
158+ const packageJson = JSON . parse (
159+ fs . readFileSync ( path . join ( tempRoot , "cli-js-app" , "package.json" ) , "utf8" )
160+ ) ;
161+ const appFile = fs . readFileSync (
162+ path . join ( tempRoot , "cli-js-app" , "src" , "app.js" ) ,
163+ "utf8"
164+ ) ;
165+ const envFile = fs . readFileSync ( path . join ( tempRoot , "cli-js-app" , ".env" ) , "utf8" ) ;
166+
167+ assert . equal ( packageJson . name , "cli-js-app" ) ;
168+ assert . equal ( packageJson . private , true ) ;
169+ assert . match ( appFile , / a p p \. g e t \( " \/ h e a l t h " / ) ;
170+ assert . equal ( envFile , "PORT=5050\n" ) ;
171+ assert . match ( output , / P r o j e c t r e a d y \. / ) ;
172+ } ) ;
173+ }
174+
175+ async function testCliScaffoldsTypeScriptProject ( ) {
176+ await withTempDir ( async ( tempRoot ) => {
177+ runCli ( [ "cli-ts-app" , "--typescript" , "--no-install" ] , tempRoot ) ;
178+
179+ const packageJson = JSON . parse (
180+ fs . readFileSync ( path . join ( tempRoot , "cli-ts-app" , "package.json" ) , "utf8" )
181+ ) ;
182+ const tsconfig = fs . readFileSync (
183+ path . join ( tempRoot , "cli-ts-app" , "tsconfig.json" ) ,
184+ "utf8"
185+ ) ;
186+
187+ assert . equal ( packageJson . name , "cli-ts-app" ) ;
188+ assert . equal ( packageJson . private , true ) ;
189+ assert . match ( tsconfig , / " t a r g e t " : " E S 2 0 2 0 " / ) ;
190+ assert . match ( tsconfig , / " f o r c e C o n s i s t e n t C a s i n g I n F i l e N a m e s " : t r u e / ) ;
191+ } ) ;
192+ }
193+
194+ async function testCliRejectsInvalidPort ( ) {
195+ const { stdout, status } = runCliExpectFailure (
196+ [ "demo-app" , "--no-install" , "--port" , "99999" ] ,
197+ process . cwd ( )
198+ ) ;
199+
200+ assert . equal ( status , 1 ) ;
201+ assert . match ( stdout , / P o r t m u s t b e a n i n t e g e r b e t w e e n 1 a n d 6 5 5 3 5 \. / ) ;
202+ }
203+
93204async function main ( ) {
94205 const tests = [
95206 [ "JavaScript template customization" , testJavaScriptTemplate ] ,
@@ -100,14 +211,34 @@ async function main() {
100211 "Existing-directory protection after final project resolution" ,
101212 testCreateAppRejectsExistingDirectoryAfterPromptResolution ,
102213 ] ,
214+ [ "CLI help output" , ( ) => maybeRunCliTest ( testCliHelpOutput ) ] ,
215+ [
216+ "CLI JavaScript scaffold smoke test" ,
217+ ( ) => maybeRunCliTest ( testCliScaffoldsJavaScriptProject ) ,
218+ ] ,
219+ [
220+ "CLI TypeScript scaffold smoke test" ,
221+ ( ) => maybeRunCliTest ( testCliScaffoldsTypeScriptProject ) ,
222+ ] ,
223+ [ "CLI invalid port handling" , ( ) => maybeRunCliTest ( testCliRejectsInvalidPort ) ] ,
103224 ] ;
225+ let passedCount = 0 ;
226+ let skippedCount = 0 ;
104227
105228 for ( const [ name , testFn ] of tests ) {
106- await testFn ( ) ;
229+ const note = await testFn ( ) ;
230+
231+ if ( note ) {
232+ skippedCount += 1 ;
233+ console . log ( `SKIP ${ name } : ${ note } ` ) ;
234+ continue ;
235+ }
236+
237+ passedCount += 1 ;
107238 console . log ( `PASS ${ name } ` ) ;
108239 }
109240
110- console . log ( `\n${ tests . length } tests passed` ) ;
241+ console . log ( `\n${ passedCount } tests passed, ${ skippedCount } skipped ` ) ;
111242}
112243
113244main ( ) . catch ( ( error ) => {
0 commit comments