create-instantsearch-app 4.11.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,60 @@
1
+ # [5.0.0](https://github.com/algolia/create-instantsearch-app/compare/4.11.1...5.0.0) (2021-12-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **dynamicWidgets:** prevent "ais.dynamicWidgets" attribute showing up ([#542](https://github.com/algolia/create-instantsearch-app/issues/542)) ([559f705](https://github.com/algolia/create-instantsearch-app/commit/559f705ebcef48a3794efd7a588abd187d6642c9))
7
+
8
+
9
+ ### Code Refactoring
10
+
11
+ * **index:** rewrite answer parsing ([#541](https://github.com/algolia/create-instantsearch-app/issues/541)) ([efd2799](https://github.com/algolia/create-instantsearch-app/commit/efd279943e23545c3c0806f8e9632ae32c83c0d6))
12
+
13
+
14
+ ### BREAKING CHANGES
15
+
16
+ * **index:** the program now asks questions if some of the parameters are sent via arguments. Before this, giving an argument would cause it not to ask any questions anymore, even if they still would be useful. You can avoid this behaviour by passing --config or --no-interactive
17
+
18
+ * postprocess answers
19
+
20
+ * e2e installs should not have any asked questions
21
+
22
+ * make e2e build test pass
23
+
24
+ * more info
25
+
26
+ * update to newer commander to fix the "name" issue
27
+
28
+ * refactor: validate appPath like appName
29
+
30
+ * error for empty string path
31
+
32
+ * clean lockfile
33
+
34
+ * refactor initial questions
35
+
36
+ * silently check for existing answers on appName
37
+
38
+ * don't ask initial questions if config is set
39
+
40
+ * introduce --no-interactive, mostly for test
41
+
42
+ * write tests
43
+
44
+ * fix argument
45
+
46
+ * undo change
47
+
48
+ * make .default apply always
49
+
50
+ * refactor templates to be part of initialQuestions
51
+
52
+ * cover initialQuestions in tests
53
+
54
+ * fix error
55
+
56
+
57
+
1
58
  ## [4.11.1](https://github.com/algolia/create-instantsearch-app/compare/4.11.0...4.11.1) (2021-11-18)
2
59
 
3
60
 
package/README.md CHANGED
@@ -71,6 +71,7 @@ $ create-instantsearch-app --help
71
71
  --library-version <libraryVersion> The version of the library
72
72
  --config <config> The configuration file to get the options from
73
73
  --no-installation Ignore dependency installation
74
+ --no-interactive Do not ask any interactive questions
74
75
  -h, --help output usage information
75
76
  ```
76
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-instantsearch-app",
3
- "version": "4.11.1",
3
+ "version": "5.0.0",
4
4
  "license": "MIT",
5
5
  "description": "⚡️ Build InstantSearch apps at the speed of thought",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "@algolia/cache-in-memory": "4.11.0",
40
40
  "algoliasearch": "4.11.0",
41
41
  "chalk": "3.0.0",
42
- "commander": "4.0.1",
42
+ "commander": "4.1.1",
43
43
  "inquirer": "8.0.0",
44
44
  "jstransformer-handlebars": "1.1.0",
45
45
  "latest-semver": "2.0.0",
@@ -1,14 +1,8 @@
1
1
  const loadJsonFile = require('load-json-file');
2
- const utils = require('../../utils');
3
- const { getConfiguration, getLibraryVersion } = require('../getConfiguration');
2
+ const getConfiguration = require('../getConfiguration');
4
3
 
5
4
  jest.mock('load-json-file');
6
5
 
7
- jest.mock('../../utils', () => ({
8
- ...require.requireActual('../../utils'),
9
- fetchLibraryVersions: jest.fn(() => Promise.resolve(['1.0.0'])),
10
- }));
11
-
12
6
  test('without template throws', async () => {
13
7
  expect.assertions(1);
14
8
 
@@ -36,21 +30,6 @@ test('with options from arguments and prompt merge', async () => {
36
30
  );
37
31
  });
38
32
 
39
- test('without stable version available', async () => {
40
- utils.fetchLibraryVersions.mockImplementationOnce(() =>
41
- Promise.resolve(['1.0.0-beta.0'])
42
- );
43
-
44
- const libraryVersion = await getLibraryVersion(
45
- {
46
- template: 'InstantSearch.js',
47
- },
48
- utils.getAppTemplateConfig(utils.getTemplatePath('InstantSearch.js'))
49
- );
50
-
51
- expect(libraryVersion).toBe('1.0.0-beta.0');
52
- });
53
-
54
33
  test('with config file overrides all options', async () => {
55
34
  loadJsonFile.mockImplementationOnce(x => Promise.resolve(x));
56
35
  const ignoredOptions = {
@@ -4,30 +4,30 @@ test('with appId undefined should ask', () => {
4
4
  expect(
5
5
  isQuestionAsked({
6
6
  question: { name: 'appId', validate: input => Boolean(input) },
7
- args: { appId: undefined },
7
+ args: { appId: undefined, interactive: true },
8
8
  })
9
- ).toBe(true);
9
+ ).toBe(false);
10
10
  });
11
11
 
12
12
  test('with appId defined should not ask', () => {
13
13
  expect(
14
14
  isQuestionAsked({
15
15
  question: { name: 'appId', validate: input => Boolean(input) },
16
- args: { appId: 'APP_ID' },
16
+ args: { appId: 'APP_ID', interactive: true },
17
17
  })
18
- ).toBe(false);
18
+ ).toBe(true);
19
19
  });
20
20
 
21
- test('with unvalid template should ask', () => {
21
+ test('with invalid template should ask', () => {
22
22
  expect(
23
23
  isQuestionAsked({
24
24
  question: {
25
25
  name: 'template',
26
26
  validate: () => false,
27
27
  },
28
- args: { template: 'Unvalid' },
28
+ args: { template: 'Unvalid', interactive: true },
29
29
  })
30
- ).toBe(true);
30
+ ).toBe(false);
31
31
  });
32
32
 
33
33
  test('with valid template should not ask', () => {
@@ -37,9 +37,9 @@ test('with valid template should not ask', () => {
37
37
  name: 'template',
38
38
  validate: () => true,
39
39
  },
40
- args: { template: 'InstantSearch.js' },
40
+ args: { template: 'InstantSearch.js', interactive: true },
41
41
  })
42
- ).toBe(false);
42
+ ).toBe(true);
43
43
  });
44
44
 
45
45
  test('with indexName should ask attributesToDisplay', () => {
@@ -48,7 +48,29 @@ test('with indexName should ask attributesToDisplay', () => {
48
48
  question: {
49
49
  name: 'attributesToDisplay',
50
50
  },
51
- args: { indexName: 'INDEX_NAME' },
51
+ args: { indexName: 'INDEX_NAME', interactive: true },
52
52
  })
53
53
  ).toBe(false);
54
54
  });
55
+
56
+ test('with config it does not ask', () => {
57
+ expect(
58
+ isQuestionAsked({
59
+ question: {
60
+ name: 'attributesToDisplay',
61
+ },
62
+ args: { config: '' },
63
+ })
64
+ ).toBe(true);
65
+ });
66
+
67
+ test('with --no-interactive it does not ask', () => {
68
+ expect(
69
+ isQuestionAsked({
70
+ question: {
71
+ name: 'attributesToDisplay',
72
+ },
73
+ args: { interactive: false },
74
+ })
75
+ ).toBe(true);
76
+ });
@@ -0,0 +1,111 @@
1
+ const postProcessAnswers = require('../postProcessAnswers');
2
+ const utils = require('../../utils');
3
+
4
+ jest.mock('../../utils', () => ({
5
+ ...require.requireActual('../../utils'),
6
+ fetchLibraryVersions: jest.fn(() => Promise.resolve(['1.0.0'])),
7
+ }));
8
+
9
+ test('merges configuration and answers', async () => {
10
+ expect(
11
+ await postProcessAnswers({
12
+ configuration: { attributesForFaceting: ['test'], b: 1 },
13
+ answers: { attributesForFaceting: [] },
14
+ templateConfig: {},
15
+ optionsFromArguments: {},
16
+ })
17
+ ).toEqual(expect.objectContaining({ attributesForFaceting: [], b: 1 }));
18
+ });
19
+
20
+ describe('libraryVersion', () => {
21
+ test('library version from answers is used', async () => {
22
+ const { libraryVersion } = await postProcessAnswers({
23
+ configuration: {},
24
+ answers: {
25
+ template: 'InstantSearch.js',
26
+ attributesForFaceting: [],
27
+ libraryVersion: '0.1.2',
28
+ },
29
+ optionsFromArguments: {},
30
+ templateConfig: utils.getAppTemplateConfig(
31
+ utils.getTemplatePath('InstantSearch.js')
32
+ ),
33
+ });
34
+
35
+ expect(libraryVersion).toBe('0.1.2');
36
+ });
37
+
38
+ test('library version falls back to beta if it is the only available', async () => {
39
+ utils.fetchLibraryVersions.mockImplementationOnce(() =>
40
+ Promise.resolve(['1.0.0-beta.0'])
41
+ );
42
+
43
+ const { libraryVersion } = await postProcessAnswers({
44
+ configuration: {},
45
+ answers: {
46
+ template: 'InstantSearch.js',
47
+ attributesForFaceting: [],
48
+ },
49
+ optionsFromArguments: {},
50
+ templateConfig: utils.getAppTemplateConfig(
51
+ utils.getTemplatePath('InstantSearch.js')
52
+ ),
53
+ });
54
+
55
+ expect(libraryVersion).toBe('1.0.0-beta.0');
56
+ });
57
+ });
58
+
59
+ test('creates alternative names', async () => {
60
+ expect(
61
+ await postProcessAnswers({
62
+ configuration: {},
63
+ answers: {
64
+ attributesForFaceting: [],
65
+ organization: 'algolia',
66
+ name: 'date-range',
67
+ },
68
+ templateConfig: {
69
+ packageNamePrefix: 'instantsearch-widget-',
70
+ },
71
+ optionsFromArguments: {},
72
+ })
73
+ ).toEqual(
74
+ expect.objectContaining({
75
+ packageName: '@algolia/instantsearch-widget-date-range',
76
+ widgetType: 'algolia.date-range',
77
+ camelCaseName: 'dateRange',
78
+ pascalCaseName: 'DateRange',
79
+ })
80
+ );
81
+ });
82
+
83
+ test('detects dynamic widgets', async () => {
84
+ expect(
85
+ await postProcessAnswers({
86
+ configuration: {},
87
+ templateConfig: {},
88
+ optionsFromArguments: {},
89
+ answers: { attributesForFaceting: ['ais.dynamicWidgets', 'test'] },
90
+ })
91
+ ).toEqual(
92
+ expect.objectContaining({
93
+ attributesForFaceting: ['test'],
94
+ flags: { dynamicWidgets: true },
95
+ })
96
+ );
97
+
98
+ expect(
99
+ await postProcessAnswers({
100
+ configuration: {},
101
+ templateConfig: {},
102
+ optionsFromArguments: {},
103
+ answers: { attributesForFaceting: ['test'] },
104
+ })
105
+ ).toEqual(
106
+ expect.objectContaining({
107
+ attributesForFaceting: ['test'],
108
+ flags: { dynamicWidgets: false },
109
+ })
110
+ );
111
+ });
@@ -4,12 +4,10 @@ module.exports = function getAnswersDefaultValues(
4
4
  template
5
5
  ) {
6
6
  return {
7
+ ...configuration,
7
8
  ...optionsFromArguments,
8
9
  template,
9
- attributesToDisplay:
10
- configuration.attributesToDisplay &&
11
- configuration.attributesToDisplay.length > 0
12
- ? configuration.attributesToDisplay
13
- : undefined,
10
+ // name has a default of '', as it's a special case in Commander
11
+ name: optionsFromArguments.name || configuration.name,
14
12
  };
15
13
  };
@@ -1,37 +1,4 @@
1
- const latestSemver = require('latest-semver');
2
1
  const loadJsonFile = require('load-json-file');
3
- const camelCase = require('lodash.camelcase');
4
-
5
- const { fetchLibraryVersions } = require('../utils');
6
-
7
- function capitalize(str) {
8
- return str.substr(0, 1).toUpperCase() + str.substr(1);
9
- }
10
-
11
- function createNameAlternatives({ organization, name, templateConfig }) {
12
- return {
13
- packageName: `@${organization}/${templateConfig.packageNamePrefix ||
14
- ''}${name}`,
15
- widgetType: `${organization}.${name}`,
16
- camelCaseName: camelCase(name),
17
- pascalCaseName: capitalize(camelCase(name)),
18
- };
19
- }
20
-
21
- async function getLibraryVersion(config, templateConfig) {
22
- const { libraryName } = templateConfig;
23
- const { libraryVersion } = config;
24
-
25
- if (libraryName && !libraryVersion) {
26
- const versions = await fetchLibraryVersions(libraryName);
27
-
28
- // Return the latest available version when
29
- // the stable version is not available
30
- return latestSemver(versions) || versions[0];
31
- }
32
-
33
- return libraryVersion;
34
- }
35
2
 
36
3
  async function getConfiguration({ options = {}, answers = {} } = {}) {
37
4
  const config = options.config
@@ -45,8 +12,4 @@ async function getConfiguration({ options = {}, answers = {} } = {}) {
45
12
  return config;
46
13
  }
47
14
 
48
- module.exports = {
49
- getConfiguration,
50
- getLibraryVersion,
51
- createNameAlternatives,
52
- };
15
+ module.exports = getConfiguration;
package/src/cli/index.js CHANGED
@@ -16,24 +16,22 @@ const {
16
16
  fetchLibraryVersions,
17
17
  getTemplatesByCategory,
18
18
  getTemplatePath,
19
+ splitArray,
19
20
  } = require('../utils');
20
- const getOptionsFromArguments = require('./getOptionsFromArguments');
21
21
  const getAttributesFromIndex = require('./getAttributesFromIndex');
22
22
  const getFacetsFromIndex = require('./getFacetsFromIndex');
23
23
  const getAnswersDefaultValues = require('./getAnswersDefaultValues');
24
24
  const isQuestionAsked = require('./isQuestionAsked');
25
- const {
26
- getConfiguration,
27
- getLibraryVersion,
28
- createNameAlternatives,
29
- } = require('./getConfiguration');
25
+ const getConfiguration = require('./getConfiguration');
26
+ const postProcessAnswers = require('./postProcessAnswers');
30
27
  const { version } = require('../../package.json');
31
28
 
32
29
  let appPathFromArgument;
33
- let options = {};
34
30
 
35
31
  program
32
+ .storeOptionsAsProperties(false)
36
33
  .version(version, '-v, --version')
34
+ .name('create-instantsearch-app')
37
35
  .arguments('<project-directory>')
38
36
  .usage(`${chalk.green('<project-directory>')} [options]`)
39
37
  .option('--name <name>', 'The name of the application or widget')
@@ -42,31 +40,29 @@ program
42
40
  .option('--index-name <indexName>', 'The main index of your search')
43
41
  .option(
44
42
  '--attributes-to-display <attributesToDisplay>',
45
- 'The attributes of your index to display'
43
+ 'The attributes of your index to display in hits',
44
+ splitArray
46
45
  )
47
46
  .option(
48
47
  '--attributes-for-faceting <attributesForFaceting>',
49
- 'The attributes for faceting'
48
+ 'The attributes to display as filters',
49
+ splitArray
50
50
  )
51
51
  .option('--template <template>', 'The InstantSearch template to use')
52
52
  .option('--library-version <version>', 'The version of the library')
53
53
  .option('--config <config>', 'The configuration file to get the options from')
54
54
  .option('--no-installation', 'Ignore dependency installation')
55
- .action((dest, opts) => {
55
+ .option('--no-interactive', 'Ask no interactive questions')
56
+ .action(dest => {
56
57
  appPathFromArgument = dest;
57
- options = opts;
58
58
  })
59
59
  .parse(process.argv);
60
60
 
61
- const optionsFromArguments = getOptionsFromArguments(options.rawArgs || []);
62
- const attributesToDisplay = (optionsFromArguments.attributesToDisplay || '')
63
- .split(',')
64
- .filter(Boolean)
65
- .map(x => x.trim());
66
- const attributesForFaceting = (optionsFromArguments.attributesForFaceting || '')
67
- .split(',')
68
- .filter(Boolean)
69
- .map(x => x.trim());
61
+ const optionsFromArguments = program.opts();
62
+ const {
63
+ attributesToDisplay = [],
64
+ attributesForFaceting = [],
65
+ } = optionsFromArguments;
70
66
 
71
67
  const getQuestions = ({ appName }) => ({
72
68
  application: [
@@ -160,7 +156,7 @@ const getQuestions = ({ appName }) => ({
160
156
  ],
161
157
  filter: attributes => attributes.filter(Boolean),
162
158
  when: ({ appId, apiKey, indexName }) =>
163
- !attributesToDisplay.length > 0 && appId && apiKey && indexName,
159
+ attributesToDisplay.length === 0 && appId && apiKey && indexName,
164
160
  },
165
161
  {
166
162
  type: 'checkbox',
@@ -202,7 +198,7 @@ const getQuestions = ({ appName }) => ({
202
198
  },
203
199
  filter: attributes => attributes.filter(Boolean),
204
200
  when: ({ appId, apiKey, indexName }) =>
205
- !attributesForFaceting.length > 0 && appId && apiKey && indexName,
201
+ attributesForFaceting.length === 0 && appId && apiKey && indexName,
206
202
  },
207
203
  ],
208
204
  widget: [
@@ -228,95 +224,141 @@ const getQuestions = ({ appName }) => ({
228
224
  },
229
225
  },
230
226
  ],
231
- });
227
+ initial: [
228
+ {
229
+ type: 'input',
230
+ name: 'appPath',
231
+ message: 'Project directory',
232
+ askAnswered: true,
233
+ validate(input) {
234
+ try {
235
+ return checkAppPath(input);
236
+ } catch (err) {
237
+ console.log();
238
+ console.error(err.message);
239
+ return false;
240
+ }
241
+ },
242
+ when(answers) {
243
+ try {
244
+ return (
245
+ answers.interactive &&
246
+ !answers.config &&
247
+ !checkAppPath(answers.appPath)
248
+ );
249
+ } catch (err) {
250
+ return true;
251
+ }
252
+ },
253
+ },
254
+ {
255
+ type: 'input',
256
+ name: 'appName',
257
+ message: 'The name of the application or widget',
258
+ askAnswered: true,
259
+ default(answers) {
260
+ return path.basename(answers.appPath);
261
+ },
262
+ validate(input) {
263
+ try {
264
+ return checkAppName(input);
265
+ } catch (err) {
266
+ console.log();
267
+ console.error(err.message);
268
+ return false;
269
+ }
270
+ },
271
+ when(answers) {
272
+ try {
273
+ return (
274
+ answers.interactive &&
275
+ !answers.config &&
276
+ !checkAppName(answers.appName)
277
+ );
278
+ } catch (err) {
279
+ return true;
280
+ }
281
+ },
282
+ },
283
+ {
284
+ type: 'list',
285
+ pageSize: 10,
286
+ name: 'template',
287
+ message: 'InstantSearch template',
288
+ askAnswered: true,
289
+ choices: () => {
290
+ const templatesByCategory = getTemplatesByCategory();
291
+
292
+ return Object.entries(templatesByCategory).reduce(
293
+ (templates, [category, values]) => [
294
+ ...templates,
295
+ new inquirer.Separator(category),
296
+ ...values,
297
+ ],
298
+ []
299
+ );
300
+ },
301
+ validate(input) {
302
+ // if a config is given, path is optional
303
+ if (optionsFromArguments.config) {
304
+ return true;
305
+ }
232
306
 
233
- async function run() {
234
- let appPath = appPathFromArgument;
235
- if (!appPath) {
236
- const answers = await inquirer.prompt([
237
- {
238
- type: 'input',
239
- name: 'appPath',
240
- message: 'Project directory',
307
+ const isValid = Boolean(input);
308
+ if (!isValid) {
309
+ console.log('template is required');
310
+ }
311
+ return isValid;
241
312
  },
242
- ]);
243
- appPath = answers.appPath;
244
- }
245
- if (appPath.startsWith('~/')) {
246
- appPath = path.join(os.homedir(), appPath.slice(2));
247
- }
248
- try {
249
- checkAppPath(appPath);
250
- } catch (err) {
251
- console.error(err.message);
252
- console.log();
313
+ when(answers) {
314
+ return answers.interactive && !answers.config && !answers.template;
315
+ },
316
+ },
317
+ ],
318
+ });
253
319
 
254
- process.exit(1);
255
- }
320
+ async function run() {
321
+ const args = {
322
+ ...optionsFromArguments,
323
+ appPath: appPathFromArgument,
324
+ appName: optionsFromArguments.name,
325
+ };
326
+ const initialQuestions = getQuestions({}).initial;
327
+ const initialAnswers = {
328
+ ...args,
329
+ ...(await inquirer.prompt(initialQuestions, args)),
330
+ };
331
+
332
+ initialQuestions.forEach(question => {
333
+ // .default doesn't get executed when "when" returns false
334
+ if (!initialAnswers[question.name] && question.default) {
335
+ const defaultValue = question.default(initialAnswers);
336
+ initialAnswers[question.name] = defaultValue;
337
+ }
338
+ if (!question.validate(initialAnswers[question.name])) {
339
+ process.exit(1);
340
+ }
341
+ });
256
342
 
257
- let appName = optionsFromArguments.name;
258
- if (!appName) {
259
- appName = (
260
- await inquirer.prompt([
261
- {
262
- type: 'input',
263
- name: 'appName',
264
- message: 'The name of the application or widget',
265
- default: path.basename(appPath),
266
- },
267
- ])
268
- ).appName;
343
+ if (initialAnswers.appPath.startsWith('~/')) {
344
+ initialAnswers.appPath = path.join(
345
+ os.homedir(),
346
+ initialAnswers.appPath.slice(2)
347
+ );
269
348
  }
270
349
 
271
- try {
272
- checkAppName(appName);
273
- } catch (err) {
274
- console.error(err.message);
275
- console.log();
276
-
277
- process.exit(1);
278
- }
350
+ const { appPath, appName, template } = initialAnswers;
279
351
 
280
352
  console.log();
281
353
  console.log(`Creating a new InstantSearch app in ${chalk.green(appPath)}.`);
282
354
  console.log();
283
355
 
284
- const { template = optionsFromArguments.template } = await inquirer.prompt(
285
- [
286
- {
287
- type: 'list',
288
- pageSize: 10,
289
- name: 'template',
290
- message: 'InstantSearch template',
291
- choices: () => {
292
- const templatesByCategory = getTemplatesByCategory();
293
-
294
- return Object.entries(templatesByCategory).reduce(
295
- (templates, [category, values]) => [
296
- ...templates,
297
- new inquirer.Separator(category),
298
- ...values,
299
- ],
300
- []
301
- );
302
- },
303
- validate(input) {
304
- return Boolean(input);
305
- },
306
- },
307
- ].filter(question =>
308
- isQuestionAsked({ question, args: optionsFromArguments })
309
- ),
310
- optionsFromArguments
311
- );
312
-
313
356
  const configuration = await getConfiguration({
314
357
  options: {
315
358
  ...optionsFromArguments,
316
359
  name: appName,
317
- attributesToDisplay,
318
360
  },
319
- answers: { template },
361
+ answers: initialAnswers,
320
362
  });
321
363
 
322
364
  const templatePath = getTemplatePath(configuration.template);
@@ -326,39 +368,21 @@ async function run() {
326
368
  templateConfig.category === 'Widget' ? 'widget' : 'application';
327
369
 
328
370
  const answers = await inquirer.prompt(
329
- getQuestions({ appName })[implementationType].filter(question =>
330
- isQuestionAsked({ question, args: optionsFromArguments })
371
+ getQuestions({ appName })[implementationType].filter(
372
+ question => !isQuestionAsked({ question, args: optionsFromArguments })
331
373
  ),
332
374
  getAnswersDefaultValues(optionsFromArguments, configuration, template)
333
375
  );
334
376
 
335
- const alternativeNames = createNameAlternatives({
336
- ...configuration,
337
- ...answers,
338
- templateConfig,
339
- });
340
-
341
- const libraryVersion = await getLibraryVersion(
342
- { ...configuration, ...answers },
343
- templateConfig
344
- );
345
-
346
377
  const app = createInstantSearchApp(
347
378
  appPath,
348
- {
349
- ...configuration,
350
- ...answers,
351
- ...alternativeNames,
352
- flags: {
353
- dynamicWidgets:
354
- Array.isArray(answers.attributesForFaceting) &&
355
- answers.attributesForFaceting.includes('ais.dynamicWidgets'),
356
- },
357
- libraryVersion,
358
- template: templatePath,
359
- installation: program.installation,
360
- currentYear: new Date().getFullYear(),
361
- },
379
+ await postProcessAnswers({
380
+ configuration,
381
+ answers,
382
+ optionsFromArguments,
383
+ templatePath,
384
+ templateConfig,
385
+ }),
362
386
  templateConfig.tasks
363
387
  );
364
388
 
@@ -1,22 +1,15 @@
1
1
  module.exports = function isQuestionAsked({ question, args }) {
2
- if (args.config) {
3
- return false;
2
+ // if there's a config, ask no questions, even if it would be invalid
3
+ if (args.config || !args.interactive) {
4
+ return true;
4
5
  }
5
6
 
6
- for (const optionName in args) {
7
- if (question.name === optionName) {
8
- // Skip if the arg in the command is valid
9
- if (
10
- !question.validate ||
11
- (question.validate && question.validate(args[optionName]))
12
- ) {
13
- return false;
14
- }
15
- } else if (!question.validate) {
16
- // Skip if the question is optional and not given in the command
17
- return false;
18
- }
7
+ const argument = args[question.name];
8
+
9
+ // Skip if the arg in the command is valid
10
+ if (question.validate) {
11
+ return question.validate(argument);
19
12
  }
20
13
 
21
- return true;
14
+ return argument !== undefined;
22
15
  };
@@ -0,0 +1,77 @@
1
+ const camelCase = require('lodash.camelcase');
2
+ const latestSemver = require('latest-semver');
3
+
4
+ const { fetchLibraryVersions } = require('../utils');
5
+
6
+ function capitalize(str) {
7
+ return str.substr(0, 1).toUpperCase() + str.substr(1);
8
+ }
9
+
10
+ function createNameAlternatives({ organization, name, templateConfig }) {
11
+ return {
12
+ packageName: `@${organization}/${templateConfig.packageNamePrefix ||
13
+ ''}${name}`,
14
+ widgetType: `${organization}.${name}`,
15
+ camelCaseName: camelCase(name),
16
+ pascalCaseName: capitalize(camelCase(name)),
17
+ };
18
+ }
19
+
20
+ async function getLibraryVersion(config, templateConfig) {
21
+ const { libraryName } = templateConfig;
22
+ const { libraryVersion } = config;
23
+
24
+ if (libraryName && !libraryVersion) {
25
+ const versions = await fetchLibraryVersions(libraryName);
26
+
27
+ // Return the latest available version when
28
+ // the stable version is not available
29
+ return latestSemver(versions) || versions[0];
30
+ }
31
+
32
+ return libraryVersion;
33
+ }
34
+
35
+ async function postProcessAnswers({
36
+ configuration,
37
+ answers,
38
+ optionsFromArguments,
39
+ templatePath,
40
+ templateConfig,
41
+ }) {
42
+ const combinedAnswers = {
43
+ ...configuration,
44
+ ...answers,
45
+ };
46
+
47
+ const alternativeNames = createNameAlternatives({
48
+ ...combinedAnswers,
49
+ templateConfig,
50
+ });
51
+
52
+ const libraryVersion = await getLibraryVersion(
53
+ combinedAnswers,
54
+ templateConfig
55
+ );
56
+
57
+ return {
58
+ ...combinedAnswers,
59
+ ...alternativeNames,
60
+ libraryVersion,
61
+ template: templatePath,
62
+ installation: optionsFromArguments.installation,
63
+ currentYear: new Date().getFullYear(),
64
+ attributesForFaceting:
65
+ Array.isArray(combinedAnswers.attributesForFaceting) &&
66
+ combinedAnswers.attributesForFaceting.filter(
67
+ attribute => attribute !== 'ais.dynamicWidgets'
68
+ ),
69
+ flags: {
70
+ dynamicWidgets:
71
+ Array.isArray(combinedAnswers.attributesForFaceting) &&
72
+ combinedAnswers.attributesForFaceting.includes('ais.dynamicWidgets'),
73
+ },
74
+ };
75
+ }
76
+
77
+ module.exports = postProcessAnswers;
@@ -33,9 +33,13 @@
33
33
  <div class="search-panel">
34
34
  {{#if attributesForFaceting}}
35
35
  <div class="search-panel__filters">
36
+ {{#if flags.dynamicWidgets}}
37
+ <div id="dynamic-widgets"></div>
38
+ {{else}}
36
39
  {{#each attributesForFaceting}}
37
40
  <div id="{{this}}-list"></div>
38
41
  {{/each}}
42
+ {{/if}}
39
43
  </div>
40
44
 
41
45
  {{/if}}
@@ -5,7 +5,3 @@ exports[`checkAppName throws with correct error message 1`] = `
5
5
  - name cannot start with a period
6
6
  - name can only contain URL-friendly characters"
7
7
  `;
8
-
9
- exports[`checkAppPath with existing file as path should throw with correct error 1`] = `"Could not create project at path path because a file of the same name already exists."`;
10
-
11
- exports[`checkAppPath with non empty directory as path should throw with correct error 1`] = `"Could not create project in destination folder \\"path\\" because it is not empty."`;
@@ -23,7 +23,7 @@ describe('checkAppName', () => {
23
23
  });
24
24
 
25
25
  describe('checkAppPath', () => {
26
- describe('with non existant directory as path', () => {
26
+ describe('with non existing directory as path', () => {
27
27
  beforeAll(() => {
28
28
  mockExistsSync.mockImplementation(() => false);
29
29
  });
@@ -45,7 +45,11 @@ describe('checkAppPath', () => {
45
45
  });
46
46
 
47
47
  test('should throw with correct error', () => {
48
- expect(() => utils.checkAppPath('path')).toThrowErrorMatchingSnapshot();
48
+ expect(() =>
49
+ utils.checkAppPath('path')
50
+ ).toThrowErrorMatchingInlineSnapshot(
51
+ `"Could not create project in destination folder \\"path\\" because it is not empty."`
52
+ );
49
53
  });
50
54
 
51
55
  afterAll(() => {
@@ -64,7 +68,31 @@ describe('checkAppPath', () => {
64
68
  });
65
69
 
66
70
  test('should throw with correct error', () => {
67
- expect(() => utils.checkAppPath('path')).toThrowErrorMatchingSnapshot();
71
+ expect(() =>
72
+ utils.checkAppPath('path')
73
+ ).toThrowErrorMatchingInlineSnapshot(
74
+ `"Could not create project at path path because a file of the same name already exists."`
75
+ );
76
+ });
77
+
78
+ afterAll(() => {
79
+ mockExistsSync.mockReset();
80
+ mockLstatSync.mockReset();
81
+ });
82
+ });
83
+
84
+ describe('with empty string as path', () => {
85
+ beforeAll(() => {
86
+ mockExistsSync.mockImplementation(() => false);
87
+ mockLstatSync.mockImplementation(() => ({
88
+ isDirectory: () => false,
89
+ }));
90
+ });
91
+
92
+ test('should throw with correct error', () => {
93
+ expect(() => utils.checkAppPath('')).toThrowErrorMatchingInlineSnapshot(
94
+ `"Could not create project without directory"`
95
+ );
68
96
  });
69
97
 
70
98
  afterAll(() => {
@@ -56,6 +56,10 @@ function checkAppPath(appPath) {
56
56
  }
57
57
  }
58
58
 
59
+ if (!appPath) {
60
+ throw new Error('Could not create project without directory');
61
+ }
62
+
59
63
  return true;
60
64
  }
61
65
 
@@ -126,7 +130,9 @@ function getTemplatePath(templateName) {
126
130
  }
127
131
 
128
132
  async function fetchLibraryVersions(libraryName) {
129
- const library = await index.getObject(libraryName);
133
+ const library = await index.getObject(libraryName, {
134
+ attributesToRetrieve: ['versions'],
135
+ });
130
136
 
131
137
  return Object.keys(library.versions).reverse();
132
138
  }
@@ -147,6 +153,12 @@ async function getEarliestLibraryVersion(...args) {
147
153
  return await getLibraryVersion(...args)(semver.minSatisfying);
148
154
  }
149
155
 
156
+ const splitArray = string =>
157
+ string
158
+ .split(',')
159
+ .filter(Boolean)
160
+ .map(x => x.trim());
161
+
150
162
  module.exports = {
151
163
  checkAppName,
152
164
  checkAppPath,
@@ -158,4 +170,5 @@ module.exports = {
158
170
  getAllTemplates,
159
171
  getTemplatePath,
160
172
  getTemplatesByCategory,
173
+ splitArray,
161
174
  };
@@ -1,72 +0,0 @@
1
- const getOptionsFromArguments = require('../getOptionsFromArguments');
2
-
3
- test('with a single option', () => {
4
- expect(getOptionsFromArguments('cmd --appId APP_ID'.split(' '))).toEqual({
5
- appId: 'APP_ID',
6
- });
7
- });
8
-
9
- test('with multiple options', () => {
10
- expect(
11
- getOptionsFromArguments([
12
- 'cmd',
13
- '--appId',
14
- 'APP_ID',
15
- '--apiKey',
16
- 'API_KEY',
17
- '--indexName',
18
- 'INDEX_NAME',
19
- '--template',
20
- 'Vue InstantSearch',
21
- ])
22
- ).toEqual({
23
- appId: 'APP_ID',
24
- apiKey: 'API_KEY',
25
- indexName: 'INDEX_NAME',
26
- template: 'Vue InstantSearch',
27
- });
28
- });
29
-
30
- test('with different commands', () => {
31
- expect(
32
- getOptionsFromArguments(['yarn', 'start', '--appId', 'APP_ID'])
33
- ).toEqual({
34
- appId: 'APP_ID',
35
- });
36
-
37
- expect(
38
- getOptionsFromArguments(['node', 'index', '--appId', 'APP_ID'])
39
- ).toEqual({
40
- appId: 'APP_ID',
41
- });
42
-
43
- expect(
44
- getOptionsFromArguments([
45
- 'npm',
46
- 'init',
47
- 'instantsearch-app',
48
- '--appId',
49
- 'APP_ID',
50
- ])
51
- ).toEqual({
52
- appId: 'APP_ID',
53
- });
54
-
55
- expect(
56
- getOptionsFromArguments([
57
- 'yarn',
58
- 'create',
59
- 'instantsearch-app',
60
- '--appId',
61
- 'APP_ID',
62
- ])
63
- ).toEqual({
64
- appId: 'APP_ID',
65
- });
66
-
67
- expect(
68
- getOptionsFromArguments(['create-instantsearch-app', '--appId', 'APP_ID'])
69
- ).toEqual({
70
- appId: 'APP_ID',
71
- });
72
- });
@@ -1,21 +0,0 @@
1
- const camelCase = require('lodash.camelcase');
2
-
3
- module.exports = function getOptionsFromArguments(rawArgs) {
4
- let argIndex = 0;
5
-
6
- return rawArgs.reduce((allArgs, currentArg) => {
7
- argIndex++;
8
-
9
- if (!currentArg.startsWith('--') || currentArg.startsWith('--no-')) {
10
- return allArgs;
11
- }
12
-
13
- const argumentName = camelCase(currentArg.split('--')[1]);
14
- const argumentValue = rawArgs[argIndex];
15
-
16
- return {
17
- ...allArgs,
18
- [argumentName]: argumentValue,
19
- };
20
- }, {});
21
- };