create-gasket-app 7.0.3-cli.0 → 7.0.4

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.
Files changed (35) hide show
  1. package/README.md +223 -3
  2. package/lib/commands/create.js +149 -0
  3. package/lib/config/default-plugins.js +15 -0
  4. package/lib/index.d.ts +310 -0
  5. package/lib/index.js +24 -9
  6. package/lib/scaffold/action-wrapper.js +37 -0
  7. package/lib/scaffold/actions/create-hooks.js +27 -0
  8. package/lib/scaffold/actions/generate-files.js +189 -0
  9. package/lib/scaffold/actions/global-prompts.js +162 -0
  10. package/lib/scaffold/actions/index.js +33 -0
  11. package/lib/scaffold/actions/install-modules.js +15 -0
  12. package/lib/scaffold/actions/link-modules.js +19 -0
  13. package/lib/scaffold/actions/load-preset.js +60 -0
  14. package/lib/scaffold/actions/mkdir.js +30 -0
  15. package/lib/scaffold/actions/post-create-hooks.js +31 -0
  16. package/lib/scaffold/actions/preset-config-hooks.js +13 -0
  17. package/lib/scaffold/actions/preset-prompt-hooks.js +17 -0
  18. package/lib/scaffold/actions/print-report.js +89 -0
  19. package/lib/scaffold/actions/prompt-hooks.js +43 -0
  20. package/lib/scaffold/actions/setup-pkg.js +34 -0
  21. package/lib/scaffold/actions/write-gasket-config.js +119 -0
  22. package/lib/scaffold/actions/write-pkg.js +19 -0
  23. package/lib/scaffold/config-builder.js +526 -0
  24. package/lib/scaffold/create-context.js +188 -0
  25. package/lib/scaffold/dump-error-context.js +28 -0
  26. package/lib/scaffold/files.js +30 -0
  27. package/lib/scaffold/readme.js +51 -0
  28. package/lib/scaffold/utils.js +22 -0
  29. package/lib/utils/create-option.js +18 -0
  30. package/lib/utils/index.js +5 -0
  31. package/lib/utils/logo.js +12 -0
  32. package/lib/utils/process-args.js +37 -0
  33. package/lib/utils/process-command.js +50 -0
  34. package/lib/utils/process-options.js +44 -0
  35. package/package.json +36 -9
@@ -0,0 +1,19 @@
1
+ import path from 'path';
2
+ import { writeFile } from 'fs/promises';
3
+ import action from '../action-wrapper.js';
4
+
5
+ /**
6
+ * Writes the contents of `pkg` to the app's package.json.
7
+ *
8
+ * @param {CreateContext} context - Create context
9
+ * @returns {Promise} promise
10
+ */
11
+ async function writePkg({ context }) {
12
+ const { dest, pkg, generatedFiles } = context;
13
+ const fileName = 'package.json';
14
+ const filePath = path.join(dest, fileName);
15
+ await writeFile(filePath, JSON.stringify(pkg, null, 2), 'utf8');
16
+ generatedFiles.add(fileName);
17
+ }
18
+
19
+ export default action('Write package.json', writePkg);
@@ -0,0 +1,526 @@
1
+ /* eslint-disable complexity, max-statements */
2
+ import deepmerge from 'deepmerge';
3
+ import { default as semver } from 'semver';
4
+ import { default as diagnostics } from 'diagnostics';
5
+ const debug = diagnostics('gasket:cli:package');
6
+
7
+ /**
8
+ * Simple object check without bringing in a large
9
+ * utility library.
10
+ *
11
+ * @param {*} value - What to test if an object
12
+ * @returns {Boolean} results
13
+ */
14
+ function isObject(value) {
15
+ return value && typeof value === 'object';
16
+ }
17
+
18
+ /*
19
+ * Known semver prefix values.
20
+ * Adapted from @vue/cli under MIT
21
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/util/mergeDeps.js#L53
22
+ */
23
+ const semverPrefixes = /^(~|\^|>=?)/;
24
+
25
+ /*
26
+ * Known package.json dependency values
27
+ * Adapted from @vue/cli under MIT
28
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/util/mergeDeps.js#L10-L11
29
+ */
30
+ const versionTypes = {
31
+ uri: /^(?:file|git|git\+ssh|git\+http|git\+https|git\+file|https?):/,
32
+ github: /^[^/]+\/[^/]+/
33
+ };
34
+
35
+ /**
36
+ * Validates if the version `v` is valid for `package.json`
37
+ * dependencies, devDependencies, etc.
38
+ *
39
+ * @param {string} v Version in package.json field.
40
+ * @returns {boolean} Value indicating if npm accepts the value
41
+ */
42
+ function isValidVersion(v) {
43
+ //
44
+ // Remark: this explicitly forbids using npm dist-tags
45
+ // as valid versions. To support this every call must hit an npm
46
+ // registry to see what dist-tags are available. This is not feasible.
47
+ //
48
+ return v === 'latest'
49
+ || v.match(versionTypes.uri) != null // eslint-disable-line eqeqeq
50
+ || v.match(versionTypes.github) != null // eslint-disable-line eqeqeq
51
+ || semver.validRange(v);
52
+ }
53
+
54
+ /**
55
+ * ConfigBuilder is an extensible data structure for **specifically**
56
+ * managing `package.json` data.
57
+ *
58
+ * @type {ConfigBuilder}
59
+ */
60
+ export class ConfigBuilder {
61
+ /**
62
+ * ConfigBuilder
63
+ *
64
+ * @param {Object} [fields] - Initial fields
65
+ * @param {Object} [options] - Additional setup options
66
+ * @param {String[]} [options.orderBy] - Preferred order to sort top-level keys
67
+ * @param {String[]} [options.orderedFields] - Fields that should be sorted
68
+ * @param {String[]} [options.objectFields] - Fields that are required to be object type
69
+ * @param {String[]} [options.semverFields] - Fields that are aware of semantic versioning
70
+ */
71
+ constructor(fields = {}, options = {}) {
72
+ this.fields = Object.assign({}, fields);
73
+ this.original = fields;
74
+
75
+ this.blame = new Map();
76
+ this.force = new Set();
77
+
78
+ this.orderBy = options.orderBy;
79
+ this.orderedFields = options.orderedFields;
80
+ this.objectFields = options.objectFields;
81
+ this.semverFields = options.semverFields;
82
+ this.warnings = options.warnings;
83
+
84
+ // Any semverFields are also object fields and ordered fields.
85
+ if (Array.isArray(this.semverFields)) {
86
+ this.orderedFields = (this.orderedFields || []).concat(this.semverFields);
87
+ this.objectFields = (this.objectFields || []).concat(this.semverFields);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Creator method to get a new instance
93
+ *
94
+ * @param {Object} [fields] - Initial fields
95
+ * @param {Object} [options] - Additional setup options
96
+ * @returns {ConfigBuilder} instance
97
+ */
98
+ static create(fields = {}, options = {}) {
99
+ return new ConfigBuilder(fields, options);
100
+ }
101
+
102
+ /**
103
+ * Create an instance configured with options for package.json files
104
+ *
105
+ * @param {Object} [fields] - Initial fields
106
+ * @param {Object} [options] - Additional setup options
107
+ * @returns {ConfigBuilder} instance
108
+ */
109
+ static createPackageJson(fields = {}, options = {}) {
110
+ return new ConfigBuilder(fields, {
111
+ ...options,
112
+ orderBy: [
113
+ 'name',
114
+ 'version',
115
+ 'description',
116
+ 'license',
117
+ 'repository',
118
+ 'scripts',
119
+ 'dependencies',
120
+ 'devDependencies',
121
+ 'peerDependencies',
122
+ 'optionalDependencies'
123
+ ],
124
+ semverFields: [
125
+ 'dependencies',
126
+ 'devDependencies',
127
+ 'peerDependencies',
128
+ 'optionalDependencies'
129
+ ],
130
+ objectFields: [
131
+ 'scripts'
132
+ ]
133
+ });
134
+ }
135
+
136
+ warn(message) {
137
+ if (this.warnings) {
138
+ this.warnings.push(message);
139
+ } else {
140
+ console.warn(message);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Adds all `[key, value]` pairs in the `fields` provided.
146
+ * @param {object|function(current)} fields - Object to merge.
147
+ * Can be a function that accepts the current fields and object to merge.
148
+ * @param {Object} source Plugin to blame if conflicts arise from this operation.
149
+ *
150
+ * Adapted from @vue/cli under MIT License:
151
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/GeneratorAPI.js#L117-L150
152
+ */
153
+ extend(fields, source) {
154
+ const current = this.fields;
155
+ const toMerge = typeof fields === 'function'
156
+ ? fields(current)
157
+ : fields;
158
+
159
+ // Silently ignore any falsey values
160
+ if (!toMerge || typeof toMerge !== 'object') {
161
+ return;
162
+ }
163
+
164
+ Object.entries(toMerge).forEach(([k, v]) => {
165
+ this.add(k, v, source || this.source);
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Performs an intelligent, domain-aware merge of the `value` for
171
+ * the given `key` into the package.json fields associated with this instance.
172
+ * @param {string} key - Field in package.json to add or extend.
173
+ * @param {*} value - Target value to set for key provided.
174
+ * @param {Object} source - Plugin to blame if conflicts arise from this operation.
175
+ * @param {object} [options] - Optional arguments for add behavior
176
+ * @param {boolean} [options.force] - Should the semver version override other attempts
177
+ *
178
+ * Adapted from @vue/cli under MIT License:
179
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/GeneratorAPI.js#L117-L150
180
+ */
181
+ add(key, value, source, options = {}) {
182
+ if (typeof value === 'undefined') return;
183
+ const existing = this.fields[key];
184
+ const { name = 'Unknown plugin' } = (source || this.source || {});
185
+
186
+ debug('add', { [key]: value, existing, from: name });
187
+ if (Array.isArray(this.objectFields) && this.objectFields.includes(key)) {
188
+ if (!isObject(value)) {
189
+ throw new Error(`${key} must be an object. Received ${value}`);
190
+ }
191
+
192
+ if (Array.isArray(this.semverFields) && this.semverFields.includes(key)) {
193
+ if (!existing) this.fields[key] = {};
194
+
195
+ this.semanticMerge({
196
+ key,
197
+ value,
198
+ existing: this.fields[key],
199
+ name,
200
+ ...options
201
+ });
202
+
203
+ return;
204
+ }
205
+
206
+ if (!existing) {
207
+ this.fields[key] = Object.assign({}, value);
208
+ return;
209
+ }
210
+
211
+ // TODO: ensure that conflicts are properly surfaced to users
212
+ this.fields[key] = Object.assign({},
213
+ existing || {},
214
+ value
215
+ );
216
+ } else if (!(key in this.fields)) {
217
+ this.fields[key] = value;
218
+ } else if (Array.isArray(value) && Array.isArray(existing)) {
219
+ this.fields[key] = this.mergeArrayDeduped(existing, value);
220
+ } else if (isObject(value) && isObject(existing)) {
221
+ this.fields[key] = deepmerge(existing, value, {
222
+ arrayMerge: this.mergeArrayDeduped
223
+ });
224
+ } else {
225
+ this.fields[key] = value;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * addPlugin - Add plugin import to the gasket file and use the value in the plugins array
231
+ * @param {string} pluginImport - name of the import used as a value - `import pluginImport...`
232
+ * @param {string} pluginName - name of the plugin import/package - `from 'pluginName'`
233
+ * @example
234
+ * addPlugin('pluginA', '@gasket/plugin-a')
235
+ *
236
+ * // gasket.js
237
+ * import pluginA from '@gasket/plugin-a';
238
+ *
239
+ * export default makeGasket({
240
+ * plugins: [
241
+ * pluginA
242
+ * ]
243
+ * });
244
+ */
245
+ addPlugin(pluginImport, pluginName) {
246
+ this.add('plugins', [`${pluginImport}`]);
247
+ this.add('pluginImports', { [pluginImport]: pluginName });
248
+ }
249
+
250
+ /**
251
+ * addImport - Add a non-plugin import to the gasket file
252
+ * @param {string} importName - name of the import used as a value - `import fs...`
253
+ * @param {string} importPath - path of the import - `from 'fs'`
254
+ * @returns {ConfigBuilder} - instance for chaining
255
+ * @example
256
+ * Can be default or named import
257
+ * addImport('{ readFileSync }', 'fs')
258
+ * addImport('fs', 'fs')
259
+ *
260
+ * // gasket.js
261
+ * import { readFileSync } from 'fs';
262
+ * import fs from 'fs';
263
+ */
264
+ addImport(importName, importPath) {
265
+ this.add('imports', { [importName]: importPath });
266
+ return this;
267
+ }
268
+
269
+ /**
270
+ * addExpression - add programmatic expression to the gasket file
271
+ * @param {string} expression - expression to add after imports
272
+ * @returns {ConfigBuilder} - instance for chaining
273
+ * @example
274
+ *
275
+ * .addImport('fs', 'fs')
276
+ * .addExpression('const file = fs.readFileSync(\'./file.txt\')')
277
+ *
278
+ * // gasket.js
279
+ * import fs from 'fs';
280
+ * const file = fs.readFileSync('./file.txt');
281
+ */
282
+ addExpression(expression) {
283
+ this.add('expressions', [expression.trim()]);
284
+ return this;
285
+ }
286
+
287
+ /**
288
+ * injectValue - Inject a value into the gasket config object
289
+ * @param {string} configKey - collapsed object path to inject value into - `express.config.routes`
290
+ * @param {string} injectedValue - string used as a value
291
+ * @example
292
+ * .addImport('{ routes }', './routes')
293
+ * .injectValue('express.routes', 'routes');
294
+ *
295
+ * // gasket.js
296
+ * export default makeGasket({
297
+ * express: {
298
+ * routes: routes
299
+ * }
300
+ * });
301
+ */
302
+ injectValue(configKey, injectedValue) {
303
+ this.add('injectionAssignments', { [configKey]: injectedValue });
304
+ }
305
+
306
+ /**
307
+ * Checks if a dependency has been already added
308
+ * @param {String} key Dependency bucket
309
+ * @param {String} value Dependency to search
310
+ * @returns {Bool} True if the dependency exists on the bucket
311
+ */
312
+ has(key, value) {
313
+ const existing = this.fields[key];
314
+
315
+ if (!existing) {
316
+ return false;
317
+ }
318
+
319
+ if (Array.isArray(existing)) {
320
+ return existing.includes(value);
321
+ } else if (isObject(existing)) {
322
+ return (value in existing);
323
+ }
324
+
325
+ return value === existing;
326
+ }
327
+
328
+ /**
329
+ * Returns the existing and target array merged without duplicates
330
+ * @param {Array} existing Partial lattice to merge.
331
+ * @param {Array} target Partial lattice to merge.
332
+ * @returns {Array} existing ∪ (i.e. union) target
333
+ *
334
+ * Adapted from @vue/cli under MIT License:
335
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/GeneratorAPI.js#L15
336
+ */
337
+ mergeArrayDeduped(existing, target) {
338
+ return Array.from(new Set([...existing, ...target]));
339
+ }
340
+
341
+ /**
342
+ * Attempts to merge all entries within the `value` provided by
343
+ * the plugin specified by `name` into the `existing` semver-aware
344
+ * Object `key` (e.g. dependencies, etc.) for this instance.
345
+ *
346
+ * Merge algorithm:
347
+ *
348
+ * - ∀ [dep, ver] := Object.entries(value)
349
+ * and [prev] := any existing version for dep
350
+ *
351
+ * - If ver is not valid semver ––> ■
352
+ * - If ¬∃ prev ––> set and blame [dep, ver]
353
+ * - If ver > prev ––> set and blame [dep, ver]
354
+ * - If ¬(ver ∩ prev) ––> Conflict. Print.
355
+ *
356
+ * @param {string} options.key {devD,peerD,optionalD,d}ependencies
357
+ * @param {Object} options.value Updates for { name: version } pairs
358
+ * @param {Object} options.existing Existing { name: version } pairs
359
+ * @param {string} options.name Plugin name providing merge `value``
360
+ * @param {boolean} [options.force] Should the semver version override other attempts
361
+ *
362
+ * Adapted from @vue/cli under MIT License:
363
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/util/mergeDeps.js
364
+ */
365
+ semanticMerge({ key, value, existing, name, force = false }) {
366
+
367
+ const setBlame = blameId => {
368
+ this.blame.set(blameId, [name]);
369
+ if (force) this.force.add(blameId);
370
+ };
371
+
372
+ Object.entries(value).forEach(([dep, ver]) => {
373
+ const prev = existing[dep];
374
+ if (!isValidVersion(ver)) {
375
+ this.warn(`Invalid "${key}" provided by ${name}: ${dep}@${ver}.`);
376
+ return;
377
+ }
378
+
379
+ // If a prerelease version, strip the range prefix
380
+ const version = ver.includes('-') ? ver.replace(/^[\^~]/, '') : ver;
381
+
382
+ const blameId = `${key}.${dep}`;
383
+ if (!prev) {
384
+ existing[dep] = version;
385
+ setBlame(blameId);
386
+ return;
387
+ }
388
+
389
+ const blamed = this.blame.get(blameId);
390
+ const forced = this.force.has(blameId);
391
+
392
+ if (prev === version) {
393
+ if (forced) return;
394
+ if (force) {
395
+ setBlame(blameId);
396
+ } else {
397
+ blamed.push(name);
398
+ }
399
+ return;
400
+ }
401
+
402
+ const prevName = blamed.join(', ');
403
+ if (!forced) {
404
+ const newer = force ? version : this.tryGetNewerRange(prev, version);
405
+ const overridden = newer === version;
406
+ if (overridden) {
407
+ existing[dep] = version;
408
+ setBlame(blameId);
409
+ }
410
+ }
411
+
412
+ if (!semver.validRange(prev) || !semver.validRange(version) || !semver.intersects(prev, version)) {
413
+ let forceMsg = force ? '(forced)' : '';
414
+ forceMsg = forced && force ? '(cannot be forced)' : forceMsg;
415
+ this.warn(`
416
+ Conflicting versions for ${dep} in "${key}":
417
+ - ${prev} provided by ${prevName} ${forced && '(forced)' || ''}
418
+ - ${version} provided by ${name} ${forceMsg}
419
+ Using ${existing[dep]}, but this may cause unexpected behavior.
420
+ `);
421
+ }
422
+ });
423
+ }
424
+
425
+ /**
426
+ * Normalizes a potential semver range into a semver string
427
+ * and returns the newest version
428
+ * @param {String} r1 Semver string (potentially invalid).
429
+ * @param {String} r2 Semver string (potentially invalid).
430
+ * @returns {String|undefined} Newest semver version.
431
+ *
432
+ * Adapted from @vue/cli under MIT License:
433
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/util/mergeDeps.js#L58-L64
434
+ */
435
+ tryGetNewerRange(r1, r2) {
436
+ if (r1 === 'latest' || r2 === 'latest') {
437
+ return r1 === 'latest' ? r1 : r2;
438
+ }
439
+
440
+ const v1 = this.rangeToVersion(r1);
441
+ const v2 = this.rangeToVersion(r2);
442
+ if (semver.valid(v1) && semver.valid(v2)) {
443
+ return semver.gt(v1, v2) ? r1 : r2;
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Performs a naive attempt to take a transform a semver range
449
+ * into a concrete version that may be used for "newness"
450
+ * comparison.
451
+ *
452
+ * @param {string} range Valid "basic" semver: ^X.Y.Z, ~A.B.C, >=2.3.x, 1.x.x
453
+ * @returns {string} Concrete as possible version: X.Y.Z, A.B.C, 2.3.0, 1.0.0
454
+ */
455
+ rangeToVersion(range) {
456
+ return range
457
+ .replace(semverPrefixes, '')
458
+ .replace(/x/g, '0')
459
+ .trim();
460
+ }
461
+
462
+ /**
463
+ * Orders top-level keys by `orderBy` options with any fields specified in
464
+ * the `orderFields` options having their keys sorted.
465
+ * @returns {Object} Ready to be serialized JavaScript object.
466
+ */
467
+ toJSON() {
468
+ if (Array.isArray(this.orderedFields)) {
469
+ this.orderedFields.forEach(k => {
470
+ if (this.fields[k]) {
471
+ this.fields[k] = this.toOrderedKeys(this.fields[k]);
472
+ }
473
+ });
474
+ }
475
+
476
+ return this.toOrderedKeys(this.fields, this.orderBy);
477
+ }
478
+
479
+ /**
480
+ * Orders the given object, `obj`, applying any (optional)
481
+ * key order specified via `orderBy`. If no `orderBy` is provided
482
+ * keys are ordered lexographically.
483
+ * @param {Object} obj Object to transform to ordered keys
484
+ * @param {string[]} [orderBy] Explicit key order to use.
485
+ * @returns {Object} Shallow clone of `obj` with ordered keys
486
+ *
487
+ * Adapted from @vue/cli under MIT License:
488
+ * https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/util/sortObject.js
489
+ */
490
+ toOrderedKeys(obj, orderBy) {
491
+ if (!obj) return;
492
+
493
+ // Create an optional order if necessary
494
+ let order;
495
+ if (Array.isArray(orderBy)) {
496
+ order = orderBy.reduce((acc, key, i) => {
497
+ acc[key] = i;
498
+ return acc;
499
+ }, {});
500
+ }
501
+
502
+ /*
503
+ * Sorts based on the `order` defined above.
504
+ */
505
+ function sortByOrder(a, b) {
506
+ const indexA = typeof order[a] === 'undefined'
507
+ ? Infinity
508
+ : order[a];
509
+
510
+ const indexB = typeof order[b] === 'undefined'
511
+ ? Infinity
512
+ : order[b];
513
+
514
+ return indexA - indexB;
515
+ }
516
+
517
+ const keys = order
518
+ ? Object.keys(obj).sort(sortByOrder)
519
+ : Object.keys(obj).sort();
520
+
521
+ return keys.reduce((acc, key) => {
522
+ acc[key] = obj[key];
523
+ return acc;
524
+ }, {});
525
+ }
526
+ }