UNPKG

5.83 kBJavaScriptView Raw
1'use strict';
2
3/* eslint-env node */
4
5const Filter = require('broccoli-persistent-filter');
6const md5Hex = require('md5-hex');
7const stringify = require('json-stable-stringify');
8const chalk = require('chalk');
9const Linter = require('ember-template-lint');
10const debug = require('debug')('template-lint:broccoli');
11const projectLocalizationAddon = require('./lib/utils/project-localization-framework');
12const testGenerators = require('aot-test-generators');
13const testGeneratorNames = Object.keys(testGenerators);
14const concat = require('broccoli-concat');
15
16function TemplateLinter(inputNode, _options) {
17 if (!(this instanceof TemplateLinter)) { return new TemplateLinter(inputNode, _options); }
18
19 let options = _options || {};
20 if (!options.hasOwnProperty('persist')) {
21 options.persist = true;
22 }
23
24 Filter.call(this, inputNode, {
25 annotation: options.annotation,
26 persist: options.persist
27 });
28
29
30 this.options = options;
31 this._console = this.options.console || console;
32 this._templatercConfig = undefined;
33
34 if (this.options.testGenerator) {
35 let testGenerator = testGenerators[this.options.testGenerator];
36 if (!testGenerator) {
37 throw new Error(`No test generator found for "testGenerator: ${this.options.testGenerator}"`);
38 }
39
40 this._testGenerator = testGenerator;
41 }
42
43 this.linter = new Linter(options);
44
45 debug('Linter config: %s', JSON.stringify(this.linter.config));
46
47 this.issueLocalizationWarningIfNeeded();
48}
49
50TemplateLinter.prototype = Object.create(Filter.prototype);
51TemplateLinter.prototype.constructor = TemplateLinter;
52
53TemplateLinter.prototype.extensions = ['hbs'];
54TemplateLinter.prototype.targetExtension = 'template.lint-test.js';
55
56TemplateLinter.prototype.baseDir = function() {
57 return __dirname;
58};
59
60TemplateLinter.prototype.cacheKeyProcessString = function(string, relativePath) {
61 return md5Hex([
62 stringify(this.linter.config),
63 this.options.testGenerator || '',
64 this.options.groupName || '',
65 string,
66 relativePath
67 ]);
68};
69
70TemplateLinter.prototype.build = function () {
71 let self = this;
72 self._errors = [];
73
74 return Filter.prototype.build.apply(this, arguments)
75 .finally(function() {
76 if (self._errors.length > 0) {
77 let label = ' Template Linting Error' + (self._errors.length > 1 ? 's' : '');
78 self._console.log('\n' + self._errors.join('\n'));
79 self._console.log(chalk.yellow('===== ' + self._errors.length + label + '\n'));
80 }
81 });
82};
83
84TemplateLinter.prototype.convertErrorToDisplayMessage = function(error) {
85 let message = error.rule + ': ' + error.message + ' (' + error.moduleId;
86
87 if (error.line && error.column) {
88 message = message + ' @ L' + error.line + ':C' + error.column;
89 }
90
91 message = message + ')';
92
93 if (error.source) {
94 message = message + ': \n`' + error.source + '`';
95 }
96
97 return message;
98};
99
100TemplateLinter.prototype.processString = function(contents, relativePath) {
101 let errors = this.linter.verify({
102 source: contents,
103 moduleId: relativePath.slice(0, -4)
104 });
105 errors = errors.filter(function(error) {
106 return error.severity > 1;
107 });
108 let passed = errors.length === 0;
109 let errorDisplay = errors.map(function(error) {
110 return this.convertErrorToDisplayMessage(error);
111 }, this)
112 .join('\n');
113
114 let output = '';
115 if (this._testGenerator) {
116 if (this.options.groupName) {
117 output = this._testGenerator.test(relativePath, passed,
118 `${relativePath} should pass TemplateLint.\n\n${errorDisplay}`);
119
120 } else {
121 output = [
122 this._testGenerator.suiteHeader(`TemplateLint | ${relativePath}`),
123 this._testGenerator.test('should pass TemplateLint', passed,
124 `${relativePath} should pass TemplateLint.\n\n${errorDisplay}`),
125 this._testGenerator.suiteFooter()
126 ].join('');
127 }
128 }
129
130 debug('Found %s errors for %s with \ncontents: \n%s\nerrors: \n%s', errors.length, relativePath, contents, errorDisplay);
131
132 return {
133 errors: errors,
134 output: output
135 };
136};
137
138TemplateLinter.prototype.postProcess = function(results) {
139 let errors = results.errors;
140
141 for (let i = 0; i < errors.length; i++) {
142 let errorDisplay = this.convertErrorToDisplayMessage(errors[i]);
143 this._errors.push(chalk.red(errorDisplay));
144 }
145
146 return results;
147};
148
149TemplateLinter.prototype.issueLocalizationWarningIfNeeded = function() {
150 if ('bare-strings' in this.linter.config.rules) {
151 return;
152 }
153
154 let project = this.options.project;
155 if (!project) {
156 return;
157 }
158
159 let addon = projectLocalizationAddon(project);
160
161 if (addon) {
162 this._console.log(chalk.yellow(
163 'The `bare-strings` rule must be configured when using a localization framework (`' + addon.name + '`). To prevent this warning, add the following to your `.template-lintrc.js`:\n\n rules: {\n \'bare-strings\': true\n }'
164 ));
165 }
166};
167
168TemplateLinter.create = function(inputNode, options) {
169 options = options || {};
170
171 if (!options.groupName) {
172 return new TemplateLinter(inputNode, options);
173 }
174
175 if (testGeneratorNames.indexOf(options.testGenerator) === -1) {
176 throw new Error(`The "groupName" options can only be used with a "testGenerator" option of: ${testGeneratorNames}`);
177 }
178
179 let testGenerator = testGenerators[options.testGenerator];
180
181 let headerName = 'TemplateLint';
182 if (options.groupName !== 'templates') {
183 headerName += ` | ${options.groupName}`;
184 }
185
186 let header = testGenerator.suiteHeader(headerName);
187 let footer = testGenerator.suiteFooter();
188
189 let lint = new TemplateLinter(inputNode, options);
190
191 return concat(lint, {
192 outputFile: `/${options.groupName}.template.lint-test.js`,
193 header,
194 inputFiles: ['**/*.template.lint-test.js'],
195 footer,
196 sourceMapConfig: { enabled: false },
197 allowNone: true
198 });
199};
200
201module.exports = TemplateLinter;