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 if (globby.hasMagic(entry)) {
176 const cwd = _.get(globbyOptions, 'cwd', process.cwd());
177 const absolutePath = !path.isAbsolute(entry) ? path.join(cwd, entry) : path.normalize(entry);
178
179 if (fs.existsSync(absolutePath)) {
180 // This glob-like path points to a file. Return an escaped path to avoid globbing
181 return fastGlob.escapePath(entry);
182 }
183 }
184
185 return entry;
186 });
187
188 if (!options.disableDefaultIgnores) {
189 fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`));
190 }
191
192 if (useCache) {
193 const stylelintVersion = pkg.version;
194 const hashOfConfig = hash(`${stylelintVersion}_${JSON.stringify(config || {})}`);
195
196 fileCache = new FileCache(cacheLocation, hashOfConfig);
197 } else {
198 // No need to calculate hash here, we just want to delete cache file.
199 fileCache = new FileCache(cacheLocation);
200 // Remove cache file if cache option is disabled
201 fileCache.destroy();
202 }
203
204 return globby(fileList, globbyOptions)
205 .then((filePaths) => {
206 // The ignorer filter needs to check paths relative to cwd
207 filePaths = filterFilePaths(
208 ignorer,
209 filePaths.map((p) => path.relative(process.cwd(), p)),
210 );
211
212 if (!filePaths.length) {
213 if (!allowEmptyInput) {
214 throw new NoFilesFoundError(fileList);
215 }
216
217 return Promise.all([]);
218 }
219
220 const cwd = _.get(globbyOptions, 'cwd', process.cwd());
221 let absoluteFilePaths = filePaths.map((filePath) => {
222 const absoluteFilepath = !path.isAbsolute(filePath)
223 ? path.join(cwd, filePath)
224 : path.normalize(filePath);
225
226 return absoluteFilepath;
227 });
228
229 if (useCache) {
230 absoluteFilePaths = absoluteFilePaths.filter(fileCache.hasFileChanged.bind(fileCache));
231 }
232
233 const getStylelintResults = absoluteFilePaths.map((absoluteFilepath) => {
234 debug(`Processing ${absoluteFilepath}`);
235
236 return stylelint
237 ._lintSource({
238 filePath: absoluteFilepath,
239 })
240 .then((postcssResult) => {
241 if (postcssResult.stylelint.stylelintError && useCache) {
242 debug(`${absoluteFilepath} contains linting errors and will not be cached.`);
243 fileCache.removeEntry(absoluteFilepath);
244 }
245
246 /**
247 * If we're fixing, save the file with changed code
248 * @type {Promise<Error | void>}
249 */
250 let fixFile = Promise.resolve();
251
252 if (
253 postcssResult.root &&
254 postcssResult.opts &&
255 !postcssResult.stylelint.ignored &&
256 options.fix &&
257 !postcssResult.stylelint.disableWritingFix
258 ) {
259 // @ts-ignore TODO TYPES toString accepts 0 arguments
260 const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax);
261
262 if (
263 postcssResult.root &&
264 postcssResult.root.source &&
265 // @ts-ignore TODO TYPES css is unknown property
266 postcssResult.root.source.input.css !== fixedCss
267 ) {
268 fixFile = writeFileAtomic(absoluteFilepath, fixedCss);
269 }
270 }
271
272 return fixFile.then(() =>
273 stylelint._createStylelintResult(postcssResult, absoluteFilepath),
274 );
275 })
276 .catch((error) => {
277 // On any error, we should not cache the lint result
278 fileCache.removeEntry(absoluteFilepath);
279
280 return handleError(stylelint, error, absoluteFilepath);
281 });
282 });
283
284 return Promise.all(getStylelintResults);
285 })
286 .then((stylelintResults) => {
287 if (useCache) {
288 fileCache.reconcile();
289 }
290
291 const rtn = prepareReturnValue(stylelintResults, options, formatterFunction);
292
293 debug(`Linting complete in ${Date.now() - startTime}ms`);
294
295 return rtn;
296 });
297};
298
299/**
300 * @param {FormatterIdentifier | undefined} selected
301 * @returns {Formatter}
302 */
303function getFormatterFunction(selected) {
304 /** @type {Formatter} */
305 let formatterFunction;
306
307 if (typeof selected === 'string') {
308 formatterFunction = formatters[selected];
309
310 if (formatterFunction === undefined) {
311 throw new Error(
312 `You must use a valid formatter option: ${getFormatterOptionsText()} or a function`,
313 );
314 }
315 } else if (typeof selected === 'function') {
316 formatterFunction = selected;
317 } else {
318 formatterFunction = formatters.json;
319 }
320
321 return formatterFunction;
322}
323
324/**
325 * @param {import('stylelint').StylelintInternalApi} stylelint
326 * @param {any} error
327 * @param {string} [filePath]
328 * @return {Promise<StylelintResult>}
329 */
330function handleError(stylelint, error, filePath = undefined) {
331 if (error.name === 'CssSyntaxError') {
332 return createStylelintResult(stylelint, undefined, filePath, error);
333 }
334
335 throw error;
336}