diff options
Diffstat (limited to 'packages/create-astro/src')
35 files changed, 1192 insertions, 0 deletions
diff --git a/packages/create-astro/src/components/App.tsx b/packages/create-astro/src/components/App.tsx new file mode 100644 index 000000000..fd9192bb6 --- /dev/null +++ b/packages/create-astro/src/components/App.tsx @@ -0,0 +1,93 @@ +import React, {FC, useEffect} from 'react'; +import { prepareTemplate, isEmpty, emptyDir } from '../utils'; +import Header from './Header'; +import Install from './Install'; +import ProjectName from './ProjectName'; +import Template from './Template'; +import Confirm from './Confirm'; +import Finalize from './Finalize'; + +interface Context { + use: 'npm'|'yarn'; + run: boolean; + projectExists?: boolean; + force?: boolean; + projectName?: string; + template?: string; + templates: string[]; + ready?: boolean; +} + +const getStep = ({ projectName, projectExists: exists, template, force, ready }: Context) => { + switch (true) { + case !projectName: return { + key: 'projectName', + Component: ProjectName + }; + case projectName && exists === true && typeof force === 'undefined': return { + key: 'force', + Component: Confirm + } + case (exists === false || force) && !template: return { + key: 'template', + Component: Template + }; + case !ready: return { + key: 'install', + Component: Install + }; + default: return { + key: 'final', + Component: Finalize + } + } +} + +const App: FC<{ context: Context }> = ({ context }) => { + const [state, setState] = React.useState(context); + const step = React.useRef(getStep(context)); + const onSubmit = (value: string|boolean) => { + const { key } = step.current; + const newState = { ...state, [key]: value }; + step.current = getStep(newState) + setState(newState) + } + + useEffect(() => { + let isSubscribed = true + if (state.projectName && typeof state.projectExists === 'undefined') { + const newState = { ...state, projectExists: !isEmpty(state.projectName) }; + step.current = getStep(newState) + if (isSubscribed) { + setState(newState); + } + } + + if (state.projectName && (state.projectExists === false || state.force) && state.template) { + if (state.force) emptyDir(state.projectName); + prepareTemplate(context.use, state.template, state.projectName).then(() => { + if (isSubscribed) { + setState(v => { + const newState = {...v, ready: true }; + step.current = getStep(newState); + return newState; + }); + } + }); + } + + return () => { + isSubscribed = false; + } + }, [state]); + const { Component } = step.current; + + return ( + <> + <Header context={state}/> + <Component context={state} onSubmit={onSubmit} /> + </> + ) +}; + +export default App; diff --git a/packages/create-astro/src/components/Confirm.tsx b/packages/create-astro/src/components/Confirm.tsx new file mode 100644 index 000000000..1fd1aa862 --- /dev/null +++ b/packages/create-astro/src/components/Confirm.tsx @@ -0,0 +1,49 @@ +import React, { FC } from 'react'; +import { Box, Text, useInput, useApp } from 'ink'; +import Spacer from './Spacer'; +import Select from './Select'; + +const Confirm: FC<{ message?: any; context: any; onSubmit: (value: boolean) => void }> = ({ message, context: { projectName }, onSubmit }) => { + const { exit } = useApp(); + const handleSubmit = (v: boolean) => { + if (!v) return exit(); + onSubmit(v); + }; + + return ( + <> + <Box display="flex"> + {!message ? ( + <> + <Text color="#FFBE2D">{'[uh-oh]'}</Text> + <Text> + {' '} + It appears <Text color="#17C083">./{projectName}</Text> is not empty. Overwrite? + </Text> + </> + ) : ( + message + )} + </Box> + <Box display="flex"> + <Spacer width={6} /> + <Select + items={[ + { + value: false, + label: 'no' + }, + { + value: true, + label: 'yes', + description: <Text color="#FF1639">overwrite</Text>, + }, + ]} + onSelect={handleSubmit} + /> + </Box> + </> + ); +}; + +export default Confirm; diff --git a/packages/create-astro/src/components/Emoji.tsx b/packages/create-astro/src/components/Emoji.tsx new file mode 100644 index 000000000..3af9ae508 --- /dev/null +++ b/packages/create-astro/src/components/Emoji.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import { Text } from 'ink'; +import { isWin } from '../utils'; + +export default ({ children }) => isWin() ? null : <Text>{children}</Text> diff --git a/packages/create-astro/src/components/Exit.tsx b/packages/create-astro/src/components/Exit.tsx new file mode 100644 index 000000000..cc3096705 --- /dev/null +++ b/packages/create-astro/src/components/Exit.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import { Box, Text } from 'ink'; +import { isDone } from '../utils'; + +const Exit: FC<{ didError?: boolean }> = ({ didError }) => isDone ? null : <Box marginTop={1} display="flex"> + <Text color={didError ? "#FF1639" : "#FFBE2D"}>[abort]</Text> + <Text> astro cancelled</Text> +</Box> +export default Exit; diff --git a/packages/create-astro/src/components/Finalize.tsx b/packages/create-astro/src/components/Finalize.tsx new file mode 100644 index 000000000..8d2a2103a --- /dev/null +++ b/packages/create-astro/src/components/Finalize.tsx @@ -0,0 +1,27 @@ +import React, { FC, useEffect } from 'react'; +import { Box, Text } from 'ink'; +import { cancelProcessListeners } from '../utils'; + +const Finalize: FC<{ context: any }> = ({ context: { use, projectName } }) => { + useEffect(() => { + cancelProcessListeners(); + process.exit(0); + }, []); + + return <> + <Box display="flex"> + <Text color="#17C083">{'[ yes ]'}</Text> + <Text> Project initialized at <Text color="#3894FF">./{projectName}</Text></Text> + </Box> + <Box display="flex" marginY={1}> + <Text dimColor>{'[ tip ]'}</Text> + <Box display="flex" marginLeft={1} flexDirection="column"> + <Text>Get started by running</Text> + <Text color="#3894FF">cd ./{projectName}</Text> + <Text color="#3894FF">{use} start</Text> + </Box> + </Box> + </>; +}; + +export default Finalize; diff --git a/packages/create-astro/src/components/Header.tsx b/packages/create-astro/src/components/Header.tsx new file mode 100644 index 000000000..1d894a60e --- /dev/null +++ b/packages/create-astro/src/components/Header.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Box, Text } from 'ink'; + +const getMessage = ({ projectName, template }) => { + switch (true) { + case !projectName: return <Text dimColor>Gathering mission details</Text>; + case !template: return <Text dimColor>Optimizing navigational system</Text>; + default: return <Text color="black" backgroundColor="white"> {projectName} </Text> + } +} + +const Header: React.FC<{ context: any }> = ({ context }) => ( + <Box width={48} display="flex" marginY={1}> + <Text backgroundColor="#882DE7" color="white">{' astro '}</Text> + <Box marginLeft={1}> + {getMessage(context)} + </Box> + </Box> +) +export default Header; diff --git a/packages/create-astro/src/components/Help.tsx b/packages/create-astro/src/components/Help.tsx new file mode 100644 index 000000000..88fcf5633 --- /dev/null +++ b/packages/create-astro/src/components/Help.tsx @@ -0,0 +1,62 @@ +import React, { FC } from 'react'; +import { Box, Text } from 'ink'; +import { ARGS, ARG } from '../config'; + +const Type: FC<{ type: any, enum?: string[] }> = ({ type, enum: e }) => { + if (type === Boolean) { + return <> + <Text color="#3894FF">true</Text> + <Text dimColor>|</Text> + <Text color="#3894FF">false</Text> + </> + } + if (e?.length > 0) { + return <> + {e.map((item, i, { length: len}) => { + if (i !== len - 1) { + return <Box key={item}> + <Text color="#17C083">{item}</Text> + <Text dimColor>|</Text> + </Box> + } + + return <Text color="#17C083" key={item}>{item}</Text> + })} + </> + } + + return <Text color="#3894FF">string</Text>; +} + +const Command: FC<{ name: string, info: ARG }> = ({ name, info: { alias, description, type, enum: e } }) => { + return ( + <Box display="flex" alignItems="flex-start"> + <Box width={24} display="flex" flexGrow={0}> + <Text color="whiteBright">--{name}</Text>{alias && <Text dimColor> -{alias}</Text>} + </Box> + <Box width={24}> + <Type type={type} enum={e} /> + </Box> + <Box> + <Text>{description}</Text> + </Box> + </Box> + ); +} + +const Help: FC<{ context: any }> = ({ context: { templates }}) => { + return ( + <> + <Box width={48} display="flex" marginY={1}> + <Text backgroundColor="#882DE7" color="white">{' astro '}</Text> + <Box marginLeft={1}> + <Text color="black" backgroundColor="white"> help </Text> + </Box> + </Box> + <Box marginBottom={1} marginLeft={2} display="flex" flexDirection="column"> + {Object.entries(ARGS).map(([name, info]) => <Command key={name} name={name} info={name === 'template' ? { ...info, enum: templates.map(({ value }) => value) } : info} /> )} + </Box> + </> + ) +}; +export default Help; diff --git a/packages/create-astro/src/components/Install.tsx b/packages/create-astro/src/components/Install.tsx new file mode 100644 index 000000000..d9d2bc9b6 --- /dev/null +++ b/packages/create-astro/src/components/Install.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react'; +import { Box, Text } from 'ink'; +import Spacer from './Spacer'; +import Spinner from './Spinner'; + +const Install: FC<{ context: any }> = ({ context: { use } }) => { + return <> + <Box display="flex"> + <Spinner/> + <Text> Initiating launch sequence...</Text> + </Box> + <Box> + <Spacer /> + <Text color="white" dimColor>(aka running <Text color="#17C083">{use === 'npm' ? 'npm install' : 'yarn'}</Text>)</Text> + </Box> + </>; +}; + +export default Install; diff --git a/packages/create-astro/src/components/ProjectName.tsx b/packages/create-astro/src/components/ProjectName.tsx new file mode 100644 index 000000000..87b976494 --- /dev/null +++ b/packages/create-astro/src/components/ProjectName.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { Box, Text } from 'ink'; +import Spacer from './Spacer'; +import TextInput from 'ink-text-input'; +// @ts-expect-error +const { default: Input } = TextInput; + +const ProjectName: FC<{ onSubmit: (value: string) => void }> = ({ onSubmit }) => { + const [value, setValue] = React.useState(''); + const handleSubmit = (v: string) => onSubmit(v); + + return <> + <Box display="flex"> + <Text color="#17C083">{'[query]'}</Text> + <Text> What is your project name?</Text> + </Box> + <Box display="flex"> + <Spacer /> + <Input value={value} onChange={setValue} onSubmit={handleSubmit} placeholder="my-project" /> + </Box> + </>; +}; + +export default ProjectName; diff --git a/packages/create-astro/src/components/Select.tsx b/packages/create-astro/src/components/Select.tsx new file mode 100644 index 000000000..acf8eb29f --- /dev/null +++ b/packages/create-astro/src/components/Select.tsx @@ -0,0 +1,32 @@ +import SelectInput from 'ink-select-input'; +import React, { FC } from 'react'; +import { Text, Box } from 'ink'; +// @ts-expect-error +const { default: Select } = SelectInput; + +interface Props { + isSelected?: boolean; + label: string; + description?: string; +} +const Indicator: FC<Props> = ({ isSelected }) => isSelected ? <Text color="#3894FF">[ </Text> : <Text> </Text> +const Item: FC<Props> = ({isSelected = false, label, description }) => ( + <Box display="flex"> + <Text color={isSelected ? '#3894FF' : 'white'} dimColor={!isSelected}>{label}</Text> + {isSelected && description && typeof description === 'string' && <Text> {description}</Text>} + {isSelected && description && typeof description !== 'string' && <Box marginLeft={1}>{description}</Box>} + </Box> +); + +interface SelectProps { + items: { value: string|number|boolean, label: string, description?: any }[] + onSelect(value: string|number|boolean): void; +} +const CustomSelect: FC<SelectProps> = ({ items, onSelect }) => { + const handleSelect = ({ value }) => onSelect(value); + return ( + <Select indicatorComponent={Indicator} itemComponent={Item} items={items} onSelect={handleSelect} /> + ) +} + +export default CustomSelect; diff --git a/packages/create-astro/src/components/Spacer.tsx b/packages/create-astro/src/components/Spacer.tsx new file mode 100644 index 000000000..1e4e14561 --- /dev/null +++ b/packages/create-astro/src/components/Spacer.tsx @@ -0,0 +1,5 @@ +import React, { FC } from 'react'; +import { Box } from 'ink'; + +const Spacer: FC<{ width?: number }> = ({ width = 8 }) => <Box width={width} /> +export default Spacer; diff --git a/packages/create-astro/src/components/Spinner.tsx b/packages/create-astro/src/components/Spinner.tsx new file mode 100644 index 000000000..2d3335e1c --- /dev/null +++ b/packages/create-astro/src/components/Spinner.tsx @@ -0,0 +1,200 @@ +import React, { FC, useEffect, useState } from 'react'; +import { Box, Text } from 'ink'; + +const Spinner: FC<{ type?: keyof typeof spinners }> = ({ type = 'countdown' }) => { + const { interval, frames } = spinners[type]; + const [i, setI] = useState(0); + useEffect(() => { + const _ = setInterval(() => { + setI(v => (v < frames.length - 1) ? v + 1 : 0) + }, interval) + + return () => clearInterval(_); + }, []) + + return frames[i] +} + +const spinners = { + countdown: { + interval: 80, + frames: [ + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + <Text backgroundColor="#17C083">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + <Text backgroundColor="#23B1AF">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + <Text backgroundColor="#2CA5D2">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + <Text backgroundColor="#3894FF">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + <Text backgroundColor="#5076F9">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + <Text backgroundColor="#6858F1">{' '}</Text> + </Box>, + <Box display="flex"> + <Text backgroundColor="#882DE7">{' '}</Text> + </Box>, + ] + } +} + +export default Spinner; diff --git a/packages/create-astro/src/components/Template.tsx b/packages/create-astro/src/components/Template.tsx new file mode 100644 index 000000000..7fbab035d --- /dev/null +++ b/packages/create-astro/src/components/Template.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import { Box, Text } from 'ink'; +import Spacer from './Spacer'; +import Select from './Select'; + +const Template: FC<{ context: any, onSubmit: (value: string) => void }> = ({ context: { templates }, onSubmit }) => { + const items = templates.map(({ title: label, ...rest }) => ({ ...rest, label })); + + return ( + <> + <Box display="flex"> + <Text color="#17C083">{'[query]'}</Text> + <Text> Which template should be used?</Text> + </Box> + <Box display="flex"> + <Spacer width={6} /> + <Select items={items} onSelect={onSubmit} /> + </Box> + </> + ); +}; + +export default Template; diff --git a/packages/create-astro/src/components/Version.tsx b/packages/create-astro/src/components/Version.tsx new file mode 100644 index 000000000..340952dc0 --- /dev/null +++ b/packages/create-astro/src/components/Version.tsx @@ -0,0 +1,6 @@ +import React, { FC } from 'react'; +import { Text } from 'ink'; +import pkg from '../../package.json'; + +const Version: FC = () => <Text color="#17C083">v{pkg.version}</Text>; +export default Version; diff --git a/packages/create-astro/src/config.ts b/packages/create-astro/src/config.ts new file mode 100644 index 000000000..3d3c07912 --- /dev/null +++ b/packages/create-astro/src/config.ts @@ -0,0 +1,49 @@ +import type * as arg from 'arg'; + +export interface ARG { + type: any; + description: string; + enum?: string[]; + alias?: string; +} + +export const ARGS: Record<string, ARG> = { + 'template': { + type: String, + description: 'specifies template to use' + }, + 'use': { + type: String, + enum: ['npm', 'yarn'], + description: 'specifies package manager to use' + }, + 'run': { + type: Boolean, + description: 'should dependencies be installed automatically?' + }, + 'force': { + type: Boolean, + alias: 'f', + description: 'should existing files be overwritten?' + }, + 'version': { + type: Boolean, + alias: 'v', + description: 'prints current version' + }, + 'help': { + type: Boolean, + alias: 'h', + description: 'prints this message' + } +} + +export const args = Object.entries(ARGS).reduce((acc, [name, info]) => { + const key = `--${name}`; + const spec = { ...acc, [key]: info.type }; + + if (info.alias) { + spec[`-${info.alias}`] = key; + } + return spec +}, {} as arg.Spec); diff --git a/packages/create-astro/src/index.tsx b/packages/create-astro/src/index.tsx new file mode 100644 index 000000000..0927eaae9 --- /dev/null +++ b/packages/create-astro/src/index.tsx @@ -0,0 +1,46 @@ +import 'source-map-support/register.js'; +import React from 'react'; +import App from './components/App'; +import Version from './components/Version'; +import Exit from './components/Exit'; +import {render} from 'ink'; +import { getTemplates, addProcessListeners } from './utils'; +import { args as argsConfig } from './config'; +import arg from 'arg'; +import Help from './components/Help'; + +/** main `create-astro` CLI */ +export default async function createAstro() { + const args = arg(argsConfig); + const projectName = args._[0]; + if (args['--version']) { + return render(<Version />); + } + const templates = await getTemplates(); + if (args['--help']) { + return render(<Help context={{ templates }} />) + } + + const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm'; + const use = (args['--use'] ?? pkgManager) as 'npm'|'yarn'; + const template = args['--template']; + const force = args['--force']; + const run = args['--run'] ?? true; + + const app = render(<App context={{ projectName, template, templates, force, run, use }} />); + + const onError = () => { + if (app) app.clear(); + render(<Exit didError />); + } + const onExit = () => { + if (app) app.clear(); + render(<Exit />); + } + addProcessListeners([ + ['uncaughtException', onError], + ['exit', onExit], + ['SIGINT', onExit], + ['SIGTERM', onExit], + ]) +} diff --git a/packages/create-astro/src/templates/blank/README.md b/packages/create-astro/src/templates/blank/README.md new file mode 100644 index 000000000..40c5841c9 --- /dev/null +++ b/packages/create-astro/src/templates/blank/README.md @@ -0,0 +1,24 @@ +# Welcome to [Astro](https://astro.build) + +> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun! + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +``` +/ +├── public/ +├── src/ +│ └── pages/ +│ └── index.astro +└── package.json +``` + +Astro looks for `.astro` or `.md.astro` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. + +Any static assets, like images, can be placed in the `public/` directory. + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://discord.gg/EsGdSGen). diff --git a/packages/create-astro/src/templates/blank/_gitignore b/packages/create-astro/src/templates/blank/_gitignore new file mode 100644 index 000000000..d436c6dad --- /dev/null +++ b/packages/create-astro/src/templates/blank/_gitignore @@ -0,0 +1,18 @@ +# build output +dist + +# dependencies +node_modules/ +.snowpack/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store diff --git a/packages/create-astro/src/templates/blank/meta.json b/packages/create-astro/src/templates/blank/meta.json new file mode 100644 index 000000000..ca942e99f --- /dev/null +++ b/packages/create-astro/src/templates/blank/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Blank", + "description": "a bare-bones, ultra-minimal template" +} diff --git a/packages/create-astro/src/templates/blank/package.json b/packages/create-astro/src/templates/blank/package.json new file mode 100644 index 000000000..e04205726 --- /dev/null +++ b/packages/create-astro/src/templates/blank/package.json @@ -0,0 +1,11 @@ +{ + "name": "TODO", + "version": "0.0.1", + "scripts": { + "start": "astro dev", + "build": "astro build" + }, + "devDependencies": { + "astro": "0.0.9" + } +} diff --git a/packages/create-astro/src/templates/blank/public/favicon.svg b/packages/create-astro/src/templates/blank/public/favicon.svg new file mode 100644 index 000000000..542f90aec --- /dev/null +++ b/packages/create-astro/src/templates/blank/public/favicon.svg @@ -0,0 +1,11 @@ +<svg width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style> + #flame { fill: #FF5D01; } + #a { fill: #000014; } + @media (prefers-color-scheme: dark) { + #a { fill: #fff; } + } + </style> + <path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" /> + <path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" /> +</svg> diff --git a/packages/create-astro/src/templates/blank/src/pages/index.astro b/packages/create-astro/src/templates/blank/src/pages/index.astro new file mode 100644 index 000000000..0ad00d28f --- /dev/null +++ b/packages/create-astro/src/templates/blank/src/pages/index.astro @@ -0,0 +1,15 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Astro</title> + + <link rel="icon" type="image/svg+xml" href="/favicon.svg"> +</head> +<body> + <main> + <h1>Welcome to <a href="https://astro.build/">Astro</a></h1> + </main> +</body> +</html> diff --git a/packages/create-astro/src/templates/starter/README.md b/packages/create-astro/src/templates/starter/README.md new file mode 100644 index 000000000..59940918f --- /dev/null +++ b/packages/create-astro/src/templates/starter/README.md @@ -0,0 +1,32 @@ +# Welcome to [Astro](https://astro.build) + +> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun! + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +``` +/ +├── public/ +│ ├── robots.txt +│ └── favicon.ico +├── src/ +│ ├── components/ +│ │ └── Tour.astro +│ └── pages/ +│ └── index.astro +└── package.json +``` + +Astro looks for `.astro` or `.md.astro` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. + + +There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. + + +Any static assets, like images, can be placed in the `public/` directory. + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://discord.gg/EsGdSGen). diff --git a/packages/create-astro/src/templates/starter/_gitignore b/packages/create-astro/src/templates/starter/_gitignore new file mode 100644 index 000000000..d436c6dad --- /dev/null +++ b/packages/create-astro/src/templates/starter/_gitignore @@ -0,0 +1,18 @@ +# build output +dist + +# dependencies +node_modules/ +.snowpack/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store diff --git a/packages/create-astro/src/templates/starter/astro.config.mjs b/packages/create-astro/src/templates/starter/astro.config.mjs new file mode 100644 index 000000000..de58ba1c5 --- /dev/null +++ b/packages/create-astro/src/templates/starter/astro.config.mjs @@ -0,0 +1,16 @@ +export default { + projectRoot: '.', + astroRoot: './src', + dist: './dist', + public: './public', + extensions: { + '.jsx': 'react', + }, + snowpack: { + optimize: { + bundle: false, + minify: true, + target: 'es2018', + }, + }, +}; diff --git a/packages/create-astro/src/templates/starter/meta.json b/packages/create-astro/src/templates/starter/meta.json new file mode 100644 index 000000000..ba775af32 --- /dev/null +++ b/packages/create-astro/src/templates/starter/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Getting Started", + "description": "a friendly starting point for new astronauts", + "rank": 999 +} diff --git a/packages/create-astro/src/templates/starter/package.json b/packages/create-astro/src/templates/starter/package.json new file mode 100644 index 000000000..e04205726 --- /dev/null +++ b/packages/create-astro/src/templates/starter/package.json @@ -0,0 +1,11 @@ +{ + "name": "TODO", + "version": "0.0.1", + "scripts": { + "start": "astro dev", + "build": "astro build" + }, + "devDependencies": { + "astro": "0.0.9" + } +} diff --git a/packages/create-astro/src/templates/starter/public/assets/logo.svg b/packages/create-astro/src/templates/starter/public/assets/logo.svg new file mode 100644 index 000000000..d751556b2 --- /dev/null +++ b/packages/create-astro/src/templates/starter/public/assets/logo.svg @@ -0,0 +1,12 @@ +<svg width="193" height="256" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style> + #flame { fill: #FF5D01; } + #a { fill: #000014; } + @media (prefers-color-scheme: dark) { + #a { fill: #fff; } + } + </style> + + <path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M131.496 18.929c1.943 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.746 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.224 180.224 0 00-52.01 17.557l43.52-142.281c1.989-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.085 1.157a16 16 0 016.488 4.806z" fill="url(#paint0_linear)"/> + <path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M136.678 180.151c-7.14 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.962 10.367-1.962 13.902 0 0-1.055 17.355 11.016 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.973-19.87 5.977-3.79 12.616-8.001 17.192-16.449a31.013 31.013 0 003.744-14.82c0-3.299-.513-6.479-1.463-9.463z" /> +</svg> diff --git a/packages/create-astro/src/templates/starter/public/favicon.svg b/packages/create-astro/src/templates/starter/public/favicon.svg new file mode 100644 index 000000000..542f90aec --- /dev/null +++ b/packages/create-astro/src/templates/starter/public/favicon.svg @@ -0,0 +1,11 @@ +<svg width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style> + #flame { fill: #FF5D01; } + #a { fill: #000014; } + @media (prefers-color-scheme: dark) { + #a { fill: #fff; } + } + </style> + <path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" /> + <path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" /> +</svg> diff --git a/packages/create-astro/src/templates/starter/public/robots.txt b/packages/create-astro/src/templates/starter/public/robots.txt new file mode 100644 index 000000000..1f53798bb --- /dev/null +++ b/packages/create-astro/src/templates/starter/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/packages/create-astro/src/templates/starter/public/style/global.css b/packages/create-astro/src/templates/starter/public/style/global.css new file mode 100644 index 000000000..b54158f92 --- /dev/null +++ b/packages/create-astro/src/templates/starter/public/style/global.css @@ -0,0 +1,28 @@ +* { + box-sizing: border-box; + margin: 0; +} + +:root { + font-family: system-ui; + font-size: 1rem; + --user-font-scale: 1rem - 16px; + font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem); +} + +body { + padding: 4rem 2rem; + width: 100vw; + min-height: 100vh; + display: grid; + justify-content: center; + background: #F9FAFB; + color: #111827; +} + +@media (prefers-color-scheme: dark) { + body { + background: #111827; + color: #fff; + } +} diff --git a/packages/create-astro/src/templates/starter/public/style/home.css b/packages/create-astro/src/templates/starter/public/style/home.css new file mode 100644 index 000000000..c4271a845 --- /dev/null +++ b/packages/create-astro/src/templates/starter/public/style/home.css @@ -0,0 +1,38 @@ +:root { + --font-mono: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + --color-light: #F3F4F6; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-light: #1F2937; + } +} + +a { + color: inherit; +} + +header > div { + font-size: clamp(2rem, -0.4742rem + 6.1856vw, 2.75rem); +} + +header > div { + display: flex; + flex-direction: column; + align-items: center; +} + +header h1 { + font-size: 1em; + font-weight: 500; +} +header img { + width: 2em; + height: 2.667em; +} + +h2 { + font-weight: 500; + font-size: clamp(1.5rem, 1rem + 1.25vw, 2rem); +} diff --git a/packages/create-astro/src/templates/starter/src/components/Tour.astro b/packages/create-astro/src/templates/starter/src/components/Tour.astro new file mode 100644 index 000000000..ca0cfafbf --- /dev/null +++ b/packages/create-astro/src/templates/starter/src/components/Tour.astro @@ -0,0 +1,82 @@ +<article> + <div class="banner"> + <p><strong>🧑🚀 Seasoned astronaut?</strong> Delete this file. Have fun!</p> + </div> + + <section> + <h2>🚀 Project Structure</h2> + <p>Inside of your Astro project, you'll see the following folders and files:</p> + + <pre><code class="tree">/ +├── public/ +│ ├── robots.txt +│ └── favicon.ico +├── src/ +│ ├── components/ +│ │ └── Tour.astro +│ └── pages/ +│ └── index.astro +└── package.json</code> + </pre> + + <p> + Astro looks for <code>.astro</code> or <code>.md.astro</code> files in the <code>src/pages/</code> directory. + Each page is exposed as a route based on its file name. + </p> + + <p> + There's nothing special about <code>src/components/</code>, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. + </p> + + <p>Any static assets, like images, can be placed in the <code>public/</code> directory.</p> + </section> + + <section> + <h2>👀 Want to learn more?</h2> + <p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://discord.gg/EsGdSGen">Discord server</a>.</p> + </section> + +</article> + +<style> + article { + padding-top: 2em; + line-height: 1.5; + } + section { + margin-top: 2em; + display: flex; + flex-direction: column; + gap: 1em; + max-width: 70ch; + } + + .banner { + text-align: center; + font-size: 1.2rem; + background: var(--color-light); + padding: 1em 1.5em; + padding-left: 0.75em; + border-radius: 4px; + } + + pre, + code { + font-family: var(--font-mono); + background: var(--color-light); + border-radius: 4px; + } + + pre { + padding: 1em 1.5em; + } + + .tree { + line-height: 1.2; + } + + code:not(.tree) { + padding: 0.125em; + margin: 0 -0.125em; + } +</style> diff --git a/packages/create-astro/src/templates/starter/src/pages/index.astro b/packages/create-astro/src/templates/starter/src/pages/index.astro new file mode 100644 index 000000000..de052e9c4 --- /dev/null +++ b/packages/create-astro/src/templates/starter/src/pages/index.astro @@ -0,0 +1,38 @@ +--- +import Tour from '../components/Tour.astro'; +--- + +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Astro</title> + + <link rel="icon" type="image/svg+xml" href="/favicon.svg"> + + <link rel="stylesheet" href="/style/global.css"> + <link rel="stylesheet" href="/style/home.css"> + + <style> + header { + display: flex; + flex-direction: column; + gap: 1em; + max-width: min(100%, 68ch); + } + </style> +</head> +<body> + <main> + <header> + <div> + <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo"> + <h1>Welcome to <a href="https://astro.build/">Astro</a></h1> + </div> + </header> + + <Tour /> + </main> +</body> +</html> diff --git a/packages/create-astro/src/utils.ts b/packages/create-astro/src/utils.ts new file mode 100644 index 000000000..790a64cf9 --- /dev/null +++ b/packages/create-astro/src/utils.ts @@ -0,0 +1,147 @@ +import { ChildProcess, spawn } from 'child_process'; +import { promises as fs, readdirSync, existsSync, lstatSync, rmdirSync, unlinkSync } from 'fs'; +import { basename, resolve } from 'path'; +import { fileURLToPath, URL } from 'url'; +import decompress from 'decompress'; + +const listeners = new Map(); + +export async function addProcessListeners(handlers: [NodeJS.Signals|string, NodeJS.SignalsListener][]) { + for (const [event,handler] of handlers) { + listeners.set(event, handler); + process.once(event as NodeJS.Signals, handler); + } +} + +export async function cancelProcessListeners() { + for (const [event, handler] of listeners.entries()) { + process.off(event, handler); + listeners.delete(event); + } +} + +export async function getTemplates() { + const templatesRoot = fileURLToPath(new URL('./templates', import.meta.url)); + const templateFiles = await fs.readdir(templatesRoot, 'utf8'); + const templates = templateFiles.filter(t => t.endsWith('.tgz')); + const metafile = templateFiles.find(t => t.endsWith('meta.json')); + + const meta = await fs.readFile(resolve(templatesRoot, metafile)).then(r => JSON.parse(r.toString())); + + return templates.map(template => { + const value = basename(template, '.tgz'); + if (meta[value]) return { ...meta[value], value }; + return { value }; + }).sort((a, b) => { + const aRank = a.rank ?? 0; + const bRank = b.rank ?? 0; + if (aRank > bRank) return -1; + if (bRank > aRank) return 1; + return 0; + }); +} + +const childrenProcesses: ChildProcess[] = []; +export let isDone = false; + +export async function rewriteFiles(projectName: string) { + const dest = resolve(projectName); + const tasks = []; + tasks.push(fs.rename(resolve(dest, '_gitignore'), resolve(dest, '.gitignore'))); + tasks.push( + fs.readFile(resolve(dest, 'package.json')) + .then(res => JSON.parse(res.toString())) + .then(json => JSON.stringify({ ...json, name: getValidPackageName(projectName) }, null, 2)) + .then(res => fs.writeFile(resolve(dest, 'package.json'), res)) + ); + + return Promise.all(tasks); +} + +export async function prepareTemplate(use: 'npm'|'yarn', name: string, dest: string) { + const projectName = dest; + dest = resolve(dest); + const template = fileURLToPath(new URL(`./templates/${name}.tgz`, import.meta.url)); + await decompress(template, dest); + await rewriteFiles(projectName); + try { + await run(use, use === 'npm' ? 'i' : null, dest); + } catch (e) { + cleanup(true); + } + isDone = true; + return; +} + +export function cleanup(didError = false) { + killChildren(); + setTimeout(() => { + process.exit(didError ? 1 : 0); + }, 200); +} + +export function killChildren() { + childrenProcesses.forEach(p => p.kill('SIGINT')); +} + +export function run(pkgManager: 'npm'|'yarn', command: string, projectPath: string, stdio: any = 'ignore'): Promise<void> { + return new Promise((resolve, reject) => { + const p = spawn(pkgManager, command ? [command] : [], { + shell: true, + stdio, + cwd: projectPath, + }); + p.once('exit', () => resolve()); + p.once('error', reject); + childrenProcesses.push(p); + }); +} + +export function isWin() { + return process.platform === 'win32'; +} + +export function isEmpty(path) { + try { + const files = readdirSync(resolve(path)); + if (files.length > 0) { + return false; + } else { + return true; + } + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + return true; +} + +export function emptyDir(dir) { + dir = resolve(dir); + if (!existsSync(dir)) { + return + } + for (const file of readdirSync(dir)) { + const abs = resolve(dir, file) + if (lstatSync(abs).isDirectory()) { + emptyDir(abs) + rmdirSync(abs) + } else { + unlinkSync(abs) + } + } +} + +export function getValidPackageName(projectName: string) { + const packageNameRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/ + + if (packageNameRegExp.test(projectName)) { + return projectName + } + + return projectName + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/^[._]/, '') + .replace(/[^a-z0-9-~]+/g, '-'); +} |