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