Skip to content

Commit 5af5eaf

Browse files
authored
stylelint: formatter-github (#1441)
1 parent 4dc8cd5 commit 5af5eaf

File tree

13 files changed

+352
-11
lines changed

13 files changed

+352
-11
lines changed

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/pack-test/dist/index.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"use strict";var e=require("node:url"),n=require("node:fs/promises"),t=require("node:path"),i=require("node:os"),o=require("node:process"),a=require("node:child_process"),r="undefined"!=typeof document?document.currentScript:null;const s="package";async function findPackageJsonFromDir(e,i=10){const o=t.join(e,"package.json");try{return await n.access(o),o}catch{if("/"===e||i<=0)throw new Error("Could not find package.json")}return findPackageJsonFromDir(t.dirname(e),i-1)}async function pack(e,i){const r=await n.mkdir(t.join(i,"pack"),{recursive:!0}),s=a.spawn("npm",["pack","--pack-destination",r],{cwd:e,shell:"win32"===o.platform}),c=await new Promise(((e,n)=>{let t="",i="";s.stdout.on("data",(e=>{t+=e.toString()})),s.stderr.on("data",(e=>{i+=e.toString()})),s.on("close",(o=>{0===o?e(t.trim()):(console.error(i),n(new Error(`npm pack exited with code ${o}`)))}))}));return t.join(r,c)}async function unpack(e,i){const o=t.join(i,s);await n.mkdir(o,{recursive:!0});const r=a.spawn("tar",["-xf",e,"-C",s,"--strip-components","1"],{cwd:i});return await new Promise(((e,n)=>{r.on("close",(t=>{0===t?e():n(new Error(`tar exited with code ${t}`))}))})),o}async function eraseDevDependenciesInfo(e){const t=JSON.parse(await n.readFile(e,"utf8"));delete t.devDependencies,await n.writeFile(e,JSON.stringify(t,null,"\t"))}async function getPackageInfo(e){return JSON.parse(await n.readFile(e,"utf8"))}async function createRootPackage(e,i){await n.writeFile(t.join(e,"package.json"),JSON.stringify({name:"@csstools/pack-test--root",private:!0,type:"module",version:"1.0.0",description:"",workspaces:[s],dependencies:i.peerDependencies??{},scripts:{test:"node --test"}},null,"\t")),await n.writeFile(t.join(e,"index.mjs"),`import '${i.name}';`)}async function runNPMInstall(e){const n=a.spawn("npm",["install","--omit","dev"],{cwd:e,stdio:"inherit",shell:"win32"===o.platform});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}async function runTest(e){const n=a.spawn("node",["index.mjs"],{cwd:e,stdio:"inherit",shell:"win32"===o.platform});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}exports.testPack=async function testPack(a){if(o.platform.startsWith("win"))return void console.log("Skipping test on Windows");if(!("resolve"in{url:"undefined"==typeof document?require("url").pathToFileURL(__filename).href:r&&r.src||new URL("index.cjs",document.baseURI).href}))return void console.log("Skipping test on platform without `import.meta.resolve` support");const s=await n.mkdtemp(t.join(i.tmpdir(),"csstools-pack-test-"));let c=!1;try{const n=new URL((void 0)(a));console.log(`Testing module: ${a}`);const i=e.fileURLToPath(n),o=await findPackageJsonFromDir(t.dirname(i)),r=t.dirname(o),c=await pack(r,s),d=await unpack(c,s),p=await getPackageInfo(t.join(d,"package.json"));await eraseDevDependenciesInfo(t.join(d,"package.json")),await createRootPackage(s,p),await runNPMInstall(s),await runTest(s)}catch(e){console.error(e),c=!0}finally{await n.rm(s,{recursive:!0})}c&&process.exit(1)};
1+
"use strict";var e=require("node:url"),n=require("node:fs/promises"),t=require("node:path"),i=require("node:os"),o=require("node:process"),a=require("node:child_process"),r="undefined"!=typeof document?document.currentScript:null;const s="package";async function findPackageJsonFromDir(e,i=10){const o=t.join(e,"package.json");try{return await n.access(o),o}catch{if("/"===e||i<=0)throw new Error("Could not find package.json")}return findPackageJsonFromDir(t.dirname(e),i-1)}async function pack(e,i){const r=await n.mkdir(t.join(i,"pack"),{recursive:!0}),s=a.spawn("npm",["pack","--pack-destination",r],{cwd:e,shell:"win32"===o.platform}),c=await new Promise(((e,n)=>{let t="",i="";s.stdout.on("data",(e=>{t+=e.toString()})),s.stderr.on("data",(e=>{i+=e.toString()})),s.on("close",(o=>{0===o?e(t.trim()):(console.error(i),n(new Error(`npm pack exited with code ${o}`)))}))}));return t.join(r,c)}async function unpack(e,i){const o=t.join(i,s);await n.mkdir(o,{recursive:!0});const r=a.spawn("tar",["-xf",e,"-C",s,"--strip-components","1"],{cwd:i});return await new Promise(((e,n)=>{r.on("close",(t=>{0===t?e():n(new Error(`tar exited with code ${t}`))}))})),o}async function eraseDevDependenciesInfo(e){const t=JSON.parse(await n.readFile(e,"utf8"));delete t.devDependencies,await n.writeFile(e,JSON.stringify(t,null,"\t"))}async function getPackageInfo(e){return JSON.parse(await n.readFile(e,"utf8"))}async function createRootPackage(e,i){await n.writeFile(t.join(e,"package.json"),JSON.stringify({name:"@csstools/pack-test--root",private:!0,type:"module",version:"1.0.0",description:"",workspaces:[s],dependencies:i.peerDependencies??{},scripts:{test:"node --test"}},null,"\t")),await n.writeFile(t.join(e,"index.mjs"),`import '${i.name}';`)}async function runNPMInstall(e){const n=a.spawn("npm",["install","--omit","dev"],{cwd:e,stdio:"inherit",shell:"win32"===o.platform});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}async function runTest(e){const n=a.spawn("node",["index.mjs"],{cwd:e,stdio:"inherit",shell:"win32"===o.platform});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}exports.testPack=async function testPack(a){if(o.platform.startsWith("win"))return void console.log("Skipping test on Windows");if(!("resolve"in{url:"undefined"==typeof document?require("url").pathToFileURL(__filename).href:r&&r.src||new URL("index.cjs",document.baseURI).href}))return void console.log("Skipping test on platform without `import.meta.resolve` support");const s=await n.mkdtemp(t.join(i.tmpdir(),"csstools-pack-test-"));try{const n=new URL((void 0)(a));console.log(`Testing module: ${a}`);const i=e.fileURLToPath(n),o=await findPackageJsonFromDir(t.dirname(i)),r=t.dirname(o),c=await pack(r,s),d=await unpack(c,s),p=await getPackageInfo(t.join(d,"package.json"));await eraseDevDependenciesInfo(t.join(d,"package.json")),await createRootPackage(s,p),await runNPMInstall(s),await runTest(s)}finally{await n.rm(s,{recursive:!0})}};

packages/pack-test/dist/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import e from"node:url";import n from"node:fs/promises";import t from"node:path";import o from"node:os";import{platform as i}from"node:process";import{spawn as a}from"node:child_process";const r="package";async function testPack(a){if(i.startsWith("win"))return void console.log("Skipping test on Windows");if(!("resolve"in import.meta))return void console.log("Skipping test on platform without `import.meta.resolve` support");const r=await n.mkdtemp(t.join(o.tmpdir(),"csstools-pack-test-"));let s=!1;try{const n=new URL(import.meta.resolve(a));console.log(`Testing module: ${a}`);const o=e.fileURLToPath(n),i=await findPackageJsonFromDir(t.dirname(o)),s=t.dirname(i),c=await pack(s,r),p=await unpack(c,r),d=await getPackageInfo(t.join(p,"package.json"));await eraseDevDependenciesInfo(t.join(p,"package.json")),await createRootPackage(r,d),await runNPMInstall(r),await runTest(r)}catch(e){console.error(e),s=!0}finally{await n.rm(r,{recursive:!0})}s&&process.exit(1)}async function findPackageJsonFromDir(e,o=10){const i=t.join(e,"package.json");try{return await n.access(i),i}catch{if("/"===e||o<=0)throw new Error("Could not find package.json")}return findPackageJsonFromDir(t.dirname(e),o-1)}async function pack(e,o){const r=await n.mkdir(t.join(o,"pack"),{recursive:!0}),s=a("npm",["pack","--pack-destination",r],{cwd:e,shell:"win32"===i}),c=await new Promise(((e,n)=>{let t="",o="";s.stdout.on("data",(e=>{t+=e.toString()})),s.stderr.on("data",(e=>{o+=e.toString()})),s.on("close",(i=>{0===i?e(t.trim()):(console.error(o),n(new Error(`npm pack exited with code ${i}`)))}))}));return t.join(r,c)}async function unpack(e,o){const i=t.join(o,r);await n.mkdir(i,{recursive:!0});const s=a("tar",["-xf",e,"-C",r,"--strip-components","1"],{cwd:o});return await new Promise(((e,n)=>{s.on("close",(t=>{0===t?e():n(new Error(`tar exited with code ${t}`))}))})),i}async function eraseDevDependenciesInfo(e){const t=JSON.parse(await n.readFile(e,"utf8"));delete t.devDependencies,await n.writeFile(e,JSON.stringify(t,null,"\t"))}async function getPackageInfo(e){return JSON.parse(await n.readFile(e,"utf8"))}async function createRootPackage(e,o){await n.writeFile(t.join(e,"package.json"),JSON.stringify({name:"@csstools/pack-test--root",private:!0,type:"module",version:"1.0.0",description:"",workspaces:[r],dependencies:o.peerDependencies??{},scripts:{test:"node --test"}},null,"\t")),await n.writeFile(t.join(e,"index.mjs"),`import '${o.name}';`)}async function runNPMInstall(e){const n=a("npm",["install","--omit","dev"],{cwd:e,stdio:"inherit",shell:"win32"===i});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}async function runTest(e){const n=a("node",["index.mjs"],{cwd:e,stdio:"inherit",shell:"win32"===i});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}export{testPack};
1+
import e from"node:url";import n from"node:fs/promises";import t from"node:path";import o from"node:os";import{platform as i}from"node:process";import{spawn as a}from"node:child_process";const r="package";async function testPack(a){if(i.startsWith("win"))return void console.log("Skipping test on Windows");if(!("resolve"in import.meta))return void console.log("Skipping test on platform without `import.meta.resolve` support");const r=await n.mkdtemp(t.join(o.tmpdir(),"csstools-pack-test-"));try{const n=new URL(import.meta.resolve(a));console.log(`Testing module: ${a}`);const o=e.fileURLToPath(n),i=await findPackageJsonFromDir(t.dirname(o)),s=t.dirname(i),c=await pack(s,r),p=await unpack(c,r),d=await getPackageInfo(t.join(p,"package.json"));await eraseDevDependenciesInfo(t.join(p,"package.json")),await createRootPackage(r,d),await runNPMInstall(r),await runTest(r)}finally{await n.rm(r,{recursive:!0})}}async function findPackageJsonFromDir(e,o=10){const i=t.join(e,"package.json");try{return await n.access(i),i}catch{if("/"===e||o<=0)throw new Error("Could not find package.json")}return findPackageJsonFromDir(t.dirname(e),o-1)}async function pack(e,o){const r=await n.mkdir(t.join(o,"pack"),{recursive:!0}),s=a("npm",["pack","--pack-destination",r],{cwd:e,shell:"win32"===i}),c=await new Promise(((e,n)=>{let t="",o="";s.stdout.on("data",(e=>{t+=e.toString()})),s.stderr.on("data",(e=>{o+=e.toString()})),s.on("close",(i=>{0===i?e(t.trim()):(console.error(o),n(new Error(`npm pack exited with code ${i}`)))}))}));return t.join(r,c)}async function unpack(e,o){const i=t.join(o,r);await n.mkdir(i,{recursive:!0});const s=a("tar",["-xf",e,"-C",r,"--strip-components","1"],{cwd:o});return await new Promise(((e,n)=>{s.on("close",(t=>{0===t?e():n(new Error(`tar exited with code ${t}`))}))})),i}async function eraseDevDependenciesInfo(e){const t=JSON.parse(await n.readFile(e,"utf8"));delete t.devDependencies,await n.writeFile(e,JSON.stringify(t,null,"\t"))}async function getPackageInfo(e){return JSON.parse(await n.readFile(e,"utf8"))}async function createRootPackage(e,o){await n.writeFile(t.join(e,"package.json"),JSON.stringify({name:"@csstools/pack-test--root",private:!0,type:"module",version:"1.0.0",description:"",workspaces:[r],dependencies:o.peerDependencies??{},scripts:{test:"node --test"}},null,"\t")),await n.writeFile(t.join(e,"index.mjs"),`import '${o.name}';`)}async function runNPMInstall(e){const n=a("npm",["install","--omit","dev"],{cwd:e,stdio:"inherit",shell:"win32"===i});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}async function runTest(e){const n=a("node",["index.mjs"],{cwd:e,stdio:"inherit",shell:"win32"===i});await new Promise(((e,t)=>{n.on("close",(n=>{0===n?e():t(new Error(`npm install exited with code ${n}`))}))}))}export{testPack};

packages/pack-test/src/index.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ export async function testPack(moduleName: string): Promise<void> {
3838
return;
3939
}
4040

41-
4241
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'csstools-pack-test-'));
43-
let didError = false;
4442

