UNPKG

create-instantsearch-app

Version:

⚡️ Build InstantSearch apps at the speed of thought

402 lines (370 loc) 11.2 kB
#!/usr/bin/env node const path = require('path'); const process = require('process'); const os = require('os'); const program = require('commander'); const inquirer = require('inquirer'); const chalk = require('chalk'); const latestSemver = require('latest-semver'); const semver = require('semver'); const createInstantSearchApp = require('../api'); const { checkAppPath, checkAppName, getAppTemplateConfig, fetchLibraryVersions, getTemplatesByCategory, getTemplatePath, splitArray, } = require('../utils'); const getAttributesFromIndex = require('./getAttributesFromIndex'); const getFacetsFromIndex = require('./getFacetsFromIndex'); const getAnswersDefaultValues = require('./getAnswersDefaultValues'); const isQuestionAsked = require('./isQuestionAsked'); const getConfiguration = require('./getConfiguration'); const postProcessAnswers = require('./postProcessAnswers'); const { version } = require('../../package.json'); let appPathFromArgument; program .storeOptionsAsProperties(false) .version(version, '-v, --version') .name('create-instantsearch-app') .arguments('<project-directory>') .usage(`${chalk.green('<project-directory>')} [options]`) .option('--name <name>', 'The name of the application or widget') .option('--app-id <appId>', 'The application ID') .option('--api-key <apiKey>', 'The Algolia search API key') .option('--index-name <indexName>', 'The main index of your search') .option( '--attributes-to-display <attributesToDisplay>', 'The attributes of your index to display in hits', splitArray ) .option( '--attributes-for-faceting <attributesForFaceting>', 'The attributes to display as filters', splitArray ) .option('--template <template>', 'The InstantSearch template to use') .option('--library-version <version>', 'The version of the library') .option('--config <config>', 'The configuration file to get the options from') .option('--no-installation', 'Ignore dependency installation') .option('--no-interactive', 'Ask no interactive questions') .action(dest => { appPathFromArgument = dest; }) .parse(process.argv); const optionsFromArguments = program.opts(); const { attributesToDisplay = [], attributesForFaceting = [], } = optionsFromArguments; const getQuestions = ({ appName }) => ({ application: [ { type: 'list', name: 'libraryVersion', message: answers => `${answers.template} version`, choices: async answers => { const templatePath = getTemplatePath(answers.template); const templateConfig = getAppTemplateConfig(templatePath); const { libraryName } = templateConfig; try { const versions = await fetchLibraryVersions(libraryName); const latestStableVersion = latestSemver(versions); if (!latestStableVersion) { return versions; } return [ new inquirer.Separator('Latest stable version (recommended)'), latestStableVersion, new inquirer.Separator('All versions'), ...versions, ]; } catch (err) { const fallbackLibraryVersion = '1.0.0'; console.log(); console.error( chalk.red( `Cannot fetch versions for library "${chalk.cyan(libraryName)}".` ) ); console.log(); console.log( `Fallback to ${chalk.cyan( fallbackLibraryVersion )}, please upgrade the dependency after generating the app.` ); console.log(); return [ new inquirer.Separator('Available versions'), fallbackLibraryVersion, ]; } }, when: answers => { const templatePath = getTemplatePath(answers.template); const templateConfig = getAppTemplateConfig(templatePath); return Boolean(templateConfig.libraryName); }, }, { type: 'input', name: 'appId', message: 'Application ID', default: 'latency', }, { type: 'input', name: 'apiKey', message: 'Search API key', default: '6be0576ff61c053d5f9a3225e2a90f76', }, { type: 'input', name: 'indexName', message: 'Index name', default: 'instant_search', }, { type: 'checkbox', name: 'attributesToDisplay', message: 'Attributes to display', suffix: `\n ${chalk.gray( 'Used to generate the default result template' )}`, pageSize: 10, choices: async answers => [ { name: 'None', value: undefined, }, new inquirer.Separator(), new inquirer.Separator('From your index'), ...(await getAttributesFromIndex(answers)), ], filter: attributes => attributes.filter(Boolean), when: ({ appId, apiKey, indexName }) => attributesToDisplay.length === 0 && appId && apiKey && indexName, }, { type: 'checkbox', name: 'attributesForFaceting', message: 'Attributes to display', suffix: `\n ${chalk.gray('Used to filter the search interface')}`, pageSize: 10, choices: async answers => { const templatePath = getTemplatePath(answers.template); const templateConfig = getAppTemplateConfig(templatePath); const selectedLibraryVersion = answers.libraryVersion; const requiredLibraryVersion = templateConfig.flags && templateConfig.flags.dynamicWidgets; const supportsDynamicWidgets = selectedLibraryVersion && requiredLibraryVersion && semver.satisfies(selectedLibraryVersion, requiredLibraryVersion, { includePrerelease: true, }); const dynamicWidgets = supportsDynamicWidgets ? [ { name: 'Dynamic widgets', value: 'ais.dynamicWidgets', checked: true, }, new inquirer.Separator(), ] : []; return [ ...dynamicWidgets, new inquirer.Separator('From your index'), ...(await getFacetsFromIndex(answers)), new inquirer.Separator(), ]; }, filter: attributes => attributes.filter(Boolean), when: ({ appId, apiKey, indexName }) => attributesForFaceting.length === 0 && appId && apiKey && indexName, }, ], widget: [ { type: 'input', name: 'organization', message: 'Organization (on npm)', validate(input) { return typeof input === 'string' && input.length > 0; }, }, { type: 'input', name: 'description', message: 'Description', default() { const splitName = appName.split('-').join(' '); return `InstantSearch widget that makes a ${splitName}`; }, validate(input) { return typeof input === 'string'; }, }, ], initial: [ { type: 'input', name: 'appPath', message: 'Project directory', askAnswered: true, validate(input) { try { return checkAppPath(input); } catch (err) { console.log(); console.error(err.message); return false; } }, when(answers) { try { return ( answers.interactive && !answers.config && !checkAppPath(answers.appPath) ); } catch (err) { return true; } }, }, { type: 'input', name: 'appName', message: 'The name of the application or widget', askAnswered: true, default(answers) { return path.basename(answers.appPath); }, validate(input) { try { return checkAppName(input); } catch (err) { console.log(); console.error(err.message); return false; } }, when(answers) { try { return ( answers.interactive && !answers.config && !checkAppName(answers.appName) ); } catch (err) { return true; } }, }, { type: 'list', pageSize: 10, name: 'template', message: 'InstantSearch template', askAnswered: true, choices: () => { const templatesByCategory = getTemplatesByCategory(); return Object.entries(templatesByCategory).reduce( (templates, [category, values]) => [ ...templates, new inquirer.Separator(category), ...values, ], [] ); }, validate(input) { // if a config is given, path is optional if (optionsFromArguments.config) { return true; } const isValid = Boolean(input); if (!isValid) { console.log('template is required'); } return isValid; }, when(answers) { return answers.interactive && !answers.config && !answers.template; }, }, ], }); async function run() { const args = { ...optionsFromArguments, appPath: appPathFromArgument, appName: optionsFromArguments.name, }; const initialQuestions = getQuestions({}).initial; const initialAnswers = { ...args, ...(await inquirer.prompt(initialQuestions, args)), }; initialQuestions.forEach(question => { // .default doesn't get executed when "when" returns false if (!initialAnswers[question.name] && question.default) { const defaultValue = question.default(initialAnswers); initialAnswers[question.name] = defaultValue; } if (!question.validate(initialAnswers[question.name])) { process.exit(1); } }); if (initialAnswers.appPath.startsWith('~/')) { initialAnswers.appPath = path.join( os.homedir(), initialAnswers.appPath.slice(2) ); } const { appPath, appName, template } = initialAnswers; console.log(); console.log(`Creating a new InstantSearch app in ${chalk.green(appPath)}.`); console.log(); const configuration = await getConfiguration({ options: { ...optionsFromArguments, name: appName, }, answers: initialAnswers, }); const templatePath = getTemplatePath(configuration.template); const templateConfig = getAppTemplateConfig(templatePath); const implementationType = templateConfig.category === 'Widget' ? 'widget' : 'application'; const answers = await inquirer.prompt( getQuestions({ appName })[implementationType].filter( question => !isQuestionAsked({ question, args: optionsFromArguments }) ), getAnswersDefaultValues(optionsFromArguments, configuration, template) ); const app = createInstantSearchApp( appPath, await postProcessAnswers({ configuration, answers, optionsFromArguments, templatePath, templateConfig, }), templateConfig.tasks ); await app.create(); } run().catch(err => { console.error(err.message); console.log(); process.exit(2); }); process.on('SIGINT', () => { process.exit(3); });