Refactor create-astro (#6082)

* refactor: new version of create-astro

* chore: update README

* fix(create-astro): update project name logic

* test(create-astro): fix test on windows

* test(create-astro): fix test on windows

* test(create-astro): remove unused import

* chore: remove log

* chore: increase test timeout

* fix: message when skipping

* fix: message for env.d.ts file

* fix: always hard exit

* fix: return from next-steps

* chore: add message

* refactor dependencies, bundle create-astro

* chore: disable create-astro typings

* chore: switch to arg

* chore: update message

* fix: split typescript into two steps, fix context test

* chore: update wording

* chore: update wording

* Update packages/create-astro/src/actions/dependencies.ts

Co-authored-by: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com>

* refactor: move tests back to mocha/chai

* chore: update cli-kit

* update test script

* chore: add comment about setStdout

* chore: update cli-kit

* Update packages/create-astro/src/messages.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update packages/create-astro/src/messages.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* chore: update lockfile

* fix(create-astro): support scoped package names, improve project-name tests

* better git initialization

* update cli-kit

---------

Co-authored-by: Nate Moore <nate@astro.build>
Co-authored-by: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Nate Moore 2023-02-06 10:19:37 -06:00 committed by GitHub
parent 91dc0f4015
commit 8d2187d8b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1230 additions and 1324 deletions

View file

@ -0,0 +1,5 @@
---
'create-astro': major
---
Redesigned `create-astro` experience

View file

@ -18,15 +18,15 @@ yarn create astro
```bash
# npm 6.x
npm create astro@latest my-astro-project --template starter
npm create astro@latest my-astro-project --template minimal
# npm 7+, extra double-dash is needed:
npm create astro@latest my-astro-project -- --template starter
npm create astro@latest my-astro-project -- --template minimal
# yarn
yarn create astro my-astro-project --template starter
yarn create astro my-astro-project --template minimal
```
[Check out the full list][examples] of example starter templates, available on GitHub.
[Check out the full list][examples] of example templates, available on GitHub.
You can also use any GitHub repo as a template:
@ -40,26 +40,13 @@ 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) |
| `--fancy` | For Windows users, `--fancy` will enable full unicode support |
| `--typescript` | Specify the [tsconfig][typescript] to use |
| `--yes`/`-y` | Skip prompts and use default values |
### 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.
```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
```
| `--template <name> | Specify your template. |
| `--install / --no-install | Install dependencies (or not). |
| `--git / --no-git | Initialize git repo (or not). |
| `--yes (-y) | Skip all prompt by accepting defaults. |
| `--no (-n) | Skip all prompt by declining defaults. |
| `--dry-run | Walk through steps without executing. |
| `--skip-houston | Skip Houston animation. |
[examples]: https://github.com/withastro/astro/tree/main/examples
[typescript]: https://github.com/withastro/astro/tree/main/packages/astro/tsconfigs

@ -0,0 +1 @@
Subproject commit 9a401ddf2e7896d7928eea910c61b5d5a29481a1

View file

@ -14,44 +14,36 @@
"exports": {
".": "./create-astro.mjs"
},
"main": "./create-astro.mjs",
"bin": {
"create-astro": "./create-astro.mjs"
},
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"build": "astro-scripts build \"src/index.ts\" --bundle && tsc",
"build:ci": "astro-scripts build \"src/index.ts\" --bundle",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 20000"
"test": "mocha --exit --timeout 20000 --parallel"
},
"files": [
"dist",
"create-astro.js",
"tsconfigs"
"create-astro.js"
],
"//a": "MOST PACKAGES SHOULD GO IN DEV_DEPENDENCIES! THEY WILL BE BUNDLED.",
"//b": "DEPENDENCIES IS FOR UNBUNDLED PACKAGES",
"dependencies": {
"@astrojs/cli-kit": "^0.1.6",
"chalk": "^5.0.1",
"comment-json": "^4.2.3",
"@astrojs/cli-kit": "^0.2.2",
"chai": "^4.3.6",
"execa": "^6.1.0",
"giget": "^1.0.0",
"kleur": "^4.1.4",
"ora": "^6.1.0",
"prompts": "^2.4.2",
"strip-ansi": "^7.0.1",
"which-pm-runs": "^1.1.0",
"yargs-parser": "^21.0.1"
"mocha": "^9.2.2"
},
"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",
"arg": "^5.0.2",
"astro-scripts": "workspace:*",
"chai": "^4.3.6",
"mocha": "^9.2.2",
"uvu": "^0.5.3"
"strip-ansi": "^7.0.1",
"strip-json-comments": "^5.0.0",
"which-pm-runs": "^1.1.0"
},
"engines": {
"node": ">=16.12.0"

View file

@ -0,0 +1,101 @@
import os from 'node:os';
import arg from 'arg';
import detectPackageManager from 'which-pm-runs';
import { prompt } from '@astrojs/cli-kit';
import { getName, getVersion } from '../messages.js';
export interface Context {
help: boolean;
prompt: typeof prompt;
cwd: string;
pkgManager: string;
username: string;
version: string;
skipHouston: boolean;
dryRun?: boolean;
yes?: boolean;
projectName?: string;
template?: string;
ref: string;
install?: boolean;
git?: boolean;
typescript?: string;
stdin?: typeof process.stdin;
stdout?: typeof process.stdout;
exit(code: number): never;
}
export async function getContext(argv: string[]): Promise<Context> {
const flags = arg({
'--template': String,
'--ref': String,
'--yes': Boolean,
'--no': Boolean,
'--install': Boolean,
'--no-install': Boolean,
'--git': Boolean,
'--no-git': Boolean,
'--typescript': String,
'--skip-houston': Boolean,
'--dry-run': Boolean,
'--help': Boolean,
'--fancy': Boolean,
'-y': '--yes',
'-n': '--no',
'-h': '--help',
}, { argv, permissive: true });
const pkgManager = detectPackageManager()?.name ?? 'npm';
const [username, version] = await Promise.all([getName(), getVersion()]);
let cwd = flags['_'][0] as string;
let {
'--help': help = false,
'--template': template,
'--no': no,
'--yes': yes,
'--install': install,
'--no-install': noInstall,
'--git': git,
'--no-git': noGit,
'--typescript': typescript,
'--fancy': fancy,
'--skip-houston': skipHouston,
'--dry-run': dryRun,
'--ref': ref,
} = flags;
let projectName = cwd;
if (no) {
yes = false;
if (install == undefined) install = false;
if (git == undefined) git = false;
if (typescript == undefined) typescript = 'strict';
}
skipHouston = ((os.platform() === 'win32' && !fancy) || skipHouston) ?? [yes, no, install, git, typescript].some((v) => v !== undefined);
const context: Context = {
help,
prompt,
pkgManager,
username,
version,
skipHouston,
dryRun,
projectName,
template,
ref: ref ?? 'latest',
yes,
install: install ?? (noInstall ? false : undefined),
git: git ?? (noGit ? false : undefined),
typescript,
cwd,
exit(code) {
process.exit(code);
}
}
return context;
}

View file

@ -0,0 +1,43 @@
import type { Context } from "./context";
import { title, info, spinner } from '../messages.js';
import { execa } from 'execa';
export async function dependencies(ctx: Pick<Context, 'install'|'yes'|'prompt'|'pkgManager'|'cwd'|'dryRun'>) {
let deps = ctx.install ?? ctx.yes;
if (deps === undefined) {
({ deps } = await ctx.prompt({
name: 'deps',
type: 'confirm',
label: title('deps'),
message: `Install dependencies?`,
hint: 'recommended',
initial: true,
}));
ctx.install = deps;
}
if (ctx.dryRun) {
await info('--dry-run', `Skipping dependency installation`);
} else if (deps) {
await spinner({
start: `Dependencies installing with ${ctx.pkgManager}...`,
end: 'Dependencies installed',
while: () => install({ pkgManager: ctx.pkgManager, cwd: ctx.cwd }),
});
} else {
await info(
ctx.yes === false ? 'deps [skip]' : 'No problem!',
'Remember to install dependencies after setup.'
);
}
}
async function install({ pkgManager, cwd }: { pkgManager: string, cwd: string }) {
const installExec = execa(pkgManager, ['install'], { cwd });
return new Promise<void>((resolve, reject) => {
installExec.on('error', (error) => reject(error));
installExec.on('close', () => resolve());
});
}

View file

@ -0,0 +1,48 @@
import type { Context } from "./context";
import fs from 'node:fs';
import path from 'node:path';
import { color } from '@astrojs/cli-kit';
import { title, info, spinner } from '../messages.js';
import { execa } from 'execa';
export async function git(ctx: Pick<Context, 'cwd'|'git'|'yes'|'prompt'|'dryRun'>) {
if (fs.existsSync(path.join(ctx.cwd, '.git'))) {
await info('Nice!', `Git has already been initialized`);
return
}
let _git = ctx.git ?? ctx.yes;
if (_git === undefined) {
({ git: _git } = await ctx.prompt({
name: 'git',
type: 'confirm',
label: title('git'),
message: `Initialize a new git repository?`,
hint: 'optional',
initial: true,
}));
}
if (ctx.dryRun) {
await info('--dry-run', `Skipping Git initialization`);
} else if (_git) {
await spinner({
start: 'Git initializing...',
end: 'Git initialized',
while: () => init({ cwd: ctx.cwd }),
});
} else {
await info(
ctx.yes === false ? 'git [skip]' : 'Sounds good!',
`You can always run ${color.reset('git init')}${color.dim(' manually.')}`
);
}
}
async function init({ cwd }: { cwd: string }) {
try {
await execa('git', ['init'], { cwd, stdio: 'ignore' });
await execa('git', ['add', '-A'], { cwd, stdio: 'ignore' });
await execa('git', ['commit', '-m', 'Initial commit from Astro', '--author="houston[bot] <astrobot-houston@users.noreply.github.com>"'], { cwd, stdio: 'ignore' });
} catch (e) {}
}

View file

@ -0,0 +1,20 @@
import { printHelp } from '../messages.js';
export function help() {
printHelp({
commandName: 'create-astro',
usage: '[dir] [...flags]',
headline: 'Scaffold Astro projects.',
tables: {
Flags: [
['--template <name>', 'Specify your template.'],
['--install / --no-install', 'Install dependencies (or not).'],
['--git / --no-git', 'Initialize git repo (or not).'],
['--yes (-y)', 'Skip all prompt by accepting defaults.'],
['--no (-n)', 'Skip all prompt by declining defaults.'],
['--dry-run', 'Walk through steps without executing.'],
['--skip-houston', 'Skip Houston animation.'],
],
},
});
}

View file

@ -0,0 +1,23 @@
import { type Context } from './context';
import { banner, welcome, say } from '../messages.js';
import { label, color } from '@astrojs/cli-kit';
import { random } from '@astrojs/cli-kit/utils';
export async function intro(ctx: Pick<Context, 'skipHouston'|'version'|'username'>) {
if (!ctx.skipHouston) {
await say([
[
'Welcome',
'to',
label('astro', color.bgGreen, color.black),
color.green(`v${ctx.version}`) + ',',
`${ctx.username}!`,
],
random(welcome),
]);
await banner(ctx.version);
} else {
await banner(ctx.version);
}
}

View file

@ -0,0 +1,15 @@
import { Context } from "./context";
import path from 'node:path';
import { nextSteps, say } from '../messages.js';
export async function next(ctx: Pick<Context, 'cwd'|'pkgManager'|'skipHouston'>) {
let projectDir = path.relative(process.cwd(), ctx.cwd);
const devCmd = ctx.pkgManager === 'npm' ? 'npm run dev' : `${ctx.pkgManager} dev`;
await nextSteps({ projectDir, devCmd });
if (!ctx.skipHouston) {
await say(['Good luck out there, astronaut! 🚀']);
}
return;
}

View file

@ -0,0 +1,58 @@
import type { Context } from "./context";
import { color, generateProjectName } from '@astrojs/cli-kit';
import { title, info, log } from '../messages.js';
import path from 'node:path';
import { isEmpty, toValidName } from './shared.js';
export async function projectName(ctx: Pick<Context, 'cwd'|'prompt'|'projectName'|'exit'>) {
await checkCwd(ctx.cwd);
if (!ctx.cwd || !isEmpty(ctx.cwd)) {
if (!isEmpty(ctx.cwd)) {
await info('Hmm...', `${color.reset(`"${ctx.cwd}"`)}${color.dim(` is not empty!`)}`);
}
const { name } = await ctx.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;
},
});
ctx.cwd = name!;
ctx.projectName = toValidName(name!);
} else {
let name = ctx.cwd;
if (name === '.' || name === './') {
const parts = process.cwd().split(path.sep);
name = parts[parts.length - 1];
} else if (name.startsWith('./') || name.startsWith('../')) {
const parts = name.split('/');
name = parts[parts.length - 1];
}
ctx.projectName = toValidName(name);
}
if (!ctx.cwd) {
ctx.exit(1);
}
}
async function checkCwd(cwd: string | undefined) {
const empty = cwd && isEmpty(cwd);
if (empty) {
log('');
await info('dir', `Using ${color.reset(cwd)}${color.dim(' as project directory')}`);
}
return empty;
}

View file

@ -0,0 +1,61 @@
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 isValidName(projectName: string) {
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
projectName,
)
}
export function toValidName(projectName: string) {
if (isValidName(projectName)) return projectName;
return projectName
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z\d\-~]+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
}

View file

@ -0,0 +1,94 @@
/* eslint no-console: 'off' */
import type { Context } from "./context";
import fs from 'node:fs';
import path from 'node:path';
import { downloadTemplate } from 'giget';
import { error } from '../messages.js';
import { color } from '@astrojs/cli-kit';
import { title, info, spinner } from '../messages.js';
export async function template(ctx: Pick<Context, 'template'|'prompt'|'dryRun'|'exit'|'exit'>) {
if (!ctx.template) {
const { template: tmpl } = await ctx.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' },
],
});
ctx.template = tmpl;
} else {
await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`);
}
if (ctx.dryRun) {
await info('--dry-run', `Skipping template copying`);
} else if (ctx.template) {
await spinner({
start: 'Template copying...',
end: 'Template copied',
while: () => copyTemplate(ctx.template!, ctx as Context),
});
} else {
ctx.exit(1);
}
}
// 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(tmpl: string, ctx: Context) {
const ref = ctx.ref || 'latest';
const isThirdParty = tmpl.includes('/');
const templateTarget = isThirdParty
? tmpl
: `github:withastro/astro/examples/${tmpl}#${ref}`;
// Copy
if (!ctx.dryRun) {
try {
await downloadTemplate(templateTarget, {
force: true,
provider: 'github',
cwd: ctx.cwd,
dir: '.',
})
} catch (err: any) {
fs.rmdirSync(ctx.cwd);
if (err.message.includes('404')) {
await error('Error', `Template ${color.reset(tmpl)} ${color.dim('does not exist!')}`);
} else {
console.error(err.message);
}
ctx.exit(1);
}
// Post-process in parallel
const removeFiles = FILES_TO_REMOVE.map(async (file) => {
const fileLoc = path.resolve(path.join(ctx.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(ctx.cwd, file));
if (fs.existsSync(fileLoc)) {
return update(fileLoc, { name: ctx.projectName! })
}
})
await Promise.all([...removeFiles, ...updateFiles]);
}
}

View file

@ -0,0 +1,91 @@
import type { Context } from "./context";
import fs from 'node:fs'
import { readFile } from 'node:fs/promises'
import path from 'node:path';
import stripJsonComments from 'strip-json-comments';
import { color } from '@astrojs/cli-kit';
import { title, info, error, typescriptByDefault, spinner } from '../messages.js';
export async function typescript(ctx: Pick<Context, 'typescript'|'yes'|'prompt'|'dryRun'|'cwd'|'exit'>) {
let ts = ctx.typescript ?? (typeof ctx.yes !== 'undefined' ? 'strict' : undefined);
if (ts === undefined) {
const { useTs } = await ctx.prompt({
name: 'useTs',
type: 'confirm',
label: title('ts'),
message: `Do you plan to write TypeScript?`,
initial: true,
});
if (!useTs) {
await typescriptByDefault();
return;
}
({ ts } = await ctx.prompt({
name: 'ts',
type: 'select',
label: title('use'),
message: `How strict should TypeScript be?`,
initial: 'strict',
choices: [
{ value: 'strict', label: 'Strict', hint: `(recommended)` },
{ value: 'strictest', label: 'Strictest' },
{ value: 'base', label: 'Relaxed' },
],
}));
} else {
if (!['strict', 'strictest', 'relaxed', 'default', 'base'].includes(ts)) {
if (!ctx.dryRun) {
fs.rmSync(ctx.cwd, { recursive: true, force: true });
}
error(
'Error',
`Unknown TypeScript option ${color.reset(ts)}${color.dim(
'! Expected strict | strictest | relaxed'
)}`
);
ctx.exit(1);
}
await info('ts', `Using ${color.reset(ts)}${color.dim(' TypeScript configuration')}`);
}
if (ctx.dryRun) {
await info('--dry-run', `Skipping TypeScript setup`);
} else if (ts && ts !== 'unsure') {
if (ts === 'relaxed' || ts === 'default') {
ts = 'base';
}
await spinner({
start: 'TypeScript customizing...',
end: 'TypeScript customized',
while: () => setupTypeScript(ts!, { cwd: ctx.cwd }),
});
} else {
}
}
export async function setupTypeScript(value: string, { cwd }: { cwd: string }) {
const templateTSConfigPath = path.join(cwd, 'tsconfig.json');
try {
const data = await readFile(templateTSConfigPath, { encoding: 'utf-8' })
const templateTSConfig = JSON.parse(stripJsonComments(data));
if (templateTSConfig && typeof templateTSConfig === 'object') {
const result = Object.assign(templateTSConfig, {
extends: `astro/tsconfigs/${value}`,
});
fs.writeFileSync(templateTSConfigPath, JSON.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")
}
} catch (err) {
if (err && (err as any).code === 'ENOENT') {
// If the template doesn't have a tsconfig.json, let's add one instead
fs.writeFileSync(
templateTSConfigPath,
JSON.stringify({ extends: `astro/tsconfigs/${value}` }, null, 2)
);
}
}
}

View file

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

View file

@ -1,398 +1,59 @@
/* eslint no-console: 'off' */
import { color, generateProjectName, label, say } from '@astrojs/cli-kit';
import { forceUnicode, random } from '@astrojs/cli-kit/utils';
import { assign, parse, stringify } from 'comment-json';
import { execa, execaCommand } from 'execa';
import fs from 'fs';
import { downloadTemplate } from 'giget';
import { bold, dim, green, reset, yellow } from 'kleur/colors';
import ora from 'ora';
import { platform } from 'os';
import path from 'path';
import prompts from 'prompts';
import detectPackageManager from 'which-pm-runs';
import yargs from 'yargs-parser';
import { loadWithRocketGradient, rocketAscii } from './gradient.js';
import { logger } from './logger.js';
import {
banner,
getName,
getVersion,
info,
nextSteps,
typescriptByDefault,
welcome,
} from './messages.js';
import { TEMPLATES } from './templates.js';
import { getContext } from './actions/context.js';
// 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.
const cleanArgv = process.argv.filter((arg) => arg !== '--');
const args = yargs(cleanArgv, { boolean: ['fancy', 'y'], alias: { y: 'yes' } });
// Always skip Houston on Windows (for now)
if (platform() === 'win32') args.skipHouston = true;
prompts.override(args);
import { setStdout } from './messages.js';
import { help } from './actions/help.js';
import { intro } from './actions/intro.js';
import { projectName } from './actions/project-name.js';
import { template } from './actions/template.js'
import { dependencies } from './actions/dependencies.js';
import { git } from './actions/git.js';
import { typescript, setupTypeScript } from './actions/typescript.js';
import { next } from './actions/next-steps.js';
// Enable full unicode support if the `--fancy` flag is passed
if (args.fancy) {
forceUnicode();
}
const exit = () => process.exit(0)
process.on('SIGINT', exit)
process.on('SIGTERM', exit)
export function mkdirp(dir: string) {
try {
fs.mkdirSync(dir, { recursive: true });
} catch (e: any) {
if (e.code === 'EEXIST') return;
throw e;
}
}
// 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 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.
// 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.
// 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 [username, version] = await Promise.all([getName(), getVersion()]);
logger.debug('Verbose logging turned on');
if (!args.skipHouston) {
await say(
[
[
'Welcome',
'to',
label('astro', color.bgGreen, color.black),
color.green(`v${version}`) + ',',
`${username}!`,
],
random(welcome),
],
{ hat: args.fancy ? '🎩' : undefined }
);
await banner(version);
// 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 versions.
const cleanArgv = process.argv.slice(2).filter((arg) => arg !== '--');
const ctx = await getContext(cleanArgv);
if (ctx.help) {
help();
return;
}
let cwd = args['_'][2] as string;
const steps = [
intro,
projectName,
template,
dependencies,
git,
typescript,
next
]
if (cwd && isValidProjectDirectory(cwd)) {
let acknowledgeProjectDir = ora({
color: 'green',
text: `Using ${bold(cwd)} as project directory.`,
});
acknowledgeProjectDir.succeed();
}
if (!cwd || !isValidProjectDirectory(cwd)) {
const notEmptyMsg = (dirPath: string) => `"${bold(dirPath)}" is not empty!`;
if (!isValidProjectDirectory(cwd)) {
let rejectProjectDir = ora({ color: 'red', text: notEmptyMsg(cwd) });
rejectProjectDir.fail();
}
const dirResponse = await prompts(
{
type: 'text',
name: 'directory',
message: 'Where would you like to create your new project?',
initial: generateProjectName(),
validate(value) {
if (!isValidProjectDirectory(value)) {
return notEmptyMsg(value);
}
return true;
},
},
{ onCancel: () => ora().info(dim('Operation cancelled. See you later, astronaut!')) }
);
cwd = dirResponse.directory;
}
if (!cwd) {
ora().info(dim('No directory provided. See you later, astronaut!'));
process.exit(1);
}
const options = await prompts(
[
{
type: 'select',
name: 'template',
message: 'How would you like to setup your new project?',
choices: TEMPLATES,
},
],
{ onCancel: () => ora().info(dim('Operation cancelled. See you later, astronaut!')) }
);
if (!options.template || options.template === true) {
ora().info(dim('No template provided. See you later, astronaut!'));
process.exit(1);
}
let templateSpinner = await loadWithRocketGradient('Copying project files...');
const hash = args.commit ? `#${args.commit}` : '';
const isThirdParty = options.template.includes('/');
const templateTarget = isThirdParty
? options.template
: `withastro/astro/examples/${options.template}#latest`;
// Copy
if (!args.dryRun) {
try {
await downloadTemplate(`${templateTarget}${hash}`, {
force: true,
provider: 'github',
cwd,
dir: '.',
});
} catch (err: any) {
fs.rmdirSync(cwd);
if (err.message.includes('404')) {
console.error(`Could not find template ${color.underline(options.template)}!`);
if (isThirdParty) {
const hasBranch = options.template.includes('#');
if (hasBranch) {
console.error('Are you sure this GitHub repo and branch exist?');
} else {
console.error(
`Are you sure this GitHub repo exists?` +
`This command uses the ${color.bold('main')} branch by default.\n` +
`If the repo doesn't have a main branch, specify a custom branch name:\n` +
color.underline(options.template + color.bold('#branch-name'))
);
}
}
} else {
console.error(err.message);
}
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, {});
}
})
);
}
templateSpinner.text = green('Template copied!');
templateSpinner.succeed();
const install = args.y
? true
: (
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);
},
}
)
).install;
if (args.dryRun) {
ora().info(dim(`--dry-run enabled, skipping.`));
} else if (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();
} else {
await info('No problem!', 'Remember to install dependencies after setup.');
}
const gitResponse = args.y
? true
: (
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);
},
}
)
).git;
if (args.dryRun) {
ora().info(dim(`--dry-run enabled, skipping.`));
} else if (gitResponse) {
// Add a check to see if there is already a .git directory and skip 'git init' if yes (with msg to output)
const gitDir = './.git';
if (fs.existsSync(gitDir)) {
ora().info(dim('A .git directory already exists. Skipping creating a new Git repository.'));
} else {
await execaCommand('git init', { cwd });
ora().succeed('Git repository created!');
}
} else {
await info(
'Sounds good!',
`You can come back and run ${color.reset(`git init`)}${color.dim(' later.')}`
);
}
if (args.y && !args.typescript) {
ora().warn(dim('--typescript <choice> missing. Defaulting to "strict"'));
args.typescript = 'strict';
}
let tsResponse =
args.typescript ||
(
await prompts(
{
type: 'select',
name: 'typescript',
message: 'How would you like to setup TypeScript?',
choices: [
{ value: 'strict', title: 'Strict', description: '(recommended)' },
{ value: 'strictest', title: 'Strictest' },
{ value: 'base', title: 'Relaxed' },
{ value: 'unsure', title: 'Help me choose' },
],
},
{
onCancel: () => {
ora().info(
dim(
'Operation cancelled. Your project folder has been created but no TypeScript configuration file was created.'
)
);
process.exit(1);
},
}
)
).typescript;
if (tsResponse === 'unsure') {
await typescriptByDefault();
tsResponse = 'base';
}
if (args.dryRun) {
ora().info(dim(`--dry-run enabled, skipping.`));
} else if (tsResponse) {
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 ?? 'base'}` }, null, 2)
);
return;
}
const templateTSConfig = parse(data.toString());
if (templateTSConfig && typeof templateTSConfig === 'object') {
const result = assign(templateTSConfig, {
extends: `astro/tsconfigs/${tsResponse ?? 'base'}`,
});
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!');
}
let projectDir = path.relative(process.cwd(), cwd);
const devCmd = pkgManager === 'npm' ? 'npm run dev' : `${pkgManager} dev`;
await nextSteps({ projectDir, devCmd });
if (!args.skipHouston) {
await say(['Good luck out there, astronaut!']);
for (const step of steps) {
await step(ctx)
}
process.exit(0);
}
function emojiWithFallback(char: string, fallback: string) {
return process.platform !== 'win32' ? char : fallback;
export {
setStdout,
getContext,
intro,
projectName,
template,
dependencies,
git,
typescript,
setupTypeScript,
next
}

View file

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

View file

@ -1,126 +1,183 @@
/* eslint no-console: 'off' */
import { color, label } from '@astrojs/cli-kit';
import { sleep } from '@astrojs/cli-kit/utils';
import { exec } from 'node:child_process';
import { get } from 'node:https';
import { color, label, spinner as load, say as houston } from '@astrojs/cli-kit';
import { sleep, align } 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 unique!`,
`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 function getName() {
return 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 stdout = process.stdout;
/** @internal Used to mock `process.stdout.write` for testing purposes */
export function setStdout(writable: typeof process.stdout) {
stdout = writable;
}
export async function say(messages: string|string[], { clear = false, hat = '' } = {}) {
return houston(messages, { clear, hat, stdout });
}
export async function spinner(args: { start: string; end: string; while: (...args: any) => Promise<any>; }) {
await load(args, { stdout });
}
export const title = (text: string) => align(label(text), 'end', 7) + ' ';
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 build the web we want.`,
`Let's make the web weird!`,
`Let's make the web a better place!`,
`Let's create a new project!`,
`Let's create something unique!`,
`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<string>((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 function getVersion() {
return 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 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 log = (message: string) => stdout.write(message + "\n");
export const banner = async (version: string) => 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 (stdout.columns < 80) {
log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(prefix)}`);
log(`${' '.repeat(9)}${color.dim(text)}`);
} else {
log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(prefix)} ${color.dim(text)}`);
}
}
export async function banner(version: string) {
return console.log(
`\n${label('astro', color.bgGreen, color.black)} ${color.green(
color.bold(`v${version}`)
)} ${color.bold('Launch sequence initiated.')}\n`
);
export const error = async (prefix: string, text: string) => {
if (stdout.columns < 80) {
log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)}`);
log(`${' '.repeat(9)}${color.dim(text)}`);
} else {
log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)} ${color.dim(text)}`);
}
}
export async function info(prefix: string, text: string) {
await sleep(100);
if (process.stdout.columns < 80) {
console.log(`${color.cyan('◼')} ${color.cyan(prefix)}`);
console.log(`${' '.repeat(3)}${color.dim(text)}\n`);
} else {
console.log(`${color.cyan('◼')} ${color.cyan(prefix)} ${color.dim(text)}\n`);
export const typescriptByDefault = async () => {
await info(`No worries!`, 'TypeScript is supported in Astro by default,');
log(`${' '.repeat(9)}${color.dim('but you are free to continue writing JavaScript instead.')}`);
await sleep(1000);
}
export const nextSteps = async ({ projectDir, devCmd }: { projectDir: string, devCmd: string }) => {
const max = stdout.columns;
const prefix = max < 80 ? ' ' : ' '.repeat(9);
await sleep(200);
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;
log(enter.join((len > max) ? '\n' + prefix : ' '));
}
log(`${prefix}Run ${color.cyan(devCmd)} to start the dev server. ${color.cyan('CTRL+C')} to stop.`)
await sleep(100);
log(`${prefix}Add frameworks like ${color.cyan(`react`)} or ${color.cyan('tailwind')} using ${color.cyan('astro add')}.`)
await sleep(100);
log(`\n${prefix}Stuck? Join us at ${color.cyan(`https://astro.build/chat`)}`)
await sleep(200);
}
export function printHelp({
commandName,
headline,
usage,
tables,
description,
}: {
commandName: string;
headline?: string;
usage?: string;
tables?: Record<string, [command: string, help: string][]>;
description?: string;
}) {
const linebreak = () => '';
const table = (rows: [string, string][], { padding }: { padding: number }) => {
const split = stdout.columns < 60;
let raw = '';
for (const row of rows) {
if (split) {
raw += ` ${row[0]}\n `;
} else {
raw += `${`${row[0]}`.padStart(padding)}`;
}
raw += ' ' + color.dim(row[1]) + '\n';
}
return raw.slice(0, -1); // remove latest \n
};
let message = [];
if (headline) {
message.push(
linebreak(),
`${title(commandName)} ${color.green(
`v${process.env.PACKAGE_VERSION ?? ''}`
)} ${headline}`
);
}
}
export async function error(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)}`);
if (usage) {
message.push(linebreak(), `${color.green(commandName)} ${color.bold(usage)}`);
}
}
export async function typescriptByDefault() {
await info(`Cool!`, 'Astro comes with TypeScript support enabled by default.');
console.log(
`${' '.repeat(3)}${color.dim(`We'll default to the most relaxed settings for you.`)}`
);
await sleep(300);
}
export async function nextSteps({ 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 : ' '));
if (tables) {
function calculateTablePadding(rows: [string, string][]) {
return rows.reduce((val, [first]) => Math.max(val, first.length), 0);
}
const tableEntries = Object.entries(tables);
const padding = Math.max(...tableEntries.map(([, rows]) => calculateTablePadding(rows)));
for (const [, tableRows] of tableEntries) {
message.push(linebreak(), table(tableRows, { padding }));
}
}
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);
if (description) {
message.push(linebreak(), `${description}`);
}
log(message.join('\n') + '\n');
}

View file

@ -1,5 +0,0 @@
export const TEMPLATES = [
{ value: 'basics', title: 'a few best practices (recommended)' },
{ value: 'blog', title: 'a personal website starter kit' },
{ value: 'minimal', title: 'an empty project' },
];

View file

@ -0,0 +1,62 @@
import { expect } from 'chai';
import os from 'node:os';
import { getContext } from '../dist/index.js';
describe('context', () => {
it('no arguments', async () => {
const ctx = await getContext([]);
expect(ctx.projectName).to.be.undefined;
expect(ctx.template).to.be.undefined;
expect(ctx.skipHouston).to.eq(os.platform() === 'win32');
expect(ctx.dryRun).to.be.undefined;
})
it('project name', async () => {
const ctx = await getContext(['foobar']);
expect(ctx.projectName).to.eq('foobar');
})
it('template', async () => {
const ctx = await getContext(['--template', 'minimal']);
expect(ctx.template).to.eq('minimal');
})
it('skip houston (explicit)', async () => {
const ctx = await getContext(['--skip-houston']);
expect(ctx.skipHouston).to.eq(true);
})
it('skip houston (yes)', async () => {
const ctx = await getContext(['-y']);
expect(ctx.skipHouston).to.eq(true);
})
it('skip houston (no)', async () => {
const ctx = await getContext(['-n']);
expect(ctx.skipHouston).to.eq(true);
})
it('skip houston (install)', async () => {
const ctx = await getContext(['--install']);
expect(ctx.skipHouston).to.eq(true);
})
it('dry run', async () => {
const ctx = await getContext(['--dry-run']);
expect(ctx.dryRun).to.eq(true);
})
it('install', async () => {
const ctx = await getContext(['--install']);
expect(ctx.install).to.eq(true);
})
it('no install', async () => {
const ctx = await getContext(['--no-install']);
expect(ctx.install).to.eq(false);
})
it('git', async () => {
const ctx = await getContext(['--git']);
expect(ctx.git).to.eq(true);
})
it('no git', async () => {
const ctx = await getContext(['--no-git']);
expect(ctx.git).to.eq(false);
})
it('typescript', async () => {
const ctx = await getContext(['--typescript', 'strict']);
expect(ctx.typescript).to.eq('strict');
})
})

View file

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

View file

@ -0,0 +1,42 @@
import { expect } from 'chai';
import { dependencies } from '../dist/index.js';
import { setup } from './utils.js';
describe('dependencies', () => {
const fixture = setup();
it('--yes', async () => {
const context = { cwd: '', yes: true, pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: true }))};
await dependencies(context);
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
})
it('prompt yes', async () => {
const context = { cwd: '', pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: true })), install: undefined };
await dependencies(context);
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
expect(context.install).to.eq(true);
})
it('prompt no', async () => {
const context = { cwd: '', pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: false })), install: undefined };
await dependencies(context);
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
expect(context.install).to.eq(false);
})
it('--install', async () => {
const context = { cwd: '', install: true, pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: false })) };
await dependencies(context);
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
expect(context.install).to.eq(true);
})
it('--no-install', async () => {
const context = { cwd: '', install: false, pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: false })) };
await dependencies(context);
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
expect(context.install).to.eq(false);
})
})

View file

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

View file

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

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,43 @@
import { expect } from 'chai';
import fs from 'fs';
import { execa } from 'execa';
import { git } from '../dist/index.js';
import { setup } from './utils.js';
describe('git', () => {
const fixture = setup();
it('none', async () => {
const context = { cwd: '', dryRun: true, prompt: (() => ({ git: false }))};
await git(context);
expect(fixture.hasMessage('Skipping Git initialization')).to.be.true;
})
it('already initialized', async () => {
const context = { git: true, cwd: './test/fixtures/not-empty', dryRun: true, prompt: (() => ({ git: false }))};
await execa('git', ['init'], { cwd: './test/fixtures/not-empty' });
await git(context);
expect(fixture.hasMessage('Git has already been initialized')).to.be.true;
// Cleanup
fs.rmSync('./test/fixtures/not-empty/.git', { recursive: true, force: true });
})
it('yes (--dry-run)', async () => {
const context = { cwd: '', dryRun: true, prompt: (() => ({ git: true }))};
await git(context);
expect(fixture.hasMessage('Skipping Git initialization')).to.be.true;
})
it('no (--dry-run)', async () => {
const context = { cwd: '', dryRun: true, prompt: (() => ({ git: false }))};
await git(context);
expect(fixture.hasMessage('Skipping Git initialization')).to.be.true;
})
})

