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
|
||||
import cli from './index.js';
|
||||
|
||||
import cli from './dist/index.js';
|
||||
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",
|
||||
"version": "0.0.9",
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"exports": {
|
||||
".": "./create-astro.js"
|
||||
},
|
||||
"bin": {
|
||||
"create-astro": "./create-astro.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "echo 'build'"
|
||||
"build": "astro-scripts build src/index.tsx",
|
||||
"postbuild": "astro-scripts copy 'src/templates/**' --tgz"
|
||||
},
|
||||
"files": [
|
||||
"templates",
|
||||
"bin.js",
|
||||
"index.js"
|
||||
"dist",
|
||||
"create-astro.js"
|
||||
],
|
||||
"bin": {
|
||||
"create-astro": "./bin.js"
|
||||
},
|
||||
"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",
|
||||
"private": true,
|
||||
"name": "TODO",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"start": "astro dev",
|
||||
|
@ -8,8 +7,5 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"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 { resolve, dirname } from 'path';
|
||||
import glob from 'tiny-glob';
|
||||
import { promises as fs, readFileSync } from 'fs';
|
||||
import { resolve, dirname, sep, join } from 'path';
|
||||
import arg from 'arg';
|
||||
import glob from 'globby';
|
||||
import tar from 'tar';
|
||||
|
||||
export default async function copy(pattern, ...args) {
|
||||
const files = await glob(pattern, { filesOnly: true });
|
||||
/** @type {import('arg').Spec} */
|
||||
const spec = {
|
||||
'--tgz': Boolean,
|
||||
};
|
||||
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "^0.11.16"
|
||||
"arg": "^5.0.0",
|
||||
"esbuild": "^0.11.16",
|
||||
"globby": "^11.0.3",
|
||||
"tar": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue