Skip to content

Commit

Permalink
feat(cli): create modules, preset/feature selection
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Chau committed Dec 30, 2018
1 parent a3eb078 commit c8ebcb7
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 18 deletions.
2 changes: 1 addition & 1 deletion packages/@nodepack/cli/src/commands/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { chalk, clearConsole, error, stopSpinner } = require('@nodepack/utils')
const validateProjectName = require('validate-npm-package-name')
const inquirer = require('inquirer')
const Creator = require('../lib/Creator')
const { getPromptModules } = require('../util/createTools')
const { getPromptModules } = require('../lib/createModules')

/**
* @param {string} projectName
Expand Down
34 changes: 34 additions & 0 deletions packages/@nodepack/cli/src/lib/CreateModuleAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** @typedef {import('./Creator')} Creator */
/** @typedef {import('inquirer').ChoiceType} ChoiceType */
/** @typedef {import('inquirer').Question} Question */
/** @typedef {import('./Creator').Preset} Preset */

module.exports = class CreateModuleAPI {
/**
* @param {Creator} creator
*/
constructor (creator) {
this.creator = creator
}

/**
* @param {ChoiceType} feature
*/
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}

/**
* @param {Question} prompt
*/
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}

/**
* @param {(answers: any, preset: Preset) => void | Promise.<void>} cb
*/
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
88 changes: 79 additions & 9 deletions packages/@nodepack/cli/src/lib/Creator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/** @typedef {import('inquirer').Question} Question */
/** @typedef {import('inquirer').ChoiceType} ChoiceType */
/** @typedef {import('@nodepack/utils').Preset} Preset */
/** @typedef {import('./CreateModuleAPI')} CreateModuleAPI */

/** @typedef {(api: CreateModuleAPI) => void} CreateModule */