View file

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

View file

@ -0,0 +1,20 @@
import { expect } from 'chai';
import { intro } from '../dist/index.js';
import { setup } from './utils.js';
describe('intro', () => {
const fixture = setup();
it('no arguments', async () => {
await intro({ skipHouston: false, version: '0.0.0', username: 'user' });
expect(fixture.hasMessage('Houston:')).to.be.true;
expect(fixture.hasMessage('Welcome to astro v0.0.0')).to.be.true;
})
it('--skip-houston', async () => {
await intro({ skipHouston: true, version: '0.0.0', username: 'user' });
expect(fixture.length()).to.eq(1);
expect(fixture.hasMessage('Houston:')).to.be.false;
expect(fixture.hasMessage('Launch sequence initiated')).to.be.true;
})
})

View file

@ -0,0 +1,20 @@
import { expect } from 'chai';
import { next } from '../dist/index.js';
import { setup } from './utils.js';
describe('next steps', () => {
const fixture = setup();
it('no arguments', async () => {
await next({ skipHouston: false, cwd: './it/fixtures/not-empty', pkgManager: 'npm' });
expect(fixture.hasMessage('Liftoff confirmed.')).to.be.true;
expect(fixture.hasMessage('npm run dev')).to.be.true;
expect(fixture.hasMessage('Good luck out there, astronaut!')).to.be.true;
})
it('--skip-houston', async () => {
await next({ skipHouston: true, cwd: './it/fixtures/not-empty', pkgManager: 'npm' });
expect(fixture.hasMessage('Good luck out there, astronaut!')).to.be.false;
})
})

