Skip to content

Commit fbd64d5

Browse files
authored
Merge pull request #346 from prettier/v5
Updates for v5, supporting stylelint v16
2 parents 5aa6c2f + d81ff2d commit fbd64d5

16 files changed

+580
-2354
lines changed

.eslintrc.cjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
3+
module.exports = {
4+
root: true,
5+
extends: [
6+
'eslint:recommended',
7+
'plugin:n/recommended',
8+
'plugin:prettier/recommended',
9+
],
10+
};

.eslintrc.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

.github/CONTRIBUTING.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ yarn run test
1818

1919
This is a [Stylelint](https://stylelint.io/) plugin. Documentation for the APIs that it uses can be found on Stylelint's [Writing Plugins](https://stylelint.io/developer-guide/plugins/) page.
2020

21-
Linting is ran as part of `yarn run test`. The build will fail if there are any linting errors. You can run `yarn run lint --fix` to fix some linting errors (including formatting to match prettier's expectations). To run the tests without linting run `yarn run jest`.
22-
23-
This plugin is used to lint itself. The style is checked when `npm test` is run, and the build will fail if there are any linting errors. You can use `npm run lint -- --fix` to fix some linting errors. To run the tests without running the linter, you can use `node_modules/.bin/mocha`.
21+
Linting is ran as part of `yarn run test`. The build will fail if there are any linting errors. You can run `yarn run lint --fix` to fix some linting errors (including formatting to match prettier's expectations). To run the tests without linting run `node --test test/*.test.js`.
2422

2523
### End to end tests
2624

.github/dependabot.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ updates:
1111
- dependency-name: 'stylelint'
1212
- dependency-name: 'prettier'
1313
- dependency-name: 'prettier-linter-helpers'
14-
# strip-ansi v7 is esm only, lets not rewrite tests just to deal with that
15-
- dependency-name: 'strip-ansi'
16-
# eslint-plugin-n v16 drops support for node 14, keep this till we drop
17-
# support too
18-
- dependency-name: 'eslint-plugin-n'
1914
groups:
2015
dev-dependencies:
2116
dependency-type: development

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
stylelint-version: [15.x]
16-
node-version: [20.x, 18.x, 16.x, 14.x]
15+
stylelint-version: [16.x]
16+
node-version: [20.x, 18.x]
1717

1818
steps:
1919
- uses: actions/checkout@v4

index.js

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import stylelint from 'stylelint';
2+
import {showInvisibles, generateDifferences} from 'prettier-linter-helpers';
3+
4+
const prettierPromise = import('prettier');
5+
6+
const {INSERT, DELETE, REPLACE} = generateDifferences;
7+
8+
let prettier;
9+
10+
const ruleName = 'prettier/prettier';
11+
const messages = stylelint.utils.ruleMessages(ruleName, {
12+
insert: (code) => `Insert "${showInvisibles(code)}"`,
13+
delete: (code) => `Delete "${showInvisibles(code)}"`,
14+
replace: (deleteCode, insertCode) =>
15+
`Replace "${showInvisibles(deleteCode)}" with "${showInvisibles(
16+
insertCode
17+
)}"`,
18+
});
19+
20+
/** @type {stylelint.Rule} */
21+
const ruleFunction = (expectation, options, context) => {
22+
return async (root, result) => {
23+
const validOptions = stylelint.utils.validateOptions(result, ruleName, {
24+
actual: expectation,
25+
});
26+
if (!validOptions) {
27+
return;
28+
}
29+
30+
// Stylelint can handle css-in-js, in which it formats object literals.
31+
// We don't want to run these extracts of JS through prettier
32+
if (root.source.lang === 'object-literal') {
33+
return;
34+
}
35+
36+
const stylelintPrettierOptions = omitStylelintSpecificOptions(options);
37+
38+
if (!prettier) {
39+
// Prettier is expensive to load, so only load it if needed.
40+
prettier = await prettierPromise;
41+
}
42+
43+
// Default to '<input>' if a filepath was not provided.
44+
// This mimics eslint's behaviour
45+
const filepath = root.source.input.file || '<input>';
46+
const source = root.source.input.css;
47+
48+
const prettierRcOptions = await prettier.resolveConfig(filepath, {
49+
editorconfig: true,
50+
});
51+
52+
const prettierFileInfo = await prettier.getFileInfo(filepath, {
53+
resolveConfig: true,
54+
plugins:
55+
prettierRcOptions?.plugins ?? stylelintPrettierOptions?.plugins ?? [],
56+
ignorePath: '.prettierignore',
57+
});
58+
59+
// Skip if file is ignored using a .prettierignore file
60+
if (prettierFileInfo.ignored) {
61+
return;
62+
}
63+
64+
const initialOptions = {};
65+
66+
// If no filepath was provided then assume the CSS parser
67+
// This is added to the options first, so that
68+
// prettierRcOptions and stylelintPrettierOptions can still override
69+
// the parser.
70+
if (filepath == '<input>') {
71+
initialOptions.parser = 'css';
72+
}
73+
74+
// Stylelint supports languages that may contain multiple types of style
75+
// languages, thus we can't rely on guessing the parser based off the
76+
// filename.
77+
78+
// In all of the following cases stylelint extracts a part of a file to
79+
// be formatted and there exists a prettier parser for the whole file.
80+
// If you're interested in prettier you'll want a fully formatted file so
81+
// you're about to run prettier over the whole file anyway.
82+
// Therefore running prettier over just the style section is wasteful, so
83+
// skip it.
84+
85+
const parserBlockList = [
86+
'babel',
87+
'flow',
88+
'typescript',
89+
'vue',
90+
'markdown',
91+
'html',
92+
'angular', // .component.html files
93+
'svelte',
94+
'astro',
95+
];
96+
if (parserBlockList.indexOf(prettierFileInfo.inferredParser) !== -1) {
97+
return;
98+
}
99+
100+
const prettierOptions = Object.assign(
101+
{},
102+
initialOptions,
103+
prettierRcOptions,
104+
stylelintPrettierOptions,
105+
{filepath}
106+
);
107+
108+
let prettierSource;
109+
110+
try {
111+
prettierSource = await prettier.format(source, prettierOptions);
112+
} catch (err) {
113+
if (!(err instanceof SyntaxError)) {
114+
throw err;
115+
}
116+
117+
let message = 'Parsing error: ' + err.message;
118+
119+
// Prettier's message contains a codeframe style preview of the
120+
// invalid code and the line/column at which the error occurred.
121+
// ESLint shows those pieces of information elsewhere already so
122+
// remove them from the message
123+
if (err.codeFrame) {
124+
message = message.replace(`\n${err.codeFrame}`, '');
125+
}
126+
if (err.loc) {
127+
message = message.replace(/ \(\d+:\d+\)$/, '');
128+
}
129+
130+
stylelint.utils.report({
131+
ruleName,
132+
result,
133+
message,
134+
node: root,
135+
index: getIndexFromLoc(source, err.loc.start),
136+
});
137+
138+
return;
139+
}
140+
141+
// Everything is the same. Nothing to do here;
142+
if (source === prettierSource) {
143+
return;
144+
}
145+
146+
// Otherwise let's generate some differences
147+
148+
const differences = generateDifferences(source, prettierSource);
149+
150+
const report = (message, index, endIndex) => {
151+
return stylelint.utils.report({
152+
ruleName,
153+
result,
154+
message,
155+
node: root,
156+
index,
157+
endIndex,
158+
});
159+
};
160+
161+
if (context.fix) {
162+
// Fixes must be processed in reverse order, as an early delete shall
163+
// change the modification offsets for anything after it
164+
const rawData = differences.reverse().reduce((rawData, difference) => {
165+
let insertText = '';
166+
let deleteText = '';
167+
switch (difference.operation) {
168+
case INSERT:
169+
insertText = difference.insertText;
170+
break;
171+
case DELETE:
172+
deleteText = difference.deleteText;
173+
break;
174+
case REPLACE:
175+
insertText = difference.insertText;
176+
deleteText = difference.deleteText;
177+
break;
178+
}
179+
180+
return (
181+
rawData.substring(0, difference.offset) +
182+
insertText +
183+
rawData.substring(difference.offset + deleteText.length)
184+
);
185+
}, root.source.input.css);
186+
187+
// If root.source.syntax exists then it means stylelint had to use
188+
// postcss-syntax to guess the postcss parser that it should use based
189+
// upon the input filename.
190+
// In that case we want to use the parser that postcss-syntax picked.
191+
// Otherwise use the syntax parser that was provided in the options
192+
const syntax = root.source.syntax || result.opts.syntax;
193+
const newRoot = syntax.parse(rawData);
194+
195+
// For reasons I don't really understand, when the original input does
196+
// not have a trailing newline, newRoot generates a trailing newline but
197+
// it does not get included in the output.
198+
// Cleaning the root raws (to remove any existing whitespace), then
199+
// adding the final new line into the root raws seems to fix this
200+
root.removeAll();
201+
root.cleanRaws();
202+
root.append(newRoot);
203+
204+
// Use the EOL whitespace from the rawData, as it could be \n or \r\n
205+
const trailingWhitespace = rawData.match(/[\s\uFEFF\xA0]+$/);
206+
if (trailingWhitespace) {
207+
root.raws.after = trailingWhitespace[0];
208+
}
209+
return;
210+
}
211+
212+
// Report in the order the differences appear in the content
213+
differences.forEach((difference) => {
214+
const {offset, deleteText = ''} = difference;
215+
switch (difference.operation) {
216+
case INSERT:
217+
report(
218+
messages.insert(difference.insertText),
219+
offset,
220+
offset + deleteText.length
221+
);
222+
break;
223+
case DELETE:
224+
report(
225+
messages.delete(difference.deleteText),
226+
difference.offset,
227+
offset + deleteText.length
228+
);
229+
break;
230+
case REPLACE:
231+
report(
232+
messages.replace(difference.deleteText, difference.insertText),
233+
difference.offset,
234+
offset + deleteText.length
235+
);
236+
break;
237+
}
238+
});
239+
};
240+
};
241+
242+
ruleFunction.ruleName = ruleName;
243+
ruleFunction.messages = messages;
244+
245+
export default stylelint.createPlugin(ruleName, ruleFunction);
246+
247+
function omitStylelintSpecificOptions(options) {
248+
const prettierOptions = Object.assign({}, options);
249+
delete prettierOptions.message;
250+
delete prettierOptions.severity;
251+
return prettierOptions;
252+
}
253+
254+
function getIndexFromLoc(source, {line, column}) {
255+
function nthIndex(str, searchValue, n) {
256+
let i = -1;
257+
while (n-- && i++ < str.length) {
258+
i = str.indexOf(searchValue, i);
259+
if (i < 0) {
260+
break;
261+
}
262+
}
263+
return i;
264+
}
265+
266+
if (line === 1) {
267+
return column - 1;
268+
}
269+
270+
return nthIndex(source, '\n', line - 1) + column;
271+
}

jest-setup.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)