UNPKG

10.2 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const createStylelint = require('./createStylelint');
5const createStylelintResult = require('./createStylelintResult');
6const debug = require('debug')('stylelint:standalone');
7const FileCache = require('./utils/FileCache');
8const filterFilePaths = require('./utils/filterFilePaths');
9const formatters = require('./formatters');
10const fs = require('fs');
11const getFormatterOptionsText = require('./utils/getFormatterOptionsText');
12const globby = require('globby');
13const hash = require('./utils/hash');
14const invalidScopeDisables = require('./invalidScopeDisables');
15const needlessDisables = require('./needlessDisables');
16const NoFilesFoundError = require('./utils/noFilesFoundError');
17const path = require('path');
18const pkg = require('../package.json');
19const { default: ignore } = require('ignore');
20const DEFAULT_IGNORE_FILENAME = '.stylelintignore';
21const FILE_NOT_FOUND_ERROR_CODE = 'ENOENT';
22const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**'];
23const writeFileAtomic = require('write-file-atomic');
24
25/** @typedef {import('stylelint').StylelintStandaloneOptions} StylelintOptions */
26/** @typedef {import('stylelint').StylelintStandaloneReturnValue} StylelintStandaloneReturnValue */
27/** @typedef {import('stylelint').StylelintResult} StylelintResult */
28/** @typedef {import('stylelint').Formatter} Formatter */
29
30/**
31 * @param {StylelintOptions} options
32 * @returns {Promise<StylelintStandaloneReturnValue>}
33 */
34module.exports = function (options) {
35 const cacheLocation = options.cacheLocation;
36 const code = options.code;
37 const codeFilename = options.codeFilename;
38 const config = options.config;
39 const configBasedir = options.configBasedir;
40 const configFile = options.configFile;
41 const configOverrides = options.configOverrides;
42 const customSyntax = options.customSyntax;
43 const globbyOptions = options.globbyOptions;
44 const files = options.files;
45 const fix = options.fix;
46 const formatter = options.formatter;
47 const ignoreDisables = options.ignoreDisables;
48 const reportNeedlessDisables = options.reportNeedlessDisables;
49 const reportInvalidScopeDisables = options.reportInvalidScopeDisables;
50 const maxWarnings = options.maxWarnings;
51 const syntax = options.syntax;
52 const allowEmptyInput = options.allowEmptyInput || false;
53 const useCache = options.cache || false;
54 /** @type {FileCache} */
55 let fileCache;
56 const startTime = Date.now();
57
58 // The ignorer will be used to filter file paths after the glob is checked,
59 // before any files are actually read
60 const ignoreFilePath = options.ignorePath || DEFAULT_IGNORE_FILENAME;
61 const absoluteIgnoreFilePath = path.isAbsolute(ignoreFilePath)
62 ? ignoreFilePath
63 : path.resolve(process.cwd(), ignoreFilePath);
64 let ignoreText = '';
65
66 try {
67 ignoreText = fs.readFileSync(absoluteIgnoreFilePath, 'utf8');
68 } catch (readError) {
69 if (readError.code !== FILE_NOT_FOUND_ERROR_CODE) throw readError;
70 }
71
72 const ignorePattern = options.ignorePattern || [];
73 const ignorer = ignore().add(ignoreText).add(ignorePattern);
74
75 const isValidCode = typeof code === 'string';
76
77 if ((!files && !isValidCode) || (files && (code || isValidCode))) {
78 throw new Error('You must pass stylelint a `files` glob or a `code` string, though not both');
79 }
80
81 /** @type {Formatter} */
82 let formatterFunction;
83
84 if (typeof formatter === 'string') {
85 formatterFunction = formatters[formatter];
86
87 if (formatterFunction === undefined) {
88 return Promise.reject(
89 new Error(
90 `You must use a valid formatter option: ${getFormatterOptionsText()} or a function`,
91 ),
92 );
93 }
94 } else if (typeof formatter === 'function') {
95 formatterFunction = formatter;
96 } else {
97 formatterFunction = formatters.json;
98 }
99
100 const stylelint = createStylelint({
101 config,
102 configFile,
103 configBasedir,
104 configOverrides,
105 ignoreDisables,
106 ignorePath: ignoreFilePath,
107 reportNeedlessDisables,
108 reportInvalidScopeDisables,
109 syntax,
110 customSyntax,
111 fix,
112 });
113
114 if (!files) {
115 const absoluteCodeFilename =
116 codeFilename !== undefined && !path.isAbsolute(codeFilename)
117 ? path.join(process.cwd(), codeFilename)
118 : codeFilename;
119
120 // if file is ignored, return nothing
121 if (
122 absoluteCodeFilename &&
123 !filterFilePaths(ignorer, [path.relative(process.cwd(), absoluteCodeFilename)]).length
124 ) {
125 return Promise.resolve(prepareReturnValue([]));
126 }
127
128 return stylelint
129 ._lintSource({
130 code,
131 codeFilename: absoluteCodeFilename,
132 })
133 .then((postcssResult) => {
134 // Check for file existence
135 return new Promise((resolve, reject) => {
136 if (!absoluteCodeFilename) {
137 reject();
138
139 return;
140 }
141
142 fs.stat(absoluteCodeFilename, (err) => {
143 if (err) {
144 reject();
145 } else {
146 resolve();
147 }
148 });
149 })
150 .then(() => {
151 return stylelint._createStylelintResult(postcssResult, absoluteCodeFilename);
152 })
153 .catch(() => {
154 return stylelint._createStylelintResult(postcssResult);
155 });
156 })
157 .catch(_.partial(handleError, stylelint))
158 .then((stylelintResult) => {
159 const postcssResult = stylelintResult._postcssResult;
160 const returnValue = prepareReturnValue([stylelintResult]);
161
162 if (options.fix && postcssResult && !postcssResult.stylelint.ignored) {
163 if (!postcssResult.stylelint.disableWritingFix) {
164 // If we're fixing, the output should be the fixed code
165 returnValue.output = postcssResult.root.toString(postcssResult.opts.syntax);
166 } else {
167 // If the writing of the fix is disabled, the input code is returned as-is
168 returnValue.output = code;
169 }
170 }
171
172 return returnValue;
173 });
174 }
175
176 let fileList = files;
177
178 if (typeof fileList === 'string') {
179 fileList = [fileList];
180 }
181
182 if (!options.disableDefaultIgnores) {
183 fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`));
184 }
185
186 if (useCache) {
187 const stylelintVersion = pkg.version;
188 const hashOfConfig = hash(`${stylelintVersion}_${JSON.stringify(config || {})}`);
189
190 fileCache = new FileCache(cacheLocation, hashOfConfig);
191 } else {
192 // No need to calculate hash here, we just want to delete cache file.
193 fileCache = new FileCache(cacheLocation);
194 // Remove cache file if cache option is disabled
195 fileCache.destroy();
196 }
197
198 return globby(fileList, globbyOptions)
199 .then((filePaths) => {
200 // The ignorer filter needs to check paths relative to cwd
201 filePaths = filterFilePaths(
202 ignorer,
203 filePaths.map((p) => path.relative(process.cwd(), p)),
204 );
205
206 if (!filePaths.length) {
207 if (!allowEmptyInput) {
208 throw new NoFilesFoundError(fileList);
209 }
210
211 return Promise.all([]);
212 }
213
214 const cwd = _.get(globbyOptions, 'cwd', process.cwd());
215 let absoluteFilePaths = filePaths.map((filePath) => {
216 const absoluteFilepath = !path.isAbsolute(filePath)
217 ? path.join(cwd, filePath)
218 : path.normalize(filePath);
219
220 return absoluteFilepath;
221 });
222
223 if (useCache) {
224 absoluteFilePaths = absoluteFilePaths.filter(fileCache.hasFileChanged.bind(fileCache));
225 }
226
227 const getStylelintResults = absoluteFilePaths.map((absoluteFilepath) => {
228 debug(`Processing ${absoluteFilepath}`);
229
230 return stylelint
231 ._lintSource({
232 filePath: absoluteFilepath,
233 })
234 .then((postcssResult) => {
235 if (postcssResult.stylelint.stylelintError && useCache) {
236 debug(`${absoluteFilepath} contains linting errors and will not be cached.`);
237 fileCache.removeEntry(absoluteFilepath);
238 }
239
240 /**
241 * If we're fixing, save the file with changed code
242 * @type {Promise<Error | void>}
243 */
244 let fixFile = Promise.resolve();
245
246 if (
247 postcssResult.root &&
248 postcssResult.opts &&
249 !postcssResult.stylelint.ignored &&
250 options.fix &&
251 !postcssResult.stylelint.disableWritingFix
252 ) {
253 // @ts-ignore TODO TYPES toString accepts 0 arguments
254 const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax);
255
256 if (
257 postcssResult.root &&
258 postcssResult.root.source &&
259 // @ts-ignore TODO TYPES css is unknown property
260 postcssResult.root.source.input.css !== fixedCss
261 ) {
262 fixFile = writeFileAtomic(absoluteFilepath, fixedCss);
263 }
264 }
265
266 return fixFile.then(() =>
267 stylelint._createStylelintResult(postcssResult, absoluteFilepath),
268 );
269 })
270 .catch((error) => {
271 // On any error, we should not cache the lint result
272 fileCache.removeEntry(absoluteFilepath);
273
274 return handleError(stylelint, error, absoluteFilepath);
275 });
276 });
277
278 return Promise.all(getStylelintResults);
279 })
280 .then((stylelintResults) => {
281 if (useCache) {
282 fileCache.reconcile();
283 }
284
285 return prepareReturnValue(stylelintResults);
286 });
287
288 /**
289 * @param {StylelintResult[]} stylelintResults
290 * @returns {StylelintStandaloneReturnValue}
291 */
292 function prepareReturnValue(stylelintResults) {
293 const errored = stylelintResults.some(
294 (result) => result.errored || result.parseErrors.length > 0,
295 );
296
297 /** @type {StylelintStandaloneReturnValue} */
298 const returnValue = {
299 errored,
300 results: [],
301 output: '',
302 };
303
304 if (reportNeedlessDisables) {
305 returnValue.needlessDisables = needlessDisables(stylelintResults);
306 }
307
308 if (reportInvalidScopeDisables) {
309 returnValue.invalidScopeDisables = invalidScopeDisables(stylelintResults);
310 }
311
312 if (maxWarnings !== undefined) {
313 const foundWarnings = stylelintResults.reduce((count, file) => {
314 return count + file.warnings.length;
315 }, 0);
316
317 if (foundWarnings > maxWarnings) {
318 returnValue.maxWarningsExceeded = { maxWarnings, foundWarnings };
319 }
320 }
321
322 returnValue.output = formatterFunction(stylelintResults, returnValue);
323 returnValue.results = stylelintResults;
324
325 debug(`Linting complete in ${Date.now() - startTime}ms`);
326
327 return returnValue;
328 }
329};
330
331/**
332 * @param {import('stylelint').StylelintInternalApi} stylelint
333 * @param {any} error
334 * @param {string} [filePath]
335 * @return {Promise<StylelintResult>}
336 */
337function handleError(stylelint, error, filePath = undefined) {
338 if (error.name === 'CssSyntaxError') {
339 return createStylelintResult(stylelint, undefined, filePath, error);
340 }
341
342 throw error;
343}