View file

@ -0,0 +1,79 @@
import { expect } from 'chai';
import { projectName } from '../dist/index.js';
import { setup } from './utils.js';
describe('project name', () => {
const fixture = setup();
it('pass in name', async () => {
const context = { projectName: '', cwd: './foo/bar/baz', prompt: (() => {})};
await projectName(context);
expect(context.cwd).to.eq('./foo/bar/baz');
expect(context.projectName).to.eq('baz');
})
it('dot', async () => {
const context = { projectName: '', cwd: '.', prompt: (() => ({ name: 'foobar' }))};
await projectName(context);
expect(fixture.hasMessage('"." is not empty!')).to.be.true;
expect(context.projectName).to.eq('foobar');
})
it('dot slash', async () => {
const context = { projectName: '', cwd: './', prompt: (() => ({ name: 'foobar' }))};
await projectName(context);
expect(fixture.hasMessage('"./" is not empty!')).to.be.true;
expect(context.projectName).to.eq('foobar');
})
it('empty', async () => {
const context = { projectName: '', cwd: './test/fixtures/empty', prompt: (() => ({ name: 'foobar' }))};
await projectName(context);
expect(fixture.hasMessage('"./test/fixtures/empty" is not empty!')).to.be.false;
expect(context.projectName).to.eq('empty');
})
it('not empty', async () => {
const context = { projectName: '', cwd: './test/fixtures/not-empty', prompt: (() => ({ name: 'foobar' }))};
await projectName(context);
expect(fixture.hasMessage('"./test/fixtures/not-empty" is not empty!')).to.be.true;
expect(context.projectName).to.eq('foobar');
})
it('basic', async () => {
const context = { projectName: '', cwd: '', prompt: (() => ({ name: 'foobar' }))};
await projectName(context);
expect(context.cwd).to.eq('foobar');
expect(context.projectName).to.eq('foobar');
})
it('normalize', async () => {
const context = { projectName: '', cwd: '', prompt: (() => ({ name: 'Invalid Name' }))};
await projectName(context);
expect(context.cwd).to.eq('Invalid Name');
expect(context.projectName).to.eq('invalid-name');
})
it('remove leading/trailing dashes', async () => {
const context = { projectName: '', cwd: '', prompt: (() => ({ name: '(invalid)' }))};
await projectName(context);
expect(context.projectName).to.eq('invalid');
})
it('handles scoped packages', async () => {
const context = { projectName: '', cwd: '', prompt: (() => ({ name: '@astro/site' }))};
await projectName(context);
expect(context.cwd).to.eq('@astro/site');
expect(context.projectName).to.eq('@astro/site');
})
})

View file

@ -0,0 +1,36 @@
import { expect } from 'chai';
import { template } from '../dist/index.js';
import { setup } from './utils.js';
describe('template', () => {
const fixture = setup();
it('none', async () => {
const context = { template: '', cwd: '', dryRun: true, prompt: (() => ({ template: 'blog' })) };
await template(context);
expect(fixture.hasMessage('Skipping template copying')).to.be.true;
expect(context.template).to.eq('blog');
})
it('minimal (--dry-run)', async () => {
const context = { template: 'minimal', cwd: '', dryRun: true, prompt: (() => {})};
await template(context);
expect(fixture.hasMessage('Using minimal as project template')).to.be.true;
})
it('basics (--dry-run)', async () => {
const context = { template: 'basics', cwd: '', dryRun: true, prompt: (() => {})};
await template(context);
expect(fixture.hasMessage('Using basics as project template')).to.be.true;
})
it('blog (--dry-run)', async () => {
const context = { template: 'blog', cwd: '', dryRun: true, prompt: (() => {})};
await template(context);
expect(fixture.hasMessage('Using blog as project template')).to.be.true;
})
})

View file

