Compare commits

...
Sign in to create a new pull request.

18 commits

Author SHA1 Message Date
Nate Moore
5b8583febb test(create-astro): test reliability 2022-10-07 15:01:42 -05:00
Nate Moore
a4fad0f2a2 test(create-astro): test reliability 2022-10-07 11:27:46 -05:00
Nate Moore
947dc2ec28 test(create-astro): test reliability 2022-10-07 10:00:10 -05:00
Nate Moore
bb2db7fef4 test(create-astro): reliability 2022-10-07 09:38:37 -05:00
Nate Moore
59d37ed6b3 test(create-astro): add --yes and --no tests 2022-10-07 09:37:13 -05:00
Nate Moore
9089791aba test(create-astro): wait for other messages 2022-10-07 09:34:28 -05:00
Nate Moore
d0743a9380 fix(create-astro): remove . from url 2022-10-07 08:30:12 -05:00
Nate Moore
097394c14f docs(create-astro): update CLI reference to include -- 2022-10-07 08:28:04 -05:00
Nate Moore
0476af580d feat(create-astro): improve error handling 2022-10-07 07:45:41 -05:00
Nate Moore
4871c42b21 feat(create-astro): support --no and -n flags 2022-10-06 22:43:06 -05:00
Nate Moore
c66f7ca360 feat(create-astro): add flags, readme, test coverage 2022-10-06 22:34:34 -05:00
Nate Moore
5968c4f8e1 chore: update lockfile 2022-10-06 19:05:59 -05:00
Nate Moore
7aa363fc24 refactor: remove vscode check 2022-10-06 19:05:22 -05:00
Nate Moore
562eedd9e2 wip: move to giget 2022-10-06 19:05:22 -05:00
Nate Moore
e76e2c8fbf test(create-astro): update create-astro tests 2022-10-06 19:05:22 -05:00
Nate Moore
cc0bfadd59 chore: add changeset 2022-10-06 19:05:22 -05:00
Nate Moore
d5c8663509 fix(create-astro): git message 2022-10-06 19:05:22 -05:00
Nate Moore
d1baaf7801 feat(create-astro): redesign create-astro UI 2022-10-06 19:05:22 -05:00
36 changed files with 969 additions and 1214 deletions

View file

@ -0,0 +1,5 @@
---
'create-astro': major
---
`create-astro@2.0.0` features a newly redesigned `create-astro` CLI.

View file

@ -1,61 +1,114 @@
# create-astro
# `create-astro`
## Scaffolding for Astro projects
`create-astro` is the fastest way to start a new Astro project from scratch. It will walk you through every step of setting up your new Astro project. It allows you to choose from a few different starter templates or provide your own using the `--template` argument.
**With NPM:**
## Interactive Mode
Run the following command in your terminal to start our handy install wizard in interactive mode.
```bash
# create a new project with npm
npm create astro@latest
```
**With Yarn:**
```bash
# create a new project with pnpm
pnpm create astro@latest
```
```bash
# create a new project with yarn
yarn create astro
```
`create-astro` automatically runs in _interactive_ mode, but you can also specify your project name and template with command line arguments.
You can run `create-astro` anywhere on your machine, so theres no need to create a new empty directory for your project before you begin. If you dont have an empty directory yet for your new project, the wizard will help create one for you automatically.
## Advanced Usage
`create-astro` supports some handy CLI arguments for advanced users.
### Directory
The first argument will be treated as your target directory.
```bash
# npm 6.x
npm create astro@latest my-astro-project --template starter
# npm 7+, extra double-dash is needed:
npm create astro@latest my-astro-project -- --template starter
# yarn
yarn create astro my-astro-project --template starter
# create a new project in a new `my-project/` directory
npm create astro@latest my-project
```
[Check out the full list][examples] of example starter templates, available on GitHub.
You can also use any GitHub repo as a template:
### Template
The `--template` flag can be passed to specify any [official starter template](https://github.com/withastro/astro/tree/main/examples) available on GitHub.
```bash
npm create astro@latest my-astro-project -- --template cassidoo/shopify-react-astro
# create a new project from the `minimal` starter template
npm create astro@latest -- --template minimal
```
### CLI Flags
May be provided in place of prompts
| Name | Description |
|:-------------|:----------------------------------------------------|
| `--template` | Specify the template name ([list][examples]) |
| `--commit` | Specify a specific Git commit or branch to use from this repo (by default, `main` branch of this repo will be used) |
### Debugging
To debug `create-astro`, you can use the `--verbose` flag which will log the output of degit and some more information about the command, this can be useful when you encounter an error and want to report it.
Any GitHub repo can be used as a template, following the `user/repo` format.
```bash
# npm 6.x
npm create astro@latest my-astro-project --verbose
# npm 7+, extra double-dash is needed:
npm create astro@latest my-astro-project -- --verbose
# yarn
yarn create astro my-astro-project --verbose
npm create astro@latest -- --template mayank99/astro-minimal-starter
```
[examples]: https://github.com/withastro/astro/tree/main/examples
### Yes / No
The `--yes` (or `-y`) flag can be used to bypass any confirmation prompts and proceed with the default answer.
```bash
npm create astro@latest -- -y
```
The `--no` (or `-n`) flag can be used to bypass any confirmations prompts and proceed without executing any actions.
```bash
npm create astro@latest -- -n
```
Combined with the above directory and template arguments, `create-astro` becomes fully non-interactive for a super quick start.
```bash
# copy minimal to my-project/ and confirm all prompts
npm create astro@latest -- my-project --template minimal -y
```
```bash
# copy minimal to my-project/ and dismiss all prompts
npm create astro@latest -- my-project --template minimal -n
```
### Dry Run
Just looking to get the hang of `create-astro`? You can pass the `--dry-run` flag to ensure no files will be created.
```bash
npm create astro@latest -- --dry-run
```
### Install
Dependency installation can be controlled with the `--install` or `--no-install` flags to bypass the installation prompt.
```bash
npm create astro@latest -- --install
```
### Git
Git initialization can be controlled with the `--git` or `--no-git` flags to bypass the git prompt.
```bash
npm create astro@latest -- --git
```
### TypeScript
TypeScript customization can be controlled with the `--typescript` flag. Valid options are `strict`, `strictest`, and `relaxed`.
```bash
npm create astro@latest -- --typescript strictest
```
## Acknowledgements
- Huge thanks to [`giget`](https://github.com/unjs/giget) for handling template downloads!

View file

@ -21,7 +21,7 @@
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 20000"
"test": "vitest"
},
"files": [
"dist",
@ -29,28 +29,20 @@
"tsconfigs"
],
"dependencies": {
"chalk": "^5.0.1",
"@astrojs/cli-kit": "^0.0.3",
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"execa": "^6.1.0",
"kleur": "^4.1.4",
"ora": "^6.1.0",
"prompts": "^2.4.2",
"giget": "^0.1.7",
"strip-ansi": "^7.0.1",
"which-pm-runs": "^1.1.0",
"yargs-parser": "^21.0.1"
},
"devDependencies": {
"@types/chai": "^4.3.1",
"@types/degit": "^2.8.3",
"@types/mocha": "^9.1.1",
"@types/prompts": "^2.0.14",
"@types/which-pm-runs": "^1.0.0",
"@types/yargs-parser": "^21.0.0",
"astro-scripts": "workspace:*",
"chai": "^4.3.6",
"mocha": "^9.2.2",
"uvu": "^0.5.3"
"cli-testing-library": "^2.0.0",
"vitest": "^0.20.3"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"

View file

@ -0,0 +1,14 @@
/* eslint no-console: 'off' */
import { isEmpty } from "./shared.js";
import { info } from "../messages.js";
import { color } from "@astrojs/cli-kit";
export default async function checkCwd(cwd: string | undefined) {
const empty = cwd && isEmpty(cwd);
if (empty) {
console.log('');
await info('dir', `Using ${color.reset(cwd)}${color.dim(' as project directory')}`);
}
return empty;
}

View file

@ -0,0 +1,62 @@
/* eslint no-console: 'off' */
import fs from 'node:fs';
import path from 'node:path';
import type { Arguments as Flags } from "yargs-parser";
import { color } from "@astrojs/cli-kit";
import { downloadTemplate } from 'giget';
import { error } from '../messages.js';
// some files are only needed for online editors when using astro.new. Remove for create-astro installs.
const FILES_TO_REMOVE = ['sandbox.config.json', 'CHANGELOG.md'];
const FILES_TO_UPDATE = {
'package.json': (file: string, overrides: { name: string }) => fs.promises.readFile(file, 'utf-8').then(value => (
fs.promises.writeFile(file, JSON.stringify(Object.assign(JSON.parse(value), Object.assign(overrides, { private: undefined })), null, '\t'), 'utf-8')
))
};
export default async function copyTemplate(template: string, { name, flags, cwd, pkgManager }: { name: string, flags: Flags, cwd: string, pkgManager: string }) {
const ref = flags.commit ? `#${flags.commit}` : '';
const isThirdParty = template.includes('/');
const templateTarget = isThirdParty
? template
: `github:withastro/astro/examples/${template}#latest`;
// Copy
if (!flags.dryRun) {
try {
await downloadTemplate(`${templateTarget}${ref}`, {
force: true,
provider: 'github',
cwd,
dir: '.',
});
} catch (err: any) {
fs.rmdirSync(cwd);
if (err.message.includes('404')) {
await error('Error', `Template ${color.reset(template)} ${color.dim('does not exist!')}`);
} else {
console.error(err.message);
}
process.exit(1);
}
// Post-process in parallel
const removeFiles = FILES_TO_REMOVE.map(async (file) => {
const fileLoc = path.resolve(path.join(cwd, file));
if (fs.existsSync(fileLoc)) {
return fs.promises.rm(fileLoc, { recursive: true });
}
});
const updateFiles = Object.entries(FILES_TO_UPDATE).map(async ([file, update]) => {
const fileLoc = path.resolve(path.join(cwd, file));
if (fs.existsSync(fileLoc)) {
return update(fileLoc, { name })
}
})
await Promise.all([...removeFiles, ...updateFiles]);
}
}

View file

@ -0,0 +1,5 @@
import { execaCommand } from 'execa';
export default async function initializeGit({ cwd }: { cwd: string }) {
return execaCommand('git init', { cwd });
}

View file

@ -0,0 +1,9 @@
import { execa } from 'execa';
export default async function installDeps({ pkgManager, cwd }: { pkgManager: string, cwd: string }) {
const installExec = execa(pkgManager, ['install'], { cwd });
return new Promise<void>((resolve, reject) => {
installExec.on('error', (error) => reject(error));
installExec.on('close', () => resolve());
});
}

View file

@ -0,0 +1,30 @@
import fs from 'node:fs';
import path from 'node:path';
import { assign, parse, stringify } from 'comment-json';
export default async function setupTypeScript(value: string, { cwd }: { cwd: string }) {
if (value === 'default') return;
const templateTSConfigPath = path.join(cwd, 'tsconfig.json');
fs.readFile(templateTSConfigPath, (err, data) => {
if (err && err.code === 'ENOENT') {
// If the template doesn't have a tsconfig.json, let's add one instead
fs.writeFileSync(
templateTSConfigPath,
stringify({ extends: `astro/tsconfigs/${value}` }, null, 2)
);
return;
}
const templateTSConfig = parse(data.toString());
if (templateTSConfig && typeof templateTSConfig === 'object') {
const result = assign(templateTSConfig, {
extends: `astro/tsconfigs/${value}`,
});
fs.writeFileSync(templateTSConfigPath, stringify(result, null, 2));
} else {
throw new Error("There was an error applying the requested TypeScript settings. This could be because the template's tsconfig.json is malformed")
}
});
}

View file

@ -0,0 +1,49 @@
import fs from 'node:fs';
// Some existing files and directories can be safely ignored when checking if a directory is a valid project directory.
// https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/create-react-app/createReactApp.js#L907-L934
const VALID_PROJECT_DIRECTORY_SAFE_LIST = [
'.DS_Store',
'.git',
'.gitkeep',
'.gitattributes',
'.gitignore',
'.gitlab-ci.yml',
'.hg',
'.hgcheck',
'.hgignore',
'.idea',
'.npmignore',
'.travis.yml',
'.yarn',
'.yarnrc.yml',
'docs',
'LICENSE',
'mkdocs.yml',
'Thumbs.db',
/\.iml$/,
/^npm-debug\.log/,
/^yarn-debug\.log/,
/^yarn-error\.log/,
];
export function isEmpty(dirPath: string) {
if (!fs.existsSync(dirPath)) {
return true;
}
const conflicts = fs.readdirSync(dirPath).filter((content) => {
return !VALID_PROJECT_DIRECTORY_SAFE_LIST.some((safeContent) => {
return typeof safeContent === 'string' ? content === safeContent : safeContent.test(content);
});
});
return conflicts.length === 0;
}
export function toValidName(name: string) {
return name
.replace(/^\.\//, '')
.replace(/[^a-zA-Z0-9-]/g, '-')
.replace(/(^\-|\-$)+/, '');
}

View file

@ -1,91 +0,0 @@
import chalk from 'chalk';
import type { Ora } from 'ora';
import ora from 'ora';
const gradientColors = [
`#ff5e00`,
`#ff4c29`,
`#ff383f`,
`#ff2453`,
`#ff0565`,
`#ff007b`,
`#f5008b`,
`#e6149c`,
`#d629ae`,
`#c238bd`,
];
export const rocketAscii = '■■▶';
// get a reference to scroll through while loading
// visual representation of what this generates:
// gradientColors: "..xxXX"
// referenceGradient: "..xxXXXXxx....xxXX"
const referenceGradient = [
...gradientColors,
// draw the reverse of the gradient without
// accidentally mutating the gradient (ugh, reverse())
...[...gradientColors].reverse(),
...gradientColors,
];
// async-friendly setTimeout
const sleep = (time: number) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});
function getGradientAnimFrames() {
const frames = [];
for (let start = 0; start < gradientColors.length * 2; start++) {
const end = start + gradientColors.length - 1;
frames.push(
referenceGradient
.slice(start, end)
.map((g) => chalk.bgHex(g)(' '))
.join('')
);
}
return frames;
}
function getIntroAnimFrames() {
const frames = [];
for (let end = 1; end <= gradientColors.length; end++) {
const leadingSpacesArr = Array.from(
new Array(Math.abs(gradientColors.length - end - 1)),
() => ' '
);
const gradientArr = gradientColors.slice(0, end).map((g) => chalk.bgHex(g)(' '));
frames.push([...leadingSpacesArr, ...gradientArr].join(''));
}
return frames;
}
/**
* Generate loading spinner with rocket flames!
* @param text display text next to rocket
* @returns Ora spinner for running .stop()
*/
export async function loadWithRocketGradient(text: string): Promise<Ora> {
const frames = getIntroAnimFrames();
const intro = ora({
spinner: {
interval: 30,
frames,
},
text: `${rocketAscii} ${text}`,
});
intro.start();
await sleep((frames.length - 1) * intro.interval);
intro.stop();
const spinner = ora({
spinner: {
interval: 80,
frames: getGradientAnimFrames(),
},
text: `${rocketAscii} ${text}`,
}).start();
return spinner;
}

View file

@ -1,450 +1,202 @@
/* eslint no-console: 'off' */
import { assign, parse, stringify } from 'comment-json';
import degit from 'degit';
import { execa, execaCommand } from 'execa';
import fs from 'fs';
import { bgCyan, black, bold, cyan, dim, gray, green, red, reset, yellow } from 'kleur/colors';
import ora from 'ora';
import os from 'os';
import path from 'path';
import prompts from 'prompts';
import detectPackageManager from 'which-pm-runs';
import fs from 'node:fs';
import path from 'node:path';
import yargs from 'yargs-parser';
import { loadWithRocketGradient, rocketAscii } from './gradient.js';
import { defaultLogLevel, logger } from './logger.js';
import { TEMPLATES } from './templates.js';
import detectPackageManager from 'which-pm-runs';
import { say, label, color, prompt, generateProjectName, spinner } from '@astrojs/cli-kit';
import { random, align } from '@astrojs/cli-kit/utils';
function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
import { banner, getName, getVersion, welcome, info, typescriptByDefault, nextSteps, error } from './messages.js';
import { isEmpty, toValidName } from './actions/shared.js';
import checkCwd from './actions/check-cwd.js';
import copyTemplate from './actions/copy-template.js';
import installDeps from './actions/install-deps.js';
import initializeGit from './actions/initialize-git.js';
import setupTypeScript from './actions/setup-typescript.js';
function logAndWait(message: string, ms = 100) {
console.log(message);
return wait(ms);
}
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
// to no longer require `--` to pass args and instead pass `--` directly to us. This
// broke our arg parser, since `--` is a special kind of flag. Filtering for `--` here
// fixes the issue so that create-astro now works on all npm version.
// fixes the issue so that create-astro now works on all npm versions.
const cleanArgv = process.argv.filter((arg) => arg !== '--');
const args = yargs(cleanArgv);
prompts.override(args);
const flags = yargs(cleanArgv, { boolean: ['yes', 'no', 'install', 'git', 'skip-houston'], alias: { 'y': 'yes', 'n': 'no' }});
export function mkdirp(dir: string) {
try {
fs.mkdirSync(dir, { recursive: true });
} catch (e: any) {
if (e.code === 'EEXIST') return;
throw e;
}
}
function isEmpty(dirPath: string) {
return !fs.existsSync(dirPath) || fs.readdirSync(dirPath).length === 0;
}
// Some existing files and directories can be safely ignored when checking if a directory is a valid project directory.
// https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/create-react-app/createReactApp.js#L907-L934
const VALID_PROJECT_DIRECTORY_SAFE_LIST = [
'.DS_Store',
'.git',
'.gitattributes',
'.gitignore',
'.gitlab-ci.yml',
'.hg',
'.hgcheck',
'.hgignore',
'.idea',
'.npmignore',
'.travis.yml',
'.yarn',
'.yarnrc.yml',
'docs',
'LICENSE',
'mkdocs.yml',
'Thumbs.db',
/\.iml$/,
/^npm-debug\.log/,
/^yarn-debug\.log/,
/^yarn-error\.log/,
];
function isValidProjectDirectory(dirPath: string) {
if (!fs.existsSync(dirPath)) {
return true;
}
const conflicts = fs.readdirSync(dirPath).filter((content) => {
return !VALID_PROJECT_DIRECTORY_SAFE_LIST.some((safeContent) => {
return typeof safeContent === 'string' ? content === safeContent : safeContent.test(content);
});
});
return conflicts.length === 0;
}
const { version } = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')
);
const FILES_TO_REMOVE = ['.stackblitzrc', 'sandbox.config.json', 'CHANGELOG.md']; // some files are only needed for online editors when using astro.new. Remove for create-astro installs.
const title = (text: string) => align(label(text), 'end', 7) + ' ';
// Please also update the installation instructions in the docs at https://github.com/withastro/docs/blob/main/src/pages/en/install/auto.md if you make any changes to the flow or wording here.
export async function main() {
const pkgManager = detectPackageManager()?.name || 'npm';
const pkgManager = detectPackageManager()?.name ?? 'npm';
const [username, version] = await Promise.all([getName(), getVersion()]);
let cwd = flags['_'][2] as string;
let { template, no, yes, install, git: init, typescript, skipHouston } = flags;
let projectName = cwd;
logger.debug('Verbose logging turned on');
console.log(`\n${bold('Welcome to Astro!')} ${gray(`(create-astro v${version})`)}`);
console.log(`Lets walk through setting up your new Astro project.\n`);
let cwd = args['_'][2] as string;
if (cwd && isValidProjectDirectory(cwd)) {
let acknowledgeProjectDir = ora({
color: 'green',
text: `Using ${bold(cwd)} as project directory.`,
});
acknowledgeProjectDir.succeed();
if (no) {
yes = false;
if (install == undefined) install = false;
if (init == undefined) init = false;
if (typescript == undefined) typescript = 'strict';
}
if (!cwd || !isValidProjectDirectory(cwd)) {
const notEmptyMsg = (dirPath: string) => `"${bold(dirPath)}" is not empty!`;
skipHouston = skipHouston ?? [yes, no, install, init, typescript].some(v => v !== undefined);
if (!isValidProjectDirectory(cwd)) {
let rejectProjectDir = ora({ color: 'red', text: notEmptyMsg(cwd) });
rejectProjectDir.fail();
if (!skipHouston) {
await say([
['Welcome', 'to', label('astro', color.bgGreen, color.black), color.green(`v${version}`) + ',', `${username}!`],
random(welcome),
]);
await banner(version);
} else {
console.log('');
await banner(version);
}
await checkCwd(cwd);
if (!cwd || !isEmpty(cwd)) {
if (!isEmpty(cwd)) {
await info('Hmm...', `${color.reset(`"${cwd}"`)}${color.dim(` is not empty!`)}`);
}
const dirResponse = await prompts(
{
type: 'text',
name: 'directory',
message: 'Where would you like to create your new project?',
initial: './my-astro-site',
validate(value) {
if (!isValidProjectDirectory(value)) {
return notEmptyMsg(value);
}
return true;
},
const { name } = await prompt({
name: 'name',
type: 'text',
label: title('dir'),
message: 'Where should we create your new project?',
initial: `./${generateProjectName()}`,
validate(value: string) {
if (!isEmpty(value)) {
return `Directory is not empty!`;
}
return true;
},
{ onCancel: () => ora().info(dim('Operation cancelled. See you later, astronaut!')) }
);
cwd = dirResponse.directory;
});
cwd = name!;
projectName = toValidName(name!);
} else {
let name = cwd;
if (name === '.' || name === './') {
const parts = process.cwd().split(path.sep);
name = parts[parts.length - 1];
}
projectName = toValidName(name);
}
if (!cwd) {
process.exit(1);
}
const options = await prompts(
[
{
type: 'select',
name: 'template',
message: 'Which template would you like to use?',
choices: TEMPLATES,
},
],
{ onCancel: () => ora().info(dim('Operation cancelled. See you later, astronaut!')) }
);
if (!options.template) {
process.exit(1);
if (!template) {
({ template } = await prompt({
name: 'template',
type: 'select',
label: title('tmpl'),
message: 'How would you like to start your new project?',
initial: 'basics',
choices: [
{ value: 'basics', label: 'Include sample files', hint: '(recommended)' },
{ value: 'blog', label: 'Use blog template' },
{ value: 'minimal', label: 'Empty' },
],
}));
} else {
await info('tmpl', `Using ${color.reset(template)}${color.dim(' as project template')}`)
}
let templateSpinner = await loadWithRocketGradient('Copying project files...');
if (flags.dryRun) {
await info('--dry-run', `Skipping template copying`);
} else if (template) {
await spinner({
start: 'Template copying...',
end: 'Template copied',
while: () => copyTemplate(template, { name: projectName, flags, cwd, pkgManager })
})
} else {
process.exit(1)
}
const hash = args.commit ? `#${args.commit}` : '';
let deps = install ?? yes;
if (deps === undefined) {
({ deps } = await prompt({
name: 'deps',
type: 'confirm',
label: title('deps'),
message: `Install dependencies?`,
hint: 'recommended',
initial: true
}));
}
if (flags.dryRun) {
await info('--dry-run', `Skipping dependency installation`);
} else if (deps) {
await spinner({ start: `Dependencies installing with ${pkgManager}...`, end: 'Dependencies installed', while: () => installDeps({ pkgManager, cwd }) });
} else {
await info(typeof install === 'boolean' ? 'deps [skip]' : 'No problem!', 'Remember to install dependencies after setup.')
}
const isThirdParty = options.template.includes('/');
const templateTarget = isThirdParty
? options.template
: `withastro/astro/examples/${options.template}#latest`;
let git = init ?? yes;
if (git === undefined) {
({ git } = await prompt({
name: 'git',
type: 'confirm',
label: title('git'),
message: `Initialize a new git repository?`,
hint: 'optional',
initial: true
}))
}
const emitter = degit(`${templateTarget}${hash}`, {
cache: false,
force: true,
verbose: defaultLogLevel === 'debug' ? true : false,
});
if (flags.dryRun) {
await info('--dry-run', `Skipping Git initialization`);
} else if (git) {
await spinner({ start: 'Git initializing...', end: 'Git initialized', while: () => initializeGit({ cwd }) });
} else {
await info(typeof init === 'boolean' ? 'git [skip]' : 'Sounds good!', `You can always run ${color.reset('git init')}${color.dim(' manually.')}`)
}
logger.debug('Initialized degit with following config:', `${templateTarget}${hash}`, {
cache: false,
force: true,
verbose: defaultLogLevel === 'debug' ? true : false,
});
// Copy
if (!args.dryRun) {
try {
emitter.on('info', (info) => {
logger.debug(info.message);
});
await emitter.clone(cwd);
// degit does not return an error when an invalid template is provided, as such we need to handle this manually
// It's not very pretty, but to the user eye, we just return a nice message and nothing weird happened
if (isValidProjectDirectory(cwd)) {
if (isEmpty(cwd)) {
fs.rmdirSync(cwd);
}
throw new Error(`Error: The provided template (${cyan(options.template)}) does not exist`);
let ts = typescript ?? (yes ? 'strict' : yes);
if (ts === undefined) {
({ ts } = await prompt({
name: 'ts',
type: 'select',
label: title('ts'),
message: `Customize TypeScript?`,
initial: 'strict',
choices: [
{ value: 'strict', label: 'Strict', hint: `(recommended)` },
{ value: 'strictest', label: 'Strictest' },
{ value: 'default', label: 'Relaxed' },
{ value: 'unsure', label: `Hmm... I'm not sure` },
]
}))
} else {
if (!['strict', 'strictest', 'relaxed', 'default'].includes(ts)) {
if (!flags.dryRun) {
fs.rmSync(cwd, { recursive: true, force: true });
}
} catch (err: any) {
templateSpinner.fail();
// degit is compiled, so the stacktrace is pretty noisy. Only report the stacktrace when using verbose mode.
logger.debug(err);
console.error(red(err.message));
// Warning for issue #655 and other corrupted cache issue
if (
err.message === 'zlib: unexpected end of file' ||
err.message === 'TAR_BAD_ARCHIVE: Unrecognized archive format'
) {
console.log(
yellow(
'Local degit cache seems to be corrupted. For more information check out this issue: https://github.com/withastro/astro/issues/655. '
)
);
const cacheIssueResponse = await prompts({
type: 'confirm',
name: 'cache',
message: 'Would you like us to clear the cache and try again?',
initial: true,
});
if (cacheIssueResponse.cache) {
const homeDirectory = os.homedir();
const cacheDir = path.join(homeDirectory, '.degit', 'github', 'withastro');
fs.rmSync(cacheDir, { recursive: true, force: true, maxRetries: 3 });
templateSpinner = await loadWithRocketGradient('Copying project files...');
try {
await emitter.clone(cwd);
} catch (e: any) {
logger.debug(e);
console.error(red(e.message));
}
} else {
console.log(
"Okay, no worries! To fix this manually, remove the folder '~/.degit/github/withastro' and rerun the command."
);
}
}
// Helpful message when encountering the "could not find commit hash for ..." error
if (err.code === 'MISSING_REF') {
console.log(
yellow(
"This seems to be an issue with degit. Please check if you have 'git' installed on your system, and install it if you don't have (https://git-scm.com)."
)
);
console.log(
yellow(
"If you do have 'git' installed, please run this command with the --verbose flag and file a new issue with the command output here: https://github.com/withastro/astro/issues"
)
);
}
error('Error', `Unknown TypeScript option ${color.reset(ts)}${color.dim('! Expected strict | strictest | relaxed')}`)
process.exit(1);
}
// Post-process in parallel
await Promise.all(
FILES_TO_REMOVE.map(async (file) => {
const fileLoc = path.resolve(path.join(cwd, file));
if (fs.existsSync(fileLoc)) {
return fs.promises.rm(fileLoc, {});
}
})
);
await info('ts', `Using ${color.reset(ts)}${color.dim(' TypeScript configuration')}`)
}
templateSpinner.text = green('Template copied!');
templateSpinner.succeed();
const installResponse = await prompts(
{
type: 'confirm',
name: 'install',
message: `Would you like to install ${pkgManager} dependencies? ${reset(
dim('(recommended)')
)}`,
initial: true,
},
{
onCancel: () => {
ora().info(
dim(
'Operation cancelled. Your project folder has already been created, however no dependencies have been installed'
)
);
process.exit(1);
},
if (flags.dryRun) {
await info('--dry-run', `Skipping TypeScript setup`);
} else if (ts && ts !== 'unsure') {
if (ts === 'relaxed') {
ts = 'default'
}
);
if (args.dryRun) {
ora().info(dim(`--dry-run enabled, skipping.`));
} else if (installResponse.install) {
const installExec = execa(pkgManager, ['install'], { cwd });
const installingPackagesMsg = `Installing packages${emojiWithFallback(' 📦', '...')}`;
const installSpinner = await loadWithRocketGradient(installingPackagesMsg);
await new Promise<void>((resolve, reject) => {
installExec.stdout?.on('data', function (data) {
installSpinner.text = `${rocketAscii} ${installingPackagesMsg}\n${bold(
`[${pkgManager}]`
)} ${data}`;
});
installExec.on('error', (error) => reject(error));
installExec.on('close', () => resolve());
});
installSpinner.text = green('Packages installed!');
installSpinner.succeed();
await spinner({ start: 'TypeScript customizing...', end: 'TypeScript customized', while: () => setupTypeScript(ts, { cwd }) });
} else {
ora().info(dim(`No problem! Remember to install dependencies after setup.`));
await typescriptByDefault();
}
const gitResponse = await prompts(
{
type: 'confirm',
name: 'git',
message: `Would you like to initialize a new git repository? ${reset(dim('(optional)'))}`,
initial: true,
},
{
onCancel: () => {
ora().info(
dim('Operation cancelled. No worries, your project folder has already been created')
);
process.exit(1);
},
}
);
if (args.dryRun) {
ora().info(dim(`--dry-run enabled, skipping.`));
} else if (gitResponse.git) {
await execaCommand('git init', { cwd });
ora().succeed('Git repository created!');
} else {
ora().info(dim(`Sounds good! You can come back and run ${cyan(`git init`)} later.`));
}
const tsResponse = await prompts(
{
type: 'select',
name: 'typescript',
message: 'How would you like to setup TypeScript?',
choices: [
{
title: 'Relaxed',
value: 'base',
},
{
title: 'Strict (recommended)',
description: 'Enable `strict` typechecking rules',
value: 'strict',
},
{
title: 'Strictest',
description: 'Enable all typechecking rules',
value: 'strictest',
},
{
title: 'I prefer not to use TypeScript',
description: `That's cool too!`,
value: 'optout',
},
],
},
{
onCancel: () => {
ora().info(
dim(
'Operation cancelled. Your project folder has been created but no TypeScript configuration file was created.'
)
);
process.exit(1);
},
}
);
if (tsResponse.typescript === 'optout') {
console.log(``);
ora().warn(yellow(bold(`Astro ❤️ TypeScript!`)));
console.log(` Astro supports TypeScript inside of ".astro" component scripts, so`);
console.log(` we still need to create some TypeScript-related files in your project.`);
console.log(` You can safely ignore these files, but don't delete them!`);
console.log(dim(' (ex: tsconfig.json, src/env.d.ts)'));
console.log(``);
tsResponse.typescript = 'base';
await wait(300);
}
if (args.dryRun) {
ora().info(dim(`--dry-run enabled, skipping.`));
} else if (tsResponse.typescript) {
const templateTSConfigPath = path.join(cwd, 'tsconfig.json');
fs.readFile(templateTSConfigPath, (err, data) => {
if (err && err.code === 'ENOENT') {
// If the template doesn't have a tsconfig.json, let's add one instead
fs.writeFileSync(
templateTSConfigPath,
stringify({ extends: `astro/tsconfigs/${tsResponse.typescript}` }, null, 2)
);
return;
}
const templateTSConfig = parse(data.toString());
if (templateTSConfig && typeof templateTSConfig === 'object') {
const result = assign(templateTSConfig, {
extends: `astro/tsconfigs/${tsResponse.typescript}`,
});
fs.writeFileSync(templateTSConfigPath, stringify(result, null, 2));
} else {
console.log(
yellow(
"There was an error applying the requested TypeScript settings. This could be because the template's tsconfig.json is malformed"
)
);
}
});
ora().succeed('TypeScript settings applied!');
}
ora().succeed('Setup complete.');
ora({ text: green('Ready for liftoff!') }).succeed();
await wait(300);
console.log(`\n${bgCyan(black(' Next steps '))}\n`);
let projectDir = path.relative(process.cwd(), cwd);
const devCmd = pkgManager === 'npm' ? 'npm run dev' : `${pkgManager} dev`;
await nextSteps({ projectDir, devCmd });
await say(['Good luck out there, astronaut! 🚀']);
// If the project dir is the current dir, no need to tell users to cd in the folder
if (projectDir !== '/') {
await logAndWait(
`You can now ${bold(cyan('cd'))} into the ${bold(cyan(projectDir))} project directory.`
);
}
await logAndWait(
`Run ${bold(cyan(devCmd))} to start the Astro dev server. ${bold(cyan('CTRL-C'))} to close.`
);
await logAndWait(
`Add frameworks like ${bold(cyan('react'))} and ${bold(
cyan('tailwind')
)} to your project using ${bold(cyan('astro add'))}`
);
await logAndWait('');
await logAndWait(`Stuck? Come join us at ${bold(cyan('https://astro.build/chat'))}`, 750);
await logAndWait(dim('Good luck out there, astronaut.'));
await logAndWait('', 300);
}
function emojiWithFallback(char: string, fallback: string) {
return process.platform !== 'win32' ? char : fallback;
process.exit(0);
}

View file

@ -1,147 +0,0 @@
import { blue, bold, dim, red, yellow } from 'kleur/colors';
import { Writable } from 'stream';
import { format as utilFormat } from 'util';
type ConsoleStream = Writable & {
fd: 1 | 2;
};
// Hey, locales are pretty complicated! Be careful modifying this logic...
// If we throw at the top-level, international users can't use Astro.
//
// Using `[]` sets the default locale properly from the system!
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters
//
// Here be the dragons we've slain:
// https://github.com/withastro/astro/issues/2625
// https://github.com/withastro/astro/issues/3309
const dt = new Intl.DateTimeFormat([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
export const defaultLogDestination = new Writable({
objectMode: true,
write(event: LogMessage, _, callback) {
let dest: Writable = process.stderr;
if (levels[event.level] < levels['error']) dest = process.stdout;
dest.write(dim(dt.format(new Date()) + ' '));
let type = event.type;
if (type) {
switch (event.level) {
case 'info':
type = bold(blue(type));
break;
case 'warn':
type = bold(yellow(type));
break;
case 'error':
type = bold(red(type));
break;
}
dest.write(`[${type}] `);
}
dest.write(utilFormat(...event.args));
dest.write('\n');
callback();
},
});
interface LogWritable<T> extends Writable {
write: (chunk: T) => boolean;
}
export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino
export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error';
export let defaultLogLevel: LoggerLevel;
if (process.argv.includes('--verbose')) {
defaultLogLevel = 'debug';
} else if (process.argv.includes('--silent')) {
defaultLogLevel = 'silent';
} else {
defaultLogLevel = 'info';
}
export interface LogOptions {
dest?: LogWritable<LogMessage>;
level?: LoggerLevel;
}
export const defaultLogOptions: Required<LogOptions> = {
dest: defaultLogDestination,
level: defaultLogLevel,
};
export interface LogMessage {
type: string | null;
level: LoggerLevel;
message: string;
args: Array<any>;
}
export const levels: Record<LoggerLevel, number> = {
debug: 20,
info: 30,
warn: 40,
error: 50,
silent: 90,
};
/** Full logging API */
export function log(
opts: LogOptions = {},
level: LoggerLevel,
type: string | null,
...args: Array<any>
) {
const logLevel = opts.level ?? defaultLogOptions.level;
const dest = opts.dest ?? defaultLogOptions.dest;
const event: LogMessage = {
type,
level,
args,
message: '',
};
// test if this level is enabled or not
if (levels[logLevel] > levels[level]) {
return; // do nothing
}
dest.write(event);
}
/** Emit a message only shown in debug mode */
export function debug(opts: LogOptions, type: string | null, ...messages: Array<any>) {
return log(opts, 'debug', type, ...messages);
}
/** Emit a general info message (be careful using this too much!) */
export function info(opts: LogOptions, type: string | null, ...messages: Array<any>) {
return log(opts, 'info', type, ...messages);
}
/** Emit a warning a user should be aware of */
export function warn(opts: LogOptions, type: string | null, ...messages: Array<any>) {
return log(opts, 'warn', type, ...messages);
}
/** Emit a fatal error message the user should address. */
export function error(opts: LogOptions, type: string | null, ...messages: Array<any>) {
return log(opts, 'error', type, ...messages);
}
// A default logger for when too lazy to pass LogOptions around.
export const logger = {
debug: debug.bind(null, defaultLogOptions, 'debug'),
info: info.bind(null, defaultLogOptions, 'info'),
warn: warn.bind(null, defaultLogOptions, 'warn'),
error: error.bind(null, defaultLogOptions, 'error'),
};

View file

@ -0,0 +1,101 @@
/* eslint no-console: 'off' */
import { exec } from 'node:child_process';
import { get } from 'node:https';
import { color, label } from '@astrojs/cli-kit';
import { sleep } from '@astrojs/cli-kit/utils';
import stripAnsi from 'strip-ansi';
export const welcome = [
`Let's claim your corner of the internet.`,
`I'll be your assistant today.`,
`Let's build something awesome!`,
`Let's build something great!`,
`Let's build something fast!`,
`Let's make the web weird!`,
`Let's make the web a better place!`,
`Let's create a new project!`,
`Let's create something unqiue!`,
`Time to build a new website.`,
`Time to build a faster website.`,
`Time to build a sweet new website.`,
`We're glad to have you on board.`,
`Keeping the internet weird since 2021.`,
`Initiating launch sequence...`,
`Initiating launch sequence... right... now!`,
`Awaiting further instructions.`,
]
export const getName = () => new Promise((resolve) => {
exec('git config user.name', { encoding: 'utf-8' }, (_1, gitName, _2) => {
if (gitName.trim()) {
return resolve(gitName.split(' ')[0].trim());
}
exec('whoami', { encoding: 'utf-8' }, (_3, whoami, _4) => {
if (whoami.trim()) {
return resolve(whoami.split(' ')[0].trim());
}
return resolve('astronaut');
});
});
});
let v: string;
export const getVersion = () => new Promise<string>((resolve) => {
if (v) return resolve(v);
get('https://registry.npmjs.org/astro/latest', (res) => {
let body = '';
res.on('data', chunk => body += chunk)
res.on('end', () => {
const { version } = JSON.parse(body);
v = version;
resolve(version);
})
})
})
export const banner = async (version: string) => console.log(`\n${label('astro', color.bgGreen, color.black)} ${color.green(color.bold(`v${version}`))} ${color.bold('Launch sequence initiated.')}`);
export const info = async (prefix: string, text: string) => {
await sleep(100)
if (process.stdout.columns < 80) {
console.log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(prefix)}`);
console.log(`${' '.repeat(9)}${color.dim(text)}`);
} else {
console.log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(prefix)} ${color.dim(text)}`);
}
}
export const error = async (prefix: string, text: string) => {
if (process.stdout.columns < 80) {
console.log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)}`);
console.log(`${' '.repeat(9)}${color.dim(text)}`);
} else {
console.log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)} ${color.dim(text)}`);
}
}
export const typescriptByDefault = async () => {
await info(`That\'s fine!`, 'Astro comes with TypeScript enabled by default.');
console.log(`${' '.repeat(9)}${color.dim(`We'll use the most relaxed settings for you.`)}`);
await sleep(300);
}
export const nextSteps = async ({ projectDir, devCmd }: { projectDir: string, devCmd: string }) => {
const max = process.stdout.columns;
const prefix = max < 80 ? ' ' : ' '.repeat(9);
await sleep(200);
console.log(`\n ${color.bgCyan(` ${color.black('next')} `)} ${color.bold('Liftoff confirmed. Explore your project!')}`)
await sleep(100);
if (projectDir !== '') {
const enter = [`\n${prefix}Enter your project directory using`, color.cyan(`cd ./${projectDir}`, '')];
const len = enter[0].length + stripAnsi(enter[1]).length;
console.log(enter.join((len > max) ? '\n' + prefix : ' '));
}
console.log(`${prefix}Run ${color.cyan(devCmd)} to start the dev server. ${color.cyan('CTRL+C')} to stop.`)
await sleep(100);
console.log(`${prefix}Add frameworks like ${color.cyan(`react`)} or ${color.cyan('tailwind')} using ${color.cyan('astro add')}.`)
await sleep(100);
console.log(`\n${prefix}Stuck? Join us at ${color.cyan(`https://astro.build/chat`)}`)
await sleep(200);
}

View file

@ -1,22 +0,0 @@
export const TEMPLATES = [
{
title: 'Just the basics (recommended)',
value: 'basics',
},
{
title: 'Blog',
value: 'blog',
},
{
title: 'Portfolio',
value: 'portfolio',
},
{
title: 'Documentation Site',
value: 'docs',
},
{
title: 'Empty project',
value: 'minimal',
},
];

View file

@ -0,0 +1,54 @@
import { test, expect } from 'vitest';
import 'cli-testing-library/extend-expect';
import { run } from './util';
test('full interactive', async () => {
const { findByText, userEvent } = await run('--dry-run')
expect(await findByText('Where should we create your new project?')).toBeInTheConsole();
expect(await findByText('./')).toBeInTheConsole()
userEvent.keyboard('foobar')
expect(await findByText('foobar')).toBeInTheConsole()
userEvent.keyboard('[Enter]')
expect(await findByText('./foobar')).toBeInTheConsole()
expect(await findByText('How would you like to start your new project?')).toBeInTheConsole()
userEvent.keyboard('[Enter]')
expect(await findByText('Install dependencies?')).toBeInTheConsole()
userEvent.keyboard('[Enter]')
expect(await findByText('Initialize a new git repository?')).toBeInTheConsole()
userEvent.keyboard('[Enter]')
expect(await findByText('Customize TypeScript?')).toBeInTheConsole()
userEvent.keyboard('[Enter]')
expect(await findByText('Liftoff confirmed')).toBeInTheConsole()
})
test('--yes', async () => {
const { findByText } = await run('foobar --template minimal --dry-run -y')
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Skipping dependency installation')).toBeInTheConsole();
expect(await findByText('Skipping Git initialization')).toBeInTheConsole();
expect(await findByText('Using strict TypeScript configuration')).toBeInTheConsole();
expect(await findByText('Liftoff confirmed')).toBeInTheConsole();
})
test('--no', async () => {
const { findByText } = await run('foobar --template minimal --dry-run -n')
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Skipping dependency installation')).toBeInTheConsole();
expect(await findByText('Skipping Git initialization')).toBeInTheConsole();
expect(await findByText('Using strict TypeScript configuration')).toBeInTheConsole();
expect(await findByText('Liftoff confirmed')).toBeInTheConsole();
})

View file

@ -1,139 +0,0 @@
import fs from 'fs';
import path from 'path';
import http from 'http';
import { green, red } from 'kleur/colors';
import { execa } from 'execa';
import glob from 'tiny-glob';
import { TEMPLATES } from '../dist/templates.js';
import { GITHUB_SHA, FIXTURES_DIR } from './helpers.js';
// helpers
async function fetch(url) {
return new Promise((resolve, reject) => {
http
.get(url, (res) => {
// not OK
if (res.statusCode !== 200) {
reject(res.statusCode);
return;
}
// OK
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => resolve({ statusCode: res.statusCode, body }));
})
.on('error', (err) => {
// other error
reject(err);
});
});
}
function assert(a, b, message) {
if (a !== b) throw new Error(red(`✘ ${message}`));
}
async function testTemplate(template) {
const templateDir = path.join(FIXTURES_DIR, template);
// test 1: install
const DOES_HAVE = ['.gitignore', 'package.json', 'public', 'src'];
const DOES_NOT_HAVE = ['.git', 'meta.json'];
// test 1a: expect template contains essential files & folders
for (const file of DOES_HAVE) {
assert(fs.existsSync(path.join(templateDir, file)), true, `[${template}] has ${file}`);
}
// test 1b: expect template DOES NOT contain files supposed to be stripped away
for (const file of DOES_NOT_HAVE) {
assert(fs.existsSync(path.join(templateDir, file)), false, `[${template}] cleaned up ${file}`);
}
// test 2: build
const MUST_HAVE_FILES = ['index.html', '_astro'];
await execa('npm', ['run', 'build'], { cwd: templateDir });
const builtFiles = await glob('**/*', { cwd: path.join(templateDir, 'dist') });
// test 2a: expect all files built successfully
for (const file of MUST_HAVE_FILES) {
assert(builtFiles.includes(file), true, `[${template}] built ${file}`);
}
// test 3: dev server (should happen after build so dependency install can be reused)
// TODO: fix dev server test in CI
if (process.env.CI === true) {
return;
}
// start dev server in background & wait until ready
const templateIndex = TEMPLATES.findIndex(({ value }) => value === template);
const port = 3000 + templateIndex; // use different port per-template
const devServer = execa('npm', ['run', 'start', '--', '--port', port], { cwd: templateDir });
let sigkill = setTimeout(() => {
throw new Error(`Dev server failed to start`); // if 10s has gone by with no update, kill process
}, 10000);
// read stdout until "Server started" appears
await new Promise((resolve, reject) => {
devServer.stdout.on('data', (data) => {
clearTimeout(sigkill);
sigkill = setTimeout(() => {
reject(`Dev server failed to start`);
}, 10000);
if (data.toString('utf8').includes('Server started')) resolve();
});
devServer.stderr.on('data', (data) => {
reject(data.toString('utf8'));
});
});
clearTimeout(sigkill); // done!
// send request to dev server that should be ready
const { statusCode, body } = (await fetch(`http://localhost:${port}`)) || {};
// test 3a: expect 200 status code
assert(statusCode, 200, `[${template}] 200 response`);
// test 3b: expect non-empty response
assert(body.length > 0, true, `[${template}] non-empty response`);
// clean up
devServer.kill();
}
async function testAll() {
// setup
await Promise.all(
TEMPLATES.map(async ({ value: template }) => {
// setup: `npm init astro`
await execa(
'../../create-astro.mjs',
[template, '--template', template, '--commit', GITHUB_SHA, '--force-overwrite'],
{
cwd: FIXTURES_DIR,
}
);
// setup: `pnpm install` (note: running multiple `pnpm`s in parallel in CI will conflict)
await execa('pnpm', ['install', '--no-package-lock', '--silent'], {
cwd: path.join(FIXTURES_DIR, template),
});
})
);
// test (note: not parallelized because Snowpack HMR reuses same port in dev)
for (let n = 0; n < TEMPLATES.length; n += 1) {
const template = TEMPLATES[n].value;
try {
await testTemplate(template);
} catch (err) {
console.error(red(`✘ [${template}]`));
throw err;
}
console.info(green(`✔ [${template}] All tests passed (${n + 1}/${TEMPLATES.length})`));
}
}
testAll();

View file

@ -0,0 +1,41 @@
import { test, expect } from 'vitest';
import 'cli-testing-library/extend-expect';
import { run } from './util';
test('flag', async () => {
const { findByText } = await run('foobar --template minimal --install --dry-run');
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Skipping dependency installation')).toBeInTheConsole();
});
test('override', async () => {
const { findByText } = await run('foobar --template minimal -y --dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Skipping dependency installation')).toBeInTheConsole();
})
test('select', async () => {
const { findByText, userEvent } = await run('foobar --template minimal --dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Install dependencies?')).toBeInTheConsole()
expect(await findByText('● Yes')).toBeInTheConsole()
userEvent.keyboard('[ArrowRight]')
expect(await findByText('● No')).toBeInTheConsole()
userEvent.keyboard('[ArrowLeft]')
expect(await findByText('● Yes')).toBeInTheConsole()
userEvent.keyboard('[Enter]')
expect(await findByText('Initialize a new git repository?')).toBeInTheConsole()
})

View file

@ -1,88 +0,0 @@
import path from 'path';
import { promises, existsSync } from 'fs';
import { PROMPT_MESSAGES, testDir, setup, promiseWithTimeout, timeout } from './utils.js';
const inputs = {
nonEmptyDir: './fixtures/select-directory/nonempty-dir',
nonEmptySafeDir: './fixtures/select-directory/nonempty-safe-dir',
emptyDir: './fixtures/select-directory/empty-dir',
nonexistentDir: './fixtures/select-directory/banana-dir',
};
describe('[create-astro] select directory', function () {
this.timeout(timeout);
it('should prompt for directory when none is provided', function () {
return promiseWithTimeout((resolve, onStdout) => {
const { stdout } = setup();
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.directory)) {
resolve();
}
});
});
});
it('should NOT proceed on a non-empty directory', function () {
return promiseWithTimeout((resolve, onStdout) => {
const { stdout } = setup([inputs.nonEmptyDir]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.directory)) {
resolve();
}
});
});
});
it('should proceed on a non-empty safe directory', function () {
return promiseWithTimeout((resolve) => {
const { stdout } = setup([inputs.nonEmptySafeDir]);
stdout.on('data', (chunk) => {
if (chunk.includes(PROMPT_MESSAGES.template)) {
resolve();
}
});
});
});
it('should proceed on an empty directory', async function () {
const resolvedEmptyDirPath = path.resolve(testDir, inputs.emptyDir);
if (!existsSync(resolvedEmptyDirPath)) {
await promises.mkdir(resolvedEmptyDirPath);
}
return promiseWithTimeout((resolve, onStdout) => {
const { stdout } = setup([inputs.emptyDir]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.template)) {
resolve();
}
});
});
});
it('should proceed when directory does not exist', function () {
return promiseWithTimeout((resolve, onStdout) => {
const { stdout } = setup([inputs.nonexistentDir]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.template)) {
resolve();
}
});
});
});
it('should error on bad directory selection in prompt', function () {
return promiseWithTimeout((resolve, onStdout) => {
let wrote = false;
const { stdout, stdin } = setup();
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes('is not empty!')) {
resolve();
}
if (!wrote && chunk.includes(PROMPT_MESSAGES.directory)) {
stdin.write(`${inputs.nonEmptyDir}\x0D`);
wrote = true;
}
});
});
});
});

View file

@ -0,0 +1,47 @@
import { test, expect } from 'vitest';
import 'cli-testing-library/extend-expect';
import { run, type } from './util';
test('interactive', async () => {
const { findByText, userEvent } = await run('--dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Where should we create your new project?')).toBeInTheConsole();
expect(await findByText('./')).toBeInTheConsole()
type(userEvent, 'foobar')
userEvent.keyboard('[Enter]')
expect(await findByText('./foobar')).toBeInTheConsole()
})
test('override', async () => {
const { findByText } = await run('foobar --dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('How would you like to start your new project?')).toBeInTheConsole()
})
test('nonempty directory', async () => {
const { findByText, userEvent } = await run('--dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Where should we create your new project?')).toBeInTheConsole();
expect(await findByText('./')).toBeInTheConsole()
type(userEvent, 'test/fixtures/nonempty-dir')
userEvent.keyboard('[Enter]')
expect(await findByText('Directory is not empty!')).toBeInTheConsole()
})
test('nonempty safe directory', async () => {
const { findByText, userEvent } = await run('--dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Where should we create your new project?')).toBeInTheConsole();
expect(await findByText('./')).toBeInTheConsole()
type(userEvent, 'test/fixtures/nonempty-safe-dir')
userEvent.keyboard('[Enter]')
expect(await findByText('How would you like to start your new project?')).toBeInTheConsole()
})

View file

@ -1,27 +0,0 @@
import assert from 'assert';
import { execa } from 'execa';
import { FIXTURES_URL } from './helpers.js';
import { existsSync } from 'fs';
async function run(outdir, template) {
//--template cassidoo/shopify-react-astro
await execa('../../create-astro.mjs', [outdir, '--template', template, '--force-overwrite'], {
cwd: FIXTURES_URL.pathname,
});
}
const testCases = [['shopify', 'cassidoo/shopify-react-astro']];
async function tests() {
for (let [dir, tmpl] of testCases) {
await run(dir, tmpl);
const outPath = new URL('' + dir, FIXTURES_URL);
assert.ok(existsSync(outPath));
}
}
tests().catch((err) => {
console.error(err);
process.exit(1);
});

View file

@ -0,0 +1,30 @@
import { test, expect } from 'vitest';
import 'cli-testing-library/extend-expect';
import { run } from './util';
test('flag', async () => {
const { findByText } = await run('foobar --template minimal --install --git --dry-run');
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Skipping dependency installation')).toBeInTheConsole();
expect(await findByText('Skipping Git initialization')).toBeInTheConsole();
});
test('override', async () => {
const { findByText } = await run('foobar --template minimal -y --dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Skipping Git initialization')).toBeInTheConsole();
})
test('select', async () => {
const { findByText, userEvent } = await run('foobar --template minimal --dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
userEvent.keyboard('[Enter]')
expect(await findByText('Initialize a new git repository?')).toBeInTheConsole()
})

View file

@ -1,9 +0,0 @@
import { execaSync } from 'execa';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
const GITHUB_SHA = process.env.GITHUB_SHA || execaSync('git', ['rev-parse', 'HEAD']).stdout; // process.env.GITHUB_SHA will be set in CI; if testing locally execa() will gather this
const FIXTURES_DIR = path.join(fileURLToPath(path.dirname(import.meta.url)), 'fixtures');
const FIXTURES_URL = pathToFileURL(FIXTURES_DIR + '/');
export { GITHUB_SHA, FIXTURES_DIR, FIXTURES_URL };

View file

@ -0,0 +1 @@
import 'cli-testing-library/extend-expect';

View file

@ -0,0 +1,45 @@
import { test, expect } from 'vitest';
import 'cli-testing-library/extend-expect';
import { run, type } from './util';
test('override', async () => {
const { findByText } = await run('foobar --template minimal --dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
})
test('override external', async () => {
const { findByText } = await run('foobar --template cassidoo/shopify-react-astro --dry-run')
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using cassidoo/shopify-react-astro as project template')).toBeInTheConsole();
})
test('select', async () => {
const { findByText, userEvent } = await run('--dry-run')
const where = await findByText('Where should we create your new project?');
expect(where).toBeInTheConsole();
expect(await findByText('./')).toBeInTheConsole()
type(userEvent, 'test/fixtures/empty-dir')
userEvent.keyboard('[Enter]')
expect(await findByText('How would you like to start your new project?')).toBeInTheConsole()
expect(await findByText('● Include sample files')).toBeInTheConsole()
userEvent.keyboard('[ArrowDown]')
expect(await findByText('● Use blog template')).toBeInTheConsole()
userEvent.keyboard('[ArrowDown]')
expect(await findByText('● Empty')).toBeInTheConsole()
userEvent.keyboard('[Enter]')
expect(await findByText('Install dependencies?')).toBeInTheConsole()
})

View file

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals", "cli-testing-library/extend-expect"]
}
}

View file

@ -1,141 +0,0 @@
import { expect } from 'chai';
import { deleteSync } from 'del';
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'fs';
import path from 'path';
import { PROMPT_MESSAGES, testDir, setup, promiseWithTimeout, timeout } from './utils.js';
const inputs = {
emptyDir: './fixtures/select-typescript/empty-dir',
};
function isEmpty(dirPath) {
return !existsSync(dirPath) || readdirSync(dirPath).length === 0;
}
function ensureEmptyDir() {
const dirPath = path.resolve(testDir, inputs.emptyDir);
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true });
} else if (!isEmpty(dirPath)) {
const globPath = path.resolve(dirPath, '*');
deleteSync(globPath, { dot: true });
}
}
function getTsConfig(installDir) {
const filePath = path.resolve(testDir, installDir, 'tsconfig.json');
return JSON.parse(readFileSync(filePath, 'utf-8'));
}
describe('[create-astro] select typescript', function () {
this.timeout(timeout);
beforeEach(ensureEmptyDir);
afterEach(ensureEmptyDir);
it('should prompt for typescript when none is provided', async function () {
return promiseWithTimeout(
(resolve, onStdout) => {
const { stdout } = setup([
inputs.emptyDir,
'--template',
'minimal',
'--install',
'0',
'--git',
'0',
]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.typescript)) {
resolve();
}
});
},
() => lastStdout
);
});
it('should not prompt for typescript when provided', async function () {
return promiseWithTimeout(
(resolve, onStdout) => {
const { stdout } = setup([
inputs.emptyDir,
'--template',
'minimal',
'--install',
'0',
'--git',
'0',
'--typescript',
'base',
]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.typescriptSucceed)) {
resolve();
}
});
},
() => lastStdout
);
});
it('should use "strict" config when specified', async function () {
return promiseWithTimeout(
(resolve, onStdout) => {
let wrote = false;
const { stdout, stdin } = setup([
inputs.emptyDir,
'--template',
'minimal',
'--install',
'0',
'--git',
'0',
]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (!wrote && chunk.includes(PROMPT_MESSAGES.typescript)) {
stdin.write('\x1B\x5B\x42\x0D');
wrote = true;
}
if (chunk.includes(PROMPT_MESSAGES.typescriptSucceed)) {
const tsConfigJson = getTsConfig(inputs.emptyDir);
expect(tsConfigJson).to.deep.equal({ extends: 'astro/tsconfigs/strict' });
resolve();
}
});
},
() => lastStdout
);
});
it('should create tsconfig.json when missing', async function () {
return promiseWithTimeout(
(resolve, onStdout) => {
const { stdout } = setup([
inputs.emptyDir,
'--template',
'cassidoo/shopify-react-astro',
'--install',
'0',
'--git',
'0',
'--typescript',
'base',
]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.typescriptSucceed)) {
const tsConfigJson = getTsConfig(inputs.emptyDir);
expect(tsConfigJson).to.deep.equal({ extends: 'astro/tsconfigs/base' });
resolve();
}
});
},
() => lastStdout
);
});
});

View file

@ -0,0 +1,41 @@
import { test, expect } from 'vitest';
import 'cli-testing-library/extend-expect';
import { run } from './util';
test('flag', async () => {
const { findByText, userEvent } = await run('foobar --template minimal --typescript relaxed --dry-run');
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
await userEvent.keyboard('[Enter]');
expect(await findByText('Initialize a new git repository?')).toBeInTheConsole();
await userEvent.keyboard('[Enter]');
expect(await findByText('Using relaxed TypeScript configuration')).toBeInTheConsole();
});
test('override', async () => {
const { findByText } = await run('foobar --template minimal -y --dry-run');
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
expect(await findByText('Using foobar as project directory')).toBeInTheConsole();
expect(await findByText('Using minimal as project template')).toBeInTheConsole();
expect(await findByText('Skipping TypeScript setup')).toBeInTheConsole();
});
test('select', async () => {
const { findByText, userEvent } = await run('foobar --template minimal --dry-run');
expect(await findByText('Launch sequence initiated.')).toBeInTheConsole();
await userEvent.keyboard('[Enter]');
expect(await findByText('Install dependencies?')).toBeInTheConsole();
await userEvent.keyboard('[Enter]');
expect(await findByText('Customize TypeScript?')).toBeInTheConsole();
expect(await findByText('● Strict')).toBeInTheConsole();
userEvent.keyboard('[ArrowDown]');
expect(await findByText('● Strictest')).toBeInTheConsole();
userEvent.keyboard('[ArrowDown]');
expect(await findByText('● Relaxed')).toBeInTheConsole();
userEvent.keyboard('[ArrowDown]');
expect(await findByText("● Hmm... I'm not sure")).toBeInTheConsole();
userEvent.keyboard('[Enter]');
expect(await findByText('Liftoff confirmed.')).toBeInTheConsole();
});

View file

@ -0,0 +1,10 @@
import { render } from 'cli-testing-library'
import { fileURLToPath } from 'url';
export function run(flags?: string) {
return render('node', ['./create-astro.mjs', '--skip-houston', ...(flags ? flags.split(' ') : [])], { cwd: fileURLToPath(new URL('../', import.meta.url)) })
}
export function type(userEvent, value: string) {
userEvent.keyboard(value, { keyboardMap: value.split('').map(hex => ({ hex })) })
}

View file

@ -1,50 +0,0 @@
import { execa } from 'execa';
import { dirname } from 'path';
import stripAnsi from 'strip-ansi';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
export const testDir = dirname(__filename);
export const timeout = 5000;
const timeoutError = function (details) {
let errorMsg = 'Timed out waiting for create-astro to respond with expected output.';
if (details) {
errorMsg += '\nLast output: "' + details + '"';
}
return new Error(errorMsg);
};
export function promiseWithTimeout(testFn) {
return new Promise((resolve, reject) => {
let lastStdout;
function onStdout(chunk) {
lastStdout = stripAnsi(chunk.toString()).trim() || lastStdout;
}
const timeoutEvent = setTimeout(() => {
reject(timeoutError(lastStdout));
}, timeout);
function resolver() {
clearTimeout(timeoutEvent);
resolve();
}
testFn(resolver, onStdout);
});
}
export const PROMPT_MESSAGES = {
directory: 'Where would you like to create your new project?',
template: 'Which template would you like to use?',
typescript: 'How would you like to setup TypeScript?',
typescriptSucceed: 'Next steps',
};
export function setup(args = []) {
const { stdout, stdin } = execa('../create-astro.mjs', [...args, '--dryrun'], { cwd: testDir });
return {
stdin,
stdout,
};
}

View file

@ -6,6 +6,7 @@
"target": "ES2020",
"module": "ES2020",
"outDir": "./dist",
"declarationDir": "./dist/types"
"emitDeclarationOnly": false,
"declaration": false
}
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
setupFiles: 'test/setup.ts'
},
})

197
pnpm-lock.yaml generated
View file

