blob: 603cb7372e68192d31c4d6cf509e8e4d7c34173e [file] [log] [blame]
Mathias Bynens79e2cf02020-05-29 16:46:17 +02001'use strict';
2
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +00003const fs = require('fs');
4const LazyResult = require('postcss/lib/lazy-result');
5const postcss = require('postcss');
6const syntaxes = require('./syntaxes');
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 Lippe2b4a9df2021-11-08 12:58:12 +000012/** @typedef {import('stylelint').StylelintInternalApi} 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 Lippe2b4a9df2021-11-08 12:58:12 +000022module.exports = function (stylelint, options = {}) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +020023 const cached = options.filePath ? stylelint._postcssResultCache.get(options.filePath) : undefined;
24
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000025 if (cached) return Promise.resolve(cached);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020026
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000027 /** @type {Promise<string> | undefined} */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020028 let getCode;
29
30 if (options.code !== undefined) {
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000031 getCode = Promise.resolve(options.code);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020032 } else if (options.filePath) {
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000033 getCode = readFile(options.filePath);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020034 }
35
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000036 if (!getCode) {
37 throw new Error('code or filePath required');
Mathias Bynens79e2cf02020-05-29 16:46:17 +020038 }
39
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000040 return getCode
41 .then((code) => {
42 /** @type {Syntax | null} */
43 let syntax = null;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020044
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000045 if (stylelint._options.customSyntax) {
46 syntax = getCustomSyntax(stylelint._options.customSyntax);
47 } else if (stylelint._options.syntax) {
48 if (stylelint._options.syntax === 'css') {
49 syntax = cssSyntax(stylelint);
50 } else {
51 const keys = Object.keys(syntaxes);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020052
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +000053 if (!keys.includes(stylelint._options.syntax)) {
54 throw new Error(
55 `You must use a valid syntax option, either: css, ${keys
56 .slice(0, -1)
57 .join(', ')} or ${keys.slice(-1)}`,
58 );
59 }
60
61 syntax = syntaxes[stylelint._options.syntax];
62 }
63 } else if (!(options.codeProcessors && options.codeProcessors.length)) {
64 const autoSyntax = require('postcss-syntax');
65
66 // TODO: investigate why lazy import HTML syntax causes
67 // JS files with the word "html" to throw TypeError
68 // https://github.com/stylelint/stylelint/issues/4793
69 const { html, ...rest } = syntaxes;
70
71 syntax = autoSyntax({
72 css: cssSyntax(stylelint),
73 jsx: syntaxes['css-in-js'],
74 ...rest,
75 });
76 }
77
78 const postcssOptions = {
79 from: options.filePath,
80 syntax,
81 };
82
83 const source = options.code ? options.codeFilename : options.filePath;
84 let preProcessedCode = code;
85
86 if (options.codeProcessors && options.codeProcessors.length) {
87 if (stylelint._options.fix) {
88 console.warn(
89 'Autofix is incompatible with processors and will be disabled. Are you sure you need a processor?',
90 );
91 stylelint._options.fix = false;
92 }
93
94 options.codeProcessors.forEach((codeProcessor) => {
95 preProcessedCode = codeProcessor(preProcessedCode, source);
96 });
97 }
98
99 const result = new LazyResult(postcssProcessor, preProcessedCode, postcssOptions);
100
101 return result;
102 })
103 .then((postcssResult) => {
104 if (options.filePath) {
105 stylelint._postcssResultCache.set(options.filePath, postcssResult);
106 }
107
108 return postcssResult;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200109 });
110};
111
112/**
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000113 * @param {CustomSyntax} customSyntax
114 * @returns {Syntax}
115 */
116function getCustomSyntax(customSyntax) {
117 let resolved;
118
119 if (typeof customSyntax === 'string') {
120 try {
121 resolved = require(customSyntax);
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +0000122 } catch {
123 throw new Error(
124 `Cannot resolve custom syntax module ${customSyntax}. Check that module ${customSyntax} is available and spelled correctly.`,
125 );
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000126 }
127
128 /*
129 * PostCSS allows for syntaxes that only contain a parser, however,
130 * it then expects the syntax to be set as the `parse` option.
131 */
132 if (!resolved.parse) {
133 resolved = {
134 parse: resolved,
135 stringify: postcss.stringify,
136 };
137 }
138
139 return resolved;
140 }
141
142 if (typeof customSyntax === 'object') {
143 if (typeof customSyntax.parse === 'function') {
144 resolved = { ...customSyntax };
145 } else {
Tim van der Lippecc71b282021-02-12 15:51:14 +0000146 throw new TypeError(
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000147 `An object provided to the "customSyntax" option must have a "parse" property. Ensure the "parse" property exists and its value is a function.`,
148 );
149 }
150
151 return resolved;
152 }
153
154 throw new Error(`Custom syntax must be a string or a Syntax object`);
155}
156
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +0000157/**
158 * @param {string} filePath
159 * @returns {Promise<string>}
160 */
161function readFile(filePath) {
162 return new Promise((resolve, reject) => {
163 fs.readFile(filePath, 'utf8', (err, content) => {
164 if (err) {
165 return reject(err);
166 }
167
168 resolve(content);
169 });
170 });
171}
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200172
173/**
174 * @param {StylelintInternalApi} stylelint
175 * @returns {Syntax}
176 */
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +0000177function cssSyntax(stylelint) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200178 return {
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +0000179 parse: stylelint._options.fix ? require('postcss-safe-parser') : postcss.parse,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200180 stringify: postcss.stringify,
181 };
182}