2021-06-08 10:10:56 -05:00
import fs from 'fs' ;
import path from 'path' ;
2022-04-29 10:45:43 -05:00
import { bgCyan , black , bold , cyan , gray , green , red , yellow } from 'kleur/colors' ;
2021-06-08 10:10:56 -05:00
import prompts from 'prompts' ;
import degit from 'degit' ;
import yargs from 'yargs-parser' ;
2022-03-21 12:33:31 -05:00
import ora from 'ora' ;
2021-06-08 11:56:37 -05:00
import { TEMPLATES } from './templates.js' ;
2022-01-20 19:00:22 -05:00
import { logger , defaultLogLevel } from './logger.js' ;
2022-04-27 19:58:18 -05:00
import { execa , execaCommand } from 'execa' ;
2022-05-11 11:38:42 -05:00
import { loadWithRocketGradient , rocketAscii } from './gradient.js' ;
2021-11-17 13:30:12 -05:00
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
2021-11-17 13:32:36 -05:00
// to no longer require `--` to pass args and instead pass `--` directly to us. This
// broke our arg parser, since `--` is a special kind of flag. Filtering for `--` here
2021-11-17 13:30:12 -05:00
// fixes the issue so that create-astro now works on all npm version.
2021-11-17 13:32:36 -05:00
const cleanArgv = process . argv . filter ( ( arg ) = > arg !== '--' ) ;
2022-03-18 17:35:45 -05:00
const args = yargs ( cleanArgv ) ;
2021-06-08 10:10:56 -05:00
prompts . override ( args ) ;
export function mkdirp ( dir : string ) {
2021-12-22 16:11:05 -05:00
try {
fs . mkdirSync ( dir , { recursive : true } ) ;
} catch ( e : any ) {
if ( e . code === 'EEXIST' ) return ;
throw e ;
}
2021-06-08 10:10:56 -05:00
}
2022-04-21 15:36:48 -05:00
function isEmpty ( dirPath : string ) {
return ! fs . existsSync ( dirPath ) || fs . readdirSync ( dirPath ) . length === 0 ;
}
2022-04-02 15:15:41 -05:00
const { version } = JSON . parse (
fs . readFileSync ( new URL ( '../package.json' , import . meta . url ) , 'utf-8' )
) ;
2021-06-08 10:10:56 -05:00
2022-04-27 19:58:18 -05:00
const FILES_TO_REMOVE = [ '.stackblitzrc' , 'sandbox.config.json' , 'CHANGELOG.md' ] ; // some files are only needed for online editors when using astro.new. Remove for create-astro installs.
2021-06-10 11:30:48 -05:00
2021-06-08 10:10:56 -05:00
export async function main() {
2022-04-26 10:24:24 -05:00
const pkgManager = pkgManagerFromUserAgent ( process . env . npm_config_user_agent ) ;
2022-01-20 19:00:22 -05:00
logger . debug ( 'Verbose logging turned on' ) ;
2021-12-22 16:11:05 -05:00
console . log ( ` \ n ${ bold ( 'Welcome to Astro!' ) } ${ gray ( ` (create-astro v ${ version } ) ` ) } ` ) ;
2022-04-21 15:36:48 -05:00
let cwd = args [ '_' ] [ 2 ] as string ;
if ( cwd && isEmpty ( cwd ) ) {
let acknowledgeProjectDir = ora ( {
color : 'green' ,
text : ` Using ${ bold ( cwd ) } as project directory. ` ,
} ) ;
acknowledgeProjectDir . succeed ( ) ;
}
if ( ! cwd || ! isEmpty ( cwd ) ) {
2022-04-26 19:38:31 -05:00
const notEmptyMsg = ( dirPath : string ) = > ` " ${ bold ( dirPath ) } " is not empty! ` ;
2022-04-21 15:36:48 -05:00
if ( ! isEmpty ( cwd ) ) {
let rejectProjectDir = ora ( { color : 'red' , text : notEmptyMsg ( cwd ) } ) ;
rejectProjectDir . fail ( ) ;
2021-12-22 16:11:05 -05:00
}
2022-04-21 15:36:48 -05:00
const dirResponse = await prompts ( {
type : 'text' ,
name : 'directory' ,
message : 'Where would you like to create your app?' ,
initial : './my-astro-site' ,
validate ( value ) {
if ( ! isEmpty ( value ) ) {
return notEmptyMsg ( value ) ;
}
return true ;
} ,
} ) ;
cwd = dirResponse . directory ;
}
if ( ! cwd ) {
process . exit ( 1 ) ;
2021-12-22 16:11:05 -05:00
}
2022-03-21 12:33:31 -05:00
const options = await prompts ( [
2021-12-22 16:11:05 -05:00
{
type : 'select' ,
name : 'template' ,
message : 'Which app template would you like to use?' ,
choices : TEMPLATES ,
} ,
] ) ;
if ( ! options . template ) {
process . exit ( 1 ) ;
}
2022-05-11 11:38:42 -05:00
const templateSpinner = await loadWithRocketGradient ( 'Copying project files...' ) ;
2021-12-22 16:11:05 -05:00
const hash = args . commit ? ` # ${ args . commit } ` : '' ;
2022-04-27 19:58:18 -05:00
const templateTarget = ` withastro/astro/examples/ ${ options . template } #latest ` ;
2021-12-22 16:11:05 -05:00
const emitter = degit ( ` ${ templateTarget } ${ hash } ` , {
cache : false ,
force : true ,
2022-01-20 19:00:22 -05:00
verbose : defaultLogLevel === 'debug' ? true : false ,
} ) ;
logger . debug ( 'Initialized degit with following config:' , ` ${ templateTarget } ${ hash } ` , {
cache : false ,
force : true ,
verbose : defaultLogLevel === 'debug' ? true : false ,
2021-12-22 16:11:05 -05:00
} ) ;
// Copy
2022-04-26 10:24:24 -05:00
if ( ! args . dryrun ) {
try {
emitter . on ( 'info' , ( info ) = > {
logger . debug ( info . message ) ;
} ) ;
await emitter . clone ( cwd ) ;
} catch ( err : any ) {
// degit is compiled, so the stacktrace is pretty noisy. Only report the stacktrace when using verbose mode.
logger . debug ( err ) ;
console . error ( red ( err . message ) ) ;
// Warning for issue #655
if ( err . message === 'zlib: unexpected end of file' ) {
console . log (
yellow (
"This seems to be a cache related problem. Remove the folder '~/.degit/github/withastro' to fix this error."
)
) ;
console . log (
yellow (
'For more information check out this issue: https://github.com/withastro/astro/issues/655'
)
) ;
}
2021-12-22 16:11:05 -05:00
2022-04-26 10:24:24 -05:00
// Helpful message when encountering the "could not find commit hash for ..." error
if ( err . code === 'MISSING_REF' ) {
console . log (
yellow (
"This seems to be an issue with degit. Please check if you have 'git' installed on your system, and install it if you don't have (https://git-scm.com)."
)
) ;
console . log (
yellow (
"If you do have 'git' installed, please run this command with the --verbose flag and file a new issue with the command output here: https://github.com/withastro/astro/issues"
)
) ;
}
2022-05-11 11:38:42 -05:00
templateSpinner . fail ( ) ;
2022-04-26 10:24:24 -05:00
process . exit ( 1 ) ;
2021-12-22 16:11:05 -05:00
}
2022-04-26 10:24:24 -05:00
// Post-process in parallel
2022-04-27 19:58:18 -05:00
await Promise . all (
FILES_TO_REMOVE . map ( async ( file ) = > {
2022-04-26 10:24:24 -05:00
const fileLoc = path . resolve ( path . join ( cwd , file ) ) ;
2022-04-27 19:58:18 -05:00
if ( fs . existsSync ( fileLoc ) ) {
return fs . promises . rm ( fileLoc , { } ) ;
2021-12-22 16:11:05 -05:00
}
2022-04-27 19:58:18 -05:00
} )
) ;
2021-12-22 16:11:05 -05:00
}
2022-05-11 11:38:42 -05:00
templateSpinner . text = green ( 'Template copied!' ) ;
templateSpinner . succeed ( ) ;
2021-12-22 16:11:05 -05:00
2022-04-26 10:24:24 -05:00
const installResponse = await prompts ( {
type : 'confirm' ,
name : 'install' ,
message : ` Would you like us to run " ${ pkgManager } install?" ` ,
initial : true ,
} ) ;
if ( ! installResponse ) {
process . exit ( 0 ) ;
}
2022-04-29 10:45:43 -05:00
if ( installResponse . install && ! args . dryrun ) {
2022-04-26 10:24:24 -05:00
const installExec = execa ( pkgManager , [ 'install' ] , { cwd } ) ;
const installingPackagesMsg = ` Installing packages ${ emojiWithFallback ( ' 📦' , '...' ) } ` ;
2022-05-11 11:38:42 -05:00
const installSpinner = await loadWithRocketGradient ( installingPackagesMsg ) ;
2022-04-29 10:45:43 -05:00
await new Promise < void > ( ( resolve , reject ) = > {
installExec . stdout ? . on ( 'data' , function ( data ) {
2022-05-11 11:38:42 -05:00
installSpinner . text = ` ${ rocketAscii } ${ installingPackagesMsg } \ n ${ bold (
` [ ${ pkgManager } ] `
) } $ { data } ` ;
2022-04-26 10:24:24 -05:00
} ) ;
2022-04-29 10:45:43 -05:00
installExec . on ( 'error' , ( error ) = > reject ( error ) ) ;
installExec . on ( 'close' , ( ) = > resolve ( ) ) ;
} ) ;
2022-05-11 11:38:42 -05:00
installSpinner . text = green ( 'Packages installed!' ) ;
installSpinner . succeed ( ) ;
2022-04-26 10:24:24 -05:00
}
2022-04-27 19:58:18 -05:00
const astroAddCommand = installResponse . install
? 'astro add --yes'
: ` ${ pkgManagerExecCommand ( pkgManager ) } astro@latest add --yes ` ;
const astroAddResponse = await prompts ( {
type : 'confirm' ,
name : 'astroAdd' ,
message : ` Run " ${ astroAddCommand } ?" This lets you optionally add component frameworks (ex. React), CSS frameworks (ex. Tailwind), and more. ` ,
initial : true ,
} ) ;
if ( ! astroAddResponse ) {
process . exit ( 0 ) ;
}
if ( ! astroAddResponse . astroAdd ) {
ora ( ) . info (
` No problem. You can always run " ${ pkgManagerExecCommand ( pkgManager ) } astro add" later! `
) ;
}
if ( astroAddResponse . astroAdd && ! args . dryrun ) {
await execaCommand (
astroAddCommand ,
astroAddCommand === 'astro add --yes'
? { cwd , stdio : 'inherit' , localDir : cwd , preferLocal : true }
: { cwd , stdio : 'inherit' }
) ;
}
2022-04-29 10:45:43 -05:00
const gitResponse = await prompts ( {
type : 'confirm' ,
name : 'git' ,
message : 'Initialize a git repository?' ,
initial : true ,
} ) ;
if ( ! gitResponse ) {
process . exit ( 0 ) ;
}
if ( gitResponse . git && ! args . dryrun ) {
await execaCommand ( 'git init' , { cwd } ) ;
}
2022-05-11 11:38:42 -05:00
ora ( { text : green ( 'Done. Ready for liftoff!' ) } ) . succeed ( ) ;
2022-04-29 10:45:43 -05:00
console . log ( ` \ n ${ bgCyan ( black ( ' Next steps ' ) ) } \ n ` ) ;
2022-05-11 11:38:42 -05:00
const projectDir = path . relative ( process . cwd ( ) , cwd ) ;
const devCmd = pkgManager === 'npm' ? 'npm run dev' : ` ${ pkgManager } dev ` ;
console . log (
` You can now ${ bold ( cyan ( 'cd' ) ) } into the ${ bold ( cyan ( projectDir ) ) } project directory. `
) ;
console . log (
` Run ${ bold ( cyan ( devCmd ) ) } to start the Astro dev server. ${ bold ( cyan ( 'CTRL-C' ) ) } to close. `
) ;
2022-04-26 10:24:24 -05:00
if ( ! installResponse . install ) {
2022-05-11 11:38:42 -05:00
console . log ( yellow ( ` Remember to install dependencies first! ` ) ) ;
2022-04-26 10:24:24 -05:00
}
2022-05-11 11:38:42 -05:00
console . log ( ` \ nStuck? Come join us at ${ bold ( cyan ( 'https://astro.build/chat' ) ) } ` ) ;
2021-06-08 10:12:07 -05:00
}
2022-04-26 10:24:24 -05:00
function emojiWithFallback ( char : string , fallback : string ) {
return process . platform !== 'win32' ? char : fallback ;
}
function pkgManagerFromUserAgent ( userAgent? : string ) {
if ( ! userAgent ) return 'npm' ;
const pkgSpec = userAgent . split ( ' ' ) [ 0 ] ;
const pkgSpecArr = pkgSpec . split ( '/' ) ;
return pkgSpecArr [ 0 ] ;
}
2022-04-27 19:58:18 -05:00
function pkgManagerExecCommand ( pkgManager : string ) {
if ( pkgManager === 'pnpm' ) {
return 'pnpx' ;
} else {
// note: yarn does not have an "npx" equivalent
return 'npx' ;
}
}