2021-06-08 10:10:56 -05:00
import fs from 'fs' ;
import path from 'path' ;
2021-07-27 15:03:53 -05:00
import { bold , cyan , gray , green , red , yellow } from 'kleur/colors' ;
2021-07-07 14:52:44 -05:00
import fetch from 'node-fetch' ;
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' ;
import { FRAMEWORKS , COUNTER_COMPONENTS , Integration } from './frameworks.js' ;
2021-06-08 11:56:37 -05:00
import { TEMPLATES } from './templates.js' ;
2021-07-07 14:52:44 -05:00
import { createConfig } from './config.js' ;
2022-01-20 19:00:22 -05:00
import { logger , defaultLogLevel } from './logger.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-06-08 10:12:07 -05:00
try {
fs . mkdirSync ( dir , { recursive : true } ) ;
2021-11-17 19:22:43 -05:00
} catch ( e : any ) {
2021-06-08 10:12:07 -05:00
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 ;
}
2021-06-08 10:10:56 -05:00
const { version } = JSON . parse (
fs . readFileSync ( new URL ( '../package.json' , import . meta . url ) , 'utf-8' )
) ;
2022-04-22 14:01:16 -05:00
const FILES_TO_REMOVE = [ '.stackblitzrc' , 'sandbox.config.json' ] ; // some files are only needed for online editors when using astro.new. Remove for create-astro installs.
2021-07-07 14:52:44 -05:00
const POSTPROCESS_FILES = [ 'package.json' , 'astro.config.mjs' , 'CHANGELOG.md' ] ; // some files need processing after copying.
2021-06-10 11:30:48 -05:00
2021-06-08 10:10:56 -05:00
export async function main() {
2022-01-20 19:00:22 -05:00
logger . debug ( 'Verbose logging turned on' ) ;
2021-08-30 16:20:41 -05:00
console . log ( ` \ n ${ bold ( 'Welcome to Astro!' ) } ${ gray ( ` (create-astro v ${ version } ) ` ) } ` ) ;
2021-11-23 17:47:05 -05:00
console . log (
` If you encounter a problem, visit ${ cyan (
'https://github.com/withastro/astro/issues'
) } to search or file a new issue . \ n `
) ;
2021-12-22 16:11:05 -05:00
2022-03-21 12:33:31 -05:00
let spinner = ora ( { color : 'green' , text : 'Prepare for liftoff.' } ) ;
spinner . succeed ( ) ;
2021-12-22 16:11:05 -05:00
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 ) ) {
const notEmptyMsg = ( dirPath : string ) = >
` " ${ bold ( dirPath ) } " is not empty. Please clear contents or choose a different path. ` ;
if ( ! isEmpty ( cwd ) ) {
let rejectProjectDir = ora ( { color : 'red' , text : notEmptyMsg ( cwd ) } ) ;
rejectProjectDir . fail ( ) ;
2021-06-08 10:12:07 -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-06-08 10:12:07 -05:00
}
2021-12-22 16:11:05 -05:00
2022-03-21 12:33:31 -05:00
const options = await prompts ( [
2021-06-08 10:12:07 -05:00
{
type : 'select' ,
name : 'template' ,
message : 'Which app template would you like to use?' ,
choices : TEMPLATES ,
} ,
] ) ;
2021-12-22 16:11:05 -05:00
2021-07-07 14:52:44 -05:00
if ( ! options . template ) {
process . exit ( 1 ) ;
}
2021-12-22 16:11:05 -05:00
2021-06-10 11:30:48 -05:00
const hash = args . commit ? ` # ${ args . commit } ` : '' ;
2021-12-22 16:11:05 -05:00
2021-11-23 17:47:05 -05:00
const templateTarget = options . template . includes ( '/' )
? options . template
: ` withastro/astro/examples/ ${ options . template } #latest ` ;
2021-12-22 16:11:05 -05:00
2021-07-06 14:14:22 -05:00
const emitter = degit ( ` ${ templateTarget } ${ hash } ` , {
2021-06-08 10:12:07 -05:00
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-06-08 10:12:07 -05:00
} ) ;
2021-12-22 16:11:05 -05:00
2021-07-07 15:10:09 -05:00
const selectedTemplate = TEMPLATES . find ( ( template ) = > template . value === options . template ) ;
2022-03-21 12:33:31 -05:00
let integrations : Integration [ ] = [ ] ;
2021-12-22 16:11:05 -05:00
2022-03-18 17:35:45 -05:00
if ( selectedTemplate ? . integrations === true ) {
2022-03-21 12:33:31 -05:00
const result = await prompts ( [
2021-07-07 14:52:44 -05:00
{
type : 'multiselect' ,
2022-03-18 17:35:45 -05:00
name : 'integrations' ,
2021-07-07 14:52:44 -05:00
message : 'Which frameworks would you like to use?' ,
choices : FRAMEWORKS ,
} ,
] ) ;
2022-03-18 17:35:45 -05:00
integrations = result . integrations ;
2021-07-07 14:52:44 -05:00
}
2021-12-22 16:11:05 -05:00
2022-03-21 12:33:31 -05:00
spinner = ora ( { color : 'green' , text : 'Copying project files...' } ) . start ( ) ;
2021-06-10 11:30:48 -05:00
// Copy
2021-06-08 10:12:07 -05:00
try {
2022-01-20 19:00:22 -05:00
emitter . on ( 'info' , ( info ) = > {
logger . debug ( info . message ) ;
} ) ;
2021-06-08 10:12:07 -05:00
await emitter . clone ( cwd ) ;
2021-11-17 19:22:43 -05:00
} catch ( err : any ) {
2022-01-20 19:00:22 -05:00
// degit is compiled, so the stacktrace is pretty noisy. Only report the stacktrace when using verbose mode.
logger . debug ( err ) ;
2021-06-08 10:12:07 -05:00
console . error ( red ( err . message ) ) ;
2021-12-22 16:11:05 -05:00
2021-07-27 15:03:53 -05:00
// Warning for issue #655
if ( err . message === 'zlib: unexpected end of file' ) {
2022-01-19 15:19:04 -05:00
console . log (
yellow (
"This seems to be a cache related problem. Remove the folder '~/.degit/github/withastro' to fix this error."
2022-04-02 15:15:41 -05:00
)
2022-01-19 15:19:04 -05:00
) ;
2021-11-23 17:47:05 -05:00
console . log (
yellow (
'For more information check out this issue: https://github.com/withastro/astro/issues/655'
2022-04-02 15:15:41 -05:00
)
2021-11-23 17:47:05 -05:00
) ;
2021-07-27 15:03:53 -05:00
}
2021-12-22 16:11:05 -05:00
2021-11-23 09:11:56 -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)."
2022-04-02 15:15:41 -05:00
)
2021-11-23 09:11:56 -05:00
) ;
2022-01-20 19:00:22 -05:00
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"
)
) ;
2021-11-23 09:11:56 -05:00
}
2022-03-21 12:33:31 -05:00
spinner . fail ( ) ;
2021-06-08 10:12:07 -05:00
process . exit ( 1 ) ;
}
2021-12-22 16:11:05 -05:00
2021-06-10 11:30:48 -05:00
// Post-process in parallel
2022-04-22 14:01:16 -05:00
await Promise . all ( [
. . . FILES_TO_REMOVE . map ( async ( file ) = > {
const fileLoc = path . resolve ( path . join ( cwd , file ) ) ;
return fs . promises . rm ( fileLoc ) ;
} ) ,
. . . POSTPROCESS_FILES . map ( async ( file ) = > {
2021-07-07 15:10:09 -05:00
const fileLoc = path . resolve ( path . join ( cwd , file ) ) ;
2021-12-22 16:11:05 -05:00
2021-07-07 15:10:09 -05:00
switch ( file ) {
case 'CHANGELOG.md' : {
if ( fs . existsSync ( fileLoc ) ) {
2021-10-01 12:18:40 -05:00
await fs . promises . unlink ( fileLoc ) ;
2021-07-07 15:10:09 -05:00
}
break ;
2021-07-07 14:52:44 -05:00
}
2021-07-07 15:10:09 -05:00
case 'astro.config.mjs' : {
2022-03-18 17:35:45 -05:00
if ( selectedTemplate ? . integrations !== true ) {
2021-07-07 15:10:09 -05:00
break ;
}
2022-03-18 17:35:45 -05:00
await fs . promises . writeFile ( fileLoc , createConfig ( { integrations } ) ) ;
2021-07-07 15:10:09 -05:00
break ;
}
case 'package.json' : {
const packageJSON = JSON . parse ( await fs . promises . readFile ( fileLoc , 'utf8' ) ) ;
delete packageJSON . snowpack ; // delete snowpack config only needed in monorepo (can mess up projects)
2022-03-18 17:35:45 -05:00
// Fetch latest versions of selected integrations
2022-03-21 12:33:31 -05:00
const integrationEntries = (
await Promise . all (
integrations . map ( ( integration ) = >
fetch ( ` https://registry.npmjs.org/ ${ integration . packageName } /latest ` )
. then ( ( res ) = > res . json ( ) )
. then ( ( res : any ) = > {
let dependencies : [ string , string ] [ ] = [ [ res [ 'name' ] , ` ^ ${ res [ 'version' ] } ` ] ] ;
if ( res [ 'peerDependencies' ] ) {
for ( const peer in res [ 'peerDependencies' ] ) {
dependencies . push ( [ peer , res [ 'peerDependencies' ] [ peer ] ] ) ;
}
}
return dependencies ;
} )
)
2021-07-07 15:10:09 -05:00
)
2022-03-21 12:33:31 -05:00
) . flat ( 1 ) ;
// merge and sort dependencies
2022-03-18 17:35:45 -05:00
packageJSON . devDependencies = {
. . . ( packageJSON . devDependencies ? ? { } ) ,
. . . Object . fromEntries ( integrationEntries ) ,
} ;
2022-03-21 12:33:31 -05:00
packageJSON . devDependencies = Object . fromEntries (
Object . entries ( packageJSON . devDependencies ) . sort ( ( a , b ) = > a [ 0 ] . localeCompare ( b [ 0 ] ) )
) ;
2021-07-07 15:10:09 -05:00
await fs . promises . writeFile ( fileLoc , JSON . stringify ( packageJSON , undefined , 2 ) ) ;
2021-06-10 11:30:48 -05:00
break ;
}
2021-07-07 14:52:44 -05:00
}
2022-04-22 14:01:16 -05:00
} ) ,
] ) ;
2021-12-22 16:11:05 -05:00
2021-07-07 14:52:44 -05:00
// Inject framework components into starter template
if ( selectedTemplate ? . value === 'starter' ) {
let importStatements : string [ ] = [ ] ;
let components : string [ ] = [ ] ;
2021-07-07 15:10:09 -05:00
await Promise . all (
2022-03-21 12:33:31 -05:00
integrations . map ( async ( integration ) = > {
const component = COUNTER_COMPONENTS [ integration . id as keyof typeof COUNTER_COMPONENTS ] ;
2021-07-07 15:10:09 -05:00
const componentName = path . basename ( component . filename , path . extname ( component . filename ) ) ;
const absFileLoc = path . resolve ( cwd , component . filename ) ;
importStatements . push (
` import ${ componentName } from ' ${ component . filename . replace ( /^src/ , '..' ) } '; `
) ;
2021-07-08 13:31:05 -05:00
components . push ( ` < ${ componentName } client:visible /> ` ) ;
2021-07-07 15:10:09 -05:00
await fs . promises . writeFile ( absFileLoc , component . content ) ;
} )
) ;
2021-12-22 16:11:05 -05:00
2021-07-07 14:52:44 -05:00
const pageFileLoc = path . resolve ( path . join ( cwd , 'src' , 'pages' , 'index.astro' ) ) ;
const content = ( await fs . promises . readFile ( pageFileLoc ) ) . toString ( ) ;
2021-07-07 16:31:42 -05:00
const newContent = content
. replace ( /^(\s*)\/\* ASTRO\:COMPONENT_IMPORTS \*\//gm , ( _ , indent ) = > {
return indent + importStatements . join ( '\n' ) ;
} )
. replace ( /^(\s*)<!-- ASTRO:COMPONENT_MARKUP -->/gm , ( _ , indent ) = > {
2021-07-07 16:32:45 -05:00
return components . map ( ( ln ) = > indent + ln ) . join ( '\n' ) ;
2021-07-07 16:31:42 -05:00
} ) ;
await fs . promises . writeFile ( pageFileLoc , newContent ) ;
2021-07-07 14:52:44 -05:00
}
2021-12-22 16:11:05 -05:00
2022-03-21 12:33:31 -05:00
spinner . succeed ( ) ;
2021-06-22 09:06:07 -05:00
console . log ( bold ( green ( '✔' ) + ' Done!' ) ) ;
2021-12-22 16:11:05 -05:00
2021-06-08 10:12:07 -05:00
console . log ( '\nNext steps:' ) ;
let i = 1 ;
const relative = path . relative ( process . cwd ( ) , cwd ) ;
if ( relative !== '' ) {
console . log ( ` ${ i ++ } : ${ bold ( cyan ( ` cd ${ relative } ` ) ) } ` ) ;
}
2021-12-22 16:11:05 -05:00
2021-06-08 10:12:07 -05:00
console . log ( ` ${ i ++ } : ${ bold ( cyan ( 'npm install' ) ) } (or pnpm install, yarn, etc) ` ) ;
console . log (
` ${ i ++ } : ${ bold (
cyan ( 'git init && git add -A && git commit -m "Initial commit"' )
) } ( optional step ) `
) ;
2021-08-19 00:07:08 -05:00
console . log ( ` ${ i ++ } : ${ bold ( cyan ( 'npm run dev' ) ) } (or pnpm, yarn, etc) ` ) ;
2021-12-22 16:11:05 -05:00
2021-06-08 10:12:07 -05:00
console . log ( ` \ nTo close the dev server, hit ${ bold ( cyan ( 'Ctrl-C' ) ) } ` ) ;
2021-08-30 16:20:41 -05:00
console . log ( ` \ nStuck? Visit us at ${ cyan ( 'https://astro.build/chat' ) } \ n ` ) ;
2021-06-08 10:12:07 -05:00
}