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:
parent
908fffb5ec
commit
7c49194ca2
11 changed files with 161 additions and 20 deletions
5
.changeset/soft-berries-admire.md
Normal file
5
.changeset/soft-berries-admire.md
Normal 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)
|
|
@ -13,7 +13,7 @@
|
|||
"build:ci": "turbo run build:ci --no-deps --scope=astro --scope=create-astro --scope=\"@astrojs/*\"",
|
||||
"build:examples": "turbo run build --scope=\"@example/*\"",
|
||||
"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:templates": "turbo run test --filter=create-astro --concurrency=1",
|
||||
"test:smoke": "node scripts/smoke/index.js",
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
|
||||
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
||||
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||
"test": "rm -rf test/fixtures && mkdir test/fixtures && node --unhandled-rejections=strict test/create-astro.test.js"
|
||||
"test": "mocha --exit --timeout 20000"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
@ -38,8 +38,12 @@
|
|||
"yargs-parser": "^21.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/yargs-parser": "^21.0.0",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
"mocha": "^9.2.2",
|
||||
"uvu": "^0.5.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
@ -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(
|
||||
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')
|
||||
);
|
||||
|
@ -47,22 +51,41 @@ export async function main() {
|
|||
|
||||
spinner.succeed();
|
||||
|
||||
const cwd = (args['_'][2] as string) || '.';
|
||||
if (fs.existsSync(cwd)) {
|
||||
if (fs.readdirSync(cwd).length > 0) {
|
||||
const response = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'forceOverwrite',
|
||||
message: 'Directory not empty. Continue [force overwrite]?',
|
||||
initial: false,
|
||||
});
|
||||
if (!response.forceOverwrite) {
|
||||
process.exit(1);
|
||||
}
|
||||
mkdirp(cwd);
|
||||
let cwd = args['_'][2] as string;
|
||||
|
||||
if (cwd && isEmpty(cwd)) {
|
||||
let acknowledgeProjectDir = ora({
|
||||
color: 'green',
|
||||
text: `Using ${bold(cwd)} as project directory.`,
|
||||
});
|
||||
acknowledgeProjectDir.succeed();
|
||||
}
|
||||
|
||||
if (!cwd || !isEmpty(cwd)) {
|
||||
const notEmptyMsg = (dirPath: string) =>
|
||||
`"${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 {
|
||||
mkdirp(cwd);
|
||||
const dirResponse = await prompts({
|
||||
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([
|
||||
|
|
1
packages/create-astro/test/.gitignore
vendored
1
packages/create-astro/test/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
fixtures
|
102
packages/create-astro/test/directory-step.test.js
Normal file
102
packages/create-astro/test/directory-step.test.js
Normal 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`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
0
packages/create-astro/test/fixtures/select-directory/nonempty-dir/astro-origin-story.php
vendored
Normal file
0
packages/create-astro/test/fixtures/select-directory/nonempty-dir/astro-origin-story.php
vendored
Normal file
|
@ -1,8 +1,8 @@
|
|||
import { execa } from 'execa';
|
||||
import { execaSync } from 'execa';
|
||||
import path from 'path';
|
||||
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_URL = pathToFileURL(FIXTURES_DIR + '/');
|
||||
|
||||
|
|
|
@ -1202,12 +1202,16 @@ importers:
|
|||
|
||||
packages/create-astro:
|
||||
specifiers:
|
||||
'@types/chai': ^4.3.1
|
||||
'@types/degit': ^2.8.3
|
||||
'@types/mocha': ^9.1.0
|
||||
'@types/prompts': ^2.0.14
|
||||
'@types/yargs-parser': ^21.0.0
|
||||
astro-scripts: workspace:*
|
||||
chai: ^4.3.6
|
||||
degit: ^2.8.4
|
||||
kleur: ^4.1.4
|
||||
mocha: ^9.2.2
|
||||
node-fetch: ^3.2.3
|
||||
ora: ^6.1.0
|
||||
prompts: ^2.4.2
|
||||
|
@ -1223,8 +1227,12 @@ importers:
|
|||
prompts: 2.4.2
|
||||
yargs-parser: 21.0.1
|
||||
devDependencies:
|
||||
'@types/chai': 4.3.1
|
||||
'@types/mocha': 9.1.0
|
||||
'@types/yargs-parser': 21.0.0
|
||||
astro-scripts: link:../../scripts
|
||||
chai: 4.3.6
|
||||
mocha: 9.2.2
|
||||
uvu: 0.5.3
|
||||
|
||||
packages/integrations/deno:
|
||||
|
|
Loading…
Reference in a new issue