blob: 10371313c4af312c65a3325c6fdbc33e50a5bcd4 [file] [log] [blame]
Mathias Bynens79e2cf02020-05-29 16:46:17 +02001'use strict';
2
Mathias Bynens79e2cf02020-05-29 16:46:17 +02003const createStylelint = require('./createStylelint');
4const createStylelintResult = require('./createStylelintResult');
5const debug = require('debug')('stylelint:standalone');
Tim van der Lippeefb716a2020-12-01 12:54:04 +00006const fastGlob = require('fast-glob');
Mathias Bynens79e2cf02020-05-29 16:46:17 +02007const 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');
Tim van der Lippe16b82282021-11-08 13:50:26 +000014const isPathNotFoundError = require('./utils/isPathNotFoundError');
Mathias Bynens79e2cf02020-05-29 16:46:17 +020015const NoFilesFoundError = require('./utils/noFilesFoundError');
Tim van der Lippe16b82282021-11-08 13:50:26 +000016const normalizePath = require('normalize-path');
Mathias Bynens79e2cf02020-05-29 16:46:17 +020017const path = require('path');
18const pkg = require('../package.json');
Tim van der Lippeefb716a2020-12-01 12:54:04 +000019const prepareReturnValue = require('./prepareReturnValue');
Mathias Bynens79e2cf02020-05-29 16:46:17 +020020const { default: ignore } = require('ignore');
Tim van der Lippe16b82282021-11-08 13:50:26 +000021
Mathias Bynens79e2cf02020-05-29 16:46:17 +020022const DEFAULT_IGNORE_FILENAME = '.stylelintignore';
Mathias Bynens79e2cf02020-05-29 16:46:17 +020023const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**'];
24const writeFileAtomic = require('write-file-atomic');
25
Tim van der Lippe16b82282021-11-08 13:50:26 +000026/** @typedef {import('stylelint').LinterOptions} LinterOptions */
27/** @typedef {import('stylelint').LinterResult} LinterResult */
28/** @typedef {import('stylelint').LintResult} StylelintResult */
Tim van der Lippeefb716a2020-12-01 12:54:04 +000029/** @typedef {import('stylelint').Formatter} Formatter */
Tim van der Lippe16b82282021-11-08 13:50:26 +000030/** @typedef {import('stylelint').FormatterType} FormatterType */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020031
32/**
Tim van der Lippe16b82282021-11-08 13:50:26 +000033 *
34 * @param {LinterOptions} options
35 * @returns {Promise<LinterResult>}
Mathias Bynens79e2cf02020-05-29 16:46:17 +020036 */
Tim van der Lippe16b82282021-11-08 13:50:26 +000037async function standalone({
38 allowEmptyInput = false,
39 cache: useCache = false,
40 cacheLocation,
41 code,
42 codeFilename,
43 config,
44 configBasedir,
45 configFile,
46 customSyntax,
47 disableDefaultIgnores,
48 files,
49 fix,
50 formatter,
51 globbyOptions,
52 ignoreDisables,
53 ignorePath = DEFAULT_IGNORE_FILENAME,
54 ignorePattern = [],
55 maxWarnings,
56 quiet,
57 reportDescriptionlessDisables,
58 reportInvalidScopeDisables,
59 reportNeedlessDisables,
60 syntax,
61}) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +020062 /** @type {FileCache} */
63 let fileCache;
64 const startTime = Date.now();
65
Tim van der Lippe16b82282021-11-08 13:50:26 +000066 const isValidCode = typeof code === 'string';
67
68 if ((!files && !isValidCode) || (files && (code || isValidCode))) {
69 return Promise.reject(
70 new Error('You must pass stylelint a `files` glob or a `code` string, though not both'),
71 );
72 }
73
Mathias Bynens79e2cf02020-05-29 16:46:17 +020074 // The ignorer will be used to filter file paths after the glob is checked,
75 // before any files are actually read
Tim van der Lippe16b82282021-11-08 13:50:26 +000076 const absoluteIgnoreFilePath = path.isAbsolute(ignorePath)
77 ? ignorePath
78 : path.resolve(process.cwd(), ignorePath);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020079 let ignoreText = '';
80
81 try {
82 ignoreText = fs.readFileSync(absoluteIgnoreFilePath, 'utf8');
83 } catch (readError) {
Tim van der Lippe16b82282021-11-08 13:50:26 +000084 if (!isPathNotFoundError(readError)) {
85 return Promise.reject(readError);
86 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +020087 }
88
Mathias Bynens79e2cf02020-05-29 16:46:17 +020089 const ignorer = ignore().add(ignoreText).add(ignorePattern);
90
Tim van der Lippeefb716a2020-12-01 12:54:04 +000091 /** @type {Formatter} */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020092 let formatterFunction;
93
Tim van der Lippeefb716a2020-12-01 12:54:04 +000094 try {
95 formatterFunction = getFormatterFunction(formatter);
96 } catch (error) {
97 return Promise.reject(error);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020098 }
99
100 const stylelint = createStylelint({
101 config,
102 configFile,
103 configBasedir,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200104 ignoreDisables,
Tim van der Lippe16b82282021-11-08 13:50:26 +0000105 ignorePath,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200106 reportNeedlessDisables,
107 reportInvalidScopeDisables,
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000108 reportDescriptionlessDisables,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200109 syntax,
110 customSyntax,
111 fix,
Tim van der Lippe16b82282021-11-08 13:50:26 +0000112 quiet,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200113 });
114
115 if (!files) {
116 const absoluteCodeFilename =
117 codeFilename !== undefined && !path.isAbsolute(codeFilename)
118 ? path.join(process.cwd(), codeFilename)
119 : codeFilename;
120
121 // if file is ignored, return nothing
122 if (
123 absoluteCodeFilename &&
124 !filterFilePaths(ignorer, [path.relative(process.cwd(), absoluteCodeFilename)]).length
125 ) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000126 return prepareReturnValue([], maxWarnings, formatterFunction);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200127 }
128
Tim van der Lippe16b82282021-11-08 13:50:26 +0000129 let stylelintResult;
130
131 try {
132 const postcssResult = await stylelint._lintSource({
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200133 code,
134 codeFilename: absoluteCodeFilename,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200135 });
Tim van der Lippe16b82282021-11-08 13:50:26 +0000136
137 stylelintResult = await stylelint._createStylelintResult(postcssResult, absoluteCodeFilename);
138 } catch (error) {
139 stylelintResult = await handleError(stylelint, error);
140 }
141
142 const postcssResult = stylelintResult._postcssResult;
143 const returnValue = prepareReturnValue([stylelintResult], maxWarnings, formatterFunction);
144
145 if (
146 fix &&
147 postcssResult &&
148 !postcssResult.stylelint.ignored &&
149 !postcssResult.stylelint.ruleDisableFix
150 ) {
151 returnValue.output =
152 !postcssResult.stylelint.disableWritingFix && postcssResult.opts
153 ? // If we're fixing, the output should be the fixed code
154 postcssResult.root.toString(postcssResult.opts.syntax)
155 : // If the writing of the fix is disabled, the input code is returned as-is
156 code;
157 }
158
159 return returnValue;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200160 }
161
Tim van der Lippe16b82282021-11-08 13:50:26 +0000162 let fileList = [files].flat().map((entry) => {
163 const cwd = (globbyOptions && globbyOptions.cwd) || process.cwd();
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000164 const absolutePath = !path.isAbsolute(entry) ? path.join(cwd, entry) : path.normalize(entry);
165
166 if (fs.existsSync(absolutePath)) {
167 // This path points to a file. Return an escaped path to avoid globbing
Tim van der Lippe16b82282021-11-08 13:50:26 +0000168 return fastGlob.escapePath(normalizePath(entry));
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000169 }
170
171 return entry;
172 });
173
Tim van der Lippe16b82282021-11-08 13:50:26 +0000174 if (!disableDefaultIgnores) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200175 fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`));
176 }
177
178 if (useCache) {
179 const stylelintVersion = pkg.version;
180 const hashOfConfig = hash(`${stylelintVersion}_${JSON.stringify(config || {})}`);
181
182 fileCache = new FileCache(cacheLocation, hashOfConfig);
183 } else {
184 // No need to calculate hash here, we just want to delete cache file.
185 fileCache = new FileCache(cacheLocation);
186 // Remove cache file if cache option is disabled
187 fileCache.destroy();
188 }
189
Tim van der Lippe16b82282021-11-08 13:50:26 +0000190 let filePaths = await globby(fileList, globbyOptions);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200191
Tim van der Lippe16b82282021-11-08 13:50:26 +0000192 // The ignorer filter needs to check paths relative to cwd
193 filePaths = filterFilePaths(
194 ignorer,
195 filePaths.map((p) => path.relative(process.cwd(), p)),
196 );
197
198 let stylelintResults;
199
200 if (filePaths.length) {
201 const cwd = (globbyOptions && globbyOptions.cwd) || process.cwd();
202 let absoluteFilePaths = filePaths.map((filePath) => {
203 const absoluteFilepath = !path.isAbsolute(filePath)
204 ? path.join(cwd, filePath)
205 : path.normalize(filePath);
206
207 return absoluteFilepath;
208 });
209
210 if (useCache) {
211 absoluteFilePaths = absoluteFilePaths.filter(fileCache.hasFileChanged.bind(fileCache));
212 }
213
214 const getStylelintResults = absoluteFilePaths.map(async (absoluteFilepath) => {
215 debug(`Processing ${absoluteFilepath}`);
216
217 try {
218 const postcssResult = await stylelint._lintSource({
219 filePath: absoluteFilepath,
220 });
221
222 if (postcssResult.stylelint.stylelintError && useCache) {
223 debug(`${absoluteFilepath} contains linting errors and will not be cached.`);
224 fileCache.removeEntry(absoluteFilepath);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200225 }
226
Tim van der Lippe16b82282021-11-08 13:50:26 +0000227 /**
228 * If we're fixing, save the file with changed code
229 */
230 if (
231 postcssResult.root &&
232 postcssResult.opts &&
233 !postcssResult.stylelint.ignored &&
234 fix &&
235 !postcssResult.stylelint.disableWritingFix
236 ) {
237 const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax);
238
239 if (
240 postcssResult.root &&
241 postcssResult.root.source &&
242 postcssResult.root.source.input.css !== fixedCss
243 ) {
244 await writeFileAtomic(absoluteFilepath, fixedCss);
245 }
246 }
247
248 return stylelint._createStylelintResult(postcssResult, absoluteFilepath);
249 } catch (error) {
250 // On any error, we should not cache the lint result
251 fileCache.removeEntry(absoluteFilepath);
252
253 return handleError(stylelint, error, absoluteFilepath);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200254 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200255 });
Tim van der Lippe16b82282021-11-08 13:50:26 +0000256
257 stylelintResults = await Promise.all(getStylelintResults);
258 } else if (allowEmptyInput) {
259 stylelintResults = await Promise.all([]);
260 } else {
261 stylelintResults = await Promise.reject(new NoFilesFoundError(fileList));
262 }
263
264 if (useCache) {
265 fileCache.reconcile();
266 }
267
268 const result = prepareReturnValue(stylelintResults, maxWarnings, formatterFunction);
269
270 debug(`Linting complete in ${Date.now() - startTime}ms`);
271
272 return result;
273}
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200274
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000275/**
Tim van der Lippe16b82282021-11-08 13:50:26 +0000276 * @param {FormatterType | Formatter | undefined} selected
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000277 * @returns {Formatter}
278 */
279function getFormatterFunction(selected) {
280 /** @type {Formatter} */
281 let formatterFunction;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200282
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000283 if (typeof selected === 'string') {
284 formatterFunction = formatters[selected];
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200285
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000286 if (formatterFunction === undefined) {
287 throw new Error(
288 `You must use a valid formatter option: ${getFormatterOptionsText()} or a function`,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200289 );
290 }
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000291 } else if (typeof selected === 'function') {
292 formatterFunction = selected;
293 } else {
294 formatterFunction = formatters.json;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200295 }
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000296
297 return formatterFunction;
298}
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200299
300/**
Tim van der Lippe16b82282021-11-08 13:50:26 +0000301 * @param {import('stylelint').InternalApi} stylelint
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200302 * @param {any} error
303 * @param {string} [filePath]
304 * @return {Promise<StylelintResult>}
305 */
306function handleError(stylelint, error, filePath = undefined) {
307 if (error.name === 'CssSyntaxError') {
308 return createStylelintResult(stylelint, undefined, filePath, error);
309 }
310
311 throw error;
312}
Tim van der Lippe16b82282021-11-08 13:50:26 +0000313
314module.exports = /** @type {typeof import('stylelint').lint} */ (standalone);