0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

Feat: [create astro] add directory prompt (#3168)

* wip: add prompt for directory with validation

* feat: wire up dir response to cwd

* feat: improve error handling on non-empty dirs

* fix: update test helpers to execaSync

* chore: add .skipped to old tests for clarity

* deps: add mocha and chai to create-astro

* feat: add directory step test with fixture

* feat: update turbo to run create-astro tests again 🥳

* chore: changeset
This commit is contained in:
Ben Holmes 2022-04-21 16:36:48 -04:00 committed by GitHub
parent 908fffb5ec
commit 7c49194ca2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 161 additions and 20 deletions

View file

@ -0,0 +1,5 @@
---
'create-astro': minor
---
Add prompt to choose a directory, now defaulting to a separate "./my-astro-site" instead of "." (current directory)

View file

@ -13,7 +13,7 @@
"build:ci": "turbo run build:ci --no-deps --scope=astro --scope=create-astro --scope=\"@astrojs/*\"", "build:ci": "turbo run build:ci --no-deps --scope=astro --scope=create-astro --scope=\"@astrojs/*\"",
"build:examples": "turbo run build --scope=\"@example/*\"", "build:examples": "turbo run build --scope=\"@example/*\"",
"dev": "turbo run dev --no-deps --no-cache --parallel --scope=astro --scope=create-astro --scope=\"@astrojs/*\"", "dev": "turbo run dev --no-deps --no-cache --parallel --scope=astro --scope=create-astro --scope=\"@astrojs/*\"",
"test": "turbo run test --filter=!create-astro --concurrency=1", "test": "turbo run test --concurrency=1",
"test:match": "cd packages/astro && pnpm run test:match", "test:match": "cd packages/astro && pnpm run test:match",
"test:templates": "turbo run test --filter=create-astro --concurrency=1", "test:templates": "turbo run test --filter=create-astro --concurrency=1",
"test:smoke": "node scripts/smoke/index.js", "test:smoke": "node scripts/smoke/index.js",

View file

@ -21,7 +21,7 @@
"build": "astro-scripts build \"src/**/*.ts\" && tsc", "build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"", "build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "rm -rf test/fixtures && mkdir test/fixtures && node --unhandled-rejections=strict test/create-astro.test.js" "test": "mocha --exit --timeout 20000"
}, },
"files": [ "files": [
"dist", "dist",
@ -38,8 +38,12 @@
"yargs-parser": "^21.0.1" "yargs-parser": "^21.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.0",
"@types/yargs-parser": "^21.0.0", "@types/yargs-parser": "^21.0.0",
"astro-scripts": "workspace:*", "astro-scripts": "workspace:*",
"chai": "^4.3.6",
"mocha": "^9.2.2",
"uvu": "^0.5.3" "uvu": "^0.5.3"
}, },
"engines": { "engines": {

View file

@ -28,6 +28,10 @@ export function mkdirp(dir: string) {
} }
} }
function isEmpty(dirPath: string) {
return !fs.existsSync(dirPath) || fs.readdirSync(dirPath).length === 0;
}
const { version } = JSON.parse( const { version } = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8') fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')
); );
@ -47,22 +51,41 @@ export async function main() {
spinner.succeed(); spinner.succeed();
const cwd = (args['_'][2] as string) || '.'; let cwd = args['_'][2] as string;
if (fs.existsSync(cwd)) {
if (fs.readdirSync(cwd).length > 0) { if (cwd && isEmpty(cwd)) {
const response = await prompts({ let acknowledgeProjectDir = ora({
type: 'confirm', color: 'green',
name: 'forceOverwrite', text: `Using ${bold(cwd)} as project directory.`,
message: 'Directory not empty. Continue [force overwrite]?', });
initial: false, acknowledgeProjectDir.succeed();
}); }
if (!response.forceOverwrite) {
process.exit(1); if (!cwd || !isEmpty(cwd)) {
} const notEmptyMsg = (dirPath: string) =>
mkdirp(cwd); `"${bold(dirPath)}" is not empty. Please clear contents or choose a different path.`;
if (!isEmpty(cwd)) {
let rejectProjectDir = ora({ color: 'red', text: notEmptyMsg(cwd) });
rejectProjectDir.fail();
} }
} else { const dirResponse = await prompts({
mkdirp(cwd); type: 'text',
name: 'directory',
message: 'Where would you like to create your app?',
initial: './my-astro-site',
validate(value) {
if (!isEmpty(value)) {
return notEmptyMsg(value);
}
return true;
},
});
cwd = dirResponse.directory;
}
if (!cwd) {
process.exit(1);
} }
const options = await prompts([ const options = await prompts([

View file

@ -1 +0,0 @@
fixtures

View file

@ -0,0 +1,102 @@
import { execa} from 'execa';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import {promises, existsSync} from 'fs'
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const createAstroError = new Error('Timed out waiting for create-astro to respond with expected output.')
const timeout = 5000;
const instructions = {
directory: 'Where would you like to create your app?',
template: 'Which app template would you like to use?',
};
const inputs = {
nonEmptyDir: './fixtures/select-directory/nonempty-dir',
emptyDir: './fixtures/select-directory/empty-dir',
nonexistentDir: './fixtures/select-directory/banana-dir',
};
function promiseWithTimeout(testFn) {
return new Promise((resolve, reject) => {
const timeoutEvent = setTimeout(() => {
reject(createAstroError);
}, timeout);
function resolver() {
clearTimeout(timeoutEvent);
resolve();
}
testFn(resolver);
})
}
function setup(args = []) {
const {stdout, stdin} = execa('../create-astro.mjs', args, { cwd: __dirname })
return {
stdin,
stdout,
}
}
describe('[create-astro] select directory', function() {
this.timeout(timeout);
it ('should prompt for directory when none is provided', function () {
return promiseWithTimeout(resolve => {
const {stdout} = setup()
stdout.on('data', chunk => {
if (chunk.includes(instructions.directory)) {
resolve()
}
})
})
})
it ('should NOT proceed on a non-empty directory', function () {
return promiseWithTimeout(resolve => {
const {stdout} = setup([inputs.nonEmptyDir])
stdout.on('data', chunk => {
if (chunk.includes(instructions.directory)) {
resolve()
}
})
})
})
it ('should proceed on an empty directory', async function () {
const resolvedEmptyDirPath = resolve(__dirname, inputs.emptyDir)
if (!existsSync(resolvedEmptyDirPath)) {
await promises.mkdir(resolvedEmptyDirPath)
}
return promiseWithTimeout(resolve => {
const {stdout} = setup([inputs.emptyDir])
stdout.on('data', chunk => {
if (chunk.includes(instructions.template)) {
resolve()
}
})
})
})
it ('should proceed when directory does not exist', function () {
return promiseWithTimeout(resolve => {
const {stdout} = setup([inputs.nonexistentDir])
stdout.on('data', chunk => {
if (chunk.includes(instructions.template)) {
resolve()
}
})
})
})
it ('should error on bad directory selection in prompt', function () {
return promiseWithTimeout(resolve => {
const {stdout, stdin} = setup()
stdout.on('data', chunk => {
if (chunk.includes('Please clear contents or choose a different path.')) {
resolve()
}
if (chunk.includes(instructions.directory)) {
stdin.write(`${inputs.nonEmptyDir}\x0D`)
}
})
})
})
})

View file

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

View file

@ -1202,12 +1202,16 @@ importers:
packages/create-astro: packages/create-astro:
specifiers: specifiers:
'@types/chai': ^4.3.1
'@types/degit': ^2.8.3 '@types/degit': ^2.8.3
'@types/mocha': ^9.1.0
'@types/prompts': ^2.0.14 '@types/prompts': ^2.0.14
'@types/yargs-parser': ^21.0.0 '@types/yargs-parser': ^21.0.0
astro-scripts: workspace:* astro-scripts: workspace:*
chai: ^4.3.6
degit: ^2.8.4 degit: ^2.8.4
kleur: ^4.1.4 kleur: ^4.1.4
mocha: ^9.2.2
node-fetch: ^3.2.3 node-fetch: ^3.2.3
ora: ^6.1.0 ora: ^6.1.0
prompts: ^2.4.2 prompts: ^2.4.2
@ -1223,8 +1227,12 @@ importers:
prompts: 2.4.2 prompts: 2.4.2
yargs-parser: 21.0.1 yargs-parser: 21.0.1
devDependencies: devDependencies:
'@types/chai': 4.3.1
'@types/mocha': 9.1.0
'@types/yargs-parser': 21.0.0 '@types/yargs-parser': 21.0.0
astro-scripts: link:../../scripts astro-scripts: link:../../scripts
chai: 4.3.6
mocha: 9.2.2
uvu: 0.5.3 uvu: 0.5.3
packages/integrations/deno: packages/integrations/deno: