create-gasket-app 7.0.2-cli.0 → 7.2.0-canary.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/README.md +222 -2
- package/lib/commands/create.js +148 -0
- package/lib/config/default-plugins.js +7 -0
- package/lib/index.d.ts +264 -0
- package/lib/index.js +24 -9
- package/lib/scaffold/action-wrapper.js +37 -0
- package/lib/scaffold/actions/create-hooks.js +25 -0
- package/lib/scaffold/actions/generate-files.js +185 -0
- package/lib/scaffold/actions/global-prompts.js +158 -0
- package/lib/scaffold/actions/index.js +33 -0
- package/lib/scaffold/actions/install-modules.js +15 -0
- package/lib/scaffold/actions/link-modules.js +19 -0
- package/lib/scaffold/actions/load-preset.js +60 -0
- package/lib/scaffold/actions/mkdir.js +30 -0
- package/lib/scaffold/actions/post-create-hooks.js +31 -0
- package/lib/scaffold/actions/preset-config-hooks.js +13 -0
- package/lib/scaffold/actions/preset-prompt-hooks.js +17 -0
- package/lib/scaffold/actions/print-report.js +89 -0
- package/lib/scaffold/actions/prompt-hooks.js +43 -0
- package/lib/scaffold/actions/setup-pkg.js +34 -0
- package/lib/scaffold/actions/write-gasket-config.js +117 -0
- package/lib/scaffold/actions/write-pkg.js +19 -0
- package/lib/scaffold/config-builder.js +523 -0
- package/lib/scaffold/create-context.js +187 -0
- package/lib/scaffold/dump-error-context.js +28 -0
- package/lib/scaffold/files.js +30 -0
- package/lib/scaffold/utils.js +22 -0
- package/lib/utils/create-option.js +18 -0
- package/lib/utils/index.js +5 -0
- package/lib/utils/logo.js +12 -0
- package/lib/utils/process-args.js +37 -0
- package/lib/utils/process-command.js +50 -0
- package/lib/utils/process-options.js +44 -0
- package/package.json +29 -7
|
@@ -0,0 +1,523 @@
|
|
|
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
|
+
const blameId = `${key}.${dep}`;
|
|
380
|
+
if (!prev) {
|
|
381
|
+
existing[dep] = ver;
|
|
382
|
+
setBlame(blameId);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const blamed = this.blame.get(blameId);
|
|
387
|
+
const forced = this.force.has(blameId);
|
|
388
|
+
|
|
389
|
+
if (prev === ver) {
|
|
390
|
+
if (forced) return;
|
|
391
|
+
if (force) {
|
|
392
|
+
setBlame(blameId);
|
|
393
|
+
} else {
|
|
394
|
+
blamed.push(name);
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const prevName = blamed.join(', ');
|
|
400
|
+
if (!forced) {
|
|
401
|
+
const newer = force ? ver : this.tryGetNewerRange(prev, ver);
|
|
402
|
+
const overridden = newer === ver;
|
|
403
|
+
if (overridden) {
|
|
404
|
+
existing[dep] = ver;
|
|
405
|
+
setBlame(blameId);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!semver.validRange(prev) || !semver.validRange(ver) || !semver.intersects(prev, ver)) {
|
|
410
|
+
let forceMsg = force ? '(forced)' : '';
|
|
411
|
+
forceMsg = forced && force ? '(cannot be forced)' : forceMsg;
|
|
412
|
+
this.warn(`
|
|
413
|
+
Conflicting versions for ${dep} in "${key}":
|
|
414
|
+
- ${prev} provided by ${prevName} ${forced && '(forced)' || ''}
|
|
415
|
+
- ${ver} provided by ${name} ${forceMsg}
|
|
416
|
+
Using ${existing[dep]}, but this may cause unexpected behavior.
|
|
417
|
+
`);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Normalizes a potential semver range into a semver string
|
|
424
|
+
* and returns the newest version
|
|
425
|
+
* @param {String} r1 Semver string (potentially invalid).
|
|
426
|
+
* @param {String} r2 Semver string (potentially invalid).
|
|
427
|
+
* @returns {String|undefined} Newest semver version.
|
|
428
|
+
*
|
|
429
|
+
* Adapted from @vue/cli under MIT License:
|
|
430
|
+
* https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/util/mergeDeps.js#L58-L64
|
|
431
|
+
*/
|
|
432
|
+
tryGetNewerRange(r1, r2) {
|
|
433
|
+
if (r1 === 'latest' || r2 === 'latest') {
|
|
434
|
+
return r1 === 'latest' ? r1 : r2;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const v1 = this.rangeToVersion(r1);
|
|
438
|
+
const v2 = this.rangeToVersion(r2);
|
|
439
|
+
if (semver.valid(v1) && semver.valid(v2)) {
|
|
440
|
+
return semver.gt(v1, v2) ? r1 : r2;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Performs a naive attempt to take a transform a semver range
|
|
446
|
+
* into a concrete version that may be used for "newness"
|
|
447
|
+
* comparison.
|
|
448
|
+
*
|
|
449
|
+
* @param {string} range Valid "basic" semver: ^X.Y.Z, ~A.B.C, >=2.3.x, 1.x.x
|
|
450
|
+
* @returns {string} Concrete as possible version: X.Y.Z, A.B.C, 2.3.0, 1.0.0
|
|
451
|
+
*/
|
|
452
|
+
rangeToVersion(range) {
|
|
453
|
+
return range
|
|
454
|
+
.replace(semverPrefixes, '')
|
|
455
|
+
.replace(/x/g, '0')
|
|
456
|
+
.trim();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Orders top-level keys by `orderBy` options with any fields specified in
|
|
461
|
+
* the `orderFields` options having their keys sorted.
|
|
462
|
+
* @returns {Object} Ready to be serialized JavaScript object.
|
|
463
|
+
*/
|
|
464
|
+
toJSON() {
|
|
465
|
+
if (Array.isArray(this.orderedFields)) {
|
|
466
|
+
this.orderedFields.forEach(k => {
|
|
467
|
+
if (this.fields[k]) {
|
|
468
|
+
this.fields[k] = this.toOrderedKeys(this.fields[k]);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return this.toOrderedKeys(this.fields, this.orderBy);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Orders the given object, `obj`, applying any (optional)
|
|
478
|
+
* key order specified via `orderBy`. If no `orderBy` is provided
|
|
479
|
+
* keys are ordered lexographically.
|
|
480
|
+
* @param {Object} obj Object to transform to ordered keys
|
|
481
|
+
* @param {string[]} [orderBy] Explicit key order to use.
|
|
482
|
+
* @returns {Object} Shallow clone of `obj` with ordered keys
|
|
483
|
+
*
|
|
484
|
+
* Adapted from @vue/cli under MIT License:
|
|
485
|
+
* https://github.com/vuejs/vue-cli/blob/f09722c/packages/%40vue/cli/lib/util/sortObject.js
|
|
486
|
+
*/
|
|
487
|
+
toOrderedKeys(obj, orderBy) {
|
|
488
|
+
if (!obj) return;
|
|
489
|
+
|
|
490
|
+
// Create an optional order if necessary
|
|
491
|
+
let order;
|
|
492
|
+
if (Array.isArray(orderBy)) {
|
|
493
|
+
order = orderBy.reduce((acc, key, i) => {
|
|
494
|
+
acc[key] = i;
|
|
495
|
+
return acc;
|
|
496
|
+
}, {});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/*
|
|
500
|
+
* Sorts based on the `order` defined above.
|
|
501
|
+
*/
|
|
502
|
+
function sortByOrder(a, b) {
|
|
503
|
+
const indexA = typeof order[a] === 'undefined'
|
|
504
|
+
? Infinity
|
|
505
|
+
: order[a];
|
|
506
|
+
|
|
507
|
+
const indexB = typeof order[b] === 'undefined'
|
|
508
|
+
? Infinity
|
|
509
|
+
: order[b];
|
|
510
|
+
|
|
511
|
+
return indexA - indexB;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const keys = order
|
|
515
|
+
? Object.keys(obj).sort(sortByOrder)
|
|
516
|
+
: Object.keys(obj).sort();
|
|
517
|
+
|
|
518
|
+
return keys.reduce((acc, key) => {
|
|
519
|
+
acc[key] = obj[key];
|
|
520
|
+
return acc;
|
|
521
|
+
}, {});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/* eslint-disable max-len, max-statements */
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { readConfig } from '../scaffold/utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The CreateRuntime represents a shallow proxy to a CreateContext
|
|
8
|
+
* that automatically adds transactional information for providing
|
|
9
|
+
* CLI users with blame information in the event of conflicts.
|
|
10
|
+
*
|
|
11
|
+
* @param {CreateContext} context - Create context.
|
|
12
|
+
* @param {Plugin} source - Gasket plugin the create context is within.
|
|
13
|
+
* @returns {Proxy} Shallow proxy to the `context` provided that will
|
|
14
|
+
* pass `source` to `context.{files,pkg}.*` methods.
|
|
15
|
+
*/
|
|
16
|
+
function makeCreateRuntime(context, source) {
|
|
17
|
+
//
|
|
18
|
+
// Create a set of overrides for our Proxy
|
|
19
|
+
//
|
|
20
|
+
const overrides = {
|
|
21
|
+
source,
|
|
22
|
+
files: {
|
|
23
|
+
add(...globs) {
|
|
24
|
+
context.files.add({ globs, source });
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
pkg: {
|
|
28
|
+
extend(fields) {
|
|
29
|
+
context.pkg.extend(fields, source);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
add(key, value, options) {
|
|
33
|
+
context.pkg.add(key, value, source, options);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
has(key, value) {
|
|
37
|
+
return context.pkg.has(key, value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return new Proxy(context, {
|
|
43
|
+
get(obj, key) {
|
|
44
|
+
if (overrides[key]) return overrides[key];
|
|
45
|
+
return obj[key];
|
|
46
|
+
},
|
|
47
|
+
set(obj, key, value) {
|
|
48
|
+
if (key !== 'pkg' && key !== 'files' && key !== 'source') {
|
|
49
|
+
obj[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function flatten(acc, values) {
|
|
56
|
+
return acc.concat(values);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create Context
|
|
61
|
+
*
|
|
62
|
+
* @type {CreateContext}
|
|
63
|
+
*
|
|
64
|
+
* -- Added by command args and options
|
|
65
|
+
*
|
|
66
|
+
* @property {String} appName - Short name of the app
|
|
67
|
+
* @property {String} cwd - Current work directory
|
|
68
|
+
* @property {String} dest - Path to the target app (Default: cwd/appName)
|
|
69
|
+
* @property {String} relDest - Relative path to the target app
|
|
70
|
+
* @property {Boolean} extant - Whether or not target directory already exists
|
|
71
|
+
* @property {[String]} localPresets - paths to the local presets packages
|
|
72
|
+
* @property {PresetDesc[]} rawPresets - Raw preset desc from args. Can include version constraint. Added by load-preset if using localPresets.
|
|
73
|
+
* @property {String[]} pkgLinks - Local packages that should be linked
|
|
74
|
+
* @property {String[]} messages - non-error/warning messages to report
|
|
75
|
+
* @property {String[]} warnings - warnings messages to report
|
|
76
|
+
* @property {String[]} errors - error messages to report but do not exit process
|
|
77
|
+
* @property {String[]} nextSteps - any next steps to report for user
|
|
78
|
+
* @property {Set<String>} generatedFiles - any generated files to show in report
|
|
79
|
+
* @property {any[]} presets - Default empty array, populated by load-preset with actual imports
|
|
80
|
+
* @property {Object} presetConfig - Default to object w/empty plugins array to be populated by `presetConfig` hook
|
|
81
|
+
*
|
|
82
|
+
* -- Added by `global-prompts`
|
|
83
|
+
*
|
|
84
|
+
* @property {String} appDescription - Description of app
|
|
85
|
+
* @property {Boolean} gitInit - Should a git repo be initialized and first commit
|
|
86
|
+
* @property {String} testPlugins - Array of testing plugins
|
|
87
|
+
* @property {String} packageManager - Which package manager to use (Default: 'npm')
|
|
88
|
+
* @property {String} installCmd - Derived install command (Default: 'npm install')
|
|
89
|
+
* @property {String} localCmd - Derived local run command (Default: 'npx gasket local')
|
|
90
|
+
* @property {Boolean} destOverride - Whether or not the user wants to override an extant directory
|
|
91
|
+
*
|
|
92
|
+
* -- Added by `load-preset`
|
|
93
|
+
*
|
|
94
|
+
* @property {PresetName[]} presets - Short name of presets
|
|
95
|
+
*
|
|
96
|
+
*
|
|
97
|
+
* -- Added by `setup-pkg`
|
|
98
|
+
*
|
|
99
|
+
* @property {ConfigBuilder} pkg - package.json builder
|
|
100
|
+
* @property {PackageManager} pkgManager - manager to execute npm or yarn commands
|
|
101
|
+
*
|
|
102
|
+
* -- Added by `setup-gasket-config`
|
|
103
|
+
*
|
|
104
|
+
* @property {ConfigBuilder} gasketConfig - gasket.config builder
|
|
105
|
+
*
|
|
106
|
+
* -- Added by `create-hooks`
|
|
107
|
+
*
|
|
108
|
+
* @property {Files} files - Use to add files and templates to generate
|
|
109
|
+
*/
|
|
110
|
+
export class CreateContext {
|
|
111
|
+
constructor(initContext = {}) {
|
|
112
|
+
Object.assign(this, initContext);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
runWith(plugin) {
|
|
116
|
+
return makeCreateRuntime(this, plugin);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Makes the initial context used through the create command flow.
|
|
122
|
+
*
|
|
123
|
+
* @param {String[]} argv - Commands args
|
|
124
|
+
* @param {Object} options - Command options
|
|
125
|
+
* @returns {CreateContext} context
|
|
126
|
+
*/
|
|
127
|
+
export function makeCreateContext(argv = [], options = {}) {
|
|
128
|
+
const appName = argv[0] || 'templated-app';
|
|
129
|
+
const {
|
|
130
|
+
presets = [],
|
|
131
|
+
npmLink = [],
|
|
132
|
+
presetPath = [],
|
|
133
|
+
packageManager,
|
|
134
|
+
prompts,
|
|
135
|
+
config,
|
|
136
|
+
configFile
|
|
137
|
+
} = options;
|
|
138
|
+
|
|
139
|
+
// Flatten the array of array created by the plugins flag – it
|
|
140
|
+
// supports both multiple instances as well as comma-separated lists.
|
|
141
|
+
const rawPresets = presets.reduce(flatten, []);
|
|
142
|
+
const localPresets = presetPath.reduce(flatten, []);
|
|
143
|
+
const pkgLinks = npmLink.reduce(flatten, []);
|
|
144
|
+
const cwd = process.cwd();
|
|
145
|
+
const dest = path.join(cwd, appName);
|
|
146
|
+
const relDest = `.${path.sep}${path.relative(cwd, dest)}`;
|
|
147
|
+
// eslint-disable-next-line no-sync
|
|
148
|
+
const extant = fs.existsSync(dest);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Input context which will be appended by prompts and passed to create hooks
|
|
152
|
+
*
|
|
153
|
+
* @type {CreateContext}
|
|
154
|
+
*/
|
|
155
|
+
const context = new CreateContext({
|
|
156
|
+
destOverride: true,
|
|
157
|
+
cwd,
|
|
158
|
+
dest,
|
|
159
|
+
relDest,
|
|
160
|
+
extant,
|
|
161
|
+
pkgLinks,
|
|
162
|
+
localPresets,
|
|
163
|
+
rawPresets,
|
|
164
|
+
messages: [],
|
|
165
|
+
warnings: [],
|
|
166
|
+
errors: [],
|
|
167
|
+
nextSteps: [],
|
|
168
|
+
generatedFiles: new Set(),
|
|
169
|
+
prompts,
|
|
170
|
+
presets: [],
|
|
171
|
+
presetConfig: {
|
|
172
|
+
plugins: []
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
readConfig(context, { config, configFile });
|
|
177
|
+
|
|
178
|
+
if (packageManager) {
|
|
179
|
+
context.packageManager = packageManager;
|
|
180
|
+
}
|
|
181
|
+
if (appName) {
|
|
182
|
+
context.appName = appName;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return context;
|
|
186
|
+
}
|
|
187
|
+
|