From b24fdc1559141d36a454ba066b25c4c6a5e6a7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 13 Jun 2018 15:31:51 +0200 Subject: [PATCH 1/6] refactor(cli): Refactor `mainAttribute` --- packages/cli/cli.js | 31 ++++++++----------------------- packages/cli/utils.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/packages/cli/cli.js b/packages/cli/cli.js index 7db492c9c..a7b5ca38a 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -5,7 +5,6 @@ const inquirer = require('inquirer'); const chalk = require('chalk'); const latestSemver = require('latest-semver'); const loadJsonFile = require('load-json-file'); -const algoliasearch = require('algoliasearch'); const createInstantSearchApp = require('../create-instantsearch-app'); const { @@ -16,7 +15,11 @@ const { getAllTemplates, getTemplatePath, } = require('../shared/utils'); -const { getOptionsFromArguments, isQuestionAsked } = require('./utils'); +const { + getOptionsFromArguments, + getAttributesFromAnswers, + isQuestionAsked, +} = require('./utils'); const { version } = require('../../package.json'); let appPath; @@ -74,7 +77,8 @@ if (!appPath) { process.exit(1); } -const appName = path.basename(appPath); +const optionsFromArguments = getOptionsFromArguments(options.rawArgs); +const appName = optionsFromArguments.name || path.basename(appPath); try { checkAppPath(appPath); @@ -86,8 +90,6 @@ try { process.exit(1); } -const optionsFromArguments = getOptionsFromArguments(options.rawArgs); - const questions = [ { type: 'list', @@ -164,24 +166,7 @@ const questions = [ type: 'list', name: 'mainAttribute', message: 'Attribute to display', - choices: async answers => { - const client = algoliasearch(answers.appId, answers.apiKey); - const index = client.initIndex(answers.indexName); - const defaultAttributes = ['title', 'name', 'description']; - let attributes = []; - - try { - const { hits } = await index.search({ hitsPerPage: 1 }); - const [firstHit] = hits; - attributes = Object.keys(firstHit._highlightResult).sort( - value => !defaultAttributes.includes(value) - ); - } catch (err) { - attributes = defaultAttributes; - } - - return attributes; - }, + choices: async answers => await getAttributesFromAnswers(answers), when: ({ appId, apiKey, indexName }) => appId && apiKey && indexName, }, ].filter(question => isQuestionAsked({ question, args: optionsFromArguments })); diff --git a/packages/cli/utils.js b/packages/cli/utils.js index bd64a6673..bd6a27533 100644 --- a/packages/cli/utils.js +++ b/packages/cli/utils.js @@ -1,3 +1,7 @@ +const algoliasearch = require('algoliasearch'); +const latestSemver = require('latest-semver'); +const { fetchLibraryVersions } = require('../shared/utils'); + function camelCase(string) { return string.replace(/-([a-z])/g, str => str[1].toUpperCase()); } @@ -22,6 +26,35 @@ function getOptionsFromArguments(rawArgs) { }, {}); } +async function getAttributesFromAnswers({ + appId, + apiKey, + indexName, + algoliasearchFn = algoliasearch, +} = {}) { + const client = algoliasearchFn(appId, apiKey); + const defaultAttributes = ['title', 'name', 'description']; + let attributes = []; + + try { + const { hits } = await client.search({ indexName, hitsPerPage: 1 }); + const [firstHit] = hits; + const highlightedAttributes = Object.keys(firstHit._highlightResult); + attributes = [ + ...new Set([ + ...defaultAttributes.map( + attribute => highlightedAttributes.includes(attribute) && attribute + ), + ...highlightedAttributes, + ]), + ]; + } catch (err) { + attributes = defaultAttributes; + } + + return attributes; +} + function isQuestionAsked({ question, args }) { for (const optionName in args) { if (question.name === optionName) { @@ -41,5 +74,6 @@ function isQuestionAsked({ question, args }) { module.exports = { camelCase, getOptionsFromArguments, + getAttributesFromAnswers, isQuestionAsked, }; From e3c15de4ff748ef279a90b831493e37d959395ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 13 Jun 2018 15:32:00 +0200 Subject: [PATCH 2/6] test(cli): Test `mainAttribute` --- packages/cli/utils.test.js | 47 +++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/cli/utils.test.js b/packages/cli/utils.test.js index 11f8e095a..d494b8bd7 100644 --- a/packages/cli/utils.test.js +++ b/packages/cli/utils.test.js @@ -79,7 +79,52 @@ describe('getOptionsFromArguments', () => { }); }); -describe('isQuestionAsked', () => { +describe('getAttributesFromAnswers', () => { + const algoliasearchSuccessFn = () => ({ + search: jest.fn(() => ({ + hits: [ + { + _highlightResult: { + brand: 'brand', + description: 'description', + name: 'name', + title: 'title', + }, + }, + ], + })), + }); + + const algoliasearchFailureFn = () => ({ + search: jest.fn(() => { + throw new Error(); + }), + }); + + test('with search success should fetch attributes', async () => { + const attributes = await utils.getAttributesFromAnswers({ + appId: 'appId', + apiKey: 'apiKey', + indexName: 'indexName', + algoliasearchFn: algoliasearchSuccessFn, + }); + + expect(attributes).toEqual(['title', 'name', 'description', 'brand']); + }); + + test('with search failure should return default attributes', async () => { + const attributes = await utils.getAttributesFromAnswers({ + appId: 'appId', + apiKey: 'apiKey', + indexName: 'indexName', + algoliasearchFn: algoliasearchFailureFn, + }); + + expect(attributes).toEqual(['title', 'name', 'description']); + }); +}); + +test('isQuestionAsked', () => { expect( utils.isQuestionAsked({ question: { name: 'appId', validate: input => Boolean(input) }, From bddd78479224699537a82b96f404962fc91ec837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 13 Jun 2018 15:47:13 +0200 Subject: [PATCH 3/6] refactor(cli): Remove unused imports --- packages/cli/utils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli/utils.js b/packages/cli/utils.js index bd6a27533..76ec60c5b 100644 --- a/packages/cli/utils.js +++ b/packages/cli/utils.js @@ -1,6 +1,4 @@ const algoliasearch = require('algoliasearch'); -const latestSemver = require('latest-semver'); -const { fetchLibraryVersions } = require('../shared/utils'); function camelCase(string) { return string.replace(/-([a-z])/g, str => str[1].toUpperCase()); From 192e0b87d4ddd64374393499b905f90d3d1a9a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 13 Jun 2018 17:12:19 +0200 Subject: [PATCH 4/6] refactor(cli): Refactor `getConfiguration()` --- packages/cli/cli.js | 43 ++++++++----------------------------------- packages/cli/utils.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/cli/cli.js b/packages/cli/cli.js index a7b5ca38a..733d29272 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -4,7 +4,6 @@ const program = require('commander'); const inquirer = require('inquirer'); const chalk = require('chalk'); const latestSemver = require('latest-semver'); -const loadJsonFile = require('load-json-file'); const createInstantSearchApp = require('../create-instantsearch-app'); const { @@ -19,6 +18,7 @@ const { getOptionsFromArguments, getAttributesFromAnswers, isQuestionAsked, + getConfiguration, } = require('./utils'); const { version } = require('../../package.json'); @@ -171,44 +171,17 @@ const questions = [ }, ].filter(question => isQuestionAsked({ question, args: optionsFromArguments })); -async function getConfig() { - let config; - - if (optionsFromArguments.config) { - // Get config from configuration file given as an argument - config = await loadJsonFile(optionsFromArguments.config); - } else { - // Get config from the arguments and the prompt - config = { - ...optionsFromArguments, - ...(await inquirer.prompt(questions)), - }; - } - - const templatePath = getTemplatePath(config.template); - let libraryVersion = config.libraryVersion; - - if (!libraryVersion) { - const templateConfig = getAppTemplateConfig(templatePath); - - libraryVersion = await fetchLibraryVersions( - templateConfig.libraryName - ).then(latestSemver); - } - - return { - ...config, - libraryVersion, - name: config.name || appName, - template: templatePath, - }; -} - async function run() { console.log(`Creating a new InstantSearch app in ${chalk.green(appPath)}.`); const config = { - ...(await getConfig()), + ...(await getConfiguration({ + options: { + ...optionsFromArguments, + name: appName, + }, + answers: await inquirer.prompt(questions), + })), installation: program.installation, }; diff --git a/packages/cli/utils.js b/packages/cli/utils.js index 76ec60c5b..f5b1eb39e 100644 --- a/packages/cli/utils.js +++ b/packages/cli/utils.js @@ -1,4 +1,12 @@ const algoliasearch = require('algoliasearch'); +const latestSemver = require('latest-semver'); +const loadJsonFile = require('load-json-file'); + +const { + getAppTemplateConfig, + fetchLibraryVersions, + getTemplatePath, +} = require('../shared/utils'); function camelCase(string) { return string.replace(/-([a-z])/g, str => str[1].toUpperCase()); @@ -69,9 +77,41 @@ function isQuestionAsked({ question, args }) { return true; } +async function getConfiguration({ + options = {}, + answers = {}, + loadJsonFileFn = loadJsonFile, +} = {}) { + const config = options.config + ? await loadJsonFileFn(options.config) // From configuration file given as an argument + : { ...options, ...answers }; // From the arguments and the prompt + + if (!config.template) { + throw new Error('The template is required in the config.'); + } + + const templatePath = getTemplatePath(config.template); + let { libraryVersion } = config; + + if (!libraryVersion) { + const templateConfig = getAppTemplateConfig(templatePath); + + libraryVersion = await fetchLibraryVersions( + templateConfig.libraryName + ).then(latestSemver); + } + + return { + ...config, + libraryVersion, + template: templatePath, + }; +} + module.exports = { camelCase, getOptionsFromArguments, getAttributesFromAnswers, isQuestionAsked, + getConfiguration, }; From 965cc5118dc3c43e3e401dd159913c27027c2356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 13 Jun 2018 17:17:45 +0200 Subject: [PATCH 5/6] test(cli): Test `getConfiguration()` --- packages/cli/utils.test.js | 70 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/cli/utils.test.js b/packages/cli/utils.test.js index d494b8bd7..28d4a0166 100644 --- a/packages/cli/utils.test.js +++ b/packages/cli/utils.test.js @@ -1,3 +1,4 @@ +const path = require('path'); const utils = require('./utils'); describe('getOptionsFromArguments', () => { @@ -182,3 +183,72 @@ describe('camelCase', () => { expect(utils.camelCase('instant-search-js')).toBe('instantSearchJs'); }); }); + +describe('getConfiguration', () => { + test('without template throws', async () => { + expect.assertions(1); + + try { + await utils.getConfiguration({}); + } catch (err) { + expect(err.message).toBe('The template is required in the config.'); + } + }); + + test('with template transforms to its relative path', async () => { + const configuration = await utils.getConfiguration({ + answers: { template: 'InstantSearch.js' }, + }); + + expect(configuration).toEqual( + expect.objectContaining({ + template: path.resolve('templates/InstantSearch.js'), + }) + ); + }); + + test('with options from arguments and prompt merge', async () => { + const configuration = await utils.getConfiguration({ + options: { + name: 'my-app', + }, + answers: { + template: 'InstantSearch.js', + libraryVersion: '1.0.0', + }, + }); + + expect(configuration).toEqual( + expect.objectContaining({ + name: 'my-app', + libraryVersion: '1.0.0', + }) + ); + }); + + test('with config file overrides all options', async () => { + const loadJsonFileFn = jest.fn(x => Promise.resolve(x)); + const ignoredOptions = { + libraryVersion: '2.0.0', + }; + const options = { + config: { + template: 'InstantSearch.js', + libraryVersion: '1.0.0', + }, + ...ignoredOptions, + }; + const answers = { + ignoredKey: 'ignoredValue', + }; + + const configuration = await utils.getConfiguration({ + options, + answers, + loadJsonFileFn, + }); + + expect(configuration).toEqual(expect.not.objectContaining(ignoredOptions)); + expect(configuration).toEqual(expect.not.objectContaining(answers)); + }); +}); From 3c8366c77a739b878131c8b087e2ea007bb95a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 13 Jun 2018 17:54:47 +0200 Subject: [PATCH 6/6] fix(cli): Skip question if config file is passed --- packages/cli/utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cli/utils.js b/packages/cli/utils.js index f5b1eb39e..b407196b1 100644 --- a/packages/cli/utils.js +++ b/packages/cli/utils.js @@ -62,6 +62,10 @@ async function getAttributesFromAnswers({ } function isQuestionAsked({ question, args }) { + if (args.config) { + return false; + } + for (const optionName in args) { if (question.name === optionName) { // Skip if the arg in the command is valid