ember-cli 7.2.0-beta.1 → 7.3.0-alpha.1

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 (27) hide show
  1. package/CHANGELOG.md +79 -37
  2. package/docs/build/data.json +1 -1
  3. package/package.json +8 -8
  4. package/packages/addon-blueprint/package.json +4 -4
  5. package/packages/app-blueprint/files/package.json +2 -2
  6. package/packages/app-blueprint/package.json +4 -4
  7. package/packages/blueprint-blueprint/files/blueprints/__name__/files/.gitkeep +0 -0
  8. package/packages/blueprint-blueprint/files/blueprints/__name__/index.js +16 -0
  9. package/packages/blueprint-blueprint/index.js +5 -0
  10. package/packages/blueprint-blueprint/package.json +13 -0
  11. package/packages/blueprint-model/index.js +1791 -0
  12. package/packages/blueprint-model/node_modules/.bin/markdown-it +21 -0
  13. package/packages/blueprint-model/package.json +31 -0
  14. package/packages/blueprint-model/utilities/clean-remove.js +31 -0
  15. package/packages/blueprint-model/utilities/directory-for-package-name.js +31 -0
  16. package/packages/blueprint-model/utilities/edit-file-diff.js +64 -0
  17. package/packages/blueprint-model/utilities/experiments.js +63 -0
  18. package/packages/blueprint-model/utilities/file-info.js +170 -0
  19. package/packages/blueprint-model/utilities/insert-into-file.js +125 -0
  20. package/packages/blueprint-model/utilities/is-addon.js +5 -0
  21. package/packages/blueprint-model/utilities/markdown-color.js +208 -0
  22. package/packages/blueprint-model/utilities/open-editor.js +44 -0
  23. package/packages/blueprint-model/utilities/prepend-emoji.js +12 -0
  24. package/packages/blueprint-model/utilities/print-command.js +102 -0
  25. package/packages/blueprint-model/utilities/process-template.js +10 -0
  26. package/packages/blueprint-model/utilities/sequence.js +42 -0
  27. package/packages/blueprint-model/utilities/walk-up-path.js +23 -0
