stylelint-webpack-plugin 1.2.3 → 2.2.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/dist/index.js CHANGED
@@ -7,9 +7,13 @@ exports.default = void 0;
7
7
 
8
8
  var _path = require("path");
9
9
 
10
- var _getOptions = _interopRequireDefault(require("./getOptions"));
10
+ var _arrify = _interopRequireDefault(require("arrify"));
11
11
 
12
- var _LintDirtyModulesPlugin = _interopRequireDefault(require("./LintDirtyModulesPlugin"));
12
+ var _fastGlob = _interopRequireDefault(require("fast-glob"));
13
+
14
+ var _micromatch = require("micromatch");
15
+
16
+ var _options = require("./options");
13
17
 
14
18
  var _linter = _interopRequireDefault(require("./linter"));
15
19
 
@@ -17,54 +21,279 @@ var _utils = require("./utils");
17
21
 
18
22
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
19
23
 
24
+ // @ts-ignore
25
+ // @ts-ignore
26
+
27
+ /** @typedef {import('webpack').Compiler} Compiler */
28
+
29
+ /** @typedef {import('webpack').Module} Module */
30
+
31
+ /** @typedef {import('./options').Options} Options */
32
+
33
+ /** @typedef {Partial<{timestamp:number} | number>} FileSystemInfoEntry */
34
+ const STYLELINT_PLUGIN = 'StylelintWebpackPlugin';
35
+ let counter = 0;
36
+
20
37
  class StylelintWebpackPlugin {
38
+ /**
39
+ * @param {Options} options
40
+ */
21
41
  constructor(options = {}) {
22
- this.options = (0, _getOptions.default)(options);
42
+ this.key = STYLELINT_PLUGIN;
43
+ this.options = (0, _options.getOptions)(options);
44
+ this.run = this.run.bind(this);
45
+ this.startTime = Date.now();
46
+ this.prevTimestamps = new Map();
23
47
  }
48
+ /**
49
+ * @param {Compiler} compiler
50
+ * @returns {void}
51
+ */
52
+
24
53
 
25
54
  apply(compiler) {
26
- const options = { ...this.options,
27
- files: (0, _utils.parseFiles)(this.options.files, this.getContext(compiler))
28
- }; // eslint-disable-next-line
55
+ // Generate key for each compilation,
56
+ // this differentiates one from the other when being cached.
57
+ this.key = compiler.name || `${this.key}_${counter += 1}`; // If `lintDirtyModulesOnly` is disabled,
58
+ // execute the linter on the build
59
+
60
+ if (!this.options.lintDirtyModulesOnly) {
61
+ compiler.hooks.run.tapPromise(this.key, this.run);
62
+ }
29
63
 
30
- const {
31
- lint
32
- } = require(options.stylelintPath);
64
+ let isFirstRun = this.options.lintDirtyModulesOnly;
65
+ compiler.hooks.watchRun.tapPromise(this.key, c => {
66
+ if (isFirstRun) {
67
+ isFirstRun = false;
68
+ return Promise.resolve();
69
+ }
33
70
 
34
- const plugin = {
35
- name: this.constructor.name
36
- };
71
+ return this.run(c);
72
+ });
73
+ }
74
+ /**
75
+ * @param {Compiler} compiler
76
+ */
77
+
78
+
79
+ async run(compiler) {
80
+ // Do not re-hook
37
81
 
38
- if (options.lintDirtyModulesOnly) {
39
- const lintDirty = new _LintDirtyModulesPlugin.default(lint, compiler, options);
40
- /* istanbul ignore next */
41
-
42
- compiler.hooks.watchRun.tapAsync(plugin, (compilation, callback) => {
43
- lintDirty.apply(compilation, callback);
44
- });
45
- } else {
46
- compiler.hooks.run.tapAsync(plugin, (compilation, callback) => {
47
- (0, _linter.default)(lint, options, compilation, callback);
48
- });
49
- /* istanbul ignore next */
50
-
51
- compiler.hooks.watchRun.tapAsync(plugin, (compilation, callback) => {
52
- (0, _linter.default)(lint, options, compilation, callback);
53
- });
82
+ /* istanbul ignore if */
83
+ if ( // @ts-ignore
84
+ compiler.hooks.thisCompilation.taps.find(({
85
+ name
86
+ }) => name === this.key)) {
87
+ return;
54
88
  }
89
+
90
+ const context = this.getContext(compiler);
91
+ const options = { ...this.options,
92
+ exclude: (0, _utils.parseFiles)(this.options.exclude || [], context),
93
+ extensions: (0, _arrify.default)(this.options.extensions),
94
+ files: (0, _utils.parseFiles)(this.options.files || '', context)
95
+ };
96
+ const wanted = (0, _utils.parseFoldersToGlobs)(options.files, options.extensions);
97
+ const exclude = (0, _utils.parseFoldersToGlobs)(this.options.exclude ? options.exclude : '**/node_modules/**', []);
98
+ compiler.hooks.thisCompilation.tap(this.key, compilation => {
99
+ /** @type {import('./linter').Linter} */
100
+ let lint;
101
+ /** @type {import('./linter').Reporter} */
102
+
103
+ let report;
104
+ /** @type number */
105
+
106
+ let threads;
107
+
108
+ try {
109
+ ({
110
+ lint,
111
+ report,
112
+ threads
113
+ } = (0, _linter.default)(this.key, options, compilation));
114
+ } catch (e) {
115
+ compilation.errors.push(e);
116
+ return;
117
+ }
118
+ /** @type {string[]} */
119
+
120
+
121
+ const files = []; // Add the file to be linted
122
+
123
+ compilation.hooks.succeedModule.tap(this.key, module => {
124
+ const filteredFiles = this.getFiles(wanted, compiler, module).filter(file => !files.includes(file) && (0, _micromatch.isMatch)(file, wanted, {
125
+ dot: true
126
+ }) && !(0, _micromatch.isMatch)(file, exclude, {
127
+ dot: true
128
+ }));
129
+
130
+ for (const file of filteredFiles) {
131
+ files.push(file);
132
+
133
+ if (threads > 1) {
134
+ lint((0, _utils.parseFiles)(file, context));
135
+ }
136
+ }
137
+ }); // Lint all files added
138
+
139
+ compilation.hooks.finishModules.tap(this.key, () => {
140
+ if (files.length > 0 && threads <= 1) {
141
+ lint((0, _utils.parseFiles)(files, context));
142
+ }
143
+ }); // await and interpret results
144
+
145
+ compilation.hooks.additionalAssets.tapPromise(this.key, processResults);
146
+
147
+ async function processResults() {
148
+ const {
149
+ errors,
150
+ warnings,
151
+ generateReportAsset
152
+ } = await report();
153
+
154
+ if (warnings && !options.failOnWarning) {
155
+ // @ts-ignore
156
+ compilation.warnings.push(warnings);
157
+ } else if (warnings && options.failOnWarning) {
158
+ // @ts-ignore
159
+ compilation.errors.push(warnings);
160
+ }
161
+
162
+ if (errors && options.failOnError) {
163
+ // @ts-ignore
164
+ compilation.errors.push(errors);
165
+ } else if (errors && !options.failOnError) {
166
+ // @ts-ignore
167
+ compilation.warnings.push(errors);
168
+ }
169
+
170
+ if (generateReportAsset) {
171
+ await generateReportAsset(compilation);
172
+ }
173
+ }
174
+ });
55
175
  }
176
+ /**
177
+ *
178
+ * @param {Compiler} compiler
179
+ * @returns {string}
180
+ */
181
+
56
182
 
57
183
  getContext(compiler) {
58
184
  if (!this.options.context) {
59
- return compiler.options.context;
185
+ return String(compiler.options.context);
60
186
  }
61
187
 
62
188
  if (!(0, _path.isAbsolute)(this.options.context)) {
63
- return (0, _path.join)(compiler.options.context, this.options.context);
189
+ return (0, _path.join)(String(compiler.options.context), this.options.context);
64
190
  }
65
191
 
66
192
  return this.options.context;
67
193
  }
194
+ /**
195
+ * @param {string[]} glob
196
+ * @param {Compiler} compiler
197
+ * @param {Module} module
198
+ * @returns {string[]}
199
+ */
200
+ // eslint-disable-next-line no-unused-vars
201
+
202
+
203
+ getFiles(glob, compiler, module) {
204
+ // TODO: how to get module dependencies on css files?
205
+ // maybe implemented on next major version v3
206
+ // Temporaly lint all css files on start webpack
207
+ // on watch lint only modified files
208
+ // on webpack 5 not safe to use `module.buildInfo.snapshot`
209
+ // on webpack `module.buildInfo.fileDependencies` not working correclty
210
+ // webpack 5
211
+
212
+ /*
213
+ if (
214
+ module.buildInfo &&
215
+ module.buildInfo.snapshot &&
216
+ module.buildInfo.snapshot.fileTimestamps
217
+ ) {
218
+ files = this.getChangedFiles(module.buildInfo.snapshot.fileTimestamps);
219
+ }
220
+ // webpack 4
221
+ else if (module.buildInfo && module.buildInfo.fileDependencies) {
222
+ files = Array.from(module.buildInfo.fileDependencies);
223
+ if (compiler.fileTimestamps && compiler.fileTimestamps.size > 0) {
224
+ const fileDependencies = new Map();
225
+ for (const [filename, timestamp] of compiler.fileTimestamps.entries()) {
226
+ if (files.includes(filename)) {
227
+ fileDependencies.set(filename, timestamp);
228
+ }
229
+ }
230
+ files = this.getChangedFiles(fileDependencies);
231
+ }
232
+ }
233
+ */
234
+ // webpack 5
235
+ if (compiler.modifiedFiles) {
236
+ return Array.from(compiler.modifiedFiles);
237
+ } // webpack 4
238
+
239
+ /* istanbul ignore next */
240
+
241
+
242
+ if (compiler.fileTimestamps && compiler.fileTimestamps.size > 0) {
243
+ return this.getChangedFiles(compiler.fileTimestamps);
244
+ }
245
+
246
+ return _fastGlob.default.sync(glob, {
247
+ dot: true
248
+ });
249
+ }
250
+ /**
251
+ * @param {Map<string, null | FileSystemInfoEntry | "ignore">} fileTimestamps
252
+ * @returns {string[]}
253
+ */
254
+
255
+ /* istanbul ignore next */
256
+
257
+
258
+ getChangedFiles(fileTimestamps) {
259
+ /**
260
+ * @param {null | FileSystemInfoEntry | "ignore"} fileSystemInfoEntry
261
+ * @returns {Partial<number>}
262
+ */
263
+ const getTimestamps = fileSystemInfoEntry => {
264
+ // @ts-ignore
265
+ if (fileSystemInfoEntry && fileSystemInfoEntry.timestamp) {
266
+ // @ts-ignore
267
+ return fileSystemInfoEntry.timestamp;
268
+ } // @ts-ignore
269
+
270
+
271
+ return fileSystemInfoEntry;
272
+ };
273
+ /**
274
+ * @param {string} filename
275
+ * @param {null | FileSystemInfoEntry | "ignore"} fileSystemInfoEntry
276
+ * @returns {boolean}
277
+ */
278
+
279
+
280
+ const hasFileChanged = (filename, fileSystemInfoEntry) => {
281
+ const prevTimestamp = getTimestamps(this.prevTimestamps.get(filename));
282
+ const timestamp = getTimestamps(fileSystemInfoEntry);
283
+ return (prevTimestamp || this.startTime) < (timestamp || Infinity);
284
+ };
285
+
286
+ const changedFiles = [];
287
+
288
+ for (const [filename, timestamp] of fileTimestamps.entries()) {
289
+ if (hasFileChanged(filename, timestamp)) {
290
+ changedFiles.push(filename);
291
+ }
292
+ }
293
+
294
+ this.prevTimestamps = fileTimestamps;
295
+ return changedFiles;
296
+ }
68
297
 
69
298
  }
70
299
 
package/dist/linter.js CHANGED
@@ -5,65 +5,195 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = linter;
7
7
 
8
+ var _path = require("path");
9
+
10
+ var _arrify = _interopRequireDefault(require("arrify"));
11
+
8
12
  var _StylelintError = _interopRequireDefault(require("./StylelintError"));
9
13
 
14
+ var _getStylelint = _interopRequireDefault(require("./getStylelint"));
15
+
10
16
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
17
 
12
- function linter(lint, options, compiler, callback) {
13
- let errors = [];
14
- let warnings = [];
15
- lint(options).then(({
16
- results
17
- }) => {
18
+ // @ts-ignore
19
+
20
+ /** @typedef {import('stylelint')} Stylelint */
21
+
22
+ /** @typedef {import('stylelint').LintResult} LintResult */
23
+
24
+ /** @typedef {import('webpack').Compiler} Compiler */
25
+
26
+ /** @typedef {import('webpack').Compilation} Compilation */
27
+
28
+ /** @typedef {import('./options').Options} Options */
29
+
30
+ /** @typedef {import('./options').FormatterType} FormatterType */
31
+
32
+ /** @typedef {((results: LintResult[]) => string)} FormatterFunction */
33
+
34
+ /** @typedef {(compilation: Compilation) => Promise<void>} GenerateReport */
35
+
36
+ /** @typedef {{errors?: StylelintError, warnings?: StylelintError, generateReportAsset?: GenerateReport}} Report */
37
+
38
+ /** @typedef {() => Promise<Report>} Reporter */
39
+
40
+ /** @typedef {(files: string|string[]) => void} Linter */
41
+
42
+ /** @typedef {{[files: string]: LintResult}} LintResultMap */
43
+
44
+ /** @type {WeakMap<Compiler, LintResultMap>} */
45
+ const resultStorage = new WeakMap();
46
+ /**
47
+ * @param {string|undefined} key
48
+ * @param {Options} options
49
+ * @param {Compilation} compilation
50
+ * @returns {{lint: Linter, report: Reporter, threads: number}}
51
+ */
52
+
53
+ function linter(key, options, compilation) {
54
+ /** @type {Stylelint} */
55
+ let stylelint;
56
+ /** @type {(files: string|string[]) => Promise<LintResult[]>} */
57
+
58
+ let lintFiles;
59
+ /** @type {() => Promise<void>} */
60
+
61
+ let cleanup;
62
+ /** @type number */
63
+
64
+ let threads;
65
+ /** @type {Promise<LintResult[]>[]} */
66
+
67
+ const rawResults = [];
68
+ const crossRunResultStorage = getResultStorage(compilation);
69
+
70
+ try {
18
71
  ({
72
+ stylelint,
73
+ lintFiles,
74
+ cleanup,
75
+ threads
76
+ } = (0, _getStylelint.default)(key, options));
77
+ } catch (e) {
78
+ throw new _StylelintError.default(e.message);
79
+ }
80
+
81
+ return {
82
+ lint,
83
+ report,
84
+ threads
85
+ };
86
+ /**
87
+ * @param {string | string[]} files
88
+ */
89
+
90
+ function lint(files) {
91
+ for (const file of (0, _arrify.default)(files)) {
92
+ delete crossRunResultStorage[file];
93
+ }
94
+
95
+ rawResults.push(lintFiles(files).catch(e => {
96
+ compilation.errors.push(e);
97
+ return [];
98
+ }));
99
+ }
100
+
101
+ async function report() {
102
+ // Filter out ignored files.
103
+ let results = removeIgnoredWarnings( // Get the current results, resetting the rawResults to empty
104
+ await flatten(rawResults.splice(0, rawResults.length)));
105
+ await cleanup();
106
+
107
+ for (const result of results) {
108
+ crossRunResultStorage[result.source] = result;
109
+ }
110
+
111
+ results = Object.values(crossRunResultStorage); // do not analyze if there are no results or stylelint config
112
+
113
+ if (!results || results.length < 1) {
114
+ return {};
115
+ }
116
+
117
+ const formatter = loadFormatter(stylelint, options.formatter);
118
+ const {
19
119
  errors,
20
120
  warnings
21
- } = parseResults(options, results));
22
- compiler.hooks.afterEmit.tapAsync('StylelintWebpackPlugin', (compilation, next) => {
23
- if (warnings.length) {
24
- compilation.warnings.push(_StylelintError.default.format(options, warnings));
25
- warnings = [];
26
- }
121
+ } = formatResults(formatter, parseResults(options, results));
122
+ return {
123
+ errors,
124
+ warnings,
125
+ generateReportAsset
126
+ };
127
+ /**
128
+ * @param {Compilation} compilation
129
+ * @returns {Promise<void>}
130
+ */
131
+
132
+ async function generateReportAsset({
133
+ compiler
134
+ }) {
135
+ const {
136
+ outputReport
137
+ } = options;
138
+ /**
139
+ * @param {string} name
140
+ * @param {string | Buffer} content
141
+ */
27
142
 
28
- if (errors.length) {
29
- compilation.errors.push(_StylelintError.default.format(options, errors));
30
- errors = [];
143
+ const save = (name, content) =>
144
+ /** @type {Promise<void>} */
145
+ new Promise((finish, bail) => {
146
+ const {
147
+ mkdir,
148
+ writeFile
149
+ } = compiler.outputFileSystem; // ensure directory exists
150
+ // @ts-ignore - the types for `outputFileSystem` are missing the 3 arg overload
151
+
152
+ mkdir((0, _path.dirname)(name), {
153
+ recursive: true
154
+ }, err => {
155
+ /* istanbul ignore if */
156
+ if (err) bail(err);else writeFile(name, content, err2 => {
157
+ /* istanbul ignore if */
158
+ if (err2) bail(err2);else finish();
159
+ });
160
+ });
161
+ });
162
+
163
+ if (!outputReport || !outputReport.filePath) {
164
+ return;
31
165
  }
32
166
 
33
- next();
34
- });
167
+ const content = outputReport.formatter ? loadFormatter(stylelint, outputReport.formatter)(results) : formatter(results);
168
+ let {
169
+ filePath
170
+ } = outputReport;
35
171
 
36
- if (options.failOnError && errors.length) {
37
- callback(_StylelintError.default.format(options, errors));
38
- } else if (options.failOnWarning && warnings.length) {
39
- callback(_StylelintError.default.format(options, warnings));
40
- } else {
41
- callback();
172
+ if (!(0, _path.isAbsolute)(filePath)) {
173
+ filePath = (0, _path.join)(compiler.outputPath, filePath);
174
+ }
175
+
176
+ await save(filePath, content);
42
177
  }
43
- }).catch(e => {
44
- compiler.hooks.afterEmit.tapAsync('StylelintWebpackPlugin', (compilation, next) => {
45
- compilation.errors.push(new _StylelintError.default(e.message));
46
- next();
47
- });
48
- callback();
49
- });
178
+ }
50
179
  }
180
+ /**
181
+ * @param {FormatterFunction} formatter
182
+ * @param {{ errors: LintResult[]; warnings: LintResult[]; }} results
183
+ * @returns {{errors?: StylelintError, warnings?: StylelintError}}
184
+ */
51
185
 
52
- function parseResults(options, results) {
53
- let errors = [];
54
- let warnings = [];
55
-
56
- if (options.emitError) {
57
- errors = results.filter(file => fileHasErrors(file) || fileHasWarnings(file));
58
- } else if (options.emitWarning) {
59
- warnings = results.filter(file => fileHasErrors(file) || fileHasWarnings(file));
60
- } else {
61
- warnings = results.filter(file => !fileHasErrors(file) && fileHasWarnings(file));
62
- errors = results.filter(fileHasErrors);
186
+
187
+ function formatResults(formatter, results) {
188
+ let errors;
189
+ let warnings;
190
+
191
+ if (results.warnings.length > 0) {
192
+ warnings = new _StylelintError.default(formatter(results.warnings));
63
193
  }
64
194
 
65
- if (options.quiet && warnings.length) {
66
- warnings = [];
195
+ if (results.errors.length > 0) {
196
+ errors = new _StylelintError.default(formatter(results.errors));
67
197
  }
68
198
 
69
199
  return {
@@ -71,11 +201,122 @@ function parseResults(options, results) {
71
201
  warnings
72
202
  };
73
203
  }
204
+ /**
205
+ * @param {Options} options
206
+ * @param {LintResult[]} results
207
+ * @returns {{errors: LintResult[], warnings: LintResult[]}}
208
+ */
209
+
210
+
211
+ function parseResults(options, results) {
212
+ /** @type {LintResult[]} */
213
+ const errors = [];
214
+ /** @type {LintResult[]} */
215
+
216
+ const warnings = [];
217
+ results.forEach(file => {
218
+ if (fileHasErrors(file)) {
219
+ const messages = file.warnings.filter(message => options.emitError && message.severity === 'error');
220
+
221
+ if (messages.length > 0) {
222
+ errors.push({ ...file,
223
+ warnings: messages
224
+ });
225
+ }
226
+ }
227
+
228
+ if (fileHasWarnings(file)) {
229
+ const messages = file.warnings.filter(message => options.emitWarning && message.severity === 'warning');
230
+
231
+ if (messages.length > 0) {
232
+ warnings.push({ ...file,
233
+ warnings: messages
234
+ });
235
+ }
236
+ }
237
+ });
238
+ return {
239
+ errors,
240
+ warnings
241
+ };
242
+ }
243
+ /**
244
+ * @param {LintResult} file
245
+ * @returns {boolean}
246
+ */
247
+
74
248
 
75
249
  function fileHasErrors(file) {
76
- return file.errored;
250
+ return !!file.errored;
77
251
  }
252
+ /**
253
+ * @param {LintResult} file
254
+ * @returns {boolean}
255
+ */
256
+
78
257
 
79
258
  function fileHasWarnings(file) {
80
- return file.warnings && file.warnings.length;
259
+ return file.warnings && file.warnings.length > 0;
260
+ }
261
+ /**
262
+ * @param {Stylelint} stylelint
263
+ * @param {FormatterType=} formatter
264
+ * @returns {FormatterFunction}
265
+ */
266
+
267
+
268
+ function loadFormatter(stylelint, formatter) {
269
+ if (typeof formatter === 'function') {
270
+ return formatter;
271
+ }
272
+
273
+ if (typeof formatter === 'string') {
274
+ try {
275
+ return stylelint.formatters[formatter];
276
+ } catch (_) {// Load the default formatter.
277
+ }
278
+ }
279
+
280
+ return stylelint.formatters.string;
281
+ }
282
+ /**
283
+ * @param {LintResult[]} results
284
+ * @returns {LintResult[]}
285
+ */
286
+
287
+
288
+ function removeIgnoredWarnings(results) {
289
+ return results.filter(result => !result.ignored);
290
+ }
291
+ /**
292
+ * @param {Promise<LintResult[]>[]} results
293
+ * @returns {Promise<LintResult[]>}
294
+ */
295
+
296
+
297
+ async function flatten(results) {
298
+ /**
299
+ * @param {LintResult[]} acc
300
+ * @param {LintResult[]} list
301
+ */
302
+ const flat = (acc, list) => [...acc, ...list];
303
+
304
+ return (await Promise.all(results)).reduce(flat, []);
305
+ }
306
+ /**
307
+ * @param {Compilation} compilation
308
+ * @returns {LintResultMap}
309
+ */
310
+
311
+
312
+ function getResultStorage({
313
+ compiler
314
+ }) {
315
+ let storage = resultStorage.get(compiler);
316
+
317
+ if (!storage) {
318
+ resultStorage.set(compiler, storage = {});
319
+ }
320
+
321
+ return storage;
81
322
  }