// API
const CreateModuleAPI = require('./CreateModuleAPI')
// Utils
const path = require('path')
const inquirer = require('inquirer')
const execa = require('execa')
Expand All @@ -9,6 +16,7 @@ const getPackageVersion = require('../util/getPackageVersion')
const {
loadGlobalOptions,
saveGlobalOptions,
defaultGlobalOptions,
clearConsole,
logWithSpinner,
stopSpinner,
Expand All @@ -28,20 +36,31 @@ const { Migrator, MigratorPlugin, MigrationOperationFile, writeFileTree } = requ
const generateReadme = require('../util/generateReadme')
const loadLocalPreset = require('../util/loadLocalPreset')
const loadRemotePreset = require('../util/loadRemotePreset')
const { formatFeatures } = require('../util/features')

// TODO presets
const isManualMode = true
const isManualMode = answers => answers.preset === '__manual__'

module.exports = class Creator {
constructor (projectName, targetDir, promptModules) {
/**
* @param {string} projectName
* @param {string} targetDir
* @param {CreateModule []} createModules
*/
constructor (projectName, targetDir, createModules) {
this.projectName = projectName
this.cwd = process.env.NODEPACK_CONTEXT = targetDir
// TODO
this.promptModules = promptModules
this.createModules = createModules

const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
this.presetPrompt = presetPrompt
this.featurePrompt = featurePrompt
/** @type {Question []} */
this.injectedPrompts = []
this.outroPrompts = this.resolveOutroPrompts()

/** @type {function []} */
this.promptCompleteCbs = []
/** @type {function []} */
this.createCompleteCbs = []

this.run = this.run.bind(this)
Expand All @@ -57,6 +76,12 @@ module.exports = class Creator {
// Are one of those vars non-empty?
const isTestOrDebug = !!(process.env.NODEPACK_TEST || process.env.NODEPACK_DEBUG)

// Apply create modules
const createModuleAPI = new CreateModuleAPI(this)
for (const createModule of this.createModules) {
await createModule(createModuleAPI)
}

// Preset resolution
if (!preset) {
preset = await this.resolvePresetFromOptions(cliOptions)
Expand Down Expand Up @@ -202,7 +227,9 @@ module.exports = class Creator {
}
answers.features = answers.features || []
// run cb registered by prompt modules to finalize the preset
this.promptCompleteCbs.forEach(cb => cb(answers, preset))
for (const cb of this.promptCompleteCbs) {
await cb(answers, preset)
}
}

// validate
Expand Down Expand Up @@ -271,9 +298,44 @@ module.exports = class Creator {
return plugins
}

run (command, args) {
if (!args) { [command, ...args] = command.split(/\s+/) }
return execa(command, args, { cwd: this.cwd })
getPresets () {
const savedOptions = loadGlobalOptions()
return Object.assign({}, savedOptions.presets, defaultGlobalOptions.presets)
}

resolveIntroPrompts () {
const presets = this.getPresets()
const presetChoices = Object.keys(presets).map(name => {
return {
name: `${name} (${formatFeatures(presets[name])})`,
value: name,
}
})
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a preset:`,
choices: [
...presetChoices,
{
name: 'Manually select features',
value: '__manual__',
},
],
}
const featurePrompt = {
name: 'features',
when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
/** @type {ChoiceType []} */
choices: [],
pageSize: 10,
}
return {
presetPrompt,
featurePrompt,
}
}

resolveOutroPrompts () {
Expand Down Expand Up @@ -335,6 +397,9 @@ module.exports = class Creator {

resolveFinalPrompts () {
const prompts = [
this.presetPrompt,
this.featurePrompt,
...this.injectedPrompts,
...this.outroPrompts,
]
return prompts
Expand Down Expand Up @@ -389,6 +454,11 @@ module.exports = class Creator {
return success
}

run (command, args) {
if (!args) { [command, ...args] = command.split(/\s+/) }
return execa(command, args, { cwd: this.cwd })
}

async writeFileToDisk (filename, source) {
await writeFileTree(this.cwd, {
[filename]: new MigrationOperationFile(this.cwd, filename, source, true),
Expand Down
19 changes: 19 additions & 0 deletions packages/@nodepack/cli/src/lib/createModules/babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** @type {import('../Creator').CreateModule} */
module.exports = api => {
api.injectFeature({
name: 'Babel',
value: 'babel',
})

api.onPromptComplete((answers, preset) => {
if (answers.features.includes('ts')) {
if (!answers.useTsWithBabel) {
return
}
} else if (!answers.features.includes('babel')) {
return
}
// @ts-ignore
preset.plugins['@nodepack/plugin-babel'] = ''
})
}
6 changes: 6 additions & 0 deletions packages/@nodepack/cli/src/lib/createModules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
exports.getPromptModules = () => {
return [
'babel',
'typescript',
].map(file => require(`./${file}`))
}
22 changes: 22 additions & 0 deletions packages/@nodepack/cli/src/lib/createModules/typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/** @type {import('../Creator').CreateModule} */
module.exports = api => {
api.injectFeature({
name: 'Typescript',
value: 'ts',
})

api.injectPrompt({
name: 'useTsWithBabel',
when: answers => answers.features.includes('ts'),
type: 'confirm',
message: 'Use Babel alongside TypeScript for auto-detected polyfills?',
default: answers => answers.features.includes('babel'),
})

api.onPromptComplete((answers, preset) => {
if (answers.features.includes('ts')) {
// @ts-ignore
preset.plugins['@nodepack/plugin-typescript'] = ''
}
})
}
8 changes: 0 additions & 8 deletions packages/@nodepack/cli/src/util/createTools.js

This file was deleted.

31 changes: 31 additions & 0 deletions packages/@nodepack/cli/src/util/features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/** @typedef {import('@nodepack/utils').Preset} Preset */

const {
chalk,
toShortPluginId,
} = require('@nodepack/utils')

/**
* @param {Preset} preset
*/
exports.getFeatures = (preset) => {
const features = []
const plugins = Object.keys(preset.plugins || []).filter(dep => {
return dep !== '@nodepack/service'
})
features.push.apply(features, plugins)
return features
}

/**
* @param {Preset} preset
* @param {string} lead
* @param {string} joiner
*/
exports.formatFeatures = (preset, lead = '', joiner = ', ') => {
const features = exports.getFeatures(preset)
return features.map(dep => {
dep = toShortPluginId(dep)
return `${lead}${chalk.yellow(dep)}`
}).join(joiner)
}

0 comments on commit c8ebcb7

Please sign in to comment.