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