summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/new-ravens-exercise.md5
-rw-r--r--packages/create-astro/README.md35
m---------packages/create-astro/grubby-group0
-rw-r--r--packages/create-astro/package.json36
-rw-r--r--packages/create-astro/src/actions/context.ts101
-rw-r--r--packages/create-astro/src/actions/dependencies.ts43
-rw-r--r--packages/create-astro/src/actions/git.ts48
-rw-r--r--packages/create-astro/src/actions/help.ts20
-rw-r--r--packages/create-astro/src/actions/intro.ts23
-rw-r--r--packages/create-astro/src/actions/next-steps.ts15
-rw-r--r--packages/create-astro/src/actions/project-name.ts58
-rw-r--r--packages/create-astro/src/actions/shared.ts61
-rw-r--r--packages/create-astro/src/actions/template.ts94
-rw-r--r--packages/create-astro/src/actions/typescript.ts91
-rw-r--r--packages/create-astro/src/gradient.ts91
-rw-r--r--packages/create-astro/src/index.ts449
-rw-r--r--packages/create-astro/src/logger.ts147
-rw-r--r--packages/create-astro/src/messages.ts271
-rw-r--r--packages/create-astro/src/templates.ts5
-rw-r--r--packages/create-astro/test/context.test.js62
-rw-r--r--packages/create-astro/test/create-astro.test.js.skipped139
-rw-r--r--packages/create-astro/test/dependencies.test.js42
-rw-r--r--packages/create-astro/test/directory-step.test.js88
-rw-r--r--packages/create-astro/test/external.test.js.skipped27
-rw-r--r--packages/create-astro/test/fixtures/empty/.gitkeep (renamed from packages/create-astro/test/fixtures/select-directory/nonempty-dir/astro-origin-story.php)0
-rw-r--r--packages/create-astro/test/fixtures/not-empty/package.json1
-rw-r--r--packages/create-astro/test/fixtures/not-empty/tsconfig.json1
-rw-r--r--packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/.gitignore0
-rw-r--r--packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/module.iml0
-rw-r--r--packages/create-astro/test/git.test.js43
-rw-r--r--packages/create-astro/test/helpers.js9
-rw-r--r--packages/create-astro/test/intro.test.js20
-rw-r--r--packages/create-astro/test/next.test.js20
-rw-r--r--packages/create-astro/test/project-name.test.js79
-rw-r--r--packages/create-astro/test/template.test.js36
-rw-r--r--packages/create-astro/test/typescript-step.test.js.skipped142
-rw-r--r--packages/create-astro/test/typescript.test.js79
-rw-r--r--packages/create-astro/test/utils.js67
-rw-r--r--packages/create-astro/tsconfig.json2
-rw-r--r--pnpm-lock.yaml107
-rw-r--r--scripts/cmd/build.js4
41 files changed, 1233 insertions, 1328 deletions
diff --git a/.changeset/new-ravens-exercise.md b/.changeset/new-ravens-exercise.md
new file mode 100644
index 000000000..60cf1e64d
--- /dev/null
+++ b/.changeset/new-ravens-exercise.md
@@ -0,0 +1,5 @@
+---
+'create-astro': major
+---
+
+Redesigned `create-astro` experience
diff --git a/packages/create-astro/README.md b/packages/create-astro/README.md
index 4c3a887ff..9d0f75e8e 100644
--- a/packages/create-astro/README.md
+++ b/packages/create-astro/README.md
@@ -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
diff --git a/packages/create-astro/grubby-group b/packages/create-astro/grubby-group
new file mode 160000
+Subproject 9a401ddf2e7896d7928eea910c61b5d5a29481a
diff --git a/packages/create-astro/package.json b/packages/create-astro/package.json
index 80bb1f4bc..48eed59dc 100644
--- a/packages/create-astro/package.json
+++ b/packages/create-astro/package.json
@@ -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"
diff --git a/packages/create-astro/src/actions/context.ts b/packages/create-astro/src/actions/context.ts
new file mode 100644
index 000000000..a73a42e13
--- /dev/null
+++ b/packages/create-astro/src/actions/context.ts
@@ -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;
+}
diff --git a/packages/create-astro/src/actions/dependencies.ts b/packages/create-astro/src/actions/dependencies.ts
new file mode 100644
index 000000000..fb935c208
--- /dev/null
+++ b/packages/create-astro/src/actions/dependencies.ts
@@ -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());
+ });
+}
+
diff --git a/packages/create-astro/src/actions/git.ts b/packages/create-astro/src/actions/git.ts
new file mode 100644
index 000000000..6510a0f24
--- /dev/null
+++ b/packages/create-astro/src/actions/git.ts
@@ -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) {}
+}
diff --git a/packages/create-astro/src/actions/help.ts b/packages/create-astro/src/actions/help.ts
new file mode 100644
index 000000000..3ab4ca8b3
--- /dev/null
+++ b/packages/create-astro/src/actions/help.ts
@@ -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.'],
+ ],
+ },
+ });
+}
diff --git a/packages/create-astro/src/actions/intro.ts b/packages/create-astro/src/actions/intro.ts
new file mode 100644
index 000000000..b3ab88122
--- /dev/null
+++ b/packages/create-astro/src/actions/intro.ts
@@ -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);
+ }
+}
diff --git a/packages/create-astro/src/actions/next-steps.ts b/packages/create-astro/src/actions/next-steps.ts
new file mode 100644
index 000000000..94b0ba71b
--- /dev/null
+++ b/packages/create-astro/src/actions/next-steps.ts
@@ -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;
+}
diff --git a/packages/create-astro/src/actions/project-name.ts b/packages/create-astro/src/actions/project-name.ts
new file mode 100644
index 000000000..c849a9060
--- /dev/null
+++ b/packages/create-astro/src/actions/project-name.ts
@@ -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;
+}
diff --git a/packages/create-astro/src/actions/shared.ts b/packages/create-astro/src/actions/shared.ts
new file mode 100644
index 000000000..838ee5e23
--- /dev/null
+++ b/packages/create-astro/src/actions/shared.ts
@@ -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(/-+$/, '')
+}
diff --git a/packages/create-astro/src/actions/template.ts b/packages/create-astro/src/actions/template.ts
new file mode 100644
index 000000000..805e9ddca
--- /dev/null
+++ b/packages/create-astro/src/actions/template.ts
@@ -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]);
+ }
+}
diff --git a/packages/create-astro/src/actions/typescript.ts b/packages/create-astro/src/actions/typescript.ts
new file mode 100644
index 000000000..0c624f889
--- /dev/null
+++ b/packages/create-astro/src/actions/typescript.ts
@@ -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)
+ );
+ }
+ }
+}
diff --git a/packages/create-astro/src/gradient.ts b/packages/create-astro/src/gradient.ts
deleted file mode 100644
index 539f3f17d..000000000
--- a/packages/create-astro/src/gradient.ts
+++ /dev/null
@@ -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;
-}
diff --git a/packages/create-astro/src/index.ts b/packages/create-astro/src/index.ts
index b760ce9bf..5ba94d53f 100644
--- a/packages/create-astro/src/index.ts
+++ b/packages/create-astro/src/index.ts
@@ -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';
-
-// 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);
-
-// Enable full unicode support if the `--fancy` flag is passed
-if (args.fancy) {
- forceUnicode();
-}
-
-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.
+import { getContext } from './actions/context.js';
+
+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';
+
+const exit = () => process.exit(0)
+process.on('SIGINT', exit)
+process.on('SIGTERM', exit)
+
+// 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);
- }
-
- 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 (!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!']);
- }
+ // 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;
+ }
+
+ const steps = [
+ intro,
+ projectName,
+ template,
+ dependencies,
+ git,
+ typescript,
+ next
+ ]
+
+ 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
}
diff --git a/packages/create-astro/src/logger.ts b/packages/create-astro/src/logger.ts
deleted file mode 100644
index 46f916c42..000000000
--- a/packages/create-astro/src/logger.ts
+++ /dev/null
@@ -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'),
-};
diff --git a/packages/create-astro/src/messages.ts b/packages/create-astro/src/messages.ts
index 2ed0608e5..c70857ada 100644
--- a/packages/create-astro/src/messages.ts
+++ b/packages/create-astro/src/messages.ts
@@ -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;
}
-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 async function say(messages: string|string[], { clear = false, hat = '' } = {}) {
+ return houston(messages, { clear, hat, stdout });
}
-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 async function spinner(args: { start: string; end: string; while: (...args: any) => Promise<any>; }) {
+ await load(args, { stdout });
}
-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 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 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 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)}`);
- }
+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 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 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 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 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 : ' '));
+
+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}`
+ );
}
- 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 (usage) {
+ message.push(linebreak(), `${color.green(commandName)} ${color.bold(usage)}`);
+ }
+
+ 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 }));
+ }
+ }
+
+ if (description) {
+ message.push(linebreak(), `${description}`);
+ }
+
+ log(message.join('\n') + '\n');
}
diff --git a/packages/create-astro/src/templates.ts b/packages/create-astro/src/templates.ts
deleted file mode 100644
index 7dff7c587..000000000
--- a/packages/create-astro/src/templates.ts
+++ /dev/null
@@ -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' },
-];
diff --git a/packages/create-astro/test/context.test.js b/packages/create-astro/test/context.test.js
new file mode 100644
index 000000000..d6cb1c70f
--- /dev/null
+++ b/packages/create-astro/test/context.test.js
@@ -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');
+ })
+})
diff --git a/packages/create-astro/test/create-astro.test.js.skipped b/packages/create-astro/test/create-astro.test.js.skipped
deleted file mode 100644
index 86a64e1f5..000000000
--- a/packages/create-astro/test/create-astro.test.js.skipped
+++ /dev/null
@@ -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();
diff --git a/packages/create-astro/test/dependencies.test.js b/packages/create-astro/test/dependencies.test.js
new file mode 100644
index 000000000..515c42294
--- /dev/null
+++ b/packages/create-astro/test/dependencies.test.js
@@ -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);
+ })
+})
diff --git a/packages/create-astro/test/directory-step.test.js b/packages/create-astro/test/directory-step.test.js
deleted file mode 100644
index 15a0479c8..000000000
--- a/packages/create-astro/test/directory-step.test.js
+++ /dev/null
@@ -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;
- }
- });
- });
- });
-});
diff --git a/packages/create-astro/test/external.test.js.skipped b/packages/create-astro/test/external.test.js.skipped
deleted file mode 100644
index 277e498e0..000000000
--- a/packages/create-astro/test/external.test.js.skipped
+++ /dev/null
@@ -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);
-});
diff --git a/packages/create-astro/test/fixtures/select-directory/nonempty-dir/astro-origin-story.php b/packages/create-astro/test/fixtures/empty/.gitkeep
index e69de29bb..e69de29bb 100644
--- a/packages/create-astro/test/fixtures/select-directory/nonempty-dir/astro-origin-story.php
+++ b/packages/create-astro/test/fixtures/empty/.gitkeep
diff --git a/packages/create-astro/test/fixtures/not-empty/package.json b/packages/create-astro/test/fixtures/not-empty/package.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/packages/create-astro/test/fixtures/not-empty/package.json
@@ -0,0 +1 @@
+{}
diff --git a/packages/create-astro/test/fixtures/not-empty/tsconfig.json b/packages/create-astro/test/fixtures/not-empty/tsconfig.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/packages/create-astro/test/fixtures/not-empty/tsconfig.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/.gitignore b/packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/.gitignore
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/.gitignore
+++ /dev/null
diff --git a/packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/module.iml b/packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/module.iml
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/create-astro/test/fixtures/select-directory/nonempty-safe-dir/module.iml
+++ /dev/null
diff --git a/packages/create-astro/test/git.test.js b/packages/create-astro/test/git.test.js
new file mode 100644
index 000000000..4b048156a
--- /dev/null
+++ b/packages/create-astro/test/git.test.js
@@ -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;
+ })
+})
diff --git a/packages/create-astro/test/helpers.js b/packages/create-astro/test/helpers.js
deleted file mode 100644
index 4f0b6ec3e..000000000
--- a/packages/create-astro/test/helpers.js
+++ /dev/null
@@ -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 };
diff --git a/packages/create-astro/test/intro.test.js b/packages/create-astro/test/intro.test.js
new file mode 100644
index 000000000..af13954d1
--- /dev/null
+++ b/packages/create-astro/test/intro.test.js
@@ -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;
+ })
+})
diff --git a/packages/create-astro/test/next.test.js b/packages/create-astro/test/next.test.js
new file mode 100644
index 000000000..46efdf67f
--- /dev/null
+++ b/packages/create-astro/test/next.test.js
@@ -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;
+ })
+})
diff --git a/packages/create-astro/test/project-name.test.js b/packages/create-astro/test/project-name.test.js
new file mode 100644
index 000000000..38f1359b6
--- /dev/null
+++ b/packages/create-astro/test/project-name.test.js
@@ -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');
+ })
+})
diff --git a/packages/create-astro/test/template.test.js b/packages/create-astro/test/template.test.js
new file mode 100644
index 000000000..53b9777ff
--- /dev/null
+++ b/packages/create-astro/test/template.test.js
@@ -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;
+ })
+})
diff --git a/packages/create-astro/test/typescript-step.test.js.skipped b/packages/create-astro/test/typescript-step.test.js.skipped
deleted file mode 100644
index d9281b21d..000000000
--- a/packages/create-astro/test/typescript-step.test.js.skipped
+++ /dev/null
@@ -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
- );
- });
-});
diff --git a/packages/create-astro/test/typescript.test.js b/packages/create-astro/test/typescript.test.js
new file mode 100644
index 000000000..599214dff
--- /dev/null
+++ b/packages/create-astro/test/typescript.test.js
@@ -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, `{}`);
+ })
+})
diff --git a/packages/create-astro/test/utils.js b/packages/create-astro/test/utils.js
index 1f437fa01..c2cbb7245 100644
--- a/packages/create-astro/test/utils.js
+++ b/packages/create-astro/test/utils.js
@@ -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;
+ }
+ }))
});
-}
-
-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',
-};
+ beforeEach(() => {
+ ctx.messages = [];
+ })
-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))
+ }
};
}
diff --git a/packages/create-astro/tsconfig.json b/packages/create-astro/tsconfig.json
index 8f0cdf74d..25bf60c24 100644
--- a/packages/create-astro/tsconfig.json
+++ b/packages/create-astro/tsconfig.json
@@ -3,6 +3,8 @@
"include": ["src", "index.d.ts"],
"compilerOptions": {
"allowJs": true,
+ "emitDeclarationOnly": false,
+ "noEmit": true,
"target": "ES2020",
"module": "ES2020",
"outDir": "./dist",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1d156cfaa..8a83bba42 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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
+ mocha: 9.2.2
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
+ arg: 5.0.2
astro-scripts: link:../../scripts
- chai: 4.3.7
- mocha: 9.2.2
- uvu: 0.5.6
+ 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==}
diff --git a/scripts/cmd/build.js b/scripts/cmd/build.js
index 1f543d70a..599d22d93 100644
--- a/scripts/cmd/build.js
+++ b/scripts/cmd/build.js
@@ -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' } : {},