UNPKG

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