Skip to content

Commit

Permalink
[RFC 638] Interactive way to create new Ember apps and addons
Browse files Browse the repository at this point in the history
  • Loading branch information
bertdeblock committed Apr 12, 2022
1 parent 519b67f commit c2d8688
Show file tree
Hide file tree
Showing 12 changed files with 538 additions and 17 deletions.
30 changes: 27 additions & 3 deletions lib/commands/new.js
Expand Up @@ -41,6 +41,13 @@ module.exports = Command.extend({
default: 'github',
description: 'Installs the default CI blueprint. Either Travis or Github Actions is supported.',
},
{
name: 'interactive',
type: Boolean,
default: false,
aliases: ['i'],
description: 'Create a new Ember app/addon in an interactive way.',
},
],

anonymousOptions: ['<app-name>'],
Expand All @@ -53,10 +60,27 @@ module.exports = Command.extend({

commandOptions.name = rawArgs.shift();

if (!projectName) {
message = `The \`ember ${this.name}\` command requires a name to be specified. For more details, use \`ember help\`.`;
if (!projectName || commandOptions.interactive) {
let answers = await this.runTask('InteractiveNew', commandOptions);

return Promise.reject(new SilentError(message));
commandOptions.blueprint = answers.blueprint;

if (answers.name) {
projectName = answers.name;
commandOptions.name = answers.name;
}

if (answers.lang) {
commandOptions.lang = answers.lang;
}

if (answers.packageManager) {
commandOptions.yarn = answers.packageManager === 'yarn';
}

if (answers.ciProvider) {
commandOptions.ciProvider = answers.ciProvider;
}
}

if (commandOptions.dryRun) {
Expand Down
160 changes: 160 additions & 0 deletions lib/tasks/interactive-new.js
@@ -0,0 +1,160 @@
'use strict';

const inquirer = require('inquirer');
const { isLangCode } = require('is-language-code');
const osLocale = require('os-locale');

const Task = require('../models/task');
const isValidProjectName = require('../utilities/valid-project-name');

const DEFAULT_LOCALE = 'en-US';

class InteractiveNewTask extends Task {
async run(newCommandOptions, _testAnswers) {
let prompt = inquirer.createPromptModule();
let questions = await this.getQuestions(newCommandOptions);
let answers = await prompt(questions, _testAnswers);

answers.lang = answers.langSelection || answers.langDifferent;

delete answers.langSelection;
delete answers.langDifferent;

return answers;
}

async getQuestions(newCommandOptions = {}) {
return [
{
name: 'blueprint',
type: 'list',
message: 'Is this an app or an addon?',
choices: [
{
name: 'App',
value: 'app',
},
{
name: 'Addon',
value: 'addon',
},
],
},
{
name: 'name',
type: 'input',
message: ({ blueprint }) => `Please provide the name of your ${blueprint}:`,
when: !newCommandOptions.name,
validate: (name) => {
if (name) {
if (isValidProjectName(name)) {
return true;
}

return `We currently do not support \`${name}\` as a name.`;
}

return 'Please provide a name.';
},
},
{
name: 'langSelection',
type: 'list',
message: ({ blueprint }) => `Please provide the spoken/content language of your ${blueprint}:`,
when: !newCommandOptions.lang,
choices: await this.getLangChoices(),
},
{
name: 'langDifferent',
type: 'input',
message: 'Please provide the different language:',
when: ({ langSelection } = {}) => !newCommandOptions.lang && !langSelection,
validate: (lang) => {
if (isLangCode(lang).res) {
return true;
}

return 'Please provide a valid locale code.';
},
},
{
name: 'packageManager',
type: 'list',
message: 'Pick the package manager to use when installing dependencies:',
when: !newCommandOptions.yarn,
choices: [
{
name: 'NPM',
value: 'npm',
},
{
name: 'Yarn',
value: 'yarn',
},
{
name: 'Ignore/Skip',
value: null,
},
],
},
{
name: 'ciProvider',
type: 'list',
message: 'Which CI provider do you want to use?',
// `newCommandOptions.ciProvider` has `github` as a default value,
// so we need to check the presence of the `--ci-provider` flag to know
// if the user provided a CI provider manually.
when: !isCliOptionProvided('--ci-provider'),
choices: [
{
name: 'GitHub Actions',
value: 'github',
},
{
name: 'Travis CI',
value: 'travis',
},
{
name: 'Ignore/Skip',
value: null,
},
],
},
];
}

async getLangChoices() {
let userLocale = await this.getUserLocale();
let langChoices = [
{
name: DEFAULT_LOCALE,
value: DEFAULT_LOCALE,
},
];

if (userLocale !== DEFAULT_LOCALE) {
langChoices.push({
name: userLocale,
value: userLocale,
});
}

langChoices.push({
name: 'I want to manually provide a different language',
value: null,
});

return langChoices;
}

getUserLocale() {
return osLocale();
}
}

function isCliOptionProvided(name) {
return process.argv.some((arg) => arg.includes(name));
}

module.exports = InteractiveNewTask;
module.exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -94,6 +94,7 @@
"heimdalljs-logger": "^0.1.10",
"http-proxy": "^1.18.1",
"inflection": "^1.13.1",
"inquirer": "^8.2.1",
"is-git-url": "^1.0.0",
"is-language-code": "^3.1.0",
"isbinaryfile": "^4.0.8",
Expand All @@ -106,6 +107,7 @@
"morgan": "^1.10.0",
"nopt": "^3.0.6",
"npm-package-arg": "^8.1.5",
"os-locale": "^5.0.0",
"p-defer": "^3.0.0",
"portfinder": "^1.0.28",
"promise-map-series": "^0.3.0",
Expand Down
14 changes: 0 additions & 14 deletions tests/acceptance/new-test.js
Expand Up @@ -162,20 +162,6 @@ describe('Acceptance: ember new', function () {
expect(blueprintTargets).to.have.same.deep.members(defaultTargets);
});

it('ember new with empty app name fails with a warning', async function () {
let err = await expect(ember(['new', ''])).to.be.rejected;

expect(err.name).to.equal('SilentError');
expect(err.message).to.contain('The `ember new` command requires a name to be specified.');
});

it('ember new without app name fails with a warning', async function () {
let err = await expect(ember(['new'])).to.be.rejected;

expect(err.name).to.equal('SilentError');
expect(err.message).to.contain('The `ember new` command requires a name to be specified.');
});

it('ember new with app name creates new directory and has a dasherized package name', async function () {
await ember(['new', 'FooApp', '--skip-npm', '--skip-bower', '--skip-git']);

Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/help/help-with-addon.txt
Expand Up @@ -140,6 +140,8 @@ ember new <app-name> <options...>
--lang (String) Sets the base human language of the application via index.html
--embroider (Boolean) (Default: false) Enables the build system to use Embroider
--ci-provider (travis, github) (Default: github) Installs the default CI blueprint. Either Travis or Github Actions is supported.
--interactive (Boolean) (Default: false) Create a new Ember app/addon in an interactive way.
aliases: -i

ember serve <options...>
Builds and serves your app, rebuilding on file changes.
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/help/help.js
Expand Up @@ -606,6 +606,14 @@ module.exports = {
description: 'Installs the default CI blueprint. Either Travis or Github Actions is supported.',
required: false,
},
{
name: 'interactive',
default: false,
aliases: ['i'],
description: 'Create a new Ember app/addon in an interactive way.',
key: 'interactive',
required: false,
},
],
anonymousOptions: ['<app-name>']
},
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/help/help.txt
Expand Up @@ -140,6 +140,8 @@ ember new <app-name> <options...>
--lang (String) Sets the base human language of the application via index.html
--embroider (Boolean) (Default: false) Enables the build system to use Embroider
--ci-provider (travis, github) (Default: github) Installs the default CI blueprint. Either Travis or Github Actions is supported.
--interactive (Boolean) (Default: false) Create a new Ember app/addon in an interactive way.
aliases: -i

ember serve <options...>
Builds and serves your app, rebuilding on file changes.
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/help/with-addon-blueprints.js
Expand Up @@ -638,6 +638,14 @@ module.exports = {
description: 'Installs the default CI blueprint. Either Travis or Github Actions is supported.',
required: false,
},
{
name: 'interactive',
default: false,
aliases: ['i'],
description: 'Create a new Ember app/addon in an interactive way.',
key: 'interactive',
required: false,
},
],
anonymousOptions: ['<app-name>']
},
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/help/with-addon-commands.js
Expand Up @@ -606,6 +606,14 @@ module.exports = {
description: 'Installs the default CI blueprint. Either Travis or Github Actions is supported.',
required: false,
},
{
name: 'interactive',
default: false,
aliases: ['i'],
description: 'Create a new Ember app/addon in an interactive way.',
key: 'interactive',
required: false,
},
],
anonymousOptions: ['<app-name>']
},
Expand Down

0 comments on commit c2d8688

Please sign in to comment.