@@ -0,0 +1,1791 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ @module ember-cli
5
+ */
6
+ const FileInfo = require('@ember-tooling/blueprint-model/utilities/file-info');
7
+ const { default: chalk } = require('chalk');
8
+ const MarkdownColor = require('@ember-tooling/blueprint-model/utilities/markdown-color');
9
+ const sequence = require('./utilities/sequence');
10
+ const printCommand = require('@ember-tooling/blueprint-model/utilities/print-command');
11
+ const insertIntoFile = require('@ember-tooling/blueprint-model/utilities/insert-into-file');
12
+ const cleanRemove = require('@ember-tooling/blueprint-model/utilities/clean-remove');
13
+ const fs = require('fs-extra');
14
+ const { pluralize } = require('inflection');
15
+ const { minimatch } = require('minimatch');
16
+ const path = require('path');
17
+ const stringUtils = require('ember-cli-string-utils');
18
+ const merge = require('lodash/merge');
19
+ const zipObject = require('lodash/zipObject');
20
+ const intersection = require('lodash/intersection');
21
+ const cloneDeep = require('lodash/cloneDeep');
22
+ const compact = require('lodash/compact');
23
+ const sortBy = require('lodash/sortBy');
24
+ const walkSync = require('walk-sync');
25
+ const SilentError = require('silent-error');
26
+ const CoreObject = require('core-object');
27
+ const EOL = require('os').EOL;
28
+ const logger = require('heimdalljs-logger')('ember-cli:blueprint');
29
+ const normalizeEntityName = require('ember-cli-normalize-entity-name');
30
+ const isAddon = require('@ember-tooling/blueprint-model/utilities/is-addon');
31
+
32
+ const initialIgnoredFiles = ['.DS_Store'];
33
+
34
+ /**
35
+ A blueprint is a bundle of template files with optional install
36
+ logic.
37
+
38
+ Blueprints follow a simple structure. Let's take the built-in
39
+ `controller` blueprint as an example:
40
+
41
+ ```
42
+ blueprints/controller
43
+ ├── files
44
+ │ ├── app
45
+ │ │ └── __path__
46
+ │ │ └── __name__.js
47
+ └── index.js
48
+
49
+ blueprints/controller-test
50
+ ├── files
51
+ │ └── tests
52
+ │ └── unit
53
+ │ └── controllers
54
+ │ └── __test__.js
55
+ └── index.js
56
+ ```
57
+
58
+ ## Files
59
+
60
+ `files` contains templates for the all the files to be
61
+ installed into the target directory.
62
+
63
+ The `__name__` token is subtituted with the dasherized
64
+ entity name at install time. For example, when the user
65
+ invokes `ember generate controller foo` then `__name__` becomes
66
+ `foo`. When the `--pod` flag is used, for example `ember
67
+ generate controller foo --pod` then `__name__` becomes
68
+ `controller`.
69
+
70
+ The `__path__` token is substituted with the blueprint
71
+ name at install time. For example, when the user invokes
72
+ `ember generate controller foo` then `__path__` becomes
73
+ `controller`. When the `--pod` flag is used, for example
74
+ `ember generate controller foo --pod` then `__path__`
75
+ becomes `foo` (or `<podModulePrefix>/foo` if the
76
+ podModulePrefix is defined). This token is primarily for
77
+ pod support, and is only necessary if the blueprint can be
78
+ used in pod structure. If the blueprint does not require pod
79
+ support, simply use the blueprint name instead of the
80
+ `__path__` token.
81
+
82
+ The `__test__` token is substituted with the dasherized
83
+ entity name and appended with `-test` at install time.
84
+ This token is primarily for pod support and only necessary
85
+ if the blueprint requires support for a pod structure. If
86
+ the blueprint does not require pod support, simply use the
87
+ `__name__` token instead.
88
+
89
+ ## Template Variables (AKA Locals)
90
+
91
+ Variables can be inserted into templates with
92
+ `<%= someVariableName %>`.
93
+
94
+ For example, the built-in `util` blueprint
95
+ `files/app/utils/__name__.js` looks like this:
96
+
97
+ ```js
98
+ export default function <%= camelizedModuleName %>() {
99
+ return true;
100
+ }
101
+ ```
102
+
103
+ `<%= camelizedModuleName %>` is replaced with the real
104
+ value at install time.
105
+
106
+ The following template variables are provided by default:
107
+
108
+ - `dasherizedPackageName`
109
+ - `classifiedPackageName`
110
+ - `dasherizedModuleName`
111
+ - `classifiedModuleName`
112
+ - `camelizedModuleName`
113
+
114
+ `packageName` is the project name as found in the project's
115
+ `package.json`.
116
+
117
+ `moduleName` is the name of the entity being generated.
118
+
119
+ The mechanism for providing custom template variables is
120
+ described below.
121
+
122
+ ## Index.js
123
+
124
+ Custom installation and uninstallation behavior can be added
125
+ by overriding the hooks documented below. `index.js` should
126
+ export a plain object, which will extend the prototype of the
127
+ `Blueprint` class. If needed, the original `Blueprint` prototype
128
+ can be accessed through the `_super` property.
129
+
130
+ ```js
131
+ module.exports = {
132
+ locals(options) {
133
+ // Return custom template variables here.
134
+ return {};
135
+ },
136
+
137
+ normalizeEntityName(entityName) {
138
+ // Normalize and validate entity name here.
139
+ return entityName;
140
+ },
141
+
142
+ fileMapTokens(options) {
143
+ // Return custom tokens to be replaced in your files
144
+ return {
145
+ __token__(options){
146
+ // logic to determine value goes here
147
+ return 'value';
148
+ }
149
+ }
150
+ },
151
+
152
+ filesPath(options) {
153
+ return path.join(this.path, 'files');
154
+ },
155
+
156
+ beforeInstall(options) {},
157
+ afterInstall(options) {},
158
+ beforeUninstall(options) {},
159
+ afterUninstall(options) {}
160
+
161
+ };
162
+ ```
163
+
164
+ ## Blueprint Hooks
165
+
166
+ ### beforeInstall & beforeUninstall
167
+
168
+ Called before any of the template files are processed and receives
169
+ the `options` and `locals` hashes as parameters. Typically used for
170
+ validating any additional command line options or for any asynchronous
171
+ setup that is needed. As an example, the `controller` blueprint validates
172
+ its `--type` option in this hook. If you need to run any asynchronous code,
173
+ wrap it in a promise and return that promise from these hooks. This will
174
+ ensure that your code is executed correctly.
175
+
176
+ ### afterInstall & afterUninstall
177
+
178
+ The `afterInstall` and `afterUninstall` hooks receives the same
179
+ arguments as `locals`. Use it to perform any custom work after the
180
+ files are processed. For example, the built-in `route` blueprint
181
+ uses these hooks to add and remove relevant route declarations in
182
+ `app/router.js`.
183
+
184
+ ### Overriding Install
185
+
186
+ If you don't want your blueprint to install the contents of
187
+ `files` you can override the `install` method. It receives the
188
+ same `options` object described above and must return a promise.
189
+ See the built-in `resource` blueprint for an example of this.
190
+
191
+ @class Blueprint
192
+ @constructor
193
+ @extends CoreObject
194
+ @param {String} [blueprintPath]
195
+ @param {Object} [blueprintOptions]
196
+ */
197
+ let Blueprint = CoreObject.extend({
198
+ availableOptions: [],
199
+ anonymousOptions: ['name'],
200
+
201
+ _printableProperties: ['name', 'description', 'availableOptions', 'anonymousOptions', 'overridden'],
202
+
203
+ /**
204
+ Indicates whether or not a blueprint is a candidate for automatic transpilation from TS to JS.
205
+ This property could be false in the case that the blueprint is written in JS and is not intended
206
+ to work with TS at all, OR in the case that the blueprint is written in TS and the author does
207
+ not intend to support transpilation to JS.
208
+
209
+ @public
210
+ @property shouldTransformTypeScript
211
+ @type Boolean
212
+ */
213
+ shouldTransformTypeScript: false,
214
+
215
+ init(blueprintPath, blueprintOptions) {
216
+ this._super();
217
+
218
+ this.path = blueprintPath;
219
+ this.name = blueprintOptions?.name ?? path.basename(blueprintPath);
220
+
221
+ this._processOptions(blueprintOptions);
222
+ },
223
+
224
+ /**
225
+ Process the options object coming from either
226
+ the `init`, `install` or `uninstall` hook.
227
+
228
+ @private
229
+ @method _processOptions
230
+ @param {Object} options
231
+ */
232
+ _processOptions(options = {}) {
233
+ this.options = options;
234
+ this.dryRun = options.dryRun;
235
+ this.pod = options.pod;
236
+ this.project = options.project;
237
+ this.ui = options.ui;
238
+ },
239
+
240
+ /**
241
+ Hook to specify the path to the blueprint's files. By default this is
242
+ `path.join(this.path, 'files)`.
243
+
244
+ This can be used to customize which set of files to install based on options
245
+ or environmental variables. It defaults to the `files` directory within the
246
+ blueprint's folder.
247
+
248
+ @public
249
+ @method filesPath
250
+ @param {Object} options
251
+ @return {String} Path to the blueprints files directory.
252
+ */
253
+ filesPath(/* options */) {
254
+ return path.join(this.path, 'files');
255
+ },
256
+
257
+ /**
258
+ Used to retrieve files for blueprint.
259
+
260
+ @public
261
+ @method files
262
+ @param {Object} options
263
+ @return {Array} Contents of the blueprint's files directory
264
+ */
265
+ files(/* options */) {
266
+ if (this._files) {
267
+ return this._files;
268
+ }
269
+
270
+ let filesPath = this.filesPath(this.options);
271
+ if (Blueprint._existsSync(filesPath)) {
272
+ this._files = walkSync(filesPath);
273
+ } else {
274
+ this._files = [];
275
+ }
276
+
277
+ return this._files;
278
+ },
279
+
280
+ /**
281
+ @method srcPath
282
+ @param {String} file
283
+ @return {String} Resolved path to the file
284
+ */
285
+ srcPath(file) {
286
+ return path.resolve(this.filesPath(this.options), file);
287
+ },
288
+
289
+ /**
290
+ Hook for normalizing entity name
291
+
292
+ Use the `normalizeEntityName` hook to add custom normalization and
293
+ validation of the provided entity name. The default hook does not
294
+ make any changes to the entity name, but makes sure an entity name
295
+ is present and that it doesn't have a trailing slash.
296
+
297
+ This hook receives the entity name as its first argument. The string
298
+ returned by this hook will be used as the new entity name.
299
+
300
+ @public
301
+ @method normalizeEntityName
302
+ @param {String} entityName
303
+ @return {null}
304
+ */
305
+ normalizeEntityName(entityName) {
306
+ return normalizeEntityName(entityName);
307
+ },
308
+
309
+ /**
310
+ Write a status and message to the UI
311
+ @private
312
+ @method _writeStatusToUI
313
+ @param {Function} chalkColor
314
+ @param {String} keyword
315
+ @param {String} message
316
+ */
317
+ _writeStatusToUI(chalkColor, keyword, message) {
318
+ if (this.ui) {
319
+ this.ui.writeLine(` ${chalkColor(keyword)} ${message}`);
320
+ }
321
+ },
322
+
323
+ /**
324
+ @private
325
+ @method _writeFile
326
+ @param {Object} info
327
+ @return {Promise}
328
+ */
329
+ async _writeFile(info) {
330
+ if (!this.dryRun) {
331
+ return fs.outputFile(info.outputPath, await info.render());
332
+ }
333
+ },
334
+
335
+ /**
336
+ Actions lookup
337
+ @private
338
+ @property _actions
339
+ @type Object
340
+ */
341
+ _actions: {
342
+ write(info) {
343
+ this._writeStatusToUI(chalk.green, 'create', info.displayPath);
344
+ return this._writeFile(info);
345
+ },
346
+ skip(info) {
347
+ let label = 'skip';
348
+
349
+ if (info.resolution === 'identical') {
350
+ label = 'identical';
351
+ }
352
+
353
+ this._writeStatusToUI(chalk.yellow, label, info.displayPath);
354
+ },
355
+
356
+ overwrite(info) {
357
+ this._writeStatusToUI(chalk.yellow, 'overwrite', info.displayPath);
358
+ return this._writeFile(info);
359
+ },
360
+
361
+ edit(info) {
362
+ this._writeStatusToUI(chalk.green, 'edited', info.displayPath);
363
+ },
364
+
365
+ remove(info) {
366
+ this._writeStatusToUI(chalk.red, 'remove', info.displayPath);
367
+ if (!this.dryRun) {
368
+ return cleanRemove(info);
369
+ }
370
+ },
371
+ },
372
+
373
+ /**
374
+ Calls an action.
375
+ @private
376
+ @method _commit
377
+ @param {Object} result
378
+ @return {Promise}
379
+ @throws {Error} Action doesn't exist.
380
+ */
381
+ _commit(result) {
382
+ let action = this._actions[result.action];
383
+
384
+ if (action) {
385
+ return action.call(this, result);
386
+ } else {
387
+ throw new Error(`Tried to call action "${result.action}" but it does not exist`);
388
+ }
389
+ },
390
+
391
+ /**
392
+ Prints warning for pod unsupported.
393
+ @private
394
+ @method _checkForPod
395
+ */
396
+ _checkForPod(verbose) {
397
+ if (!this.hasPathToken && this.pod && verbose) {
398
+ this.ui.writeLine(
399
+ chalk.yellow(
400
+ 'You specified the pod flag, but this' +
401
+ ' blueprint does not support pod structure. It will be generated with' +
402
+ ' the default structure.'
403
+ )
404
+ );
405
+ }
406
+ },
407
+
408
+ /**
409
+ @private
410
+ @method _normalizeEntityName
411
+ @param {Object} entity
412
+ */
413
+ _normalizeEntityName(entity) {
414
+ if (entity) {
415
+ entity.name = this.normalizeEntityName(entity.name);
416
+ }
417
+ },
418
+
419
+ /**
420
+ @private
421
+ @method _checkInRepoAddonExists
422
+ @param {Object} options
423
+ */
424
+ _checkInRepoAddonExists(options) {
425
+ let addon;
426
+
427
+ if (options.inRepoAddon) {
428
+ addon = findAddonByName(this.project, options.inRepoAddon);
429
+
430
+ if (!addon) {
431
+ throw new SilentError(
432
+ `You specified the 'in-repo-addon' flag, but the ` +
433
+ `in-repo-addon '${options.inRepoAddon}' does not exist. Please check the name and try again.`
434
+ );
435
+ }
436
+ }
437
+
438
+ if (options.in) {
439
+ if (!ensureTargetDirIsAddon(options.in)) {
440
+ throw new SilentError(
441
+ `You specified the 'in' flag, but the ` +
442
+ `in repo addon '${options.in}' does not exist. Please check the name and try again.`
443
+ );
444
+ }
445
+ }
446
+ },
447
+
448
+ /**
449
+ @private
450
+ @method _process
451
+ @param {Object} options
452
+ @param {Function} beforeHook
453
+ @param {Function} process
454
+ @param {Function} afterHook
455
+ */
456
+ async _process(options, beforeHook, process, afterHook) {
457
+ let intoDir = options.target;
458
+
459
+ let locals = await this._locals(options);
460
+
461
+ // run beforeInstall/beforeUninstall userland hooks
462
+ await beforeHook.call(this, options, locals);
463
+
464
+ // gather fileInfos to be processed
465
+ let fileInfos = await process.call(this, intoDir, locals, options);
466
+
467
+ // commit changes for each FileInfo (with prompting as needed)
468
+ await Promise.all(fileInfos.map((info) => this._commit(info)));
469
+
470
+ // run afterInstall/afterUninstall userland hooks
471
+ await afterHook.call(this, options);
472
+ },
473
+
474
+ /**
475
+ @private
476
+ @method shouldConvertToJS
477
+ @param {Object} options
478
+ @param {FileInfo} fileInfo
479
+ @return {Boolean}
480
+ */
481
+ shouldConvertToJS(options, fileInfo) {
482
+ // If this isn't turned on, it doesn't matter what else was passed, we're not touching it.
483
+ if (!this.shouldTransformTypeScript) {
484
+ return false;
485
+ }
486
+
487
+ // If the blueprint isn't a TS file to begin with, there's nothing to convert.
488
+ if (!isTypeScriptFile(fileInfo.outputPath)) {
489
+ return false;
490
+ }
491
+
492
+ // Indicates when the user explicitly passed either `--typescript` or `--no-typescript` as opposed
493
+ // to not passing a flag at all and allowing for default behavior
494
+ const userExplicitlySelectedTypeScriptStatus = options.typescript !== undefined;
495
+
496
+ // Indicates when the user has asked for TypeScript either globally (by setting
497
+ // `isTypeScriptProject` to true) or locally (by passing the `--typescript` flag when they
498
+ // invoked the generator). Although ember-cli merges `.ember-cli` and all of the flag values into
499
+ // one object, we thought the DX would be improved by differentiating between what is intended
500
+ // to be global vs. local config.
501
+ const shouldUseTypeScript = userExplicitlySelectedTypeScriptStatus
502
+ ? options.typescript
503
+ : options.isTypeScriptProject;
504
+
505
+ // if the user wants TS output and we have a TS file available, we do *not* want to downlevel to JS
506
+ if (shouldUseTypeScript) {
507
+ return false;
508
+ }
509
+
510
+ return true;
511
+ },
512
+
513
+ /**
514
+ @private
515
+ @method convertToJS
516
+ @param {FileInfo} fileInfo
517
+ @return {Promise}
518
+ */
519
+ async convertToJS(fileInfo) {
520
+ let rendered = await fileInfo.render();
521
+
522
+ fileInfo.rendered = await this.removeTypes(path.extname(fileInfo.displayPath), rendered);
523
+ fileInfo.displayPath = replaceTypeScriptExtension(fileInfo.displayPath);
524
+ fileInfo.outputPath = replaceTypeScriptExtension(fileInfo.outputPath);
525
+
526
+ return fileInfo;
527
+ },
528
+
529
+ /**
530
+ @private
531
+ @method removeTypes
532
+ @param {string} extension
533
+ @param {string} code
534
+ @return {Promise}
535
+ */
536
+ async removeTypes(extension, code) {
537
+ const { removeTypes } = require('babel-remove-types');
538
+
539
+ if (extension === '.gts') {
540
+ const { Preprocessor } = require('content-tag');
541
+ const preprocessor = new Preprocessor();
542
+ // Strip template tags
543
+ const replacementClassMember = (i) => `template = __TEMPLATE_TAG_${i}__;`;
544
+ const replacementExpression = (i) => `__TEMPLATE_TAG_${i}__`;
545
+ const templateTagMatches = preprocessor.parse(code);
546
+ let strippedCode = code;
547
+ for (let i = 0; i < templateTagMatches.length; i++) {
548
+ const match = templateTagMatches[i];
549
+ const templateTag = substringBytes(code, match.range.startByte, match.range.endByte);
550
+ if (match.type === 'class-member') {
551
+ strippedCode = strippedCode.replace(templateTag, replacementClassMember(i));
552
+ } else {
553
+ strippedCode = strippedCode.replace(templateTag, replacementExpression(i));
554
+ }
555
+ }
556
+
557
+ // Remove types
558
+ const transformed = await removeTypes(strippedCode);
559
+
560
+ // Readd stripped template tags
561
+ let transformedWithTemplateTag = transformed;
562
+ for (let i = 0; i < templateTagMatches.length; i++) {
563
+ const match = templateTagMatches[i];
564
+ const templateTag = substringBytes(code, match.range.startByte, match.range.endByte);
565
+ if (match.type === 'class-member') {
566
+ transformedWithTemplateTag = transformedWithTemplateTag.replace(replacementClassMember(i), templateTag);
567
+ } else {
568
+ // babel-remove-types uses prettier under the hood, and adds trailing `;` where allowed,
569
+ // so we need to take that into account when restoring the template tags:
570
+ transformedWithTemplateTag = transformedWithTemplateTag.replace(`${replacementExpression(i)};`, templateTag);
571
+ transformedWithTemplateTag = transformedWithTemplateTag.replace(replacementExpression(i), templateTag);
572
+ }
573
+ }
574
+
575
+ return transformedWithTemplateTag;
576
+ }
577
+
578
+ return await removeTypes(code);
579
+ },
580
+
581
+ /**
582
+ @method install
583
+ @param {Object} options
584
+ @return {Promise}
585
+ */
586
+ install(options) {
587
+ this._processOptions(options);
588
+
589
+ this.hasPathToken = hasPathToken(this.files(this.options));
590
+
591
+ this.ui.writeLine(`installing ${this.name}`);
592
+
593
+ if (this.dryRun) {
594
+ this.ui.writeLine(chalk.yellow('You specified the `dry-run` flag, so no changes will be written.'));
595
+ }
596
+
597
+ this._normalizeEntityName(options.entity);
598
+ this._checkForPod(options.verbose);
599
+ this._checkInRepoAddonExists(options);
600
+
601
+ logger.info('START: processing blueprint: `%s`', this.name);
602
+ let start = new Date();
603
+ return this._process(options, this.beforeInstall, this.processFiles, this.afterInstall).finally(() =>
604
+ logger.info('END: processing blueprint: `%s` in (%dms)', this.name, new Date() - start)
605
+ );
606
+ },
607
+
608
+ /**
609
+ @method uninstall
610
+ @param {Object} options
611
+ @return {Promise}
612
+ */
613
+ uninstall(options) {
614
+ this._processOptions(options);
615
+
616
+ this.hasPathToken = hasPathToken(this.files(this.options));
617
+
618
+ this.ui.writeLine(`uninstalling ${this.name}`);
619
+
620
+ if (this.dryRun) {
621
+ this.ui.writeLine(chalk.yellow('You specified the `dry-run` flag, so no files will be deleted.'));
622
+ }
623
+
624
+ this._normalizeEntityName(options.entity);
625
+ this._checkForPod(options.verbose);
626
+
627
+ return this._process(options, this.beforeUninstall, this.processFilesForUninstall, this.afterUninstall);
628
+ },
629
+
630
+ /**
631
+ Hook for running operations before install.
632
+ @method beforeInstall
633
+ @return {Promise|null}
634
+ */
635
+ beforeInstall() {},
636
+
637
+ /**
638
+ Hook for running operations after install.
639
+ @method afterInstall
640
+ @return {Promise|null}
641
+ */
642
+ afterInstall() {},
643
+
644
+ /**
645
+ Hook for running operations before uninstall.
646
+ @method beforeUninstall
647
+ @return {Promise|null}
648
+ */
649
+ beforeUninstall() {},
650
+
651
+ /**
652
+ Hook for running operations after uninstall.
653
+ @method afterUninstall
654
+ @return {Promise|null}
655
+ */
656
+ afterUninstall() {},
657
+
658
+ filesToRemove: [],
659
+
660
+ /**
661
+ Hook for adding custom template variables.
662
+
663
+ When the following is called on the command line:
664
+
665
+ ```sh
666
+ ember generate controller foo --type=array --dry-run isAdmin:true
667
+ ```
668
+
669
+ The object passed to `locals` looks like this:
670
+
671
+ ```js
672
+ {
673
+ entity: {
674
+ name: 'foo',
675
+ options: {
676
+ isAdmin: true
677
+ }
678
+ },
679
+ dryRun: true
680
+ type: "array"
681
+ // more keys
682
+ }
683
+ ```
684
+
685
+ This hook must return an object or a Promise which resolves to an object.
686
+ The resolved object will be merged with the aforementioned default locals.
687
+
688
+ @public
689
+ @method locals
690
+ @param {Object} options General and entity-specific options
691
+ @return {Object|Promise|null}
692
+ */
693
+ locals(/* options */) {},
694
+
695
+ /**
696
+ Hook to add additional or override existing fileMap tokens.
697
+
698
+ Use `fileMapTokens` to add custom fileMap tokens for use
699
+ in the `mapFile` method. The hook must return an object in the
700
+ following pattern:
701
+
702
+ ```js
703
+ {
704
+ __token__(options){
705
+ // logic to determine value goes here
706
+ return 'value';
707
+ }
708
+ }
709
+ ```
710
+
711
+ It will be merged with the default `fileMapTokens`, and can be used
712
+ to override any of the default tokens.
713
+
714
+ Tokens are used in the files folder (see `files`), and get replaced with
715
+ values when the `mapFile` method is called.
716
+
717
+ @public
718
+ @method fileMapTokens
719
+ @return {Object|null}
720
+ */
721
+ fileMapTokens() {},
722
+
723
+ /**
724
+ @private
725
+ @method _fileMapTokens
726
+ @param {Object} options
727
+ @return {Object}
728
+ */
729
+ _fileMapTokens(options) {
730
+ let { project } = this;
731
+ let standardTokens = {
732
+ __name__(options) {
733
+ if (options.pod && options.hasPathToken) {
734
+ return options.blueprintName;
735
+ }
736
+ return options.dasherizedModuleName;
737
+ },
738
+ __path__(options) {
739
+ let blueprintName = options.blueprintName;
740
+
741
+ if (/-test/.test(blueprintName)) {
742
+ blueprintName = options.blueprintName.slice(0, options.blueprintName.indexOf('-test'));
743
+ }
744
+ if (options.pod && options.hasPathToken) {
745
+ return path.join(options.podPath, options.dasherizedModuleName);
746
+ }
747
+ return pluralize(blueprintName);
748
+ },
749
+ __root__(options) {
750
+ if (options.inRepoAddon) {
751
+ let addon = findAddonByName(project, options.inRepoAddon);
752
+ let relativeAddonPath = path.relative(project.root, addon.root);
753
+ return path.join(relativeAddonPath, 'addon');
754
+ }
755
+ if (options.in) {
756
+ let relativeAddonPath = path.relative(project.root, options.in);
757
+ return path.join(relativeAddonPath, 'addon');
758
+ }
759
+ if (options.inDummy) {
760
+ return path.join('tests', 'dummy', 'app');
761
+ }
762
+ if (options.inAddon) {
763
+ return 'addon';
764
+ }
765
+ return 'app';
766
+ },
767
+ __test__(options) {
768
+ if (options.pod && options.hasPathToken) {
769
+ return options.blueprintName;
770
+ }
771
+ return `${options.dasherizedModuleName}-test`;
772
+ },
773
+ };
774
+
775
+ let customTokens = this.fileMapTokens(options) || options.fileMapTokens || {};
776
+ return merge(standardTokens, customTokens);
777
+ },
778
+
779
+ /**
780
+ Used to generate fileMap tokens for mapFile.
781
+
782
+ @method generateFileMap
783
+ @param {Object} fileMapVariables
784
+ @return {Object}
785
+ */
786
+ generateFileMap(fileMapVariables) {
787
+ let tokens = this._fileMapTokens(fileMapVariables);
788
+ let fileMapValues = Object.values(tokens);
789
+ let tokenValues = fileMapValues.map((token) => token(fileMapVariables));
790
+ let tokenKeys = Object.keys(tokens);
791
+ return zipObject(tokenKeys, tokenValues);
792
+ },
793
+
794
+ /**
795
+ @method buildFileInfo
796
+ @param {Function} destPath
797
+ @param {Object} templateVariables
798
+ @param {String} file
799
+ @param {Object} options may be used when buildFileInfo is customized in a blueprint
800
+ @return {FileInfo | null}
801
+ */
802
+ buildFileInfo(intoDir, templateVariables, file /*, options */) {
803
+ let mappedPath = this.mapFile(file, templateVariables);
804
+
805
+ return new FileInfo({
806
+ action: 'write',
807
+ outputBasePath: path.normalize(intoDir),
808
+ outputPath: path.join(intoDir, mappedPath),
809
+ displayPath: path.normalize(mappedPath),
810
+ inputPath: this.srcPath(file),
811
+ templateVariables,
812
+ ui: this.ui,
813
+ });
814
+ },
815
+
816
+ /**
817
+ @method isUpdate
818
+ @return {Boolean}
819
+ */
820
+ isUpdate() {
821
+ if (this.project && this.project.isEmberCLIProject) {
822
+ return this.project.isEmberCLIProject();
823
+ }
824
+ },
825
+
826
+ /**
827
+ @private
828
+ @method _getFileInfos
829
+ @param {Array} files
830
+ @param {String} intoDir
831
+ @param {Object} templateVariables
832
+ @param {Object} options
833
+ @return {Array} file infos
834
+ */
835
+ _getFileInfos(files, intoDir, templateVariables, options) {
836
+ return files
837
+ .map((file) => this.buildFileInfo.call(this, intoDir, templateVariables, file, options))
838
+ .filter(Boolean);
839
+ },
840
+
841
+ /**
842
+ Add update files to ignored files or reset them
843
+ @private
844
+ @method _ignoreUpdateFiles
845
+ */
846
+ _ignoreUpdateFiles() {
847
+ if (this.isUpdate()) {
848
+ Blueprint.ignoredFiles = Blueprint.ignoredFiles.concat(Blueprint.ignoredUpdateFiles);
849
+ } else {
850
+ Blueprint.ignoredFiles = initialIgnoredFiles;
851
+ }
852
+ },
853
+
854
+ /**
855
+ @private
856
+ @method _getFilesForInstall
857
+ @param {Array} targetFiles
858
+ @return {Array} files
859
+ */
860
+ _getFilesForInstall(targetFiles) {
861
+ let files = this.files(this.options);
862
+
863
+ // if we've defined targetFiles, get file info on ones that match
864
+ return (targetFiles && targetFiles.length > 0 && intersection(files, targetFiles)) || files;
865
+ },
866
+
867
+ /**
868
+ @private
869
+ @method _checkForNoMatch
870
+ @param {Array} fileInfos
871
+ @param {String} rawArgs
872
+ */
873
+ _checkForNoMatch(fileInfos, rawArgs) {
874
+ if (fileInfos.filter(isFilePath).length < 1 && rawArgs) {
875
+ this.ui.writeLine(
876
+ chalk.yellow(`The globPattern "${rawArgs}" ` + `did not match any files, so no file updates will be made.`)
877
+ );
878
+ }
879
+ },
880
+
881
+ /**
882
+ @method processFiles
883
+ @param {String} intoDir
884
+ @param {Object} templateVariables
885
+ @param {Object} options
886
+ @return {Promise<FileInfo[]>}
887
+ */
888
+ processFiles(intoDir, templateVariables, options) {
889
+ let files = this._getFilesForInstall(templateVariables.targetFiles);
890
+ let fileInfos = this._getFileInfos(files, intoDir, templateVariables, options);
891
+ this._checkForNoMatch(fileInfos, templateVariables.rawArgs);
892
+
893
+ this._ignoreUpdateFiles();
894
+
895
+ let fileInfosToRemove = this._getFileInfos(this.filesToRemove, intoDir, templateVariables);
896
+
897
+ fileInfosToRemove = finishProcessingForUninstall(fileInfosToRemove);
898
+
899
+ return Promise.all(fileInfos.filter(isValidFile).map(prepareConfirm))
900
+ .then(finishProcessingForInstall)
901
+ .then((fileInfos) => {
902
+ return Promise.all(
903
+ fileInfos.map((info) => {
904
+ if (this.shouldConvertToJS(this.options, info)) {
905
+ return this.convertToJS(info);
906
+ }
907
+
908
+ return info;
909
+ })
910
+ );
911
+ })
912
+ .then((fileInfos) => fileInfos.concat(fileInfosToRemove));
913
+ },
914
+
915
+ /**
916
+ @method processFilesForUninstall
917
+ @param {String} intoDir
918
+ @param {Object} templateVariables
919
+ */
920
+ processFilesForUninstall(intoDir, templateVariables) {
921
+ let fileInfos = this._getFileInfos(this.files(this.options), intoDir, templateVariables);
922
+
923
+ this._ignoreUpdateFiles();
924
+
925
+ fileInfos = fileInfos.filter(isValidFile).reduce((acc, info) => {
926
+ // if it's possible that this blueprint could have produced either typescript OR javascript, we have to do some
927
+ // work to figure out which files to delete.
928
+ if (this.shouldTransformTypeScript) {
929
+ if (this.options.typescript === true) {
930
+ // if the user explicitly passed `--typescript`, we only want to delete TS files, so we stick with the existing
931
+ // info object since it will contain a .ts outputPath (since we know this blueprint is authored in TS because
932
+ // of our check above)
933
+ acc.push(info);
934
+ return acc;
935
+ }
936
+
937
+ const jsInfo = new FileInfo({
938
+ ...info,
939
+ outputPath: replaceTypeScriptExtension(info.outputPath),
940
+ displayPath: replaceTypeScriptExtension(info.displayPath),
941
+ });
942
+
943
+ if (this.options.typescript === false) {
944
+ // if the user explicitly passed `--no-typescript`, we only want to delete JS file, so we return our newly
945
+ // created jsInfo object since it contains the javascript version of the output path.
946
+ acc.push(jsInfo);
947
+ return acc;
948
+ }
949
+
950
+ if (this.options.typescript === undefined) {
951
+ // if the user didn't specify one way or the other, then both the JS and TS paths are possibilities, so we add
952
+ // both of them to the list. `finishProcessingForUninstall` will actually look to see which of them exists and
953
+ // delete whatever it finds.
954
+ acc.push(info, jsInfo);
955
+ return acc;
956
+ }
957
+ }
958
+
959
+ acc.push(info);
960
+ return acc;
961
+ }, []);
962
+
963
+ return finishProcessingForUninstall(fileInfos);
964
+ },
965
+
966
+ /**
967
+ @method mapFile
968
+ @param {String} file
969
+ @param locals
970
+ @return {String}
971
+ */
972
+ mapFile(file, locals) {
973
+ let pattern, i;
974
+ let fileMap = locals.fileMap || { __name__: locals.dasherizedModuleName };
975
+ file = Blueprint.renamedFiles[file] || file;
976
+ for (i in fileMap) {
977
+ pattern = new RegExp(i, 'g');
978
+ file = file.replace(pattern, fileMap[i]);
979
+ }
980
+ return file;
981
+ },
982
+
983
+ /**
984
+ Looks for a __root__ token in the files folder. Must be present for
985
+ the blueprint to support addon tokens. The `server`, `blueprints`, and `test`
986
+
987
+ @private
988
+ @method supportsAddon
989
+ @return {Boolean}
990
+ */
991
+ supportsAddon() {
992
+ return /__root__/.test(this.files(this.options).join());
993
+ },
994
+
995
+ /**
996
+ @private
997
+ @method _generateFileMapVariables
998
+ @param {String} moduleName
999
+ @param locals
1000
+ @param {Object} options
1001
+ @return {Object}
1002
+ */
1003
+ _generateFileMapVariables(moduleName, locals, options) {
1004
+ let originBlueprintName = options.originBlueprintName || this.name;
1005
+ let podModulePrefix = this.project.config().podModulePrefix || '';
1006
+ let podPath = podModulePrefix.substr(podModulePrefix.lastIndexOf('/') + 1);
1007
+ let inAddon = this.project.isEmberCLIAddon() || !!options.inRepoAddon;
1008
+ let inDummy = this.project.isEmberCLIAddon() ? options.dummy : false;
1009
+
1010
+ return {
1011
+ pod: this.pod,
1012
+ podPath,
1013
+ hasPathToken: this.hasPathToken,
1014
+ inAddon,
1015
+ inRepoAddon: options.inRepoAddon,
1016
+ in: options.in,
1017
+ inDummy,
1018
+ blueprintName: this.name,
1019
+ originBlueprintName,
1020
+ dasherizedModuleName: stringUtils.dasherize(moduleName),
1021
+ locals,
1022
+ };
1023
+ },
1024
+
1025
+ /**
1026
+ @private
1027
+ @method _locals
1028
+ @param {Object} options
1029
+ @return {Object}
1030
+ */
1031
+ _locals(options) {
1032
+ let packageName = options.project.name();
1033
+ let moduleName = (options.entity && options.entity.name) || packageName;
1034
+ let sanitizedModuleName = moduleName.replace(/\//g, '-');
1035
+
1036
+ return new Promise((resolve) => {
1037
+ resolve(this.locals(options));
1038
+ }).then((customLocals) => {
1039
+ let fileMapVariables = this._generateFileMapVariables(moduleName, customLocals, options);
1040
+ let fileMap = this.generateFileMap(fileMapVariables);
1041
+ let standardLocals = {
1042
+ dasherizedPackageName: stringUtils.dasherize(packageName),
1043
+ classifiedPackageName: stringUtils.classify(packageName),
1044
+ dasherizedModuleName: stringUtils.dasherize(moduleName),
1045
+ classifiedModuleName: stringUtils.classify(sanitizedModuleName),
1046
+ camelizedModuleName: stringUtils.camelize(sanitizedModuleName),
1047
+ decamelizedModuleName: stringUtils.decamelize(sanitizedModuleName),
1048
+ fileMap,
1049
+ hasPathToken: this.hasPathToken,
1050
+ targetFiles: options.targetFiles,
1051
+ rawArgs: options.rawArgs,
1052
+ };
1053
+
1054
+ return merge({}, standardLocals, customLocals);
1055
+ });
1056
+ },
1057
+
1058
+ /**
1059
+ Used to add a package to the project's `package.json`.
1060
+
1061
+ Generally, this would be done from the `afterInstall` hook, to
1062
+ ensure that a package that is required by a given blueprint is
1063
+ available.
1064
+
1065
+ @method addPackageToProject
1066
+ @param {String} packageName
1067
+ @param {String} target
1068
+ @return {Promise}
1069
+ */
1070
+ addPackageToProject(packageName, target) {
1071
+ let packageObject = { name: packageName };
1072
+
1073
+ if (target) {
1074
+ packageObject.target = target;
1075
+ }
1076
+
1077
+ return this.addPackagesToProject([packageObject]);
1078
+ },
1079
+
1080
+ /**
1081
+ Used to add multiple packages to the project's `package.json`.
1082
+
1083
+ Generally, this would be done from the `afterInstall` hook, to
1084
+ ensure that a package that is required by a given blueprint is
1085
+ available.
1086
+
1087
+ Expects each array item to be an object with a `name`. Each object
1088
+ may optionally have a `target` to specify a specific version.
1089
+
1090
+ @method addPackagesToProject
1091
+ @param {Array} packages
1092
+ @return {Promise}
1093
+
1094
+ @example
1095
+ ```js
1096
+ this.addPackagesToProject([
1097
+ { name: 'lodash' },
1098
+ { name: 'moment', target: '^2.17.0' },
1099
+ ]);
1100
+ ```
1101
+ */
1102
+ addPackagesToProject(packages) {
1103
+ let task = this.taskFor('npm-install');
1104
+ let installText = packages.length > 1 ? 'install packages' : 'install package';
1105
+ let packageNames = [];
1106
+ let packageArray = [];
1107
+
1108
+ for (let i = 0; i < packages.length; i++) {
1109
+ packageNames.push(packages[i].name);
1110
+
1111
+ let packageNameAndVersion = packages[i].name;
1112
+
1113
+ if (packages[i].target) {
1114
+ packageNameAndVersion += `@${packages[i].target}`;
1115
+ }
1116
+
1117
+ packageArray.push(packageNameAndVersion);
1118
+ }
1119
+
1120
+ this._writeStatusToUI(chalk.green, installText, packageNames.join(', '));
1121
+
1122
+ return task.run({
1123
+ 'save-dev': true,
1124
+ verbose: false,
1125
+ packages: packageArray,
1126
+ });
1127
+ },
1128
+
1129
+ /**
1130
+ Used to remove a package from the project's `package.json`.
1131
+
1132
+ Generally, this would be done from the `afterInstall` hook, to
1133
+ ensure that any package conflicts can be resolved before the
1134
+ addon is used.
1135
+
1136
+ @method removePackageFromProject
1137
+ @param {String} packageName
1138
+ @return {Promise}
1139
+ */
1140
+ removePackageFromProject(packageName) {
1141
+ let packageObject = { name: packageName };
1142
+
1143
+ return this.removePackagesFromProject([packageObject]);
1144
+ },
1145
+
1146
+ /**
1147
+ Used to remove multiple packages from the project's `package.json`.
1148
+
1149
+ Generally, this would be done from the `afterInstall` hook, to
1150
+ ensure that any package conflicts can be resolved before the
1151
+ addon is used.
1152
+
1153
+ Expects each array item to be an object with a `name` property.
1154
+
1155
+ @method removePackagesFromProject
1156
+ @param {Array} packages
1157
+ @return {Promise}
1158
+ */
1159
+ removePackagesFromProject(packages) {
1160
+ let task = this.taskFor('npm-uninstall');
1161
+ let installText = packages.length > 1 ? 'uninstall packages' : 'uninstall package';
1162
+ let packageNames = [];
1163
+
1164
+ let projectDependencies = this.project.dependencies();
1165
+
1166
+ for (let i = 0; i < packages.length; i++) {
1167
+ let packageName = packages[i].name;
1168
+ if (packageName in projectDependencies) {
1169
+ packageNames.push(packageName);
1170
+ }
1171
+ }
1172
+
1173
+ if (packageNames.length === 0) {
1174
+ this._writeStatusToUI(chalk.yellow, 'remove', 'Skipping uninstall because no matching package is installed.');
1175
+ return Promise.resolve();
1176
+ }
1177
+
1178
+ this._writeStatusToUI(chalk.green, installText, packageNames.join(', '));
1179
+
1180
+ return task.run({
1181
+ 'save-dev': true,
1182
+ verbose: false,
1183
+ packages: packageNames,
1184
+ });
1185
+ },
1186
+
1187
+ /**
1188
+ Used to add an addon to the project's `package.json` and run it's
1189
+ `defaultBlueprint` if it provides one.
1190
+
1191
+ Generally, this would be done from the `afterInstall` hook, to
1192
+ ensure that a package that is required by a given blueprint is
1193
+ available.
1194
+
1195
+ @method addAddonToProject
1196
+ @param {Object} options
1197
+ @return {Promise}
1198
+ */
1199
+ addAddonToProject(options) {
1200
+ return this.addAddonsToProject({
1201
+ packages: [options],
1202
+ extraArgs: options.extraArgs || {},
1203
+ blueprintOptions: options.blueprintOptions || {},
1204
+ });
1205
+ },
1206
+
1207
+ /**
1208
+ Used to add multiple addons to the project's `package.json` and run their
1209
+ `defaultBlueprint` if they provide one.
1210
+
1211
+ Generally, this would be done from the `afterInstall` hook, to
1212
+ ensure that a package that is required by a given blueprint is
1213
+ available.
1214
+
1215
+ @method addAddonsToProject
1216
+ @param {Object} options
1217
+ @return {Promise}
1218
+ */
1219
+ addAddonsToProject(options) {
1220
+ let taskOptions = {
1221
+ packages: [],
1222
+ extraArgs: options.extraArgs || [],
1223
+ blueprintOptions: options.blueprintOptions || {},
1224
+ };
1225
+
1226
+ let packages = options.packages;
1227
+ if (packages && packages.length) {
1228
+ taskOptions.packages = packages.map((pkg) => {
1229
+ if (typeof pkg === 'string') {
1230
+ return pkg;
1231
+ }
1232
+
1233
+ if (!pkg.name) {
1234
+ throw new SilentError('You must provide a package `name` to addAddonsToProject');
1235
+ }
1236
+
1237
+ if (pkg.target) {
1238
+ pkg.name += `@${pkg.target}`;
1239
+ }
1240
+
1241
+ return pkg.name;
1242
+ });
1243
+ } else {
1244
+ throw new SilentError('You must provide package to addAddonsToProject');
1245
+ }
1246
+
1247
+ let installText = packages.length > 1 ? 'install addons' : 'install addon';
1248
+ this._writeStatusToUI(chalk.green, installText, taskOptions['packages'].join(', '));
1249
+
1250
+ let previousCwd;
1251
+ if (!this.project.isEmberCLIProject()) {
1252
+ // The install task adds dependencies based on the current working directory.
1253
+ // But in case we created the new project by calling the blueprint with a custom target directory (options.target),
1254
+ // the current directory will *not* be the one the project is created in, so we must adjust this here.
1255
+ previousCwd = process.cwd();
1256
+ process.chdir(options.blueprintOptions.target);
1257
+ }
1258
+
1259
+ let result = this.taskFor('addon-install').run(taskOptions);
1260
+ if (previousCwd) {
1261
+ return result.then(() => process.chdir(previousCwd));
1262
+ }
1263
+
1264
+ return result;
1265
+ },
1266
+
1267
+ /**
1268
+ Used to retrieve a task with the given name. Passes the new task
1269
+ the standard information available (like `ui`, `project`, etc).
1270
+
1271
+ @method taskFor
1272
+ @param dasherizedName
1273
+ @public
1274
+ */
1275
+ taskFor(dasherizedName) {
1276
+ const Task = require(`../tasks/${dasherizedName}`);
1277
+
1278
+ return new Task({
1279
+ ui: this.ui,
1280
+ project: this.project,
1281
+ });
1282
+ },
1283
+
1284
+ /**
1285
+ Inserts the given content into a file. If the `contentsToInsert` string is already
1286
+ present in the current contents, the file will not be changed unless `force` option
1287
+ is passed.
1288
+
1289
+ If `options.before` is specified, `contentsToInsert` will be inserted before
1290
+ the first instance of that string. If `options.after` is specified, the
1291
+ contents will be inserted after the first instance of that string.
1292
+ If the string specified by options.before or options.after is not in the file,
1293
+ no change will be made.
1294
+
1295
+ If neither `options.before` nor `options.after` are present, `contentsToInsert`
1296
+ will be inserted at the end of the file.
1297
+
1298
+ Example:
1299
+ ```
1300
+ // app/router.js
1301
+ Router.map(function () {
1302
+ });
1303
+ ```
1304
+
1305
+ ```
1306
+ insertIntoFile('app/router.js', ' this.route("admin");', {
1307
+ after: 'Router.map(function () {' + EOL
1308
+ }).then(function() {
1309
+ // file has been inserted into!
1310
+ });
1311
+
1312
+
1313
+ ```
1314
+
1315
+ ```
1316
+ // app/router.js
1317
+ Router.map(function () {
1318
+ this.route("admin");
1319
+ });
1320
+ ```
1321
+
1322
+ @method insertIntoFile
1323
+ @param {String} pathRelativeToProjectRoot
1324
+ @param {String} contentsToInsert
1325
+ @param {Object} providedOptions
1326
+ @return {Promise}
1327
+ */
1328
+ insertIntoFile(pathRelativeToProjectRoot, contentsToInsert, providedOptions) {
1329
+ let fullPath = path.join(this.project.root, pathRelativeToProjectRoot);
1330
+ return insertIntoFile(fullPath, contentsToInsert, providedOptions);
1331
+ },
1332
+
1333
+ _printCommand: printCommand,
1334
+
1335
+ printBasicHelp(verbose) {
1336
+ let initialMargin = ' ';
1337
+ let output = initialMargin;
1338
+ if (this.overridden) {
1339
+ output += chalk.grey(`(overridden) ${this.name}`);
1340
+ } else {
1341
+ output += this.name;
1342
+
1343
+ output += this._printCommand(initialMargin, true);
1344
+
1345
+ if (verbose) {
1346
+ output += EOL + this.printDetailedHelp(this.availableOptions);
1347
+ }
1348
+ }
1349
+
1350
+ return output;
1351
+ },
1352
+
1353
+ printDetailedHelp() {
1354
+ let markdownColor = new MarkdownColor();
1355
+ let filePath = getDetailedHelpPath(this.path);
1356
+
1357
+ if (Blueprint._existsSync(filePath)) {
1358
+ return markdownColor.renderFile(filePath, { indent: ' ' });
1359
+ }
1360
+ return '';
1361
+ },
1362
+
1363
+ getJson(verbose) {
1364
+ let json = {};
1365
+ this._printableProperties.forEach((key) => {
1366
+ let value = this[key];
1367
+ if (key === 'availableOptions') {
1368
+ value = cloneDeep(value);
1369
+ value.forEach((option) => {
1370
+ if (typeof option.type === 'function') {
1371
+ option.type = option.type.name;
1372
+ }
1373
+ });
1374
+ }
1375
+ json[key] = value;
1376
+ });
1377
+
1378
+ if (verbose) {
1379
+ let detailedHelp = this.printDetailedHelp(this.availableOptions);
1380
+ if (detailedHelp) {
1381
+ json.detailedHelp = detailedHelp;
1382
+ }
1383
+ }
1384
+
1385
+ return json;
1386
+ },
1387
+
1388
+ /**
1389
+ Used to retrieve a blueprint with the given name.
1390
+
1391
+ @method lookupBlueprint
1392
+ @param {String} dasherizedName
1393
+ @return {Blueprint}
1394
+ @public
1395
+ */
1396
+ lookupBlueprint(dasherizedName) {
1397
+ let projectPaths = this.project ? this.project.blueprintLookupPaths() : [];
1398
+
1399
+ return Blueprint.lookup(dasherizedName, {
1400
+ paths: projectPaths,
1401
+ });
1402
+ },
1403
+ });
1404
+
1405
+ const builtInBlueprints = new Map([
1406
+ ['app', '@ember-tooling/classic-build-app-blueprint'],
1407
+ ['addon', '@ember-tooling/classic-build-addon-blueprint'],
1408
+ ['blueprint', '@ember-tooling/blueprint-blueprint'],
1409
+ ]);
1410
+
1411
+ /**
1412
+ @static
1413
+ @method lookup
1414
+ @namespace Blueprint
1415
+ @param {String} name
1416
+ @param {Object} [options]
1417
+ @param {Array} [options.paths] Extra paths to search for blueprints
1418
+ @param {Boolean} [options.ignoreMissing] Throw a `SilentError` if a
1419
+ matching Blueprint could not be found
1420
+ @param {Object} [options.blueprintOptions] Options object that will be passed
1421
+ along to the Blueprint instance on creation.
1422
+ @return {Blueprint}
1423
+ */
1424
+ Blueprint.lookup = function (name, options) {
1425
+ options = options || {};
1426
+
1427
+ let lookupPaths = new Set(options.paths);
1428
+
1429
+ for (let path of options?.project?.blueprintLookupPaths() ?? []) {
1430
+ lookupPaths.add(path);
1431
+ }
1432
+
1433
+ for (let lookupPath of lookupPaths) {
1434
+ let blueprintPath = path.resolve(lookupPath, name);
1435
+
1436
+ if (Blueprint._existsSync(blueprintPath)) {
1437
+ return Blueprint.load(blueprintPath, options.blueprintOptions);
1438
+ }
1439
+ }
1440
+
1441
+ // Check if `name` itself is a path to a blueprint:
1442
+ if (name.includes(path.sep)) {
1443
+ let blueprintPath = path.resolve(name);
1444
+
1445
+ if (Blueprint._existsSync(blueprintPath)) {
1446
+ return Blueprint.load(blueprintPath, options.blueprintOptions);
1447
+ }
1448
+ }
1449
+
1450
+ // Check for the built in blueprints
1451
+ if (builtInBlueprints.has(name)) {
1452
+ let blueprintPath = require.resolve(builtInBlueprints.get(name));
1453
+
1454
+ if (Blueprint._existsSync(blueprintPath)) {
1455
+ return Blueprint.load(path.dirname(blueprintPath), options.blueprintOptions);
1456
+ }
1457
+ }
1458
+
1459
+ if (!options.ignoreMissing) {
1460
+ throw new SilentError(`Unknown blueprint: ${name}`);
1461
+ }
1462
+ };
1463
+
1464
+ /**
1465
+ Loads a blueprint from given path.
1466
+ @static
1467
+ @method load
1468
+ @namespace Blueprint
1469
+ @param {String} blueprintPath
1470
+ @param {Object} [blueprintOptions]
1471
+ @return {Blueprint} blueprint instance
1472
+ */
1473
+ Blueprint.load = function (blueprintPath, blueprintOptions) {
1474
+ if (fs.lstatSync(blueprintPath).isDirectory()) {
1475
+ let Constructor = Blueprint;
1476
+
1477
+ let constructorPath = path.resolve(blueprintPath, 'index.js');
1478
+ if (Blueprint._existsSync(constructorPath)) {
1479
+ let blueprintModule = require(constructorPath);
1480
+
1481
+ if (blueprintModule.__esModule) {
1482
+ blueprintModule = blueprintModule.default;
1483
+ }
1484
+
1485
+ if (typeof blueprintModule === 'function') {
1486
+ Constructor = blueprintModule;
1487
+ } else {
1488
+ Constructor = Blueprint.extend(blueprintModule);
1489
+ }
1490
+ }
1491
+
1492
+ return new Constructor(blueprintPath, blueprintOptions);
1493
+ }
1494
+ };
1495
+
1496
+ /**
1497
+ @static
1498
+ @method list
1499
+ @namespace Blueprint
1500
+ @param {Object} [options]
1501
+ @param {Array} [options.paths] Extra paths to search for blueprints
1502
+ @return {Array}
1503
+ */
1504
+ Blueprint.list = function (options) {
1505
+ options = options || {};
1506
+
1507
+ let lookupPaths = new Set(options.paths);
1508
+
1509
+ for (let path of options?.project?.blueprintLookupPaths() ?? []) {
1510
+ lookupPaths.add(path);
1511
+ }
1512
+
1513
+ let seen = [];
1514
+
1515
+ return Array.from(lookupPaths).map((lookupPath) => {
1516
+ let source;
1517
+ let packagePath = path.join(lookupPath, '../package.json');
1518
+ if (Blueprint._existsSync(packagePath)) {
1519
+ source = require(packagePath).name;
1520
+ } else {
1521
+ source = path.basename(path.join(lookupPath, '..'));
1522
+ }
1523
+
1524
+ let blueprints = dir(lookupPath).map((blueprintPath) => {
1525
+ let blueprint = Blueprint.load(blueprintPath);
1526
+ if (blueprint) {
1527
+ let name = blueprint.name;
1528
+ blueprint.overridden = seen.includes(name);
1529
+ seen.push(name);
1530
+
1531
+ return blueprint;
1532
+ }
1533
+ });
1534
+
1535
+ // if we're listing blueprints from ember-cli then we should add the built-in blueprints
1536
+ if (source === 'ember-cli') {
1537
+ for (let [blueprintName, blueprintPackage] of builtInBlueprints.entries()) {
1538
+ let blueprint = Blueprint.load(path.dirname(require.resolve(blueprintPackage)), {
1539
+ name: blueprintName,
1540
+ });
1541
+
1542
+ let name = blueprintName;
1543
+ blueprint.overridden = seen.includes(name);
1544
+ seen.push(name);
1545
+
1546
+ blueprints.push(blueprint);
1547
+ }
1548
+ }
1549
+
1550
+ return {
1551
+ source,
1552
+ blueprints: sortBy(compact(blueprints), 'name'),
1553
+ };
1554
+ });
1555
+ };
1556
+
1557
+ Blueprint._existsSync = function (path, parent) {
1558
+ return fs.existsSync(path, parent);
1559
+ };
1560
+
1561
+ Blueprint._readdirSync = function (path) {
1562
+ return fs.readdirSync(path);
1563
+ };
1564
+
1565
+ /**
1566
+ Files that are renamed when installed into the target directory.
1567
+ This allows including files in the blueprint that would have an effect
1568
+ on another process, such as a file named `.gitignore`.
1569
+
1570
+ The keys are the filenames used in the files folder.
1571
+ The values are the filenames used in the target directory.
1572
+
1573
+ @static
1574
+ @property renamedFiles
1575
+ */
1576
+ Blueprint.renamedFiles = {
1577
+ gitignore: '.gitignore',
1578
+ };
1579
+
1580
+ /**
1581
+ @static
1582
+ @property ignoredFiles
1583
+ */
1584
+ Blueprint.ignoredFiles = initialIgnoredFiles;
1585
+
1586
+ /**
1587
+ @static
1588
+ @property ignoredUpdateFiles
1589
+ */
1590
+ Blueprint.ignoredUpdateFiles = ['.gitkeep', 'app.css', 'LICENSE.md'];
1591
+
1592
+ /**
1593
+ @private
1594
+ @method prepareConfirm
1595
+ @param {FileInfo} info
1596
+ @return {Promise}
1597
+ */
1598
+ function prepareConfirm(info) {
1599
+ return info.checkForConflict().then((resolution) => {
1600
+ info.resolution = resolution;
1601
+ return info;
1602
+ });
1603
+ }
1604
+
1605
+ /**
1606
+ @private
1607
+ @method markIdenticalToBeSkipped
1608
+ @param {FileInfo} info
1609
+ */
1610
+ function markIdenticalToBeSkipped(info) {
1611
+ if (info.resolution === 'identical') {
1612
+ info.action = 'skip';
1613
+ }
1614
+ }
1615
+
1616
+ /**
1617
+ @private
1618
+ @method markToBeRemoved
1619
+ @param {FileInfo} info
1620
+ */
1621
+ function markToBeRemoved(info) {
1622
+ info.action = 'remove';
1623
+ }
1624
+
1625
+ /**
1626
+ @private
1627
+ @method gatherConfirmationMessages
1628
+ @param {Array} collection
1629
+ @param {FileInfo} info
1630
+ @return {Array}
1631
+ */
1632
+ function gatherConfirmationMessages(collection, info) {
1633
+ if (info.resolution === 'confirm') {
1634
+ collection.push(info.confirmOverwriteTask());
1635
+ }
1636
+ return collection;
1637
+ }
1638
+
1639
+ /**
1640
+ @private
1641
+ @method isIgnored
1642
+ @param {FileInfo} info
1643
+ @return {Boolean}
1644
+ */
1645
+ function isIgnored(info) {
1646
+ let fn = info.inputPath;
1647
+
1648
+ return Blueprint.ignoredFiles.some((ignoredFile) => minimatch(fn, ignoredFile, { matchBase: true }));
1649
+ }
1650
+
1651
+ /**
1652
+ Looks for a __path__ token in the files folder. Must be present for
1653
+ the blueprint to support pod tokens.
1654
+
1655
+ @private
1656
+ @method hasPathToken
1657
+ @param {files} files
1658
+ @return {Boolean}
1659
+ */
1660
+ function hasPathToken(files) {
1661
+ return /__path__/.test(files.join());
1662
+ }
1663
+
1664
+ function findAddonByName(addonOrProject, name) {
1665
+ let addon = addonOrProject.addons.find((addon) => addon.name === name);
1666
+
1667
+ if (addon) {
1668
+ return addon;
1669
+ }
1670
+
1671
+ return addonOrProject.addons.find((addon) => findAddonByName(addon, name));
1672
+ }
1673
+
1674
+ function ensureTargetDirIsAddon(addonPath) {
1675
+ let projectInfo;
1676
+
1677
+ try {
1678
+ projectInfo = require(path.join(addonPath, 'package.json'));
1679
+ } catch (err) {
1680
+ if (err.code === 'MODULE_NOT_FOUND') {
1681
+ throw new Error(`The directory ${addonPath} does not appear to be a valid addon directory.`);
1682
+ } else {
1683
+ throw err;
1684
+ }
1685
+ }
1686
+
1687
+ return isAddon(projectInfo.keywords);
1688
+ }
1689
+
1690
+ /**
1691
+ @private
1692
+ @method isValidFile
1693
+ @param {Object} fileInfo
1694
+ @return {Promise}
1695
+ */
1696
+ function isValidFile(fileInfo) {
1697
+ if (isIgnored(fileInfo)) {
1698
+ return false;
1699
+ } else {
1700
+ return isFilePath(fileInfo);
1701
+ }
1702
+ }
1703
+
1704
+ /**
1705
+ @private
1706
+ @method isFilePath
1707
+ @param {Object} fileInfo
1708
+ @return {Promise}
1709
+ */
1710
+ function isFilePath(fileInfo) {
1711
+ return fs.statSync(fileInfo.inputPath).isFile();
1712
+ }
1713
+
1714
+ /**
1715
+ @private
1716
+ @method dir
1717
+ @return {Array} list of files in the given directory or and empty array if no directory exists
1718
+ */
1719
+ function dir(fullPath) {
1720
+ if (Blueprint._existsSync(fullPath)) {
1721
+ return Blueprint._readdirSync(fullPath).map((fileName) => path.join(fullPath, fileName));
1722
+ } else {
1723
+ return [];
1724
+ }
1725
+ }
1726
+
1727
+ /**
1728
+ @private
1729
+ @method getDetailedHelpPath
1730
+ @param {String} thisPath
1731
+ @return {String} help path
1732
+ */
1733
+ function getDetailedHelpPath(thisPath) {
1734
+ return path.join(thisPath, './HELP.md');
1735
+ }
1736
+
1737
+ function finishProcessingForInstall(infos) {
1738
+ infos.forEach(markIdenticalToBeSkipped);
1739
+
1740
+ let infosNeedingConfirmation = infos.reduce(gatherConfirmationMessages, []);
1741
+
1742
+ return sequence(infosNeedingConfirmation).then(() => infos);
1743
+ }
1744
+
1745
+ function finishProcessingForUninstall(infos) {
1746
+ let validInfos = infos.filter((info) => fs.existsSync(info.outputPath));
1747
+ validInfos.forEach(markToBeRemoved);
1748
+
1749
+ return validInfos;
1750
+ }
1751
+
1752
+ function replaceExtension(filePath, newExt) {
1753
+ const { dir, name } = path.parse(filePath);
1754
+
1755
+ return path.format({
1756
+ dir,
1757
+ name,
1758
+ ext: newExt,
1759
+ });
1760
+ }
1761
+
1762
+ function replaceTypeScriptExtension(filePath) {
1763
+ const extensionMap = {
1764
+ '.ts': '.js',
1765
+ '.gts': '.gjs',
1766
+ };
1767
+ const ext = path.extname(filePath);
1768
+ const newExt = extensionMap[ext];
1769
+
1770
+ return replaceExtension(filePath, newExt);
1771
+ }
1772
+
1773
+ function isTypeScriptFile(filePath) {
1774
+ return path.extname(filePath) === '.ts' || path.extname(filePath) === '.gts';
1775
+ }
1776
+
1777
+ /**
1778
+ * Takes a substring of a string based on byte offsets.
1779
+ * @private
1780
+ * @method substringBytes
1781
+ * @param {string} value : The input string.
1782
+ * @param {number} start : The byte index of the substring start.
1783
+ * @param {number} end : The byte index of the substring end.
1784
+ * @return {string} : The substring.
1785
+ */
1786
+ function substringBytes(value, start, end) {
1787
+ let buf = Buffer.from(value);
1788
+ return buf.subarray(start, end).toString();
1789
+ }
1790
+
1791
+ module.exports = Blueprint;