Skip to content
This repository was archived by the owner on Dec 16, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 16 additions & 58 deletions packages/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +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 algoliasearch = require('algoliasearch');

const createInstantSearchApp = require('../create-instantsearch-app');
const {
Expand All @@ -16,7 +14,12 @@ const {
getAllTemplates,
getTemplatePath,
} = require('../shared/utils');
const { getOptionsFromArguments, isQuestionAsked } = require('./utils');
const {
getOptionsFromArguments,
getAttributesFromAnswers,
isQuestionAsked,
getConfiguration,
} = require('./utils');
const { version } = require('../../package.json');

let appPath;
Expand Down Expand Up @@ -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);
Expand All @@ -86,8 +90,6 @@ try {
process.exit(1);
}

const optionsFromArguments = getOptionsFromArguments(options.rawArgs);

const questions = [
{
type: 'list',
Expand Down Expand Up @@ -164,66 +166,22 @@ 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 }));

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,
};

Expand Down
76 changes: 76 additions & 0 deletions packages/cli/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
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());
}
Expand All @@ -22,7 +32,40 @@ 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 }) {
if (args.config) {
return false;
}

for (const optionName in args) {
if (question.name === optionName) {
// Skip if the arg in the command is valid
Expand All @@ -38,8 +81,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,
};
117 changes: 116 additions & 1 deletion packages/cli/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const path = require('path');
const utils = require('./utils');

describe('getOptionsFromArguments', () => {
Expand Down Expand Up @@ -79,7 +80,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) },
Expand Down Expand Up @@ -137,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));
});
});