@ -1,142 +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)) {
// Enter (strict is default)
stdin.write('\n');
wrote = true;
}
if (chunk.includes(PROMPT_MESSAGES.typescriptSucceed)) {
const tsConfigJson = getTsConfig(inputs.emptyDir);
expect(tsConfigJson).to.deep.equal({ extends: 'astro/tsconfigs/strict' });
resolve();
}
});
},
() => lastStdout
);
});
it('should create tsconfig.json when missing', async function () {
return promiseWithTimeout(
(resolve, onStdout) => {
const { stdout } = setup([
inputs.emptyDir,
'--template',
'cassidoo/shopify-react-astro',
'--install',
'0',
'--git',
'0',
'--typescript',
'base',
]);
stdout.on('data', (chunk) => {
onStdout(chunk);
if (chunk.includes(PROMPT_MESSAGES.typescriptSucceed)) {
const tsConfigJson = getTsConfig(inputs.emptyDir);
expect(tsConfigJson).to.deep.equal({ extends: 'astro/tsconfigs/base' });
resolve();
}
});
},
() => lastStdout
);
});
});

View file

@ -0,0 +1,79 @@
import { expect } from 'chai';
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { typescript, setupTypeScript } from '../dist/index.js';
import { setup } from './utils.js';
describe('typescript', () => {
const fixture = setup();
it('none', async () => {
const context = { cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict', useTs: true }))};
await typescript(context);
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
})
it('use false', async () => {
const context = { cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict', useTs: false }))};
await typescript(context);
expect(fixture.hasMessage('No worries')).to.be.true;
})
it('strict', async () => {
const context = { typescript: 'strict', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' }))};
await typescript(context);
expect(fixture.hasMessage('Using strict TypeScript configuration')).to.be.true;
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
})
it('default', async () => {
const context = { typescript: 'default', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' }))};
await typescript(context);
expect(fixture.hasMessage('Using default TypeScript configuration')).to.be.true;
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
})
it('relaxed', async () => {
const context = { typescript: 'relaxed', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' }))};
await typescript(context);
expect(fixture.hasMessage('Using relaxed TypeScript configuration')).to.be.true;
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
})
it('other', async () => {
const context = { typescript: 'other', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' })), exit(code) { throw code }};
let err = null;
try {
await typescript(context);
} catch (e) {
err = e;
}
expect(err).to.eq(1)
})
})
describe('typescript: setup', () => {
it('none', async () => {
const root = new URL('./fixtures/empty/', import.meta.url);
const tsconfig = new URL('./tsconfig.json', root);
await setupTypeScript('strict', { cwd: fileURLToPath(root) })
expect(JSON.parse(fs.readFileSync(tsconfig, { encoding: 'utf-8' }))).to.deep.eq({ "extends": "astro/tsconfigs/strict" });
fs.rmSync(tsconfig);
})
it('exists', async () => {
const root = new URL('./fixtures/not-empty/', import.meta.url);
const tsconfig = new URL('./tsconfig.json', root);
await setupTypeScript('strict', { cwd: fileURLToPath(root) })
expect(JSON.parse(fs.readFileSync(tsconfig, { encoding: 'utf-8' }))).to.deep.eq({ "extends": "astro/tsconfigs/strict" });
fs.writeFileSync(tsconfig, `{}`);
})
})

View file

@ -1,52 +1,29 @@
import { execa } from 'execa';
import { dirname } from 'path';
import { setStdout } from '../dist/index.js';
import stripAnsi from 'strip-ansi';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
export const testDir = dirname(__filename);
export const timeout = 25000;
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 function setup() {
const ctx = { messages: [] };
before(() => {
setStdout(Object.assign({}, process.stdout, {
write(buf) {
ctx.messages.push(stripAnsi(String(buf)).trim())
return true;
}
}))
});
}
beforeEach(() => {
ctx.messages = [];
})
export const PROMPT_MESSAGES = {
directory: 'Where would you like to create your new project?',
template: 'How would you like to setup your new project?',
typescript: 'How would you like to setup TypeScript?',
typescriptSucceed: 'next',
};
export function setup(args = []) {
const { stdout, stdin } = execa('../create-astro.mjs', [...args, '--skip-houston', '--dry-run'], {
cwd: testDir,
});
return {
stdin,
stdout,
messages() {
return ctx.messages
},
length() {
return ctx.messages.length
},
hasMessage(content) {
return !!ctx.messages.find(msg => msg.includes(content))
}
};
}

View file

@ -3,6 +3,8 @@
"include": ["src", "index.d.ts"],
"compilerOptions": {
"allowJs": true,
"emitDeclarationOnly": false,
"noEmit": true,
"target": "ES2020",
"module": "ES2020",
"outDir": "./dist",

View file

@ -2555,50 +2555,33 @@ importers:
packages/create-astro:
specifiers:
'@astrojs/cli-kit': ^0.1.6
'@types/chai': ^4.3.1
'@types/degit': ^2.8.3
'@types/mocha': ^9.1.1
'@types/prompts': ^2.0.14
'@astrojs/cli-kit': ^0.2.2
'@types/which-pm-runs': ^1.0.0
'@types/yargs-parser': ^21.0.0
arg: ^5.0.2
astro-scripts: workspace:*
chai: ^4.3.6
chalk: ^5.0.1
comment-json: ^4.2.3
execa: ^6.1.0
giget: ^1.0.0
kleur: ^4.1.4
mocha: ^9.2.2
ora: ^6.1.0
prompts: ^2.4.2
strip-ansi: ^7.0.1
uvu: ^0.5.3
strip-json-comments: ^5.0.0
which-pm-runs: ^1.1.0
yargs-parser: ^21.0.1
dependencies:
'@astrojs/cli-kit': 0.1.6
chalk: 5.2.0
comment-json: 4.2.3
'@astrojs/cli-kit': 0.2.2
chai: 4.3.7
execa: 6.1.0
giget: 1.0.0
kleur: 4.1.5
ora: 6.1.2
prompts: 2.4.2
strip-ansi: 7.0.1
which-pm-runs: 1.1.0
yargs-parser: 21.1.1
devDependencies:
'@types/chai': 4.3.4
'@types/degit': 2.8.3
'@types/mocha': 9.1.1
'@types/prompts': 2.4.2
'@types/which-pm-runs': 1.0.0
'@types/yargs-parser': 21.0.0
astro-scripts: link:../../scripts
chai: 4.3.7
mocha: 9.2.2
uvu: 0.5.6
devDependencies:
'@types/which-pm-runs': 1.0.0
arg: 5.0.2
astro-scripts: link:../../scripts
strip-ansi: 7.0.1
strip-json-comments: 5.0.0
which-pm-runs: 1.1.0
packages/create-astro/test/fixtures/not-empty:
specifiers: {}
packages/integrations/alpinejs:
specifiers:
@ -3863,8 +3846,8 @@ packages:
lite-youtube-embed: 0.2.0
dev: false
/@astrojs/cli-kit/0.1.6:
resolution: {integrity: sha512-hC0Z7kh4T5QdtfPJVyZ6qmNCqWFYg67zS64AxPm9Y8QVYfeXOdXfL3PaNPGbNtGmczmYJ7cBn/ImgXd/RTTc5g==}
/@astrojs/cli-kit/0.2.2:
resolution: {integrity: sha512-9AniGN+jib2QMRAg4J8WYQxNhDld0zegrb7lig5oNkh1ReDa7rBxaKF9Tor31sjhnGISqavPkKKcQrEm53mzWg==}
dependencies:
chalk: 5.2.0
log-update: 5.0.1
@ -6895,10 +6878,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
@ -7329,7 +7308,6 @@ packages:
/@ungap/promise-all-settled/1.1.2:
resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==}
dev: true
/@ungap/structured-clone/0.3.4:
resolution: {integrity: sha512-TSVh8CpnwNAsPC5wXcIyh92Bv1gq6E9cNDeeLu7Z4h8V4/qWtXJp7y42qljRkqcpmsve1iozwv1wr+3BNdILCg==}
@ -7735,7 +7713,6 @@ packages:
/ansi-colors/4.1.1:
resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==}
engines: {node: '>=6'}
dev: true
/ansi-colors/4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
@ -7756,7 +7733,6 @@ packages:
/ansi-regex/6.0.1:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
dev: false
/ansi-styles/3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
@ -7804,16 +7780,11 @@ packages:
/argparse/2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
/array-iterate/2.0.1:
resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==}
dev: false
/array-timsort/1.0.3:
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
dev: false
/array-union/2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
@ -8104,7 +8075,6 @@ packages:
/browser-stdout/1.3.1:
resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
dev: true
/browserslist/4.21.5:
resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==}
@ -8372,7 +8342,6 @@ packages:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
dev: true
/cliui/8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
@ -8440,17 +8409,6 @@ packages:
/commander/2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
/comment-json/4.2.3:
resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==}
engines: {node: '>= 6'}
dependencies:
array-timsort: 1.0.3
core-util-is: 1.0.3
esprima: 4.0.1
has-own-prop: 2.0.0
repeat-string: 1.6.1
dev: false
/common-ancestor-path/1.0.1:
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
dev: false
@ -8516,6 +8474,7 @@ packages:
/core-util-is/1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: true
/cron-schedule/3.0.6:
resolution: {integrity: sha512-izfGgKyzzIyLaeb1EtZ3KbglkS6AKp9cv7LxmiyoOu+fXfol1tQDC0Cof0enVZGNtudTHW+3lfuW9ZkLQss4Wg==}
@ -8675,7 +8634,6 @@ packages:
dependencies:
ms: 2.1.2
supports-color: 8.1.1
dev: true
/debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
@ -8704,7 +8662,6 @@ packages:
/decamelize/4.0.0:
resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
engines: {node: '>=10'}
dev: true
/decode-named-character-reference/1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
@ -8853,7 +8810,6 @@ packages:
/diff/5.0.0:
resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
engines: {node: '>=0.3.1'}
dev: true
/diff/5.1.0:
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
@ -9952,7 +9908,6 @@ packages:
/flat/5.0.2:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true
dev: true
/flatted/3.2.7:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
@ -10164,7 +10119,6 @@ packages:
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
dev: true
/glob/7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
@ -10297,7 +10251,6 @@ packages:
/growl/1.10.5:
resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==}
engines: {node: '>=4.x'}
dev: true
/gzip-size/6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
@ -10322,11 +10275,6 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
/has-own-prop/2.0.0:
resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==}
engines: {node: '>=8'}
dev: false
/has-package-exports/1.3.0:
resolution: {integrity: sha512-e9OeXPQnmPhYoJ63lXC4wWe34TxEGZDZ3OQX9XRqp2VwsfLl3bQBy7VehLnd34g3ef8CmYlBLGqEMKXuz8YazQ==}
dependencies:
@ -10522,7 +10470,6 @@ packages:
/he/1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
dev: true
/hosted-git-info/2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
@ -10863,7 +10810,6 @@ packages:
/is-plain-obj/2.1.0:
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
engines: {node: '>=8'}
dev: true
/is-plain-obj/4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
@ -10947,7 +10893,6 @@ packages:
/is-unicode-supported/0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
dev: true
/is-unicode-supported/1.3.0:
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
@ -11021,7 +10966,6 @@ packages:
hasBin: true
dependencies:
argparse: 2.0.1
dev: true
/jsesc/0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
@ -11235,7 +11179,6 @@ packages:
dependencies:
chalk: 4.1.2
is-unicode-supported: 0.1.0
dev: true
/log-symbols/5.1.0:
resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
@ -12000,7 +11943,6 @@ packages:
engines: {node: '>=10'}
dependencies:
brace-expansion: 1.1.11
dev: true
/minimatch/5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
@ -12082,7 +12024,6 @@ packages:
yargs: 16.2.0
yargs-parser: 20.2.4
yargs-unparser: 2.0.0
dev: true
/mri/1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
@ -12112,7 +12053,6 @@ packages:
resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: true
/nanoid/3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
@ -13558,11 +13498,6 @@ packages:
unified: 10.1.2
dev: true
/repeat-string/1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
dev: false
/require-directory/2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -13859,7 +13794,6 @@ packages:
resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
dependencies:
randombytes: 2.1.0
dev: true
/server-destroy/1.0.1:
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
@ -14226,7 +14160,6 @@ packages:
engines: {node: '>=12'}
dependencies:
ansi-regex: 6.0.1
dev: false
/strip-bom-string/1.0.0:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
@ -14269,6 +14202,10 @@ packages:
/strip-json-comments/3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
/strip-json-comments/5.0.0:
resolution: {integrity: sha512-V1LGY4UUo0jgwC+ELQ2BNWfPa17TIuwBLg+j1AA/9RPzKINl1lhxVEu2r+ZTTO8aetIsUzE5Qj6LMSBkoGYKKw==}
engines: {node: '>=14.16'}
dev: true
/strnum/1.0.5:
@ -15648,7 +15585,6 @@ packages:
/workerpool/6.2.0:
resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==}
dev: true
/wrangler/2.9.0:
resolution: {integrity: sha512-5nyyR4bXKG/Rwz0dH+nOx4SWvJWmTZVSbceLyTV+ZOH1sd2vvPnnW14NUzTNEjY3XaT93XH+28mc5+UNSYsFHw==}
@ -15785,7 +15721,6 @@ packages:
/yargs-parser/20.2.4:
resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==}
engines: {node: '>=10'}
dev: true
/yargs-parser/21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
@ -15799,7 +15734,6 @@ packages:
decamelize: 4.0.0
flat: 5.0.2
is-plain-obj: 2.1.0
dev: true
/yargs/15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
@ -15829,7 +15763,6 @@ packages:
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 20.2.4
dev: true
/yargs/17.6.2:
resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}

View file

@ -48,6 +48,7 @@ export default async function build(...args) {
);
const noClean = args.includes('--no-clean-dist');
const bundle = args.includes('--bundle');
const forceCJS = args.includes('--force-cjs');
const {
@ -68,7 +69,8 @@ export default async function build(...args) {
if (!isDev) {
await esbuild.build({
...config,
bundle: false,
bundle,
external: bundle ? Object.keys(dependencies) : undefined,
entryPoints,
outdir,
outExtension: forceCJS ? { '.js': '.cjs' } : {},