@@ -14,11 +14,15 @@ import { createWriteStream } from "node:fs";
1414import fs from "node:fs/promises" ;
1515import { join } from "node:path" ;
1616import { stdin , stdout } from "node:process" ;
17+ import readline from "node:readline" ;
1718import { createInterface } from "node:readline/promises" ;
1819import { Readable } from "node:stream" ;
1920
20- const userAgent = process . env . npm_config_user_agent ;
21+ /**
22+ * Constants
23+ */
2124
25+ const userAgent = process . env . npm_config_user_agent ;
2226const runtime = ( ( ) => {
2327 if ( typeof Deno === "object" ) {
2428 return "deno"
@@ -31,6 +35,65 @@ const runtime = (() => {
3135 return "node" ;
3236 }
3337} ) ( ) ;
38+ const packageManager = ( ( ) => {
39+ if ( runtime === "deno" ) return "deno" ;
40+ switch ( userAgent ?. split ( "/" ) [ 0 ] ) {
41+ case "pnpm" : return "pnpm" ;
42+ case "yarn" : return "yarn" ;
43+ case "bun" : return "bun" ;
44+ default : return "npm" ;
45+ }
46+ } ) ( ) ;
47+
48+
49+ /**
50+ * Helper Functions
51+ */
52+
53+ /**
54+ * @template {string} T
55+ * @param {string } question
56+ * @param {T[] } options
57+ * @returns {Promise<T> }
58+ */
59+ const select = async ( question , options ) =>
60+ new Promise ( resolve => {
61+ let index = 0 ;
62+
63+ const render = ( ) => {
64+ console . clear ( ) ;
65+ console . log ( question ) ;
66+ options . forEach ( ( opt , i ) => {
67+ console . log ( i === index ? `● ${ opt } ` : `\x1b[2m○ ${ opt } \x1b[0m` ) ;
68+ } ) ;
69+ }
70+ render ( ) ;
71+
72+ process . stdin . on ( "keypress" , ( _ , key ) => {
73+ switch ( key . name ) {
74+ case "c" : {
75+ if ( key . ctrl ) {
76+ console . clear ( ) ;
77+ process . exit ( ) ;
78+ }
79+ return ;
80+ }
81+ case "up" : {
82+ index = ( index - 1 + options . length ) % options . length ;
83+ return render ( ) ;
84+ }
85+ case "down" : {
86+ index = ( index + 1 ) % options . length ;
87+ return render ( ) ;
88+ }
89+ case "return" : {
90+ console . clear ( ) ;
91+ process . stdin . removeAllListeners ( "keypress" ) ;
92+ return resolve ( options [ index ] ) ;
93+ }
94+ }
95+ } ) ;
96+ } ) ;
3497
3598
3699/**
@@ -66,19 +129,12 @@ const execCmd = (cmd) =>
66129 } ) )
67130 ) ;
68131
69- const repoName = `template-basic-${ runtime } ` ;
70- const repoUrl = `https://github.com/mastrojs/${ repoName } /archive/refs/heads/main.zip` ;
71- const zipFilePromise = fetch ( repoUrl ) ;
72-
73- const rl = createInterface ( { input : stdin , output : stdout , crlfDelay : Infinity } ) ;
74- const dir = await rl . question ( "What folder should we create for your new project?\n" ) ;
75- rl . close ( ) ;
76- stdin . destroy ( ) ;
77-
78- if ( dir ) {
79- const outDir = repoName + "-main" ; // this cannot be changed and is determined by the zip file
80- const zipFileName = outDir + ".zip" ;
81- const res = await zipFilePromise ;
132+ /**
133+ * @param { {fetchZipPromise: Promise<Response>; zipFileName: string } } opts
134+ */
135+ const unzip = async ( opts ) => {
136+ const { fetchZipPromise, zipFileName } = opts ;
137+ const res = await fetchZipPromise ;
82138 if ( res . ok && res . body ) {
83139 await writeFile ( zipFileName , res . body ) ;
84140 }
@@ -92,30 +148,75 @@ if (dir) {
92148 }
93149 await fs . rm ( zipFileName , { force : true , recursive : true } ) ;
94150
95- if ( unzipSuccess ) {
96- await fs . rename ( outDir , dir ) ;
151+ if ( ! unzipSuccess ) {
152+ process . exit ( - 1 ) ;
153+ }
154+ }
97155
98- const packageManager = ( ( ) => {
99- switch ( userAgent ?. split ( "/" ) [ 0 ] ) {
100- case "pnpm" : return "pnpm" ;
101- case "yarn" : return "yarn" ;
102- case "bun" : return "bun" ;
103- default : return "npm" ;
104- }
105- } ) ( ) ;
156+ /**
157+ * @param { string } dir
158+ * @param { (dependencies: Record<string, string>) => void } cb
159+ */
160+ const updateDeps = async ( dir , cb ) => {
161+ const path = join ( dir , runtime === "deno" ? "deno.json" : "package.json" ) ;
162+ const json = JSON . parse ( await fs . readFile ( path , { encoding : "utf8" } ) ) ;
163+ cb ( json [ runtime === "deno" ? "imports" : "dependencies" ] ) ;
164+ await fs . writeFile ( path , JSON . stringify ( json , null , 2 ) ) ;
165+ }
166+
167+
168+ /**
169+ * Main function
170+ */
171+ const main = async ( ) => {
172+ const repoName = `template-basic-${ runtime } ` ;
173+ const fetchZipPromise = fetch ( `https://github.com/mastrojs/${ repoName } /archive/refs/heads/main.zip` ) ;
174+
175+ const rl = createInterface ( { input : stdin , output : stdout , crlfDelay : Infinity } ) ;
176+ const dir = await rl . question ( "What name should we use for your new project folder?\n" ) ;
177+ if ( dir ) {
178+ readline . emitKeypressEvents ( process . stdin ) ;
179+ process . stdin . setRawMode ( true ) ;
180+
181+ const template = await select ( "Which template would you like to start with?" , [ "basic" , "blog" ] ) ;
182+ const templateFetchZipPromise = template === "basic"
183+ ? undefined
184+ : fetch ( `https://github.com/mastrojs/mastro/archive/refs/heads/main.zip` ) ;
185+
186+ const zipOutDir = repoName + "-main" ; // this cannot be changed and is determined by the zip file
187+ await unzip ( { fetchZipPromise, zipFileName : zipOutDir + ".zip" } ) ;
188+ await fs . rename ( zipOutDir , dir ) ;
106189
107190 if ( packageManager === "npm" ) {
108191 try {
109- const path = join ( dir , "package.json" ) ;
110- const packageJson = JSON . parse ( await fs . readFile ( path , { encoding : "utf8" } ) ) ;
111- packageJson . dependencies [ "@mastrojs/mastro" ] = "npm:@jsr/mastrojs__mastro@^0" ;
112- await fs . writeFile ( path , JSON . stringify ( packageJson , null , 2 ) ) ;
192+ await updateDeps ( dir , dependencies => {
193+ dependencies [ "@mastrojs/mastro" ] = "npm:@jsr/mastrojs__mastro@^0" ;
194+ } ) ;
113195 await fs . writeFile ( join ( dir , ".npmrc" ) , "@jsr:registry=https://npm.jsr.io" ) ;
114196 } catch ( e ) {
115197 console . error ( `Created folder ${ dir } but failed to patch it for npm. Please use pnpm instead.` ) ;
116198 }
117199 }
118200
201+ if ( templateFetchZipPromise ) {
202+ // Update dir with things from @mastrojs /mastro's `examples/blog/` folder.
203+
204+ const templateOutDir = "mastro-main" ;
205+ await unzip ( { fetchZipPromise : templateFetchZipPromise , zipFileName : templateOutDir + ".zip" } ) ;
206+
207+ await Promise . all ( [ "components" , "data" , "routes" ] . map ( async folder => {
208+ await fs . rm ( join ( dir , folder ) , { recursive : true , force : true } ) ;
209+ return fs . rename ( join ( templateOutDir , "examples" , "blog" , folder ) , join ( dir , folder ) ) ;
210+ } ) ) ;
211+ await updateDeps ( dir , deps => {
212+ deps [ "@mastrojs/markdown" ] = packageManager === "npm"
213+ ? "npm:@jsr/mastrojs__markdown@^0"
214+ : "jsr:@mastrojs/markdown@^0" ;
215+ } ) ;
216+
217+ await fs . rm ( templateOutDir , { recursive : true } ) ;
218+ }
219+
119220 const installInstr = runtime === "deno"
120221 ? ""
121222 : `\n\nThen install dependencies with: ${ packageManager } install\n` ;
@@ -134,5 +235,9 @@ Enter the newly created folder with: %ccd ${dir}${installInstr}
134235 "" ,
135236 codeStyle ,
136237 ) ;
238+
239+ rl . close ( ) ;
240+ stdin . destroy ( ) ;
137241 }
138242}
243+ await main ( ) ;
0 commit comments