4543
try {
4644
const moduleURL = new URL(import.meta.resolve(moduleName));
@@ -63,16 +61,9 @@ export async function testPack(moduleName: string): Promise<void> {
6361
await runNPMInstall(tempDir);
6462

6563
await runTest(tempDir);
66-
} catch (err) {
67-
console.error(err);
68-
didError = true;
6964
} finally {
7065
await fs.rm(tempDir, { recursive: true });
7166
}
72-
73-
if (didError) {
74-
process.exit(1);
75-
}
7667
}
7768

7869
async function findPackageJsonFromDir(dir: string, ceil = 10): Promise<string> {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
### Unreleased (major)
4+
5+
Initial release
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
MIT No Attribution (MIT-0)
2+
3+
Copyright 2023 Romain Menke, Antonio Laguna <antonio@laguna.es>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the “Software”), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9+
of the Software, and to permit persons to whom the Software is furnished to do
10+
so.
11+
12+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18+
SOFTWARE.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# [@csstools/stylelint-formatter-github](https://www.npmjs.com/package/@csstools/stylelint-formatter-github)
2+
3+
[![version](https://img.shields.io/npm/v/@csstools/stylelint-formatter-github.svg)](https://www.npmjs.com/package/@csstools/stylelint-formatter-github)
4+
5+
Format Stylelint output as GitHub Actions annotations.
6+
7+
## Usage
8+
9+
```sh
10+
npm install --save-dev @csstools/stylelint-formatter-github
11+
```
12+
13+
```sh
14+
stylelint --custom-formatter @csstools/stylelint-formatter-github
15+
```
16+
17+
## `--formatter github`
18+
19+
The build-in `github` formatter was deprecated.
20+
This package provides a drop-in replacement.
21+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @see https://github.com/stylelint/stylelint/blob/8fefd145f86b74f182286550675881c47b164fba/lib/formatters/githubFormatter.mjs
3+
*
4+
* @license MIT https://github.com/stylelint/stylelint/blob/main/LICENSE
5+
*/
6+
7+
import preprocessWarnings from './preprocess-warnings.mjs';
8+
9+
/**
10+
* @see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
11+
*
12+
* @type {import('stylelint').Formatter}
13+
*/
14+
export default function githubFormatter(results, returnValue) {
15+
const title = 'Stylelint problem';
16+
const metadata = returnValue.ruleMetadata;
17+
18+
const lines = results.flatMap((result) => {
19+
const { source, warnings } = preprocessWarnings(result);
20+
21+
return warnings.map(({ line, column, endLine, endColumn, text, severity, rule }) => {
22+
const msg = buildMessage(text, metadata[rule]);
23+
24+
return endLine === undefined
25+
? `::${severity} file=${source},line=${line},col=${column},title=${title}::${msg}`
26+
: `::${severity} file=${source},line=${line},col=${column},endLine=${endLine},endColumn=${endColumn},title=${title}::${msg}`;
27+
});
28+
});
29+
30+
lines.push('');
31+
32+
return lines.join('\n');
33+
}
34+
35+
/**
36+
* @param {string} msg
37+
* @param {Partial<import('stylelint').RuleMeta> | undefined} metadata
38+
* @returns {string}
39+
*/
40+
function buildMessage(msg, metadata) {
41+
if (!metadata) {
42+
return msg;
43+
}
44+
45+
const url = metadata.url ? ` - ${metadata.url}` : '';
46+
47+
let additional = [
48+
metadata.fixable ? 'maybe fixable' : '',
49+
metadata.deprecated ? 'deprecated' : '',
50+
]
51+
.filter(Boolean)
52+
.join(', ');
53+
54+
additional = additional ? ` [${additional}]` : '';
55+
56+
return `${msg}${additional}${url}`;
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { spawn } from 'node:child_process';
2+
import { platform } from 'node:process';
3+
import assert from 'node:assert';
4+
5+
{
6+
const child = spawn(
7+
'npx',
8+
[
9+
'stylelint',
10+
'--stdin',
11+
'--stdin-filename',
12+
'/example.css',
13+
'--custom-formatter',
14+
'@csstools/stylelint-formatter-github',
15+
],
16+
{
17+
shell: platform === 'win32',
18+
},
19+
);
20+
21+
let stdoutBuffer = '';
22+
let stderrBuffer = '';
23+
24+
child.stdout.setEncoding('utf8');
25+
child.stdout.on('data', (data) => {
26+
data = data.toString();
27+
stdoutBuffer += data;
28+
});
29+
30+
child.stderr.setEncoding('utf8');
31+
child.stderr.on('data', (data) => {
32+
data = data.toString();
33+
stderrBuffer += data;
34+
});
35+
36+
child.stdin.write(`
37+
a {}
38+
b {
39+
color: red;
40+
color: blue;
41+
}
42+
`);
43+
child.stdin.end();
44+
45+
child.on('close', () => {
46+
assert.strictEqual(
47+
stdoutBuffer,
48+
'',
49+
);
50+
51+
assert.strictEqual(
52+
stderrBuffer,
53+
'::warning file=/example.css,line=3,col=3,endLine=6,endColumn=4,title=Stylelint problem::Expected empty line before rule (rule-empty-line-before) [maybe fixable] - https://stylelint.io/user-guide/rules/rule-empty-line-before\n' +
54+
'::error file=/example.css,line=4,col=4,endLine=4,endColumn=9,title=Stylelint problem::Unexpected duplicate "color" (declaration-block-no-duplicate-properties) [maybe fixable] - https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties\n',
55+
);
56+
});
57+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { testPack } from '@csstools/pack-test';
2+
3+
await testPack('@csstools/stylelint-formatter-github');
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "@csstools/stylelint-formatter-github",
3+
"description": "Format Stylelint errors as GitHub annotations",
4+
"version": "0.0.0",
5+
"contributors": [
6+
{
7+
"name": "Antonio Laguna",
8+
"email": "antonio@laguna.es",
9+
"url": "https://antonio.laguna.es"
10+
},
11+
{
12+
"name": "Romain Menke",
13+
"email": "romainmenke@gmail.com"
14+
}
15+
],
16+
"license": "MIT-0",
17+
"funding": [
18+
{
19+
"type": "github",
20+
"url": "https://github.com/sponsors/csstools"
21+
},
22+
{
23+
"type": "opencollective",
24+
"url": "https://opencollective.com/csstools"
25+
}
26+
],
27+
"engines": {
28+
"node": "^14 || ^16 || >=18"
29+
},
30+
"type": "module",
31+
"main": "index.mjs",
32+
"files": [
33+
"LICENSE.md",
34+
"README.md",
35+
"index.mjs",
36+
"preprocess-warnings.mjs"
37+
],
38+
"peerDependencies": {
39+
"stylelint": "^16.6.0"
40+
},
41+
"devDependencies": {
42+
"@csstools/pack-test": "^0.0.0",
43+
"stylelint": "^16.6.0"
44+
},
45+
"scripts": {
46+
"lint": "node ../../.github/bin/format-package-json.mjs",
47+
"test": "node --test"
48+
},
49+
"homepage": "https://github.com/csstools/postcss-plugins/blob/main/plugins-stylelint/formatter-github#readme",
50+
"repository": {
51+
"type": "git",
52+
"url": "git+https://github.com/csstools/postcss-plugins.git",
53+
"directory": "plugins-stylelint/formatter-github"
54+
},
55+
"bugs": "https://github.com/csstools/postcss-plugins/issues",
56+
"keywords": [
57+
"github",
58+
"stylelint",
59+
"stylelint-formatter"
60+
],
61+
"volta": {
62+
"extends": "../../package.json"
63+
}
64+
}

0 commit comments

Comments
 (0)