blob: 4b00c5aa87b53fe301d6e585051154c70d7d564b [file] [log] [blame]
Mathias Bynens79e2cf02020-05-29 16:46:17 +02001'use strict';
2
Tim van der Lippe16b82282021-11-08 13:50:26 +00003const LazyResult = require('postcss/lib/lazy-result').default;
4const path = require('path');
5const { default: postcss } = require('postcss');
6const { promises: fs } = require('fs');
Mathias Bynens79e2cf02020-05-29 16:46:17 +02007
Tim van der Lippeefb716a2020-12-01 12:54:04 +00008/** @typedef {import('postcss').Result} Result */
9/** @typedef {import('postcss').Syntax} Syntax */
10/** @typedef {import('stylelint').CustomSyntax} CustomSyntax */
11/** @typedef {import('stylelint').GetPostcssOptions} GetPostcssOptions */
Tim van der Lippe16b82282021-11-08 13:50:26 +000012/** @typedef {import('stylelint').InternalApi} StylelintInternalApi */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020013
14const postcssProcessor = postcss();
15
16/**
17 * @param {StylelintInternalApi} stylelint
Tim van der Lippeefb716a2020-12-01 12:54:04 +000018 * @param {GetPostcssOptions} options
Mathias Bynens79e2cf02020-05-29 16:46:17 +020019 *
Tim van der Lippeefb716a2020-12-01 12:54:04 +000020 * @returns {Promise<Result>}
Mathias Bynens79e2cf02020-05-29 16:46:17 +020021 */
Tim van der Lippe16b82282021-11-08 13:50:26 +000022module.exports = async function getPostcssResult(stylelint, options = {}) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +020023 const cached = options.filePath ? stylelint._postcssResultCache.get(options.filePath) : undefined;
24
Tim van der Lippe16b82282021-11-08 13:50:26 +000025 if (cached) {
26 return cached;
27 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +020028
Tim van der Lippe16b82282021-11-08 13:50:26 +000029 if (stylelint._options.syntax) {
30 let error = 'The "syntax" option is no longer available. ';
31
32 error +=
33 stylelint._options.syntax === 'css'
34 ? 'You can remove the "--syntax" CLI flag as stylelint will now parse files as CSS by default'
35 : `You should install an appropriate syntax, e.g. postcss-scss, and use the "customSyntax" option`;
36
37 return Promise.reject(new Error(error));
38 }
39
40 const syntax = options.customSyntax
41 ? getCustomSyntax(options.customSyntax)
42 : cssSyntax(stylelint, options.filePath);
43
44 const postcssOptions = {
45 from: options.filePath,
46 syntax,
47 };
48
49 /** @type {string | undefined} */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020050 let getCode;
51
52 if (options.code !== undefined) {
Tim van der Lippe16b82282021-11-08 13:50:26 +000053 getCode = options.code;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020054 } else if (options.filePath) {
Tim van der Lippe16b82282021-11-08 13:50:26 +000055 getCode = await fs.readFile(options.filePath, 'utf8');
Mathias Bynens79e2cf02020-05-29 16:46:17 +020056 }
57
Tim van der Lippe16b82282021-11-08 13:50:26 +000058 if (getCode === undefined) {
59 return Promise.reject(new Error('code or filePath required'));
Mathias Bynens79e2cf02020-05-29 16:46:17 +020060 }
61
Tim van der Lippe16b82282021-11-08 13:50:26 +000062 if (options.codeProcessors && options.codeProcessors.length) {
63 if (stylelint._options.fix) {
64 console.warn(
65 'Autofix is incompatible with processors and will be disabled. Are you sure you need a processor?',
66 );
67 stylelint._options.fix = false;
68 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +020069
Tim van der Lippe16b82282021-11-08 13:50:26 +000070 const sourceName = options.code ? options.codeFilename : options.filePath;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020071
Tim van der Lippe4cb09742022-01-07 14:25:03 +010072 for (const codeProcessor of options.codeProcessors) {
Tim van der Lippe16b82282021-11-08 13:50:26 +000073 getCode = codeProcessor(getCode, sourceName);
Tim van der Lippe4cb09742022-01-07 14:25:03 +010074 }
Tim van der Lippe16b82282021-11-08 13:50:26 +000075 }
76
77 const postcssResult = await new LazyResult(postcssProcessor, getCode, postcssOptions);
78
79 if (options.filePath) {
80 stylelint._postcssResultCache.set(options.filePath, postcssResult);
81 }
82
83 return postcssResult;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020084};
85
86/**
Tim van der Lippeefb716a2020-12-01 12:54:04 +000087 * @param {CustomSyntax} customSyntax
88 * @returns {Syntax}
89 */
90function getCustomSyntax(customSyntax) {
91 let resolved;
92
93 if (typeof customSyntax === 'string') {
94 try {
95 resolved = require(customSyntax);
Tim van der Lippe16b82282021-11-08 13:50:26 +000096 } catch (error) {
Tim van der Lippe4cb09742022-01-07 14:25:03 +010097 if (
98 error &&
99 typeof error === 'object' &&
100 // @ts-expect-error -- TS2571: Object is of type 'unknown'.
101 error.code === 'MODULE_NOT_FOUND' &&
102 // @ts-expect-error -- TS2571: Object is of type 'unknown'.
103 error.message.includes(customSyntax)
104 ) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000105 throw new Error(
Tim van der Lippe4cb09742022-01-07 14:25:03 +0100106 `Cannot resolve custom syntax module "${customSyntax}". Check that module "${customSyntax}" is available and spelled correctly.\n\nCaused by: ${error}`,
Tim van der Lippe16b82282021-11-08 13:50:26 +0000107 );
108 }
109
110 throw error;
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000111 }
112
113 /*
114 * PostCSS allows for syntaxes that only contain a parser, however,
115 * it then expects the syntax to be set as the `parse` option.
116 */
117 if (!resolved.parse) {
118 resolved = {
119 parse: resolved,
120 stringify: postcss.stringify,
121 };
122 }
123
124 return resolved;
125 }
126
127 if (typeof customSyntax === 'object') {
128 if (typeof customSyntax.parse === 'function') {
129 resolved = { ...customSyntax };
130 } else {
Tim van der Lippecc71b282021-02-12 15:51:14 +0000131 throw new TypeError(
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000132 `An object provided to the "customSyntax" option must have a "parse" property. Ensure the "parse" property exists and its value is a function.`,
133 );
134 }
135
136 return resolved;
137 }
138
139 throw new Error(`Custom syntax must be a string or a Syntax object`);
140}
141
Tim van der Lippe16b82282021-11-08 13:50:26 +0000142/** @type {{ [key: string]: string }} */
143const previouslyInferredExtensions = {
144 html: 'postcss-html',
145 js: '@stylelint/postcss-css-in-js',
146 jsx: '@stylelint/postcss-css-in-js',
147 less: 'postcss-less',
148 md: 'postcss-markdown',
149 sass: 'postcss-sass',
150 sss: 'sugarss',
151 scss: 'postcss-scss',
152 svelte: 'postcss-html',
153 ts: '@stylelint/postcss-css-in-js',
154 tsx: '@stylelint/postcss-css-in-js',
155 vue: 'postcss-html',
156 xml: 'postcss-html',
157 xst: 'postcss-html',
158};
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200159
160/**
161 * @param {StylelintInternalApi} stylelint
Tim van der Lippe16b82282021-11-08 13:50:26 +0000162 * @param {string|undefined} filePath
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200163 * @returns {Syntax}
164 */
Tim van der Lippe16b82282021-11-08 13:50:26 +0000165function cssSyntax(stylelint, filePath) {
166 const fileExtension = filePath ? path.extname(filePath).slice(1).toLowerCase() : '';
167 const extensions = ['css', 'pcss', 'postcss'];
168
169 if (previouslyInferredExtensions[fileExtension]) {
170 console.warn(
171 `${filePath}: When linting something other than CSS, you should install an appropriate syntax, e.g. "${previouslyInferredExtensions[fileExtension]}", and use the "customSyntax" option`,
172 );
173 }
174
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200175 return {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000176 parse:
177 stylelint._options.fix && extensions.includes(fileExtension)
178 ? require('postcss-safe-parser')
179 : postcss.parse,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200180 stringify: postcss.stringify,
181 };
182}