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