Refactor create-astro
(#6082)
* refactor: new version of create-astro * chore: update README * fix(create-astro): update project name logic * test(create-astro): fix test on windows * test(create-astro): fix test on windows * test(create-astro): remove unused import * chore: remove log * chore: increase test timeout * fix: message when skipping * fix: message for env.d.ts file * fix: always hard exit * fix: return from next-steps * chore: add message * refactor dependencies, bundle create-astro * chore: disable create-astro typings * chore: switch to arg * chore: update message * fix: split typescript into two steps, fix context test * chore: update wording * chore: update wording * Update packages/create-astro/src/actions/dependencies.ts Co-authored-by: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com> * refactor: move tests back to mocha/chai * chore: update cli-kit * update test script * chore: add comment about setStdout * chore: update cli-kit * Update packages/create-astro/src/messages.ts Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update packages/create-astro/src/messages.ts Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * chore: update lockfile * fix(create-astro): support scoped package names, improve project-name tests * better git initialization * update cli-kit --------- Co-authored-by: Nate Moore <nate@astro.build> Co-authored-by: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
91dc0f4015
commit
8d2187d8b8
41 changed files with 1230 additions and 1324 deletions
5
.changeset/new-ravens-exercise.md
Normal file
5
.changeset/new-ravens-exercise.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'create-astro': major
|
||||
---
|
||||
|
||||
Redesigned `create-astro` experience
|
|
@ -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
|
||||
|
|
1
packages/create-astro/grubby-group
Submodule
1
packages/create-astro/grubby-group
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 9a401ddf2e7896d7928eea910c61b5d5a29481a1
|
|
@ -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"
|
||||
|
|
101
packages/create-astro/src/actions/context.ts
Normal file
101
packages/create-astro/src/actions/context.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import os from 'node:os';
|
||||
import arg from 'arg';
|
||||
import detectPackageManager from 'which-pm-runs';
|
||||
import { prompt } from '@astrojs/cli-kit';
|
||||
|
||||
import { getName, getVersion } from '../messages.js';
|
||||
|
||||
export interface Context {
|
||||
help: boolean;
|
||||
prompt: typeof prompt;
|
||||
cwd: string;
|
||||
pkgManager: string;
|
||||
username: string;
|
||||
version: string;
|
||||
skipHouston: boolean;
|
||||
dryRun?: boolean;
|
||||
yes?: boolean;
|
||||
projectName?: string;
|
||||
template?: string;
|
||||
ref: string;
|
||||
install?: boolean;
|
||||
git?: boolean;
|
||||
typescript?: string;
|
||||
stdin?: typeof process.stdin;
|
||||
stdout?: typeof process.stdout;
|
||||
exit(code: number): never;
|
||||
}
|
||||
|
||||
|
||||
export async function getContext(argv: string[]): Promise<Context> {
|
||||
const flags = arg({
|
||||
'--template': String,
|
||||
'--ref': String,
|
||||
'--yes': Boolean,
|
||||
'--no': Boolean,
|
||||
'--install': Boolean,
|
||||
'--no-install': Boolean,
|
||||
'--git': Boolean,
|
||||
'--no-git': Boolean,
|
||||
'--typescript': String,
|
||||
'--skip-houston': Boolean,
|
||||
'--dry-run': Boolean,
|
||||
'--help': Boolean,
|
||||
'--fancy': Boolean,
|
||||
|
||||
'-y': '--yes',
|
||||
'-n': '--no',
|
||||
'-h': '--help',
|
||||
}, { argv, permissive: true });
|
||||
|
||||
const pkgManager = detectPackageManager()?.name ?? 'npm';
|
||||
const [username, version] = await Promise.all([getName(), getVersion()]);
|
||||
let cwd = flags['_'][0] as string;
|
||||
let {
|
||||
'--help': help = false,
|
||||
'--template': template,
|
||||
'--no': no,
|
||||
'--yes': yes,
|
||||
'--install': install,
|
||||
'--no-install': noInstall,
|
||||
'--git': git,
|
||||
'--no-git': noGit,
|
||||
'--typescript': typescript,
|
||||
'--fancy': fancy,
|
||||
'--skip-houston': skipHouston,
|
||||
'--dry-run': dryRun,
|
||||
'--ref': ref,
|
||||
} = flags;
|
||||
let projectName = cwd;
|
||||
|
||||
if (no) {
|
||||
yes = false;
|
||||
if (install == undefined) install = false;
|
||||
if (git == undefined) git = false;
|
||||
if (typescript == undefined) typescript = 'strict';
|
||||
}
|
||||
|
||||
skipHouston = ((os.platform() === 'win32' && !fancy) || skipHouston) ?? [yes, no, install, git, typescript].some((v) => v !== undefined);
|
||||
|
||||
const context: Context = {
|
||||
help,
|
||||
prompt,
|
||||
pkgManager,
|
||||
username,
|
||||
version,
|
||||
skipHouston,
|
||||
dryRun,
|
||||
projectName,
|
||||
template,
|
||||
ref: ref ?? 'latest',
|
||||
yes,
|
||||
install: install ?? (noInstall ? false : undefined),
|
||||
git: git ?? (noGit ? false : undefined),
|
||||
typescript,
|
||||
cwd,
|
||||
exit(code) {
|
||||
process.exit(code);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
43
packages/create-astro/src/actions/dependencies.ts
Normal file
43
packages/create-astro/src/actions/dependencies.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { Context } from "./context";
|
||||
|
||||
import { title, info, spinner } from '../messages.js';
|
||||
import { execa } from 'execa';
|
||||
|
||||
export async function dependencies(ctx: Pick<Context, 'install'|'yes'|'prompt'|'pkgManager'|'cwd'|'dryRun'>) {
|
||||
let deps = ctx.install ?? ctx.yes;
|
||||
if (deps === undefined) {
|
||||
({ deps } = await ctx.prompt({
|
||||
name: 'deps',
|
||||
type: 'confirm',
|
||||
label: title('deps'),
|
||||
message: `Install dependencies?`,
|
||||
hint: 'recommended',
|
||||
initial: true,
|
||||
}));
|
||||
ctx.install = deps;
|
||||
}
|
||||
|
||||
if (ctx.dryRun) {
|
||||
await info('--dry-run', `Skipping dependency installation`);
|
||||
} else if (deps) {
|
||||
await spinner({
|
||||
start: `Dependencies installing with ${ctx.pkgManager}...`,
|
||||
end: 'Dependencies installed',
|
||||
while: () => install({ pkgManager: ctx.pkgManager, cwd: ctx.cwd }),
|
||||
});
|
||||
} else {
|
||||
await info(
|
||||
ctx.yes === false ? 'deps [skip]' : 'No problem!',
|
||||
'Remember to install dependencies after setup.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function install({ pkgManager, cwd }: { pkgManager: string, cwd: string }) {
|
||||
const installExec = execa(pkgManager, ['install'], { cwd });
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
installExec.on('error', (error) => reject(error));
|
||||
installExec.on('close', () => resolve());
|
||||
});
|
||||
}
|
||||
|
48
packages/create-astro/src/actions/git.ts
Normal file
48
packages/create-astro/src/actions/git.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import type { Context } from "./context";
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { color } from '@astrojs/cli-kit';
|
||||
import { title, info, spinner } from '../messages.js';
|
||||
import { execa } from 'execa';
|
||||
|
||||
export async function git(ctx: Pick<Context, 'cwd'|'git'|'yes'|'prompt'|'dryRun'>) {
|
||||
if (fs.existsSync(path.join(ctx.cwd, '.git'))) {
|
||||
await info('Nice!', `Git has already been initialized`);
|
||||
return
|
||||
}
|
||||
let _git = ctx.git ?? ctx.yes;
|
||||
if (_git === undefined) {
|
||||
({ git: _git } = await ctx.prompt({
|
||||
name: 'git',
|
||||
type: 'confirm',
|
||||
label: title('git'),
|
||||
message: `Initialize a new git repository?`,
|
||||
hint: 'optional',
|
||||
initial: true,
|
||||
}));
|
||||
}
|
||||
|
||||
if (ctx.dryRun) {
|
||||
await info('--dry-run', `Skipping Git initialization`);
|
||||
} else if (_git) {
|
||||
await spinner({
|
||||
start: 'Git initializing...',
|
||||
end: 'Git initialized',
|
||||
while: () => init({ cwd: ctx.cwd }),
|
||||
});
|
||||
} else {
|
||||
await info(
|
||||
ctx.yes === false ? 'git [skip]' : 'Sounds good!',
|
||||
`You can always run ${color.reset('git init')}${color.dim(' manually.')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function init({ cwd }: { cwd: string }) {
|
||||
try {
|
||||
await execa('git', ['init'], { cwd, stdio: 'ignore' });
|
||||
await execa('git', ['add', '-A'], { cwd, stdio: 'ignore' });
|
||||
await execa('git', ['commit', '-m', 'Initial commit from Astro', '--author="houston[bot] <astrobot-houston@users.noreply.github.com>"'], { cwd, stdio: 'ignore' });
|
||||
} catch (e) {}
|
||||
}
|
20
packages/create-astro/src/actions/help.ts
Normal file
20
packages/create-astro/src/actions/help.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { printHelp } from '../messages.js';
|
||||
|
||||
export function help() {
|
||||
printHelp({
|
||||
commandName: 'create-astro',
|
||||
usage: '[dir] [...flags]',
|
||||
headline: 'Scaffold Astro projects.',
|
||||
tables: {
|
||||
Flags: [
|
||||
['--template <name>', 'Specify your template.'],
|
||||
['--install / --no-install', 'Install dependencies (or not).'],
|
||||
['--git / --no-git', 'Initialize git repo (or not).'],
|
||||
['--yes (-y)', 'Skip all prompt by accepting defaults.'],
|
||||
['--no (-n)', 'Skip all prompt by declining defaults.'],
|
||||
['--dry-run', 'Walk through steps without executing.'],
|
||||
['--skip-houston', 'Skip Houston animation.'],
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
23
packages/create-astro/src/actions/intro.ts
Normal file
23
packages/create-astro/src/actions/intro.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { type Context } from './context';
|
||||
|
||||
import { banner, welcome, say } from '../messages.js';
|
||||
import { label, color } from '@astrojs/cli-kit';
|
||||
import { random } from '@astrojs/cli-kit/utils';
|
||||
|
||||
export async function intro(ctx: Pick<Context, 'skipHouston'|'version'|'username'>) {
|
||||
if (!ctx.skipHouston) {
|
||||
await say([
|
||||
[
|
||||
'Welcome',
|
||||
'to',
|
||||
label('astro', color.bgGreen, color.black),
|
||||
color.green(`v${ctx.version}`) + ',',
|
||||
`${ctx.username}!`,
|
||||
],
|
||||
random(welcome),
|
||||
]);
|
||||
await banner(ctx.version);
|
||||
} else {
|
||||
await banner(ctx.version);
|
||||
}
|
||||
}
|
15
packages/create-astro/src/actions/next-steps.ts
Normal file
15
packages/create-astro/src/actions/next-steps.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Context } from "./context";
|
||||
import path from 'node:path';
|
||||
|
||||
import { nextSteps, say } from '../messages.js';
|
||||
|
||||
export async function next(ctx: Pick<Context, 'cwd'|'pkgManager'|'skipHouston'>) {
|
||||
let projectDir = path.relative(process.cwd(), ctx.cwd);
|
||||
const devCmd = ctx.pkgManager === 'npm' ? 'npm run dev' : `${ctx.pkgManager} dev`;
|
||||
await nextSteps({ projectDir, devCmd });
|
||||
|
||||
if (!ctx.skipHouston) {
|
||||
await say(['Good luck out there, astronaut! 🚀']);
|
||||
}
|
||||
return;
|
||||
}
|
58
packages/create-astro/src/actions/project-name.ts
Normal file
58
packages/create-astro/src/actions/project-name.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import type { Context } from "./context";
|
||||
|
||||
import { color, generateProjectName } from '@astrojs/cli-kit';
|
||||
import { title, info, log } from '../messages.js';
|
||||
import path from 'node:path';
|
||||
|
||||
import { isEmpty, toValidName } from './shared.js';
|
||||
|
||||
export async function projectName(ctx: Pick<Context, 'cwd'|'prompt'|'projectName'|'exit'>) {
|
||||
await checkCwd(ctx.cwd);
|
||||
|
||||
if (!ctx.cwd || !isEmpty(ctx.cwd)) {
|
||||
if (!isEmpty(ctx.cwd)) {
|
||||
await info('Hmm...', `${color.reset(`"${ctx.cwd}"`)}${color.dim(` is not empty!`)}`);
|
||||
}
|
||||
|
||||
const { name } = await ctx.prompt({
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
label: title('dir'),
|
||||
message: 'Where should we create your new project?',
|
||||
initial: `./${generateProjectName()}`,
|
||||
validate(value: string) {
|
||||
if (!isEmpty(value)) {
|
||||
return `Directory is not empty!`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
ctx.cwd = name!;
|
||||
ctx.projectName = toValidName(name!);
|
||||
} else {
|
||||
let name = ctx.cwd;
|
||||
if (name === '.' || name === './') {
|
||||
const parts = process.cwd().split(path.sep);
|
||||
name = parts[parts.length - 1];
|
||||
} else if (name.startsWith('./') || name.startsWith('../')) {
|
||||
const parts = name.split('/');
|
||||
name = parts[parts.length - 1];
|
||||
}
|
||||
ctx.projectName = toValidName(name);
|
||||
}
|
||||
|
||||
if (!ctx.cwd) {
|
||||
ctx.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCwd(cwd: string | undefined) {
|
||||
const empty = cwd && isEmpty(cwd);
|
||||
if (empty) {
|
||||
log('');
|
||||
await info('dir', `Using ${color.reset(cwd)}${color.dim(' as project directory')}`);
|
||||
}
|
||||
|
||||
return empty;
|
||||
}
|
61
packages/create-astro/src/actions/shared.ts
Normal file
61
packages/create-astro/src/actions/shared.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import fs from 'node:fs';
|
||||
|
||||
// Some existing files and directories can be safely ignored when checking if a directory is a valid project directory.
|
||||
// https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/create-react-app/createReactApp.js#L907-L934
|
||||
const VALID_PROJECT_DIRECTORY_SAFE_LIST = [
|
||||
'.DS_Store',
|
||||
'.git',
|
||||
'.gitkeep',
|
||||
'.gitattributes',
|
||||
'.gitignore',
|
||||
'.gitlab-ci.yml',
|
||||
'.hg',
|
||||
'.hgcheck',
|
||||
'.hgignore',
|
||||
'.idea',
|
||||
'.npmignore',
|
||||
'.travis.yml',
|
||||
'.yarn',
|
||||
'.yarnrc.yml',
|
||||
'docs',
|
||||
'LICENSE',
|
||||
'mkdocs.yml',
|
||||
'Thumbs.db',
|
||||
/\.iml$/,
|
||||
/^npm-debug\.log/,
|
||||
/^yarn-debug\.log/,
|
||||
/^yarn-error\.log/,
|
||||
];
|
||||
|
||||
export function isEmpty(dirPath: string) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const conflicts = fs.readdirSync(dirPath).filter((content) => {
|
||||
return !VALID_PROJECT_DIRECTORY_SAFE_LIST.some((safeContent) => {
|
||||
return typeof safeContent === 'string' ? content === safeContent : safeContent.test(content);
|
||||
});
|
||||
});
|
||||
|
||||
return conflicts.length === 0;
|
||||
}
|
||||
|
||||
export function isValidName(projectName: string) {
|
||||
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
|
||||
projectName,
|
||||
)
|
||||
}
|
||||
|
||||
export function toValidName(projectName: string) {
|
||||
if (isValidName(projectName)) return projectName;
|
||||
|
||||
return projectName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/^[._]/, '')
|
||||
.replace(/[^a-z\d\-~]+/g, '-')
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '')
|
||||
}
|
94
packages/create-astro/src/actions/template.ts
Normal file
94
packages/create-astro/src/actions/template.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/* eslint no-console: 'off' */
|
||||
import type { Context } from "./context";
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { downloadTemplate } from 'giget';
|
||||
import { error } from '../messages.js';
|
||||
import { color } from '@astrojs/cli-kit';
|
||||
import { title, info, spinner } from '../messages.js';
|
||||
|
||||
export async function template(ctx: Pick<Context, 'template'|'prompt'|'dryRun'|'exit'|'exit'>) {
|
||||
if (!ctx.template) {
|
||||
const { template: tmpl } = await ctx.prompt({
|
||||
name: 'template',
|
||||
type: 'select',
|
||||
label: title('tmpl'),
|
||||
message: 'How would you like to start your new project?',
|
||||
initial: 'basics',
|
||||
choices: [
|
||||
{ value: 'basics', label: 'Include sample files', hint: '(recommended)' },
|
||||
{ value: 'blog', label: 'Use blog template' },
|
||||
{ value: 'minimal', label: 'Empty' },
|
||||
],
|
||||
});
|
||||
ctx.template = tmpl;
|
||||
} else {
|
||||
await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`);
|
||||
}
|
||||
|
||||
if (ctx.dryRun) {
|
||||
await info('--dry-run', `Skipping template copying`);
|
||||
} else if (ctx.template) {
|
||||
await spinner({
|
||||
start: 'Template copying...',
|
||||
end: 'Template copied',
|
||||
while: () => copyTemplate(ctx.template!, ctx as Context),
|
||||
});
|
||||
} else {
|
||||
ctx.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// some files are only needed for online editors when using astro.new. Remove for create-astro installs.
|
||||
const FILES_TO_REMOVE = ['sandbox.config.json', 'CHANGELOG.md'];
|
||||
const FILES_TO_UPDATE = {
|
||||
'package.json': (file: string, overrides: { name: string }) => fs.promises.readFile(file, 'utf-8').then(value => (
|
||||
fs.promises.writeFile(file, JSON.stringify(Object.assign(JSON.parse(value), Object.assign(overrides, { private: undefined })), null, '\t'), 'utf-8')
|
||||
))
|
||||
}
|
||||
|
||||
export default async function copyTemplate(tmpl: string, ctx: Context) {
|
||||
const ref = ctx.ref || 'latest';
|
||||
const isThirdParty = tmpl.includes('/');
|
||||
|
||||
const templateTarget = isThirdParty
|
||||
? tmpl
|
||||
: `github:withastro/astro/examples/${tmpl}#${ref}`;
|
||||
|
||||
// Copy
|
||||
if (!ctx.dryRun) {
|
||||
try {
|
||||
await downloadTemplate(templateTarget, {
|
||||
force: true,
|
||||
provider: 'github',
|
||||
cwd: ctx.cwd,
|
||||
dir: '.',
|
||||
})
|
||||
} catch (err: any) {
|
||||
fs.rmdirSync(ctx.cwd);
|
||||
if (err.message.includes('404')) {
|
||||
await error('Error', `Template ${color.reset(tmpl)} ${color.dim('does not exist!')}`);
|
||||
} else {
|
||||
console.error(err.message);
|
||||
}
|
||||
ctx.exit(1);
|
||||
}
|
||||
|
||||
// Post-process in parallel
|
||||
const removeFiles = FILES_TO_REMOVE.map(async (file) => {
|
||||
const fileLoc = path.resolve(path.join(ctx.cwd, file));
|
||||
if (fs.existsSync(fileLoc)) {
|
||||
return fs.promises.rm(fileLoc, { recursive: true });
|
||||
}
|
||||
});
|
||||
const updateFiles = Object.entries(FILES_TO_UPDATE).map(async ([file, update]) => {
|
||||
const fileLoc = path.resolve(path.join(ctx.cwd, file));
|
||||
if (fs.existsSync(fileLoc)) {
|
||||
return update(fileLoc, { name: ctx.projectName! })
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all([...removeFiles, ...updateFiles]);
|
||||
}
|
||||
}
|
91
packages/create-astro/src/actions/typescript.ts
Normal file
91
packages/create-astro/src/actions/typescript.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import type { Context } from "./context";
|
||||
|
||||
import fs from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { color } from '@astrojs/cli-kit';
|
||||
import { title, info, error, typescriptByDefault, spinner } from '../messages.js';
|
||||
|
||||
export async function typescript(ctx: Pick<Context, 'typescript'|'yes'|'prompt'|'dryRun'|'cwd'|'exit'>) {
|
||||
let ts = ctx.typescript ?? (typeof ctx.yes !== 'undefined' ? 'strict' : undefined);
|
||||
if (ts === undefined) {
|
||||
const { useTs } = await ctx.prompt({
|
||||
name: 'useTs',
|
||||
type: 'confirm',
|
||||
label: title('ts'),
|
||||
message: `Do you plan to write TypeScript?`,
|
||||
initial: true,
|
||||
});
|
||||
if (!useTs) {
|
||||
await typescriptByDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
({ ts } = await ctx.prompt({
|
||||
name: 'ts',
|
||||
type: 'select',
|
||||
label: title('use'),
|
||||
message: `How strict should TypeScript be?`,
|
||||
initial: 'strict',
|
||||
choices: [
|
||||
{ value: 'strict', label: 'Strict', hint: `(recommended)` },
|
||||
{ value: 'strictest', label: 'Strictest' },
|
||||
{ value: 'base', label: 'Relaxed' },
|
||||
],
|
||||
}));
|
||||
} else {
|
||||
if (!['strict', 'strictest', 'relaxed', 'default', 'base'].includes(ts)) {
|
||||
if (!ctx.dryRun) {
|
||||
fs.rmSync(ctx.cwd, { recursive: true, force: true });
|
||||
}
|
||||
error(
|
||||
'Error',
|
||||
`Unknown TypeScript option ${color.reset(ts)}${color.dim(
|
||||
'! Expected strict | strictest | relaxed'
|
||||
)}`
|
||||
);
|
||||
ctx.exit(1);
|
||||
}
|
||||
await info('ts', `Using ${color.reset(ts)}${color.dim(' TypeScript configuration')}`);
|
||||
}
|
||||
|
||||
if (ctx.dryRun) {
|
||||
await info('--dry-run', `Skipping TypeScript setup`);
|
||||
} else if (ts && ts !== 'unsure') {
|
||||
if (ts === 'relaxed' || ts === 'default') {
|
||||
ts = 'base';
|
||||
}
|
||||
await spinner({
|
||||
start: 'TypeScript customizing...',
|
||||
end: 'TypeScript customized',
|
||||
while: () => setupTypeScript(ts!, { cwd: ctx.cwd }),
|
||||
});
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupTypeScript(value: string, { cwd }: { cwd: string }) {
|
||||
const templateTSConfigPath = path.join(cwd, 'tsconfig.json');
|
||||
try {
|
||||
const data = await readFile(templateTSConfigPath, { encoding: 'utf-8' })
|
||||
const templateTSConfig = JSON.parse(stripJsonComments(data));
|
||||
if (templateTSConfig && typeof templateTSConfig === 'object') {
|
||||
const result = Object.assign(templateTSConfig, {
|
||||
extends: `astro/tsconfigs/${value}`,
|
||||
});
|
||||
|
||||
fs.writeFileSync(templateTSConfigPath, JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
throw new Error("There was an error applying the requested TypeScript settings. This could be because the template's tsconfig.json is malformed")
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && (err as any).code === 'ENOENT') {
|
||||
// If the template doesn't have a tsconfig.json, let's add one instead
|
||||
fs.writeFileSync(
|
||||
templateTSConfigPath,
|
||||
JSON.stringify({ extends: `astro/tsconfigs/${value}` }, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
import chalk from 'chalk';
|
||||
import type { Ora } from 'ora';
|
||||
import ora from 'ora';
|
||||
|
||||
const gradientColors = [
|
||||
`#ff5e00`,
|
||||
`#ff4c29`,
|
||||
`#ff383f`,
|
||||
`#ff2453`,
|
||||
`#ff0565`,
|
||||
`#ff007b`,
|
||||
`#f5008b`,
|
||||
`#e6149c`,
|
||||
`#d629ae`,
|
||||
`#c238bd`,
|
||||
];
|
||||
|
||||
export const rocketAscii = '■■▶';
|
||||
|
||||
// get a reference to scroll through while loading
|
||||
// visual representation of what this generates:
|
||||
// gradientColors: "..xxXX"
|
||||
// referenceGradient: "..xxXXXXxx....xxXX"
|
||||
const referenceGradient = [
|
||||
...gradientColors,
|
||||
// draw the reverse of the gradient without
|
||||
// accidentally mutating the gradient (ugh, reverse())
|
||||
...[...gradientColors].reverse(),
|
||||
...gradientColors,
|
||||
];
|
||||
|
||||
// async-friendly setTimeout
|
||||
const sleep = (time: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
|
||||
function getGradientAnimFrames() {
|
||||
const frames = [];
|
||||
for (let start = 0; start < gradientColors.length * 2; start++) {
|
||||
const end = start + gradientColors.length - 1;
|
||||
frames.push(
|
||||
referenceGradient
|
||||
.slice(start, end)
|
||||
.map((g) => chalk.bgHex(g)(' '))
|
||||
.join('')
|
||||
);
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
function getIntroAnimFrames() {
|
||||
const frames = [];
|
||||
for (let end = 1; end <= gradientColors.length; end++) {
|
||||
const leadingSpacesArr = Array.from(
|
||||
new Array(Math.abs(gradientColors.length - end - 1)),
|
||||
() => ' '
|
||||
);
|
||||
const gradientArr = gradientColors.slice(0, end).map((g) => chalk.bgHex(g)(' '));
|
||||
frames.push([...leadingSpacesArr, ...gradientArr].join(''));
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate loading spinner with rocket flames!
|
||||
* @param text display text next to rocket
|
||||
* @returns Ora spinner for running .stop()
|
||||
*/
|
||||
export async function loadWithRocketGradient(text: string): Promise<Ora> {
|
||||
const frames = getIntroAnimFrames();
|
||||
const intro = ora({
|
||||
spinner: {
|
||||
interval: 30,
|
||||
frames,
|
||||
},
|
||||
text: `${rocketAscii} ${text}`,
|
||||
});
|
||||
intro.start();
|
||||
await sleep((frames.length - 1) * intro.interval);
|
||||
intro.stop();
|
||||
const spinner = ora({
|
||||
spinner: {
|
||||
interval: 80,
|
||||
frames: getGradientAnimFrames(),
|
||||
},
|
||||
text: `${rocketAscii} ${text}`,
|
||||
}).start();
|
||||
|
||||
return spinner;
|
||||
}
|
|
@ -1,398 +1,59 @@
|
|||
/* eslint no-console: 'off' */
|
||||
import { color, generateProjectName, label, say } from '@astrojs/cli-kit';
|
||||
import { forceUnicode, random } from '@astrojs/cli-kit/utils';
|
||||
import { assign, parse, stringify } from 'comment-json';
|
||||
import { execa, execaCommand } from 'execa';
|
||||
import fs from 'fs';
|
||||
import { downloadTemplate } from 'giget';
|
||||
import { bold, dim, green, reset, yellow } from 'kleur/colors';
|
||||
import ora from 'ora';
|
||||
import { platform } from 'os';
|
||||
import path from 'path';
|
||||
import prompts from 'prompts';
|
||||
import detectPackageManager from 'which-pm-runs';
|
||||
import yargs from 'yargs-parser';
|
||||
import { loadWithRocketGradient, rocketAscii } from './gradient.js';
|
||||
import { logger } from './logger.js';
|
||||
import {
|
||||
banner,
|
||||
getName,
|
||||
getVersion,
|
||||
info,
|
||||
nextSteps,
|
||||
typescriptByDefault,
|
||||
welcome,
|
||||
} from './messages.js';
|
||||
import { TEMPLATES } from './templates.js';
|
||||
import { getContext } from './actions/context.js';
|
||||
|
||||
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
|
||||
// to no longer require `--` to pass args and instead pass `--` directly to us. This
|
||||
// broke our arg parser, since `--` is a special kind of flag. Filtering for `--` here
|
||||
// fixes the issue so that create-astro now works on all npm version.
|
||||
const cleanArgv = process.argv.filter((arg) => arg !== '--');
|
||||
const args = yargs(cleanArgv, { boolean: ['fancy', 'y'], alias: { y: 'yes' } });
|
||||
// Always skip Houston on Windows (for now)
|
||||
if (platform() === 'win32') args.skipHouston = true;
|
||||
prompts.override(args);
|
||||
import { setStdout } from './messages.js';
|
||||
import { help } from './actions/help.js';
|
||||
import { intro } from './actions/intro.js';
|
||||
import { projectName } from './actions/project-name.js';
|
||||
import { template } from './actions/template.js'
|
||||
import { dependencies } from './actions/dependencies.js';
|
||||
import { git } from './actions/git.js';
|
||||
import { typescript, setupTypeScript } from './actions/typescript.js';
|
||||
import { next } from './actions/next-steps.js';
|
||||
|
||||
// Enable full unicode support if the `--fancy` flag is passed
|
||||
if (args.fancy) {
|
||||
forceUnicode();
|
||||
}
|
||||
const exit = () => process.exit(0)
|
||||
process.on('SIGINT', exit)
|
||||
process.on('SIGTERM', exit)
|
||||
|
||||
export function mkdirp(dir: string) {
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
} catch (e: any) {
|
||||
if (e.code === 'EEXIST') return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Some existing files and directories can be safely ignored when checking if a directory is a valid project directory.
|
||||
// https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/create-react-app/createReactApp.js#L907-L934
|
||||
const VALID_PROJECT_DIRECTORY_SAFE_LIST = [
|
||||
'.DS_Store',
|
||||
'.git',
|
||||
'.gitattributes',
|
||||
'.gitignore',
|
||||
'.gitlab-ci.yml',
|
||||
'.hg',
|
||||
'.hgcheck',
|
||||
'.hgignore',
|
||||
'.idea',
|
||||
'.npmignore',
|
||||
'.travis.yml',
|
||||
'.yarn',
|
||||
'.yarnrc.yml',
|
||||
'docs',
|
||||
'LICENSE',
|
||||
'mkdocs.yml',
|
||||
'Thumbs.db',
|
||||
/\.iml$/,
|
||||
/^npm-debug\.log/,
|
||||
/^yarn-debug\.log/,
|
||||
/^yarn-error\.log/,
|
||||
];
|
||||
|
||||
function isValidProjectDirectory(dirPath: string) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const conflicts = fs.readdirSync(dirPath).filter((content) => {
|
||||
return !VALID_PROJECT_DIRECTORY_SAFE_LIST.some((safeContent) => {
|
||||
return typeof safeContent === 'string' ? content === safeContent : safeContent.test(content);
|
||||
});
|
||||
});
|
||||
|
||||
return conflicts.length === 0;
|
||||
}
|
||||
|
||||
const FILES_TO_REMOVE = ['.stackblitzrc', 'sandbox.config.json', 'CHANGELOG.md']; // some files are only needed for online editors when using astro.new. Remove for create-astro installs.
|
||||
|
||||
// Please also update the installation instructions in the docs at https://github.com/withastro/docs/blob/main/src/pages/en/install/auto.md if you make any changes to the flow or wording here.
|
||||
// Please also update the installation instructions in the docs at
|
||||
// https://github.com/withastro/docs/blob/main/src/pages/en/install/auto.md
|
||||
// if you make any changes to the flow or wording here.
|
||||
export async function main() {
|
||||
const pkgManager = detectPackageManager()?.name || 'npm';
|
||||
const [username, version] = await Promise.all([getName(), getVersion()]);
|
||||
|
||||
logger.debug('Verbose logging turned on');
|
||||
if (!args.skipHouston) {
|
||||
await say(
|
||||
[
|
||||
[
|
||||
'Welcome',
|
||||
'to',
|
||||
label('astro', color.bgGreen, color.black),
|
||||
color.green(`v${version}`) + ',',
|
||||
`${username}!`,
|
||||
],
|
||||
random(welcome),
|
||||
],
|
||||
{ hat: args.fancy ? '🎩' : undefined }
|
||||
);
|
||||
await banner(version);
|
||||
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
|
||||
// to no longer require `--` to pass args and instead pass `--` directly to us. This
|
||||
// broke our arg parser, since `--` is a special kind of flag. Filtering for `--` here
|
||||
// fixes the issue so that create-astro now works on all npm versions.
|
||||
const cleanArgv = process.argv.slice(2).filter((arg) => arg !== '--');
|
||||
const ctx = await getContext(cleanArgv);
|
||||
if (ctx.help) {
|
||||
help();
|
||||
return;
|
||||
}
|
||||
|
||||
let cwd = args['_'][2] as string;
|
||||
const steps = [
|
||||
intro,
|
||||
projectName,
|
||||
template,
|
||||
dependencies,
|
||||
git,
|
||||
typescript,
|
||||
next
|
||||
]
|
||||
|
||||
if (cwd && isValidProjectDirectory(cwd)) {
|
||||
let acknowledgeProjectDir = ora({
|
||||
color: 'green',
|
||||
text: `Using ${bold(cwd)} as project directory.`,
|
||||
});
|
||||
acknowledgeProjectDir.succeed();
|
||||
}
|
||||
|
||||
if (!cwd || !isValidProjectDirectory(cwd)) {
|
||||
const notEmptyMsg = (dirPath: string) => `"${bold(dirPath)}" is not empty!`;
|
||||
|
||||
if (!isValidProjectDirectory(cwd)) {
|
||||
let rejectProjectDir = ora({ color: 'red', text: notEmptyMsg(cwd) });
|
||||
rejectProjectDir.fail();
|
||||
}
|
||||
const dirResponse = await prompts(
|
||||
{
|
||||
type: 'text',
|
||||
name: 'directory',
|
||||
message: 'Where would you like to create your new project?',
|
||||
initial: generateProjectName(),
|
||||
validate(value) {
|
||||
if (!isValidProjectDirectory(value)) {
|
||||
return notEmptyMsg(value);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{ onCancel: () => ora().info(dim('Operation cancelled. See you later, astronaut!')) }
|
||||
);
|
||||
cwd = dirResponse.directory;
|
||||
}
|
||||
|
||||
if (!cwd) {
|
||||
ora().info(dim('No directory provided. See you later, astronaut!'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const options = await prompts(
|
||||
[
|
||||
{
|
||||
type: 'select',
|
||||
name: 'template',
|
||||
message: 'How would you like to setup your new project?',
|
||||
choices: TEMPLATES,
|
||||
},
|
||||
],
|
||||
{ onCancel: () => ora().info(dim('Operation cancelled. See you later, astronaut!')) }
|
||||
);
|
||||
|
||||
if (!options.template || options.template === true) {
|
||||
ora().info(dim('No template provided. See you later, astronaut!'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let templateSpinner = await loadWithRocketGradient('Copying project files...');
|
||||
|
||||
const hash = args.commit ? `#${args.commit}` : '';
|
||||
|
||||
const isThirdParty = options.template.includes('/');
|
||||
const templateTarget = isThirdParty
|
||||
? options.template
|
||||
: `withastro/astro/examples/${options.template}#latest`;
|
||||
|
||||
// Copy
|
||||
if (!args.dryRun) {
|
||||
try {
|
||||
await downloadTemplate(`${templateTarget}${hash}`, {
|
||||
force: true,
|
||||
provider: 'github',
|
||||
cwd,
|
||||
dir: '.',
|
||||
});
|
||||
} catch (err: any) {
|
||||
fs.rmdirSync(cwd);
|
||||
if (err.message.includes('404')) {
|
||||
console.error(`Could not find template ${color.underline(options.template)}!`);
|
||||
if (isThirdParty) {
|
||||
const hasBranch = options.template.includes('#');
|
||||
if (hasBranch) {
|
||||
console.error('Are you sure this GitHub repo and branch exist?');
|
||||
} else {
|
||||
console.error(
|
||||
`Are you sure this GitHub repo exists?` +
|
||||
`This command uses the ${color.bold('main')} branch by default.\n` +
|
||||
`If the repo doesn't have a main branch, specify a custom branch name:\n` +
|
||||
color.underline(options.template + color.bold('#branch-name'))
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Post-process in parallel
|
||||
await Promise.all(
|
||||
FILES_TO_REMOVE.map(async (file) => {
|
||||
const fileLoc = path.resolve(path.join(cwd, file));
|
||||
if (fs.existsSync(fileLoc)) {
|
||||
return fs.promises.rm(fileLoc, {});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
templateSpinner.text = green('Template copied!');
|
||||
templateSpinner.succeed();
|
||||
|
||||
const install = args.y
|
||||
? true
|
||||
: (
|
||||
await prompts(
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'install',
|
||||
message: `Would you like to install ${pkgManager} dependencies? ${reset(
|
||||
dim('(recommended)')
|
||||
)}`,
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
ora().info(
|
||||
dim(
|
||||
'Operation cancelled. Your project folder has already been created, however no dependencies have been installed'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
},
|
||||
}
|
||||
)
|
||||
).install;
|
||||
|
||||
if (args.dryRun) {
|
||||
ora().info(dim(`--dry-run enabled, skipping.`));
|
||||
} else if (install) {
|
||||
const installExec = execa(pkgManager, ['install'], { cwd });
|
||||
const installingPackagesMsg = `Installing packages${emojiWithFallback(' 📦', '...')}`;
|
||||
const installSpinner = await loadWithRocketGradient(installingPackagesMsg);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
installExec.stdout?.on('data', function (data) {
|
||||
installSpinner.text = `${rocketAscii} ${installingPackagesMsg}\n${bold(
|
||||
`[${pkgManager}]`
|
||||
)} ${data}`;
|
||||
});
|
||||
installExec.on('error', (error) => reject(error));
|
||||
installExec.on('close', () => resolve());
|
||||
});
|
||||
installSpinner.text = green('Packages installed!');
|
||||
installSpinner.succeed();
|
||||
} else {
|
||||
await info('No problem!', 'Remember to install dependencies after setup.');
|
||||
}
|
||||
|
||||
const gitResponse = args.y
|
||||
? true
|
||||
: (
|
||||
await prompts(
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'git',
|
||||
message: `Would you like to initialize a new git repository? ${reset(
|
||||
dim('(optional)')
|
||||
)}`,
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
ora().info(
|
||||
dim('Operation cancelled. No worries, your project folder has already been created')
|
||||
);
|
||||
process.exit(1);
|
||||
},
|
||||
}
|
||||
)
|
||||
).git;
|
||||
|
||||
if (args.dryRun) {
|
||||
ora().info(dim(`--dry-run enabled, skipping.`));
|
||||
} else if (gitResponse) {
|
||||
// Add a check to see if there is already a .git directory and skip 'git init' if yes (with msg to output)
|
||||
const gitDir = './.git';
|
||||
if (fs.existsSync(gitDir)) {
|
||||
ora().info(dim('A .git directory already exists. Skipping creating a new Git repository.'));
|
||||
} else {
|
||||
await execaCommand('git init', { cwd });
|
||||
ora().succeed('Git repository created!');
|
||||
}
|
||||
} else {
|
||||
await info(
|
||||
'Sounds good!',
|
||||
`You can come back and run ${color.reset(`git init`)}${color.dim(' later.')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (args.y && !args.typescript) {
|
||||
ora().warn(dim('--typescript <choice> missing. Defaulting to "strict"'));
|
||||
args.typescript = 'strict';
|
||||
}
|
||||
|
||||
let tsResponse =
|
||||
args.typescript ||
|
||||
(
|
||||
await prompts(
|
||||
{
|
||||
type: 'select',
|
||||
name: 'typescript',
|
||||
message: 'How would you like to setup TypeScript?',
|
||||
choices: [
|
||||
{ value: 'strict', title: 'Strict', description: '(recommended)' },
|
||||
{ value: 'strictest', title: 'Strictest' },
|
||||
{ value: 'base', title: 'Relaxed' },
|
||||
{ value: 'unsure', title: 'Help me choose' },
|
||||
],
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
ora().info(
|
||||
dim(
|
||||
'Operation cancelled. Your project folder has been created but no TypeScript configuration file was created.'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
},
|
||||
}
|
||||
)
|
||||
).typescript;
|
||||
|
||||
if (tsResponse === 'unsure') {
|
||||
await typescriptByDefault();
|
||||
tsResponse = 'base';
|
||||
}
|
||||
if (args.dryRun) {
|
||||
ora().info(dim(`--dry-run enabled, skipping.`));
|
||||
} else if (tsResponse) {
|
||||
const templateTSConfigPath = path.join(cwd, 'tsconfig.json');
|
||||
fs.readFile(templateTSConfigPath, (err, data) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
// If the template doesn't have a tsconfig.json, let's add one instead
|
||||
fs.writeFileSync(
|
||||
templateTSConfigPath,
|
||||
stringify({ extends: `astro/tsconfigs/${tsResponse ?? 'base'}` }, null, 2)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const templateTSConfig = parse(data.toString());
|
||||
|
||||
if (templateTSConfig && typeof templateTSConfig === 'object') {
|
||||
const result = assign(templateTSConfig, {
|
||||
extends: `astro/tsconfigs/${tsResponse ?? 'base'}`,
|
||||
});
|
||||
|
||||
fs.writeFileSync(templateTSConfigPath, stringify(result, null, 2));
|
||||
} else {
|
||||
console.log(
|
||||
yellow(
|
||||
"There was an error applying the requested TypeScript settings. This could be because the template's tsconfig.json is malformed"
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
ora().succeed('TypeScript settings applied!');
|
||||
}
|
||||
|
||||
let projectDir = path.relative(process.cwd(), cwd);
|
||||
const devCmd = pkgManager === 'npm' ? 'npm run dev' : `${pkgManager} dev`;
|
||||
await nextSteps({ projectDir, devCmd });
|
||||
|
||||
if (!args.skipHouston) {
|
||||
await say(['Good luck out there, astronaut!']);
|
||||
for (const step of steps) {
|
||||
await step(ctx)
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function emojiWithFallback(char: string, fallback: string) {
|
||||
return process.platform !== 'win32' ? char : fallback;
|
||||
export {
|
||||
setStdout,
|
||||
getContext,
|
||||
intro,
|
||||
projectName,
|
||||
template,
|
||||
dependencies,
|
||||
git,
|
||||
typescript,
|
||||
setupTypeScript,
|
||||
next
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
};
|
|
@ -1,126 +1,183 @@
|
|||
/* eslint no-console: 'off' */
|
||||
import { color, label } from '@astrojs/cli-kit';
|
||||
import { sleep } from '@astrojs/cli-kit/utils';
|
||||
import { exec } from 'node:child_process';
|
||||
import { get } from 'node:https';
|
||||
import { color, label, spinner as load, say as houston } from '@astrojs/cli-kit';
|
||||
import { sleep, align } from '@astrojs/cli-kit/utils';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
export const welcome = [
|
||||
`Let's claim your corner of the internet.`,
|
||||
`I'll be your assistant today.`,
|
||||
`Let's build something awesome!`,
|
||||
`Let's build something great!`,
|
||||
`Let's build something fast!`,
|
||||
`Let's make the web weird!`,
|
||||
`Let's make the web a better place!`,
|
||||
`Let's create a new project!`,
|
||||
`Let's create something unique!`,
|
||||
`Time to build a new website.`,
|
||||
`Time to build a faster website.`,
|
||||
`Time to build a sweet new website.`,
|
||||
`We're glad to have you on board.`,
|
||||
`Keeping the internet weird since 2021.`,
|
||||
`Initiating launch sequence...`,
|
||||
`Initiating launch sequence... right... now!`,
|
||||
`Awaiting further instructions.`,
|
||||
];
|
||||
|
||||
export function getName() {
|
||||
return new Promise((resolve) => {
|
||||
exec('git config user.name', { encoding: 'utf-8' }, (_1, gitName, _2) => {
|
||||
if (gitName.trim()) {
|
||||
return resolve(gitName.split(' ')[0].trim());
|
||||
}
|
||||
exec('whoami', { encoding: 'utf-8' }, (_3, whoami, _4) => {
|
||||
if (whoami.trim()) {
|
||||
return resolve(whoami.split(' ')[0].trim());
|
||||
}
|
||||
return resolve('astronaut');
|
||||
});
|
||||
});
|
||||
});
|
||||
let stdout = process.stdout;
|
||||
/** @internal Used to mock `process.stdout.write` for testing purposes */
|
||||
export function setStdout(writable: typeof process.stdout) {
|
||||
stdout = writable;
|
||||
}
|
||||
|
||||
export async function say(messages: string|string[], { clear = false, hat = '' } = {}) {
|
||||
return houston(messages, { clear, hat, stdout });
|
||||
}
|
||||
|
||||
export async function spinner(args: { start: string; end: string; while: (...args: any) => Promise<any>; }) {
|
||||
await load(args, { stdout });
|
||||
}
|
||||
|
||||
export const title = (text: string) => align(label(text), 'end', 7) + ' ';
|
||||
|
||||
export const welcome = [
|
||||
`Let's claim your corner of the internet.`,
|
||||
`I'll be your assistant today.`,
|
||||
`Let's build something awesome!`,
|
||||
`Let's build something great!`,
|
||||
`Let's build something fast!`,
|
||||
`Let's build the web we want.`,
|
||||
`Let's make the web weird!`,
|
||||
`Let's make the web a better place!`,
|
||||
`Let's create a new project!`,
|
||||
`Let's create something unique!`,
|
||||
`Time to build a new website.`,
|
||||
`Time to build a faster website.`,
|
||||
`Time to build a sweet new website.`,
|
||||
`We're glad to have you on board.`,
|
||||
`Keeping the internet weird since 2021.`,
|
||||
`Initiating launch sequence...`,
|
||||
`Initiating launch sequence... right... now!`,
|
||||
`Awaiting further instructions.`,
|
||||
]
|
||||
|
||||
export const getName = () => new Promise<string>((resolve) => {
|
||||
exec('git config user.name', { encoding: 'utf-8' }, (_1, gitName, _2) => {
|
||||
if (gitName.trim()) {
|
||||
return resolve(gitName.split(' ')[0].trim());
|
||||
}
|
||||
exec('whoami', { encoding: 'utf-8' }, (_3, whoami, _4) => {
|
||||
if (whoami.trim()) {
|
||||
return resolve(whoami.split(' ')[0].trim());
|
||||
}
|
||||
return resolve('astronaut');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let v: string;
|
||||
export function getVersion() {
|
||||
return new Promise<string>((resolve) => {
|
||||
if (v) return resolve(v);
|
||||
get('https://registry.npmjs.org/astro/latest', (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => (body += chunk));
|
||||
res.on('end', () => {
|
||||
const { version } = JSON.parse(body);
|
||||
v = version;
|
||||
resolve(version);
|
||||
});
|
||||
});
|
||||
});
|
||||
export const getVersion = () => new Promise<string>((resolve) => {
|
||||
if (v) return resolve(v);
|
||||
get('https://registry.npmjs.org/astro/latest', (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk)
|
||||
res.on('end', () => {
|
||||
const { version } = JSON.parse(body);
|
||||
v = version;
|
||||
resolve(version);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export const log = (message: string) => stdout.write(message + "\n");
|
||||
export const banner = async (version: string) => log(`\n${label('astro', color.bgGreen, color.black)} ${color.green(color.bold(`v${version}`))} ${color.bold('Launch sequence initiated.')}`);
|
||||
|
||||
export const info = async (prefix: string, text: string) => {
|
||||
await sleep(100)
|
||||
if (stdout.columns < 80) {
|
||||
log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(prefix)}`);
|
||||
log(`${' '.repeat(9)}${color.dim(text)}`);
|
||||
} else {
|
||||
log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(prefix)} ${color.dim(text)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function banner(version: string) {
|
||||
return console.log(
|
||||
`\n${label('astro', color.bgGreen, color.black)} ${color.green(
|
||||
color.bold(`v${version}`)
|
||||
)} ${color.bold('Launch sequence initiated.')}\n`
|
||||
);
|
||||
export const error = async (prefix: string, text: string) => {
|
||||
if (stdout.columns < 80) {
|
||||
log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)}`);
|
||||
log(`${' '.repeat(9)}${color.dim(text)}`);
|
||||
} else {
|
||||
log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)} ${color.dim(text)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function info(prefix: string, text: string) {
|
||||
await sleep(100);
|
||||
if (process.stdout.columns < 80) {
|
||||
console.log(`${color.cyan('◼')} ${color.cyan(prefix)}`);
|
||||
console.log(`${' '.repeat(3)}${color.dim(text)}\n`);
|
||||
} else {
|
||||
console.log(`${color.cyan('◼')} ${color.cyan(prefix)} ${color.dim(text)}\n`);
|
||||
export const typescriptByDefault = async () => {
|
||||
await info(`No worries!`, 'TypeScript is supported in Astro by default,');
|
||||
log(`${' '.repeat(9)}${color.dim('but you are free to continue writing JavaScript instead.')}`);
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
export const nextSteps = async ({ projectDir, devCmd }: { projectDir: string, devCmd: string }) => {
|
||||
const max = stdout.columns;
|
||||
const prefix = max < 80 ? ' ' : ' '.repeat(9);
|
||||
await sleep(200);
|
||||
log(`\n ${color.bgCyan(` ${color.black('next')} `)} ${color.bold('Liftoff confirmed. Explore your project!')}`)
|
||||
|
||||
await sleep(100);
|
||||
if (projectDir !== '') {
|
||||
const enter = [`\n${prefix}Enter your project directory using`, color.cyan(`cd ./${projectDir}`, '')];
|
||||
const len = enter[0].length + stripAnsi(enter[1]).length;
|
||||
log(enter.join((len > max) ? '\n' + prefix : ' '));
|
||||
}
|
||||
log(`${prefix}Run ${color.cyan(devCmd)} to start the dev server. ${color.cyan('CTRL+C')} to stop.`)
|
||||
await sleep(100);
|
||||
log(`${prefix}Add frameworks like ${color.cyan(`react`)} or ${color.cyan('tailwind')} using ${color.cyan('astro add')}.`)
|
||||
await sleep(100);
|
||||
log(`\n${prefix}Stuck? Join us at ${color.cyan(`https://astro.build/chat`)}`)
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
|
||||
export function printHelp({
|
||||
commandName,
|
||||
headline,
|
||||
usage,
|
||||
tables,
|
||||
description,
|
||||
}: {
|
||||
commandName: string;
|
||||
headline?: string;
|
||||
usage?: string;
|
||||
tables?: Record<string, [command: string, help: string][]>;
|
||||
description?: string;
|
||||
}) {
|
||||
const linebreak = () => '';
|
||||
const table = (rows: [string, string][], { padding }: { padding: number }) => {
|
||||
const split = stdout.columns < 60;
|
||||
let raw = '';
|
||||
|
||||
for (const row of rows) {
|
||||
if (split) {
|
||||
raw += ` ${row[0]}\n `;
|
||||
} else {
|
||||
raw += `${`${row[0]}`.padStart(padding)}`;
|
||||
}
|
||||
raw += ' ' + color.dim(row[1]) + '\n';
|
||||
}
|
||||
|
||||
return raw.slice(0, -1); // remove latest \n
|
||||
};
|
||||
|
||||
let message = [];
|
||||
|
||||
if (headline) {
|
||||
message.push(
|
||||
linebreak(),
|
||||
`${title(commandName)} ${color.green(
|
||||
`v${process.env.PACKAGE_VERSION ?? ''}`
|
||||
)} ${headline}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function error(prefix: string, text: string) {
|
||||
if (process.stdout.columns < 80) {
|
||||
console.log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)}`);
|
||||
console.log(`${' '.repeat(9)}${color.dim(text)}`);
|
||||
} else {
|
||||
console.log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)} ${color.dim(text)}`);
|
||||
if (usage) {
|
||||
message.push(linebreak(), `${color.green(commandName)} ${color.bold(usage)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function typescriptByDefault() {
|
||||
await info(`Cool!`, 'Astro comes with TypeScript support enabled by default.');
|
||||
console.log(
|
||||
`${' '.repeat(3)}${color.dim(`We'll default to the most relaxed settings for you.`)}`
|
||||
);
|
||||
await sleep(300);
|
||||
}
|
||||
|
||||
export async function nextSteps({ projectDir, devCmd }: { projectDir: string; devCmd: string }) {
|
||||
const max = process.stdout.columns;
|
||||
const prefix = max < 80 ? ' ' : ' '.repeat(9);
|
||||
await sleep(200);
|
||||
console.log(
|
||||
`\n ${color.bgCyan(` ${color.black('next')} `)} ${color.bold(
|
||||
'Liftoff confirmed. Explore your project!'
|
||||
)}`
|
||||
);
|
||||
|
||||
await sleep(100);
|
||||
if (projectDir !== '') {
|
||||
const enter = [
|
||||
`\n${prefix}Enter your project directory using`,
|
||||
color.cyan(`cd ./${projectDir}`, ''),
|
||||
];
|
||||
const len = enter[0].length + stripAnsi(enter[1]).length;
|
||||
console.log(enter.join(len > max ? '\n' + prefix : ' '));
|
||||
if (tables) {
|
||||
function calculateTablePadding(rows: [string, string][]) {
|
||||
return rows.reduce((val, [first]) => Math.max(val, first.length), 0);
|
||||
}
|
||||
const tableEntries = Object.entries(tables);
|
||||
const padding = Math.max(...tableEntries.map(([, rows]) => calculateTablePadding(rows)));
|
||||
for (const [, tableRows] of tableEntries) {
|
||||
message.push(linebreak(), table(tableRows, { padding }));
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`${prefix}Run ${color.cyan(devCmd)} to start the dev server. ${color.cyan('CTRL+C')} to stop.`
|
||||
);
|
||||
await sleep(100);
|
||||
console.log(
|
||||
`${prefix}Add frameworks like ${color.cyan(`react`)} or ${color.cyan(
|
||||
'tailwind'
|
||||
)} using ${color.cyan('astro add')}.`
|
||||
);
|
||||
await sleep(100);
|
||||
console.log(`\n${prefix}Stuck? Join us at ${color.cyan(`https://astro.build/chat`)}`);
|
||||
await sleep(200);
|
||||
|
||||
if (description) {
|
||||
message.push(linebreak(), `${description}`);
|
||||
}
|
||||
|
||||
log(message.join('\n') + '\n');
|
||||
}
|
||||
|
|
|
@ -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' },
|
||||
];
|
62
packages/create-astro/test/context.test.js
Normal file
62
packages/create-astro/test/context.test.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import os from 'node:os';
|
||||
import { getContext } from '../dist/index.js';
|
||||
|
||||
describe('context', () => {
|
||||
it('no arguments', async () => {
|
||||
const ctx = await getContext([]);
|
||||
expect(ctx.projectName).to.be.undefined;
|
||||
expect(ctx.template).to.be.undefined;
|
||||
expect(ctx.skipHouston).to.eq(os.platform() === 'win32');
|
||||
expect(ctx.dryRun).to.be.undefined;
|
||||
})
|
||||
it('project name', async () => {
|
||||
const ctx = await getContext(['foobar']);
|
||||
expect(ctx.projectName).to.eq('foobar');
|
||||
})
|
||||
it('template', async () => {
|
||||
const ctx = await getContext(['--template', 'minimal']);
|
||||
expect(ctx.template).to.eq('minimal');
|
||||
})
|
||||
it('skip houston (explicit)', async () => {
|
||||
const ctx = await getContext(['--skip-houston']);
|
||||
expect(ctx.skipHouston).to.eq(true);
|
||||
})
|
||||
it('skip houston (yes)', async () => {
|
||||
const ctx = await getContext(['-y']);
|
||||
expect(ctx.skipHouston).to.eq(true);
|
||||
})
|
||||
it('skip houston (no)', async () => {
|
||||
const ctx = await getContext(['-n']);
|
||||
expect(ctx.skipHouston).to.eq(true);
|
||||
})
|
||||
it('skip houston (install)', async () => {
|
||||
const ctx = await getContext(['--install']);
|
||||
expect(ctx.skipHouston).to.eq(true);
|
||||
})
|
||||
it('dry run', async () => {
|
||||
const ctx = await getContext(['--dry-run']);
|
||||
expect(ctx.dryRun).to.eq(true);
|
||||
})
|
||||
it('install', async () => {
|
||||
const ctx = await getContext(['--install']);
|
||||
expect(ctx.install).to.eq(true);
|
||||
})
|
||||
it('no install', async () => {
|
||||
const ctx = await getContext(['--no-install']);
|
||||
expect(ctx.install).to.eq(false);
|
||||
})
|
||||
it('git', async () => {
|
||||
const ctx = await getContext(['--git']);
|
||||
expect(ctx.git).to.eq(true);
|
||||
})
|
||||
it('no git', async () => {
|
||||
const ctx = await getContext(['--no-git']);
|
||||
expect(ctx.git).to.eq(false);
|
||||
})
|
||||
it('typescript', async () => {
|
||||
const ctx = await getContext(['--typescript', 'strict']);
|
||||
expect(ctx.typescript).to.eq('strict');
|
||||
})
|
||||
})
|
|
@ -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();
|
42
packages/create-astro/test/dependencies.test.js
Normal file
42
packages/create-astro/test/dependencies.test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { dependencies } from '../dist/index.js';
|
||||
import { setup } from './utils.js';
|
||||
|
||||
describe('dependencies', () => {
|
||||
const fixture = setup();
|
||||
|
||||
it('--yes', async () => {
|
||||
const context = { cwd: '', yes: true, pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: true }))};
|
||||
await dependencies(context);
|
||||
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
|
||||
})
|
||||
|
||||
it('prompt yes', async () => {
|
||||
const context = { cwd: '', pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: true })), install: undefined };
|
||||
await dependencies(context);
|
||||
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
|
||||
expect(context.install).to.eq(true);
|
||||
})
|
||||
|
||||
it('prompt no', async () => {
|
||||
const context = { cwd: '', pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: false })), install: undefined };
|
||||
await dependencies(context);
|
||||
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
|
||||
expect(context.install).to.eq(false);
|
||||
})
|
||||
|
||||
it('--install', async () => {
|
||||
const context = { cwd: '', install: true, pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: false })) };
|
||||
await dependencies(context);
|
||||
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
|
||||
expect(context.install).to.eq(true);
|
||||
})
|
||||
|
||||
it('--no-install', async () => {
|
||||
const context = { cwd: '', install: false, pkgManager: 'npm', dryRun: true, prompt: (() => ({ deps: false })) };
|
||||
await dependencies(context);
|
||||
expect(fixture.hasMessage('Skipping dependency installation')).to.be.true;
|
||||
expect(context.install).to.eq(false);
|
||||
})
|
||||
})
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
1
packages/create-astro/test/fixtures/not-empty/package.json
vendored
Normal file
1
packages/create-astro/test/fixtures/not-empty/package.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
packages/create-astro/test/fixtures/not-empty/tsconfig.json
vendored
Normal file
1
packages/create-astro/test/fixtures/not-empty/tsconfig.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
43
packages/create-astro/test/git.test.js
Normal file
43
packages/create-astro/test/git.test.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import fs from 'fs';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { git } from '../dist/index.js';
|
||||
import { setup } from './utils.js';
|
||||
|
||||
describe('git', () => {
|
||||
const fixture = setup();
|
||||
|
||||
it('none', async () => {
|
||||
const context = { cwd: '', dryRun: true, prompt: (() => ({ git: false }))};
|
||||
await git(context);
|
||||
|
||||
expect(fixture.hasMessage('Skipping Git initialization')).to.be.true;
|
||||
})
|
||||
|
||||
it('already initialized', async () => {
|
||||
const context = { git: true, cwd: './test/fixtures/not-empty', dryRun: true, prompt: (() => ({ git: false }))};
|
||||
await execa('git', ['init'], { cwd: './test/fixtures/not-empty' });
|
||||
await git(context);
|
||||
|
||||
expect(fixture.hasMessage('Git has already been initialized')).to.be.true;
|
||||
|
||||
// Cleanup
|
||||
fs.rmSync('./test/fixtures/not-empty/.git', { recursive: true, force: true });
|
||||
})
|
||||
|
||||
it('yes (--dry-run)', async () => {
|
||||
const context = { cwd: '', dryRun: true, prompt: (() => ({ git: true }))};
|
||||
await git(context);
|
||||
|
||||
expect(fixture.hasMessage('Skipping Git initialization')).to.be.true;
|
||||
})
|
||||
|
||||
it('no (--dry-run)', async () => {
|
||||
const context = { cwd: '', dryRun: true, prompt: (() => ({ git: false }))};
|
||||
await git(context);
|
||||
|
||||
expect(fixture.hasMessage('Skipping Git initialization')).to.be.true;
|
||||
})
|
||||
})
|
|
@ -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 };
|
20
packages/create-astro/test/intro.test.js
Normal file
20
packages/create-astro/test/intro.test.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { intro } from '../dist/index.js';
|
||||
import { setup } from './utils.js';
|
||||
|
||||
describe('intro', () => {
|
||||
const fixture = setup();
|
||||
|
||||
it('no arguments', async () => {
|
||||
await intro({ skipHouston: false, version: '0.0.0', username: 'user' });
|
||||
expect(fixture.hasMessage('Houston:')).to.be.true;
|
||||
expect(fixture.hasMessage('Welcome to astro v0.0.0')).to.be.true;
|
||||
})
|
||||
it('--skip-houston', async () => {
|
||||
await intro({ skipHouston: true, version: '0.0.0', username: 'user' });
|
||||
expect(fixture.length()).to.eq(1);
|
||||
expect(fixture.hasMessage('Houston:')).to.be.false;
|
||||
expect(fixture.hasMessage('Launch sequence initiated')).to.be.true;
|
||||
})
|
||||
})
|
20
packages/create-astro/test/next.test.js
Normal file
20
packages/create-astro/test/next.test.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { next } from '../dist/index.js';
|
||||
import { setup } from './utils.js';
|
||||
|
||||
describe('next steps', () => {
|
||||
const fixture = setup();
|
||||
|
||||
it('no arguments', async () => {
|
||||
await next({ skipHouston: false, cwd: './it/fixtures/not-empty', pkgManager: 'npm' });
|
||||
expect(fixture.hasMessage('Liftoff confirmed.')).to.be.true;
|
||||
expect(fixture.hasMessage('npm run dev')).to.be.true;
|
||||
expect(fixture.hasMessage('Good luck out there, astronaut!')).to.be.true;
|
||||
})
|
||||
|
||||
it('--skip-houston', async () => {
|
||||
await next({ skipHouston: true, cwd: './it/fixtures/not-empty', pkgManager: 'npm' });
|
||||
expect(fixture.hasMessage('Good luck out there, astronaut!')).to.be.false;
|
||||
})
|
||||
})
|
79
packages/create-astro/test/project-name.test.js
Normal file
79
packages/create-astro/test/project-name.test.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { projectName } from '../dist/index.js';
|
||||
import { setup } from './utils.js';
|
||||
|
||||
describe('project name', () => {
|
||||
const fixture = setup();
|
||||
|
||||
it('pass in name', async () => {
|
||||
const context = { projectName: '', cwd: './foo/bar/baz', prompt: (() => {})};
|
||||
await projectName(context);
|
||||
|
||||
expect(context.cwd).to.eq('./foo/bar/baz');
|
||||
expect(context.projectName).to.eq('baz');
|
||||
})
|
||||
|
||||
it('dot', async () => {
|
||||
const context = { projectName: '', cwd: '.', prompt: (() => ({ name: 'foobar' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(fixture.hasMessage('"." is not empty!')).to.be.true;
|
||||
expect(context.projectName).to.eq('foobar');
|
||||
})
|
||||
|
||||
it('dot slash', async () => {
|
||||
const context = { projectName: '', cwd: './', prompt: (() => ({ name: 'foobar' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(fixture.hasMessage('"./" is not empty!')).to.be.true;
|
||||
expect(context.projectName).to.eq('foobar');
|
||||
})
|
||||
|
||||
it('empty', async () => {
|
||||
const context = { projectName: '', cwd: './test/fixtures/empty', prompt: (() => ({ name: 'foobar' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(fixture.hasMessage('"./test/fixtures/empty" is not empty!')).to.be.false;
|
||||
expect(context.projectName).to.eq('empty');
|
||||
})
|
||||
|
||||
it('not empty', async () => {
|
||||
const context = { projectName: '', cwd: './test/fixtures/not-empty', prompt: (() => ({ name: 'foobar' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(fixture.hasMessage('"./test/fixtures/not-empty" is not empty!')).to.be.true;
|
||||
expect(context.projectName).to.eq('foobar');
|
||||
})
|
||||
|
||||
it('basic', async () => {
|
||||
const context = { projectName: '', cwd: '', prompt: (() => ({ name: 'foobar' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(context.cwd).to.eq('foobar');
|
||||
expect(context.projectName).to.eq('foobar');
|
||||
})
|
||||
|
||||
it('normalize', async () => {
|
||||
const context = { projectName: '', cwd: '', prompt: (() => ({ name: 'Invalid Name' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(context.cwd).to.eq('Invalid Name');
|
||||
expect(context.projectName).to.eq('invalid-name');
|
||||
})
|
||||
|
||||
it('remove leading/trailing dashes', async () => {
|
||||
const context = { projectName: '', cwd: '', prompt: (() => ({ name: '(invalid)' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(context.projectName).to.eq('invalid');
|
||||
})
|
||||
|
||||
it('handles scoped packages', async () => {
|
||||
const context = { projectName: '', cwd: '', prompt: (() => ({ name: '@astro/site' }))};
|
||||
await projectName(context);
|
||||
|
||||
expect(context.cwd).to.eq('@astro/site');
|
||||
expect(context.projectName).to.eq('@astro/site');
|
||||
})
|
||||
})
|
36
packages/create-astro/test/template.test.js
Normal file
36
packages/create-astro/test/template.test.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { template } from '../dist/index.js';
|
||||
import { setup } from './utils.js';
|
||||
|
||||
describe('template', () => {
|
||||
const fixture = setup();
|
||||
|
||||
it('none', async () => {
|
||||
const context = { template: '', cwd: '', dryRun: true, prompt: (() => ({ template: 'blog' })) };
|
||||
await template(context);
|
||||
|
||||
expect(fixture.hasMessage('Skipping template copying')).to.be.true;
|
||||
expect(context.template).to.eq('blog');
|
||||
})
|
||||
|
||||
it('minimal (--dry-run)', async () => {
|
||||
const context = { template: 'minimal', cwd: '', dryRun: true, prompt: (() => {})};
|
||||
await template(context);
|
||||
expect(fixture.hasMessage('Using minimal as project template')).to.be.true;
|
||||
})
|
||||
|
||||
it('basics (--dry-run)', async () => {
|
||||
const context = { template: 'basics', cwd: '', dryRun: true, prompt: (() => {})};
|
||||
await template(context);
|
||||
|
||||
expect(fixture.hasMessage('Using basics as project template')).to.be.true;
|
||||
})
|
||||
|
||||
it('blog (--dry-run)', async () => {
|
||||
const context = { template: 'blog', cwd: '', dryRun: true, prompt: (() => {})};
|
||||
await template(context);
|
||||
|
||||
expect(fixture.hasMessage('Using blog as project template')).to.be.true;
|
||||
})
|
||||
})
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
79
packages/create-astro/test/typescript.test.js
Normal file
79
packages/create-astro/test/typescript.test.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import fs from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { typescript, setupTypeScript } from '../dist/index.js';
|
||||
import { setup } from './utils.js';
|
||||
|
||||
describe('typescript', () => {
|
||||
const fixture = setup();
|
||||
|
||||
it('none', async () => {
|
||||
const context = { cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict', useTs: true }))};
|
||||
await typescript(context);
|
||||
|
||||
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
|
||||
})
|
||||
|
||||
it('use false', async () => {
|
||||
const context = { cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict', useTs: false }))};
|
||||
await typescript(context);
|
||||
|
||||
expect(fixture.hasMessage('No worries')).to.be.true;
|
||||
})
|
||||
|
||||
it('strict', async () => {
|
||||
const context = { typescript: 'strict', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' }))};
|
||||
await typescript(context);
|
||||
|
||||
expect(fixture.hasMessage('Using strict TypeScript configuration')).to.be.true;
|
||||
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
|
||||
})
|
||||
|
||||
it('default', async () => {
|
||||
const context = { typescript: 'default', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' }))};
|
||||
await typescript(context);
|
||||
|
||||
expect(fixture.hasMessage('Using default TypeScript configuration')).to.be.true;
|
||||
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
|
||||
})
|
||||
|
||||
it('relaxed', async () => {
|
||||
const context = { typescript: 'relaxed', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' }))};
|
||||
await typescript(context);
|
||||
|
||||
expect(fixture.hasMessage('Using relaxed TypeScript configuration')).to.be.true;
|
||||
expect(fixture.hasMessage('Skipping TypeScript setup')).to.be.true;
|
||||
})
|
||||
|
||||
it('other', async () => {
|
||||
const context = { typescript: 'other', cwd: '', dryRun: true, prompt: (() => ({ ts: 'strict' })), exit(code) { throw code }};
|
||||
let err = null;
|
||||
try {
|
||||
await typescript(context);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(err).to.eq(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('typescript: setup', () => {
|
||||
it('none', async () => {
|
||||
const root = new URL('./fixtures/empty/', import.meta.url);
|
||||
const tsconfig = new URL('./tsconfig.json', root);
|
||||
|
||||
await setupTypeScript('strict', { cwd: fileURLToPath(root) })
|
||||
expect(JSON.parse(fs.readFileSync(tsconfig, { encoding: 'utf-8' }))).to.deep.eq({ "extends": "astro/tsconfigs/strict" });
|
||||
fs.rmSync(tsconfig);
|
||||
})
|
||||
|
||||
it('exists', async () => {
|
||||
const root = new URL('./fixtures/not-empty/', import.meta.url);
|
||||
const tsconfig = new URL('./tsconfig.json', root);
|
||||
await setupTypeScript('strict', { cwd: fileURLToPath(root) })
|
||||
expect(JSON.parse(fs.readFileSync(tsconfig, { encoding: 'utf-8' }))).to.deep.eq({ "extends": "astro/tsconfigs/strict" });
|
||||
fs.writeFileSync(tsconfig, `{}`);
|
||||
})
|
||||
})
|
|
@ -1,52 +1,29 @@
|
|||
import { execa } from 'execa';
|
||||
import { dirname } from 'path';
|
||||
import { setStdout } from '../dist/index.js';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
export const testDir = dirname(__filename);
|
||||
export const timeout = 25000;
|
||||
|
||||
const timeoutError = function (details) {
|
||||
let errorMsg = 'Timed out waiting for create-astro to respond with expected output.';
|
||||
if (details) {
|
||||
errorMsg += '\nLast output: "' + details + '"';
|
||||
}
|
||||
return new Error(errorMsg);
|
||||
};
|
||||
|
||||
export function promiseWithTimeout(testFn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let lastStdout;
|
||||
function onStdout(chunk) {
|
||||
lastStdout = stripAnsi(chunk.toString()).trim() || lastStdout;
|
||||
}
|
||||
|
||||
const timeoutEvent = setTimeout(() => {
|
||||
reject(timeoutError(lastStdout));
|
||||
}, timeout);
|
||||
function resolver() {
|
||||
clearTimeout(timeoutEvent);
|
||||
resolve();
|
||||
}
|
||||
|
||||
testFn(resolver, onStdout);
|
||||
export function setup() {
|
||||
const ctx = { messages: [] };
|
||||
before(() => {
|
||||
setStdout(Object.assign({}, process.stdout, {
|
||||
write(buf) {
|
||||
ctx.messages.push(stripAnsi(String(buf)).trim())
|
||||
return true;
|
||||
}
|
||||
}))
|
||||
});
|
||||
}
|
||||
beforeEach(() => {
|
||||
ctx.messages = [];
|
||||
})
|
||||
|
||||
export const PROMPT_MESSAGES = {
|
||||
directory: 'Where would you like to create your new project?',
|
||||
template: 'How would you like to setup your new project?',
|
||||
typescript: 'How would you like to setup TypeScript?',
|
||||
typescriptSucceed: 'next',
|
||||
};
|
||||
|
||||
export function setup(args = []) {
|
||||
const { stdout, stdin } = execa('../create-astro.mjs', [...args, '--skip-houston', '--dry-run'], {
|
||||
cwd: testDir,
|
||||
});
|
||||
return {
|
||||
stdin,
|
||||
stdout,
|
||||
messages() {
|
||||
return ctx.messages
|
||||
},
|
||||
length() {
|
||||
return ctx.messages.length
|
||||
},
|
||||
hasMessage(content) {
|
||||
return !!ctx.messages.find(msg => msg.includes(content))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
"include": ["src", "index.d.ts"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"outDir": "./dist",
|
||||
|
|
111
pnpm-lock.yaml
111
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
|
||||
devDependencies:
|
||||
'@types/chai': 4.3.4
|
||||
'@types/degit': 2.8.3
|
||||
'@types/mocha': 9.1.1
|
||||
'@types/prompts': 2.4.2
|
||||
'@types/which-pm-runs': 1.0.0
|
||||
'@types/yargs-parser': 21.0.0
|
||||
astro-scripts: link:../../scripts
|
||||
chai: 4.3.7
|
||||
mocha: 9.2.2
|
||||
uvu: 0.5.6
|
||||
devDependencies:
|
||||
'@types/which-pm-runs': 1.0.0
|
||||
arg: 5.0.2
|
||||
astro-scripts: link:../../scripts
|
||||
strip-ansi: 7.0.1
|
||||
strip-json-comments: 5.0.0
|
||||
which-pm-runs: 1.1.0
|
||||
|
||||
packages/create-astro/test/fixtures/not-empty:
|
||||
specifiers: {}
|
||||
|
||||
packages/integrations/alpinejs:
|
||||
specifiers:
|
||||
|
@ -3863,8 +3846,8 @@ packages:
|
|||
lite-youtube-embed: 0.2.0
|
||||
dev: false
|
||||
|
||||
/@astrojs/cli-kit/0.1.6:
|
||||
resolution: {integrity: sha512-hC0Z7kh4T5QdtfPJVyZ6qmNCqWFYg67zS64AxPm9Y8QVYfeXOdXfL3PaNPGbNtGmczmYJ7cBn/ImgXd/RTTc5g==}
|
||||
/@astrojs/cli-kit/0.2.2:
|
||||
resolution: {integrity: sha512-9AniGN+jib2QMRAg4J8WYQxNhDld0zegrb7lig5oNkh1ReDa7rBxaKF9Tor31sjhnGISqavPkKKcQrEm53mzWg==}
|
||||
dependencies:
|
||||
chalk: 5.2.0
|
||||
log-update: 5.0.1
|
||||
|
@ -6895,10 +6878,6 @@ packages:
|
|||
dependencies:
|
||||
'@types/ms': 0.7.31
|
||||
|
||||
/@types/degit/2.8.3:
|
||||
resolution: {integrity: sha512-CL7y71j2zaDmtPLD5Xq5S1Gv2dFoHl0/GBZm6s39Mj/ls28L3NzAOqf7H4H0/2TNVMgMjMVf9CAFYSjmXhi3bw==}
|
||||
dev: true
|
||||
|
||||
/@types/diff/5.0.2:
|
||||
resolution: {integrity: sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==}
|
||||
dev: true
|
||||
|
@ -7329,7 +7308,6 @@ packages:
|
|||
|
||||
/@ungap/promise-all-settled/1.1.2:
|
||||
resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==}
|
||||
dev: true
|
||||
|
||||
/@ungap/structured-clone/0.3.4:
|
||||
resolution: {integrity: sha512-TSVh8CpnwNAsPC5wXcIyh92Bv1gq6E9cNDeeLu7Z4h8V4/qWtXJp7y42qljRkqcpmsve1iozwv1wr+3BNdILCg==}
|
||||
|
@ -7735,7 +7713,6 @@ packages:
|
|||
/ansi-colors/4.1.1:
|
||||
resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/ansi-colors/4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
|
@ -7756,7 +7733,6 @@ packages:
|
|||
/ansi-regex/6.0.1:
|
||||
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/ansi-styles/3.2.1:
|
||||
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
||||
|
@ -7804,16 +7780,11 @@ packages:
|
|||
|
||||
/argparse/2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
dev: true
|
||||
|
||||
/array-iterate/2.0.1:
|
||||
resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==}
|
||||
dev: false
|
||||
|
||||
/array-timsort/1.0.3:
|
||||
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
||||
dev: false
|
||||
|
||||
/array-union/2.1.0:
|
||||
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -8104,7 +8075,6 @@ packages:
|
|||
|
||||
/browser-stdout/1.3.1:
|
||||
resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
|
||||
dev: true
|
||||
|
||||
/browserslist/4.21.5:
|
||||
resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==}
|
||||
|
@ -8372,7 +8342,6 @@ packages:
|
|||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
dev: true
|
||||
|
||||
/cliui/8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
|
@ -8440,17 +8409,6 @@ packages:
|
|||
/commander/2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
/comment-json/4.2.3:
|
||||
resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
array-timsort: 1.0.3
|
||||
core-util-is: 1.0.3
|
||||
esprima: 4.0.1
|
||||
has-own-prop: 2.0.0
|
||||
repeat-string: 1.6.1
|
||||
dev: false
|
||||
|
||||
/common-ancestor-path/1.0.1:
|
||||
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
|
||||
dev: false
|
||||
|
@ -8516,6 +8474,7 @@ packages:
|
|||
|
||||
/core-util-is/1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
dev: true
|
||||
|
||||
/cron-schedule/3.0.6:
|
||||
resolution: {integrity: sha512-izfGgKyzzIyLaeb1EtZ3KbglkS6AKp9cv7LxmiyoOu+fXfol1tQDC0Cof0enVZGNtudTHW+3lfuW9ZkLQss4Wg==}
|
||||
|
@ -8675,7 +8634,6 @@ packages:
|
|||
dependencies:
|
||||
ms: 2.1.2
|
||||
supports-color: 8.1.1
|
||||
dev: true
|
||||
|
||||
/debug/4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
|
@ -8704,7 +8662,6 @@ packages:
|
|||
/decamelize/4.0.0:
|
||||
resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/decode-named-character-reference/1.0.2:
|
||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||
|
@ -8853,7 +8810,6 @@ packages:
|
|||
/diff/5.0.0:
|
||||
resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
dev: true
|
||||
|
||||
/diff/5.1.0:
|
||||
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
|
||||
|
@ -9952,7 +9908,6 @@ packages:
|
|||
/flat/5.0.2:
|
||||
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/flatted/3.2.7:
|
||||
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
||||
|
@ -10164,7 +10119,6 @@ packages:
|
|||
minimatch: 3.1.2
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
dev: true
|
||||
|
||||
/glob/7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
|
@ -10297,7 +10251,6 @@ packages:
|
|||
/growl/1.10.5:
|
||||
resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==}
|
||||
engines: {node: '>=4.x'}
|
||||
dev: true
|
||||
|
||||
/gzip-size/6.0.0:
|
||||
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
||||
|
@ -10322,11 +10275,6 @@ packages:
|
|||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/has-own-prop/2.0.0:
|
||||
resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/has-package-exports/1.3.0:
|
||||
resolution: {integrity: sha512-e9OeXPQnmPhYoJ63lXC4wWe34TxEGZDZ3OQX9XRqp2VwsfLl3bQBy7VehLnd34g3ef8CmYlBLGqEMKXuz8YazQ==}
|
||||
dependencies:
|
||||
|
@ -10522,7 +10470,6 @@ packages:
|
|||
/he/1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/hosted-git-info/2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
@ -10863,7 +10810,6 @@ packages:
|
|||
/is-plain-obj/2.1.0:
|
||||
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-plain-obj/4.1.0:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
|
@ -10947,7 +10893,6 @@ packages:
|
|||
/is-unicode-supported/0.1.0:
|
||||
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/is-unicode-supported/1.3.0:
|
||||
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
|
||||
|
@ -11021,7 +10966,6 @@ packages:
|
|||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
dev: true
|
||||
|
||||
/jsesc/0.5.0:
|
||||
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
|
||||
|
@ -11235,7 +11179,6 @@ packages:
|
|||
dependencies:
|
||||
chalk: 4.1.2
|
||||
is-unicode-supported: 0.1.0
|
||||
dev: true
|
||||
|
||||
/log-symbols/5.1.0:
|
||||
resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==}
|
||||
|
@ -12000,7 +11943,6 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
dev: true
|
||||
|
||||
/minimatch/5.1.6:
|
||||
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
|
||||
|
@ -12082,7 +12024,6 @@ packages:
|
|||
yargs: 16.2.0
|
||||
yargs-parser: 20.2.4
|
||||
yargs-unparser: 2.0.0
|
||||
dev: true
|
||||
|
||||
/mri/1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
|
@ -12112,7 +12053,6 @@ packages:
|
|||
resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid/3.3.4:
|
||||
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
|
||||
|
@ -13558,11 +13498,6 @@ packages:
|
|||
unified: 10.1.2
|
||||
dev: true
|
||||
|
||||
/repeat-string/1.6.1:
|
||||
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
|
||||
engines: {node: '>=0.10'}
|
||||
dev: false
|
||||
|
||||
/require-directory/2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -13859,7 +13794,6 @@ packages:
|
|||
resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
dev: true
|
||||
|
||||
/server-destroy/1.0.1:
|
||||
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
|
||||
|
@ -14226,7 +14160,6 @@ packages:
|
|||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
ansi-regex: 6.0.1
|
||||
dev: false
|
||||
|
||||
/strip-bom-string/1.0.0:
|
||||
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
|
||||
|
@ -14269,6 +14202,10 @@ packages:
|
|||
/strip-json-comments/3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/strip-json-comments/5.0.0:
|
||||
resolution: {integrity: sha512-V1LGY4UUo0jgwC+ELQ2BNWfPa17TIuwBLg+j1AA/9RPzKINl1lhxVEu2r+ZTTO8aetIsUzE5Qj6LMSBkoGYKKw==}
|
||||
engines: {node: '>=14.16'}
|
||||
dev: true
|
||||
|
||||
/strnum/1.0.5:
|
||||
|
@ -15648,7 +15585,6 @@ packages:
|
|||
|
||||
/workerpool/6.2.0:
|
||||
resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==}
|
||||
dev: true
|
||||
|
||||
/wrangler/2.9.0:
|
||||
resolution: {integrity: sha512-5nyyR4bXKG/Rwz0dH+nOx4SWvJWmTZVSbceLyTV+ZOH1sd2vvPnnW14NUzTNEjY3XaT93XH+28mc5+UNSYsFHw==}
|
||||
|
@ -15785,7 +15721,6 @@ packages:
|
|||
/yargs-parser/20.2.4:
|
||||
resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/yargs-parser/21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
|
@ -15799,7 +15734,6 @@ packages:
|
|||
decamelize: 4.0.0
|
||||
flat: 5.0.2
|
||||
is-plain-obj: 2.1.0
|
||||
dev: true
|
||||
|
||||
/yargs/15.4.1:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
|
@ -15829,7 +15763,6 @@ packages:
|
|||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 20.2.4
|
||||
dev: true
|
||||
|
||||
/yargs/17.6.2:
|
||||
resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}
|
||||
|
|
|
@ -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' } : {},
|
||||
|
|
Loading…
Reference in a new issue