create-astro
UI (#164)
* refactor: improve create-astro layout, build script * feat(create-astro): v0.1.0 * docs(create-astro): add README * feat(create-astro): add meta files to starter templates
This commit is contained in:
parent
467820996f
commit
ed631329e7
44 changed files with 2870 additions and 1699 deletions
7
.changeset/thirty-planes-repeat.md
Normal file
7
.changeset/thirty-planes-repeat.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'create-astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added **interactive mode** with a fesh new UI.
|
||||||
|
|
||||||
|
Included a new **blank** starter to get up and running even faster.
|
36
packages/create-astro/README.md
Normal file
36
packages/create-astro/README.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# create-astro
|
||||||
|
## Scaffolding for Astro projects
|
||||||
|
|
||||||
|
**With NPM:**
|
||||||
|
```bash
|
||||||
|
npm init astro
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Yarn:**
|
||||||
|
```bash
|
||||||
|
yarn create astro
|
||||||
|
```
|
||||||
|
|
||||||
|
`create-astro` automatically runs in _interactive_ mode, but you can also specify your project name and template with command line arguments.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm 6.x
|
||||||
|
npm init astro my-astro-project --template blank
|
||||||
|
|
||||||
|
# npm 7+, extra double-dash is needed:
|
||||||
|
npm init astro my-astro-project -- --template blank
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn create astro my-astro-project --template blank
|
||||||
|
```
|
||||||
|
|
||||||
|
To see all available options, use the `--help` flag.
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
|
||||||
|
The following templates are included:
|
||||||
|
|
||||||
|
- `starter`
|
||||||
|
- `blank`
|
||||||
|
|
||||||
|
Feel free to [open a PR](https://github.com/snowpackjs/astro/pulls) to add additional templates.
|
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import cli from './index.js';
|
import cli from './dist/index.js';
|
||||||
|
|
||||||
cli(process.argv);
|
cli(process.argv);
|
|
@ -1,47 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
import decompress from 'decompress';
|
|
||||||
import { fileURLToPath, URL } from 'url';
|
|
||||||
import { join } from 'node:path';
|
|
||||||
|
|
||||||
const log = (...args) => console.log(' ', ...args);
|
|
||||||
export default async function createAstro(argv) {
|
|
||||||
const [name] = argv.slice(2);
|
|
||||||
const templateRoot = fileURLToPath(new URL('../create-astro/templates', import.meta.url));
|
|
||||||
if (!name) {
|
|
||||||
log();
|
|
||||||
log(`npm init astro <dest>`);
|
|
||||||
log(`Provide a destination!`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
log();
|
|
||||||
const dest = resolve(process.cwd(), name);
|
|
||||||
const relDest = name.slice(0, 2) === './' ? name : `./${name}`;
|
|
||||||
if (isEmpty(relDest)) {
|
|
||||||
await decompress(fs.readFileSync(join(templateRoot, 'starter.tar.gz')), dest);
|
|
||||||
log(`Your Astro project has been scaffolded at "${relDest}"`);
|
|
||||||
log();
|
|
||||||
log(`Next steps:`);
|
|
||||||
log();
|
|
||||||
log(` cd ${relDest}`);
|
|
||||||
log(` npm install`);
|
|
||||||
log(` npm run start`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEmpty(path) {
|
|
||||||
try {
|
|
||||||
const files = fs.readdirSync(resolve(process.cwd(), path));
|
|
||||||
if (files.length > 0) {
|
|
||||||
log(`It looks like "${path}" isn't empty!`);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
log(`Scaffolding Astro project at "${path}"`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ENOENT') throw err;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
|
@ -2,19 +2,29 @@
|
||||||
"name": "create-astro",
|
"name": "create-astro",
|
||||||
"version": "0.0.9",
|
"version": "0.0.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./index.js",
|
"exports": {
|
||||||
|
".": "./create-astro.js"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"create-astro": "./create-astro.js"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'build'"
|
"build": "astro-scripts build src/index.tsx",
|
||||||
|
"postbuild": "astro-scripts copy 'src/templates/**' --tgz"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"templates",
|
"dist",
|
||||||
"bin.js",
|
"create-astro.js"
|
||||||
"index.js"
|
|
||||||
],
|
],
|
||||||
"bin": {
|
|
||||||
"create-astro": "./bin.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decompress": "^4.2.1"
|
"decompress": "^4.2.1",
|
||||||
|
"ink": "^3.0.8",
|
||||||
|
"ink-select-input": "^4.2.0",
|
||||||
|
"ink-text-input": "^4.0.1",
|
||||||
|
"react": "~17.0.2",
|
||||||
|
"source-map-support": "^0.5.19"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"astro-scripts": "0.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
93
packages/create-astro/src/components/App.tsx
Normal file
93
packages/create-astro/src/components/App.tsx
Normal file
|
@ -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;
|
49
packages/create-astro/src/components/Confirm.tsx
Normal file
49
packages/create-astro/src/components/Confirm.tsx
Normal file
|
@ -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;
|
5
packages/create-astro/src/components/Emoji.tsx
Normal file
5
packages/create-astro/src/components/Emoji.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text } from 'ink';
|
||||||
|
import { isWin } from '../utils';
|
||||||
|
|
||||||
|
export default ({ children }) => isWin() ? null : <Text>{children}</Text>
|
9
packages/create-astro/src/components/Exit.tsx
Normal file
9
packages/create-astro/src/components/Exit.tsx
Normal file
|
@ -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;
|
27
packages/create-astro/src/components/Finalize.tsx
Normal file
27
packages/create-astro/src/components/Finalize.tsx
Normal file
|
@ -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;
|
20
packages/create-astro/src/components/Header.tsx
Normal file
20
packages/create-astro/src/components/Header.tsx
Normal file
|
@ -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;
|
62
packages/create-astro/src/components/Help.tsx
Normal file
62
packages/create-astro/src/components/Help.tsx
Normal file
|
@ -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;
|
19
packages/create-astro/src/components/Install.tsx
Normal file
19
packages/create-astro/src/components/Install.tsx
Normal file
|
@ -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;
|
24
packages/create-astro/src/components/ProjectName.tsx
Normal file
24
packages/create-astro/src/components/ProjectName.tsx
Normal file
|
@ -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;
|
32
packages/create-astro/src/components/Select.tsx
Normal file
32
packages/create-astro/src/components/Select.tsx
Normal file
|
@ -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;
|
5
packages/create-astro/src/components/Spacer.tsx
Normal file
5
packages/create-astro/src/components/Spacer.tsx
Normal file
|
@ -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;
|
200
packages/create-astro/src/components/Spinner.tsx
Normal file
200
packages/create-astro/src/components/Spinner.tsx
Normal file
|
@ -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;
|
23
packages/create-astro/src/components/Template.tsx
Normal file
23
packages/create-astro/src/components/Template.tsx
Normal file
|
@ -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;
|
6
packages/create-astro/src/components/Version.tsx
Normal file
6
packages/create-astro/src/components/Version.tsx
Normal file
|
@ -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;
|
49
packages/create-astro/src/config.ts
Normal file
49
packages/create-astro/src/config.ts
Normal file
|
@ -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);
|
46
packages/create-astro/src/index.tsx
Normal file
46
packages/create-astro/src/index.tsx
Normal file
|
@ -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],
|
||||||
|
])
|
||||||
|
}
|
24
packages/create-astro/src/templates/blank/README.md
Normal file
24
packages/create-astro/src/templates/blank/README.md
Normal file
|
@ -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).
|
18
packages/create-astro/src/templates/blank/_gitignore
Normal file
18
packages/create-astro/src/templates/blank/_gitignore
Normal file
|
@ -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
|
4
packages/create-astro/src/templates/blank/meta.json
Normal file
4
packages/create-astro/src/templates/blank/meta.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"title": "Blank",
|
||||||
|
"description": "a bare-bones, ultra-minimal template"
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@example/starter",
|
"name": "TODO",
|
||||||
"private": true,
|
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
|
@ -8,8 +7,5 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "0.0.9"
|
"astro": "0.0.9"
|
||||||
},
|
|
||||||
"snowpack": {
|
|
||||||
"workspaceRoot": "../.."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -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>
|
18
packages/create-astro/src/templates/starter/_gitignore
Normal file
18
packages/create-astro/src/templates/starter/_gitignore
Normal file
|
@ -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
|
5
packages/create-astro/src/templates/starter/meta.json
Normal file
5
packages/create-astro/src/templates/starter/meta.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "Getting Started",
|
||||||
|
"description": "a friendly starting point for new astronauts",
|
||||||
|
"rank": 999
|
||||||
|
}
|
11
packages/create-astro/src/templates/starter/package.json
Normal file
11
packages/create-astro/src/templates/starter/package.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "TODO",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"astro": "0.0.9"
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -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>
|
After Width: | Height: | Size: 1.2 KiB |
147
packages/create-astro/src/utils.ts
Normal file
147
packages/create-astro/src/utils.ts
Normal file
|
@ -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, '-');
|
||||||
|
}
|
Binary file not shown.
|
@ -1,11 +1,74 @@
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs, readFileSync } from 'fs';
|
||||||
import { resolve, dirname } from 'path';
|
import { resolve, dirname, sep, join } from 'path';
|
||||||
import glob from 'tiny-glob';
|
import arg from 'arg';
|
||||||
|
import glob from 'globby';
|
||||||
|
import tar from 'tar';
|
||||||
|
|
||||||
export default async function copy(pattern, ...args) {
|
/** @type {import('arg').Spec} */
|
||||||
const files = await glob(pattern, { filesOnly: true });
|
const spec = {
|
||||||
await Promise.all(files.map(file => {
|
'--tgz': Boolean,
|
||||||
const dest = resolve(file.replace(/^[^/]+/, 'dist'));
|
};
|
||||||
return fs.mkdir(dirname(dest), { recursive: true }).then(() => fs.copyFile(resolve(file), dest))
|
|
||||||
}));
|
export default async function copy() {
|
||||||
|
let { _: patterns, ['--tgz']: isCompress } = arg(spec);
|
||||||
|
patterns = patterns.slice(1);
|
||||||
|
|
||||||
|
if (isCompress) {
|
||||||
|
const files = await glob(patterns, { gitignore: true });
|
||||||
|
const rootDir = resolveRootDir(files);
|
||||||
|
const destDir = rootDir.replace(/^[^/]+/, 'dist');
|
||||||
|
|
||||||
|
const templates = files.reduce((acc, curr) => {
|
||||||
|
const name = curr.replace(rootDir, '').slice(1).split(sep)[0];
|
||||||
|
if (acc[name]) {
|
||||||
|
acc[name].push(resolve(curr));
|
||||||
|
} else {
|
||||||
|
acc[name] = [resolve(curr)];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let meta = {};
|
||||||
|
return Promise.all(
|
||||||
|
Object.entries(templates).map(([template, files]) => {
|
||||||
|
const cwd = resolve(join(rootDir, template));
|
||||||
|
const dest = join(destDir, `${template}.tgz`);
|
||||||
|
const metafile = files.find(f => f.endsWith('meta.json'));
|
||||||
|
if (metafile) {
|
||||||
|
files = files.filter(f => f !== metafile);
|
||||||
|
meta[template] = JSON.parse(readFileSync(metafile).toString());
|
||||||
|
}
|
||||||
|
return fs.mkdir(dirname(dest), { recursive: true }).then(() => tar.create({
|
||||||
|
gzip: true,
|
||||||
|
portable: true,
|
||||||
|
file: dest,
|
||||||
|
cwd,
|
||||||
|
}, files.map(f => f.replace(cwd, '').slice(1))));
|
||||||
|
})
|
||||||
|
).then(() => {
|
||||||
|
if (Object.keys(meta).length > 0) {
|
||||||
|
return fs.writeFile(resolve(destDir, 'meta.json'), JSON.stringify(meta, null, 2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await glob(patterns);
|
||||||
|
await Promise.all(files.map(file => {
|
||||||
|
const dest = resolve(file.replace(/^[^/]+/, 'dist'));
|
||||||
|
return fs.mkdir(dirname(dest), { recursive: true }).then(() => fs.copyFile(resolve(file), dest))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRootDir(files) {
|
||||||
|
return files
|
||||||
|
.reduce((acc, curr) => {
|
||||||
|
const currParts = curr.split(sep);
|
||||||
|
if (acc.length === 0) return currParts;
|
||||||
|
const result = [];
|
||||||
|
currParts.forEach((part, i) => {
|
||||||
|
if (acc[i] === part) result.push(part);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, [])
|
||||||
|
.join(sep);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
"astro-scripts": "./index.js"
|
"astro-scripts": "./index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.11.16"
|
"arg": "^5.0.0",
|
||||||
|
"esbuild": "^0.11.16",
|
||||||
|
"globby": "^11.0.3",
|
||||||
|
"tar": "^6.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue