Compare commits
18 commits
main
...
feat/creat
Author | SHA1 | Date | |
---|---|---|---|
|
5b8583febb | ||
|
a4fad0f2a2 | ||
|
947dc2ec28 | ||
|
bb2db7fef4 | ||
|
59d37ed6b3 | ||
|
9089791aba | ||
|
d0743a9380 | ||
|
097394c14f | ||
|
0476af580d | ||
|
4871c42b21 | ||
|
c66f7ca360 | ||
|
5968c4f8e1 | ||
|
7aa363fc24 | ||
|
562eedd9e2 | ||
|
e76e2c8fbf | ||
|
cc0bfadd59 | ||
|
d5c8663509 | ||
|
d1baaf7801 |
36 changed files with 969 additions and 1214 deletions
5
.changeset/nice-pens-drive.md
Normal file
5
.changeset/nice-pens-drive.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'create-astro': major
|
||||
---
|
||||
|
||||
`create-astro@2.0.0` features a newly redesigned `create-astro` CLI.
|
|
@ -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 there’s no need to create a new empty directory for your project before you begin. If you don’t 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!
|
||||
|
|
|
@ -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"
|
||||
|
|
14
packages/create-astro/src/actions/check-cwd.ts
Normal file
14
packages/create-astro/src/actions/check-cwd.ts
Normal 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;
|
||||
}
|
62
packages/create-astro/src/actions/copy-template.ts
Normal file
62
packages/create-astro/src/actions/copy-template.ts
Normal 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]);
|
||||
}
|
||||
}
|
5
packages/create-astro/src/actions/initialize-git.ts
Normal file
5
packages/create-astro/src/actions/initialize-git.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { execaCommand } from 'execa';
|
||||
|
||||
export default async function initializeGit({ cwd }: { cwd: string }) {
|
||||
return execaCommand('git init', { cwd });
|
||||
}
|
9
packages/create-astro/src/actions/install-deps.ts
Normal file
9
packages/create-astro/src/actions/install-deps.ts
Normal 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());
|
||||
});
|
||||
}
|
30
packages/create-astro/src/actions/setup-typescript.ts
Normal file
30
packages/create-astro/src/actions/setup-typescript.ts
Normal 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")
|
||||
}
|
||||
});
|
||||
}
|
49
packages/create-astro/src/actions/shared.ts
Normal file
49
packages/create-astro/src/actions/shared.ts
Normal 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(/(^\-|\-$)+/, '');
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
};
|
101
packages/create-astro/src/messages.ts
Normal file
101
packages/create-astro/src/messages.ts
Normal 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);
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
];
|
54
packages/create-astro/test/basic.test.ts
Normal file
54
packages/create-astro/test/basic.test.ts
Normal 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();
|
||||
})
|
|
@ -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();
|
41
packages/create-astro/test/dependencies.test.ts
Normal file
41
packages/create-astro/test/dependencies.test.ts
Normal 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()
|
||||
})
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
47
packages/create-astro/test/directory.test.ts
Normal file
47
packages/create-astro/test/directory.test.ts
Normal 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()
|
||||
})
|
|
@ -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);
|
||||
});
|
0
packages/create-astro/test/fixtures/nonempty-safe-dir/module.iml
vendored
Normal file
0
packages/create-astro/test/fixtures/nonempty-safe-dir/module.iml
vendored
Normal file
30
packages/create-astro/test/git.test.ts
Normal file
30
packages/create-astro/test/git.test.ts
Normal 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()
|
||||
})
|
|
@ -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 };
|
1
packages/create-astro/test/setup.ts
Normal file
1
packages/create-astro/test/setup.ts
Normal file
|
@ -0,0 +1 @@
|
|||
import 'cli-testing-library/extend-expect';
|
45
packages/create-astro/test/template.test.ts
Normal file
45
packages/create-astro/test/template.test.ts
Normal 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()
|
||||
})
|
6
packages/create-astro/test/tsconfig.json
Normal file
6
packages/create-astro/test/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals", "cli-testing-library/extend-expect"]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
41
packages/create-astro/test/typescript.test.ts
Normal file
41
packages/create-astro/test/typescript.test.ts
Normal 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();
|
||||
});
|
10
packages/create-astro/test/util.ts
Normal file
10
packages/create-astro/test/util.ts
Normal 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 })) })
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"outDir": "./dist",
|
||||
"declarationDir": "./dist/types"
|
||||
"emitDeclarationOnly": false,
|
||||
"declaration": false
|
||||
}
|
||||
}
|
||||
|
|
8
packages/create-astro/vitest.config.js
Normal file
8
packages/create-astro/vitest.config.js
Normal 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
197
pnpm-lock.yaml
generated
|
@ -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==}
|
||||
|
|
Loading…
Add table
Reference in a new issue