@ -2397,48 +2397,32 @@ importers:
packages/create-astro:
specifiers:
'@types/chai': ^4.3.1
'@types/degit': ^2.8.3
'@types/mocha': ^9.1.1
'@types/prompts': ^2.0.14
'@astrojs/cli-kit': ^0.0.3
'@types/which-pm-runs': ^1.0.0
'@types/yargs-parser': ^21.0.0
astro-scripts: workspace:*
chai: ^4.3.6
chalk: ^5.0.1
cli-testing-library: ^2.0.0
comment-json: ^4.2.3
degit: ^2.8.4
execa: ^6.1.0
kleur: ^4.1.4
mocha: ^9.2.2
ora: ^6.1.0
prompts: ^2.4.2
giget: ^0.1.7
strip-ansi: ^7.0.1
uvu: ^0.5.3
vitest: ^0.20.3
which-pm-runs: ^1.1.0
yargs-parser: ^21.0.1
dependencies:
chalk: 5.1.0
'@astrojs/cli-kit': 0.0.3
comment-json: 4.2.3
degit: 2.8.4
execa: 6.1.0
kleur: 4.1.5
ora: 6.1.2
prompts: 2.4.2
giget: 0.1.7
strip-ansi: 7.0.1
which-pm-runs: 1.1.0
yargs-parser: 21.1.1
devDependencies:
'@types/chai': 4.3.3
'@types/degit': 2.8.3
'@types/mocha': 9.1.1
'@types/prompts': 2.4.1
'@types/which-pm-runs': 1.0.0
'@types/yargs-parser': 21.0.0
astro-scripts: link:../../scripts
chai: 4.3.6
mocha: 9.2.2
uvu: 0.5.6
cli-testing-library: 2.0.0
vitest: 0.20.3
packages/integrations/alpinejs:
specifiers:
@ -3703,6 +3687,14 @@ packages:
lite-youtube-embed: 0.2.0
dev: false
/@astrojs/cli-kit/0.0.3:
resolution: {integrity: sha512-hpLqK5+KVZ0zEGP1r+Fzp+T8qdMLEkyEwDyby//TYixs9F8X2mqKkHMelGhGH1EZOdw9L2vJm9R5oN8CJclobw==}
dependencies:
chalk: 5.1.0
log-update: 5.0.1
sisteransi: 1.0.5
dev: false
/@astrojs/compiler/0.19.0:
resolution: {integrity: sha512-8nvyxZTfCXLyRmYfTttpJT6EPhfBRg0/q4J/Jj3/pNPLzp+vs05ZdktsY6QxAREaOMAnNEtSqcrB4S5DsXOfRg==}
dev: true
@ -9372,7 +9364,6 @@ packages:
resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==}
dependencies:
'@types/chai': 4.3.3
dev: false
/@types/chai/4.3.3:
resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==}
@ -9396,10 +9387,6 @@ packages:
dependencies:
'@types/ms': 0.7.31
/@types/degit/2.8.3:
resolution: {integrity: sha512-CL7y71j2zaDmtPLD5Xq5S1Gv2dFoHl0/GBZm6s39Mj/ls28L3NzAOqf7H4H0/2TNVMgMjMVf9CAFYSjmXhi3bw==}
dev: true
/@types/diff/5.0.2:
resolution: {integrity: sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==}
dev: true
@ -10251,6 +10238,13 @@ packages:
engines: {node: '>=6'}
dev: true
/ansi-escapes/5.0.0:
resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==}
engines: {node: '>=12'}
dependencies:
type-fest: 1.4.0
dev: false
/ansi-regex/2.1.1:
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
engines: {node: '>=0.10.0'}
@ -10277,6 +10271,11 @@ packages:
dependencies:
color-convert: 2.0.1
/ansi-styles/5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
dev: true
/ansi-styles/6.1.1:
resolution: {integrity: sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==}
engines: {node: '>=12'}
@ -10373,6 +10372,11 @@ packages:
dependencies:
tslib: 2.4.0
/astral-regex/2.0.0:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
dev: true
/astring/1.8.3:
resolution: {integrity: sha512-sRpyiNrx2dEYIMmUXprS8nlpRg2Drs8m9ElX9vVEXaCB4XEAJhKfs7IcX0IwShjuOAjLR6wzIrgoptz1n19i1A==}
hasBin: true
@ -10834,6 +10838,22 @@ packages:
engines: {node: '>=6'}
dev: false
/cli-testing-library/2.0.0:
resolution: {integrity: sha512-xjJNyR4G/DkhCSSzvNMURpZ80AaoPW5b3O8xDxh8uzOF4WLdgY38hlJjt3o8TgWri9fFOwXA1Gc1akapWEiTgA==}
engines: {node: '>=12'}
dependencies:
'@babel/code-frame': 7.18.6
'@babel/runtime': 7.19.0
jest-matcher-utils: 27.5.1
lz-string: 1.4.4
pretty-format: 27.5.1
redent: 3.0.0
slice-ansi: 4.0.0
strip-ansi: 6.0.1
strip-final-newline: 2.0.0
tree-kill: 1.2.2
dev: true
/cliui/6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
dependencies:
@ -11249,6 +11269,10 @@ packages:
resolution: {integrity: sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==}
dev: false
/defu/6.1.0:
resolution: {integrity: sha512-pOFYRTIhoKujrmbTRhcW5lYQLBXw/dlTwfI8IguF1QCDJOcJzNH1w+YFjxqy6BAuJrClTy6MUE8q+oKJ2FLsIw==}
dev: false
/degenerator/3.0.2:
resolution: {integrity: sha512-c0mef3SNQo56t6urUU6tdQAs+ThoD0o9B9MJ8HEt7NQcGEILCRFqQb7ZbP9JAv+QF1Ky5plydhMR/IrqWDm+TQ==}
engines: {node: '>= 6'}
@ -11259,12 +11283,6 @@ packages:
vm2: 3.9.11
dev: true
/degit/2.8.4:
resolution: {integrity: sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==}
engines: {node: '>=8.0.0'}
hasBin: true
dev: false
/del/7.0.0:
resolution: {integrity: sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==}
engines: {node: '>=14.16'}
@ -11331,6 +11349,11 @@ packages:
/didyoumean/1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
/diff-sequences/27.5.1:
resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dev: true
/diff/5.0.0:
resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
engines: {node: '>=0.3.1'}
@ -11719,7 +11742,7 @@ packages:
optional: true
/esbuild-linux-64/0.14.54:
resolution: {integrity: sha1-3l/boclWZs9yNp9StAsDvnEiZlI=}
resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
@ -12728,6 +12751,18 @@ packages:
- supports-color
dev: true
/giget/0.1.7:
resolution: {integrity: sha512-bvIVgRxfiYDTr6MWOdNjTI5o87sDUjbiFdad4P7j5yYrBJN3c3l0vaNICSrVE81X0Z6qFG0GkAoDgTrJ3K63XA==}
hasBin: true
dependencies:
colorette: 2.0.19
defu: 6.1.0
mri: 1.2.0
node-fetch-native: 0.1.7
pathe: 0.3.8
tar: 6.1.11
dev: false
/github-from-package/0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
@ -13403,6 +13438,11 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
/is-fullwidth-code-point/4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
engines: {node: '>=12'}
dev: false
/is-glob/4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@ -13576,6 +13616,31 @@ packages:
minimatch: 3.1.2
dev: false
/jest-diff/27.5.1:
resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
chalk: 4.1.2
diff-sequences: 27.5.1
jest-get-type: 27.5.1
pretty-format: 27.5.1
dev: true
/jest-get-type/27.5.1:
resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dev: true
/jest-matcher-utils/27.5.1:
resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
chalk: 4.1.2
jest-diff: 27.5.1
jest-get-type: 27.5.1
pretty-format: 27.5.1
dev: true
/jest-worker/26.6.2:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
@ -13776,7 +13841,6 @@ packages:
/local-pkg/0.4.2:
resolution: {integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==}
engines: {node: '>=14'}
dev: false
/locate-path/3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
@ -13834,6 +13898,17 @@ packages:
is-unicode-supported: 1.3.0
dev: false
/log-update/5.0.1:
resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
ansi-escapes: 5.0.0
cli-cursor: 4.0.0
slice-ansi: 5.0.0
strip-ansi: 7.0.1
wrap-ansi: 8.0.1
dev: false
/longest-streak/3.0.1:
resolution: {integrity: sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==}
@ -14768,6 +14843,10 @@ packages:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
/node-fetch-native/0.1.7:
resolution: {integrity: sha512-hps7dFJM0IEF056JftDSSjWDAwW9v2clwHoUJiHyYgl+ojoqjKyWybljMlpTmlC1O+864qovNlRLyAIjRxu9Ag==}
dev: false
/node-fetch/2.6.7:
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
engines: {node: 4.x || >=6.0.0}
@ -15248,6 +15327,10 @@ packages:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
dev: false
/pathe/0.3.8:
resolution: {integrity: sha512-c71n61F1skhj/jzZe+fWE9XDoTYjWbUwIKVwFftZ5IOgiX44BVkTkD+/803YDgR50tqeO4eXWxLyVHBLWQAD1g==}
dev: false
/pathval/1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
@ -15806,6 +15889,15 @@ packages:
engines: {node: ^14.13.1 || >=16.0.0}
dev: true
/pretty-format/27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
ansi-regex: 5.0.1
ansi-styles: 5.2.0
react-is: 17.0.2
dev: true
/pretty-format/3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
dev: false
@ -15936,6 +16028,10 @@ packages:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: false
/react-is/17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: true
/react-lifecycles-compat/3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false
@ -16704,6 +16800,23 @@ packages:
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
engines: {node: '>=12'}
/slice-ansi/4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
dev: true
/slice-ansi/5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
dependencies:
ansi-styles: 6.1.1
is-fullwidth-code-point: 4.0.0
dev: false
/smart-buffer/4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@ -16977,7 +17090,6 @@ packages:
/strip-final-newline/2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: false
/strip-final-newline/3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
@ -17253,12 +17365,10 @@ packages:
/tinypool/0.2.4:
resolution: {integrity: sha512-Vs3rhkUH6Qq1t5bqtb816oT+HeJTXfwt2cbPH17sWHIYKTotQIFPk3tf2fgqRrVyMDVOc1EnPgzIxfIulXVzwQ==}
engines: {node: '>=14.0.0'}
dev: false
/tinyspy/1.0.2:
resolution: {integrity: sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==}
engines: {node: '>=14.0.0'}
dev: false
/tmp/0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
@ -17304,7 +17414,6 @@ packages:
/tree-kill/1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
dev: false
/trim-lines/3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@ -17554,6 +17663,11 @@ packages:
engines: {node: '>=8'}
dev: true
/type-fest/1.4.0:
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
engines: {node: '>=10'}
dev: false
/type-fest/2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
@ -18003,7 +18117,6 @@ packages:
- stylus
- supports-color
- terser
dev: false
/vm2/3.9.11:
resolution: {integrity: sha512-PFG8iJRSjvvBdisowQ7iVF580DXb1uCIiGaXgm7tynMR1uTBlv7UJlB1zdv5KJ+Tmq1f0Upnj3fayoEOPpCBKg==}