@codebend3r/gale 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/bin/aarch64-apple-darwin/gale +0 -0
- package/bin/aarch64-unknown-linux-gnu/gale +0 -0
- package/bin/gale +54 -0
- package/bin/x86_64-apple-darwin/gale +0 -0
- package/bin/x86_64-unknown-linux-gnu/gale +0 -0
- package/index.cjs +60 -0
- package/index.mjs +543 -0
- package/package.json +47 -0
- package/test.mjs +208 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Gale
|
|
2
|
+
|
|
3
|
+
**An extremely fast CSS linter. Drop-in replacement for Stylelint.**
|
|
4
|
+
|
|
5
|
+
100x-400x faster. Same config. Zero migration.
|
|
6
|
+
|
|
7
|
+
> **Compatibility:** Gale targets **Stylelint v17** semantics.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -D @codebend3r/gale
|
|
11
|
+
|
|
12
|
+
# Uses your existing .stylelintrc
|
|
13
|
+
npx gale "src/**/*.css"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Programmatic API
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import { lint, resolveConfig, formatters } from '@codebend3r/gale';
|
|
20
|
+
|
|
21
|
+
const result = await lint({
|
|
22
|
+
files: 'src/**/*.css',
|
|
23
|
+
config: { rules: { 'block-no-empty': true } },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log(result.errored);
|
|
27
|
+
console.log(result.results);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See the full documentation at [github.com/LyricalString/gale](https://github.com/LyricalString/gale).
|
|
Binary file
|
|
Binary file
|
package/bin/gale
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
platform="$(uname -s)-$(uname -m)"
|
|
6
|
+
|
|
7
|
+
case "$platform" in
|
|
8
|
+
Darwin-arm64)
|
|
9
|
+
target="aarch64-apple-darwin"
|
|
10
|
+
;;
|
|
11
|
+
Darwin-x86_64)
|
|
12
|
+
target="x86_64-apple-darwin"
|
|
13
|
+
;;
|
|
14
|
+
Linux-aarch64|Linux-arm64)
|
|
15
|
+
target="aarch64-unknown-linux-gnu"
|
|
16
|
+
;;
|
|
17
|
+
Linux-x86_64)
|
|
18
|
+
target="x86_64-unknown-linux-gnu"
|
|
19
|
+
;;
|
|
20
|
+
*)
|
|
21
|
+
echo "Unsupported platform: $platform" >&2
|
|
22
|
+
echo "Supported: Darwin-arm64, Darwin-x86_64, Linux-aarch64, Linux-x86_64" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
script="$0"
|
|
28
|
+
|
|
29
|
+
while [ -L "$script" ]; do
|
|
30
|
+
script_dir="$(CDPATH= cd "$(dirname "$script")" && pwd)"
|
|
31
|
+
link="$(readlink "$script")"
|
|
32
|
+
|
|
33
|
+
case "$link" in
|
|
34
|
+
/*)
|
|
35
|
+
script="$link"
|
|
36
|
+
;;
|
|
37
|
+
*)
|
|
38
|
+
script="$script_dir/$link"
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
bin_dir="$(CDPATH= cd "$(dirname "$script")" && pwd)"
|
|
44
|
+
binary="$bin_dir/$target/gale"
|
|
45
|
+
|
|
46
|
+
if [ ! -x "$binary" ]; then
|
|
47
|
+
echo "Gale binary missing for $target at $binary" >&2
|
|
48
|
+
echo "This package should ship prebuilt binaries for $target." >&2
|
|
49
|
+
echo "Try reinstalling the package, or build from source:" >&2
|
|
50
|
+
echo " cargo install gale-lint" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
exec "$binary" "$@"
|
|
Binary file
|
|
Binary file
|
package/index.cjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// CommonJS entry point.
|
|
2
|
+
//
|
|
3
|
+
// The implementation lives in index.mjs (ESM). CommonJS cannot `require()` an
|
|
4
|
+
// ES module synchronously, so this file bridges to it with a dynamic import.
|
|
5
|
+
// Every bridged function is already async, which makes the bridge transparent.
|
|
6
|
+
//
|
|
7
|
+
// This file also exists so that tools like `resolve-bin` and `require.resolve()`
|
|
8
|
+
// can locate the package.
|
|
9
|
+
|
|
10
|
+
let modulePromise;
|
|
11
|
+
|
|
12
|
+
function load() {
|
|
13
|
+
if (!modulePromise) {
|
|
14
|
+
modulePromise = import("./index.mjs");
|
|
15
|
+
}
|
|
16
|
+
return modulePromise;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lint CSS files or code. See index.mjs for the full option list.
|
|
21
|
+
* @returns {Promise<object>} a Stylelint-compatible LinterResult
|
|
22
|
+
*/
|
|
23
|
+
async function lint(options) {
|
|
24
|
+
return (await load()).lint(options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the effective config for a file path.
|
|
29
|
+
* @returns {Promise<object|undefined>}
|
|
30
|
+
*/
|
|
31
|
+
async function resolveConfig(filePath, options) {
|
|
32
|
+
return (await load()).resolveConfig(filePath, options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Stub for Stylelint's createPlugin(). Gale uses built-in Rust rules.
|
|
37
|
+
* Kept synchronous to match Stylelint's signature.
|
|
38
|
+
*/
|
|
39
|
+
function createPlugin(ruleName, ruleFunction) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`[gale] createPlugin("${ruleName}"): Gale uses built-in rules instead of JS plugins. ` +
|
|
42
|
+
"This plugin will not be executed.",
|
|
43
|
+
);
|
|
44
|
+
return { ruleName, rule: ruleFunction };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Stylelint exposes each formatter as a promise-returning getter.
|
|
48
|
+
const FORMATTER_NAMES = ["json", "string", "compact", "verbose", "tap", "unix"];
|
|
49
|
+
const formatters = {};
|
|
50
|
+
for (const name of FORMATTER_NAMES) {
|
|
51
|
+
Object.defineProperty(formatters, name, {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
get() {
|
|
54
|
+
return load().then((m) => m.formatters[name]);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { lint, resolveConfig, createPlugin, formatters };
|
|
60
|
+
module.exports.default = module.exports;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @codebend3r/gale — Stylelint-compatible programmatic API
|
|
3
|
+
*
|
|
4
|
+
* This module wraps the native Gale binary and exposes the same API surface
|
|
5
|
+
* as `stylelint.lint()`, `stylelint.formatters`, and `stylelint.resolveConfig`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { existsSync, mkdtempSync, writeFileSync, unlinkSync, rmSync } from "node:fs";
|
|
10
|
+
import { join, dirname, resolve } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Binary resolution
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function findBinary() {
|
|
23
|
+
// 1. Check the bin/ directory within the npm package
|
|
24
|
+
const localBin = join(__dirname, "bin", "gale");
|
|
25
|
+
if (existsSync(localBin)) {
|
|
26
|
+
return localBin;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Fall back to gale on PATH
|
|
30
|
+
return "gale";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Spawn helper
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Spawn the gale binary and return { stdout, stderr, exitCode }.
|
|
39
|
+
* If `stdinData` is provided it is piped to the process.
|
|
40
|
+
*/
|
|
41
|
+
function runGale(args, { stdinData, cwd } = {}) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const bin = findBinary();
|
|
44
|
+
const proc = spawn(bin, args, {
|
|
45
|
+
cwd,
|
|
46
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
47
|
+
env: { ...process.env },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const stdoutChunks = [];
|
|
51
|
+
const stderrChunks = [];
|
|
52
|
+
|
|
53
|
+
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
54
|
+
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
55
|
+
|
|
56
|
+
proc.on("error", (err) => {
|
|
57
|
+
if (err.code === "ENOENT") {
|
|
58
|
+
reject(
|
|
59
|
+
new Error(
|
|
60
|
+
`Gale binary not found. Looked for "${bin}". ` +
|
|
61
|
+
"Install @codebend3r/gale or ensure gale is on your PATH.",
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
reject(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
proc.on("close", (exitCode) => {
|
|
70
|
+
resolve({
|
|
71
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
72
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
73
|
+
exitCode,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (stdinData != null) {
|
|
78
|
+
proc.stdin.write(stdinData);
|
|
79
|
+
proc.stdin.end();
|
|
80
|
+
} else {
|
|
81
|
+
proc.stdin.end();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Temp config helper
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function writeTempConfig(config) {
|
|
91
|
+
const dir = mkdtempSync(join(tmpdir(), "gale-"));
|
|
92
|
+
const file = join(dir, "gale.json");
|
|
93
|
+
writeFileSync(file, JSON.stringify(config, null, 2));
|
|
94
|
+
return { file, dir };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cleanupTempConfig({ file, dir }) {
|
|
98
|
+
try {
|
|
99
|
+
unlinkSync(file);
|
|
100
|
+
rmSync(dir, { recursive: true, force: true });
|
|
101
|
+
} catch {
|
|
102
|
+
// best-effort cleanup
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Parse Gale JSON output into Stylelint-shaped results
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
function parseJsonOutput(jsonString) {
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = JSON.parse(jsonString);
|
|
114
|
+
} catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!Array.isArray(raw)) return [];
|
|
119
|
+
|
|
120
|
+
return raw.map((entry) => ({
|
|
121
|
+
source: entry.source || "",
|
|
122
|
+
warnings: (entry.warnings || []).map((w) => ({
|
|
123
|
+
line: w.line,
|
|
124
|
+
column: w.column,
|
|
125
|
+
rule: w.rule,
|
|
126
|
+
severity: w.severity || "warning",
|
|
127
|
+
text: w.text,
|
|
128
|
+
})),
|
|
129
|
+
deprecations: [],
|
|
130
|
+
invalidOptionWarnings: [],
|
|
131
|
+
parseErrors: [],
|
|
132
|
+
errored: (entry.warnings || []).some((w) => w.severity === "error"),
|
|
133
|
+
ignored: false,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// lint()
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Lint CSS files or code, returning a Stylelint-compatible `LinterResult`.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} options
|
|
145
|
+
* @param {string|string[]} [options.files] - Glob pattern(s) for files to lint
|
|
146
|
+
* @param {string} [options.code] - CSS code string to lint instead of files
|
|
147
|
+
* @param {string} [options.codeFilename] - Virtual filename for `code` (for syntax detection)
|
|
148
|
+
* @param {object} [options.config] - Inline config object
|
|
149
|
+
* @param {string} [options.configFile] - Path to config file
|
|
150
|
+
* @param {boolean|string} [options.fix] - Enable autofix (true, "strict", or "lax")
|
|
151
|
+
* @param {string|Function} [options.formatter] - Formatter name or function
|
|
152
|
+
* @param {boolean} [options.quiet] - Only report errors
|
|
153
|
+
* @param {boolean} [options.cache] - Enable caching
|
|
154
|
+
* @param {string} [options.cacheLocation] - Override cache file location
|
|
155
|
+
* @param {number} [options.maxWarnings] - Max warnings before erroring
|
|
156
|
+
* @param {boolean} [options.allowEmptyInput] - Don't error when no files match
|
|
157
|
+
* @param {string} [options.ignorePath] - Path to a custom ignore file
|
|
158
|
+
* @param {boolean} [options.ignoreDisables] - Ignore all stylelint-disable comments
|
|
159
|
+
* @param {boolean} [options.reportNeedlessDisables] - Report needless disable comments
|
|
160
|
+
* @param {boolean} [options.reportInvalidScopeDisables] - Report invalid-scope disable comments
|
|
161
|
+
* @param {boolean} [options.reportDescriptionlessDisables] - Report descriptionless disable comments
|
|
162
|
+
* @param {string} [options.cwd] - Working directory
|
|
163
|
+
* @returns {Promise<LinterResult>}
|
|
164
|
+
*/
|
|
165
|
+
export async function lint(options = {}) {
|
|
166
|
+
const cwd = options.cwd || process.cwd();
|
|
167
|
+
const args = [];
|
|
168
|
+
let tempConfig = null;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// -- Formatter: always use JSON internally to get structured results --
|
|
172
|
+
args.push("--formatter", "json");
|
|
173
|
+
|
|
174
|
+
// -- Config --
|
|
175
|
+
if (options.config) {
|
|
176
|
+
tempConfig = writeTempConfig(options.config);
|
|
177
|
+
args.push("--config", tempConfig.file);
|
|
178
|
+
} else if (options.configFile) {
|
|
179
|
+
args.push("--config", resolve(cwd, options.configFile));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// -- Fix --
|
|
183
|
+
if (options.fix) {
|
|
184
|
+
if (typeof options.fix === "string") {
|
|
185
|
+
args.push(`--fix=${options.fix}`);
|
|
186
|
+
} else {
|
|
187
|
+
args.push("--fix");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// -- Quiet --
|
|
192
|
+
if (options.quiet) {
|
|
193
|
+
args.push("--quiet");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// -- Cache --
|
|
197
|
+
if (options.cache) {
|
|
198
|
+
args.push("--cache");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// -- Max warnings --
|
|
202
|
+
if (options.maxWarnings != null) {
|
|
203
|
+
args.push("--max-warnings", String(options.maxWarnings));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// -- Cache location --
|
|
207
|
+
if (options.cacheLocation) {
|
|
208
|
+
args.push("--cache-location", resolve(cwd, options.cacheLocation));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// -- Allow empty input --
|
|
212
|
+
if (options.allowEmptyInput) {
|
|
213
|
+
args.push("--allow-empty-input");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// -- Ignore path --
|
|
217
|
+
if (options.ignorePath) {
|
|
218
|
+
args.push("--ignore-path", resolve(cwd, options.ignorePath));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// -- Ignore disables --
|
|
222
|
+
if (options.ignoreDisables) {
|
|
223
|
+
args.push("--ignore-disables");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// -- Report needless disables --
|
|
227
|
+
if (options.reportNeedlessDisables) {
|
|
228
|
+
args.push("--report-needless-disables");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// -- Report invalid scope disables --
|
|
232
|
+
if (options.reportInvalidScopeDisables) {
|
|
233
|
+
args.push("--report-invalid-scope-disables");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// -- Report descriptionless disables --
|
|
237
|
+
if (options.reportDescriptionlessDisables) {
|
|
238
|
+
args.push("--report-descriptionless-disables");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// -- Input source --
|
|
242
|
+
let stdinData = null;
|
|
243
|
+
|
|
244
|
+
if (options.code != null) {
|
|
245
|
+
args.push("--stdin");
|
|
246
|
+
if (options.codeFilename) {
|
|
247
|
+
args.push("--stdin-filename", options.codeFilename);
|
|
248
|
+
}
|
|
249
|
+
stdinData = options.code;
|
|
250
|
+
} else if (options.files) {
|
|
251
|
+
const patterns = Array.isArray(options.files) ? options.files : [options.files];
|
|
252
|
+
args.push(...patterns);
|
|
253
|
+
} else {
|
|
254
|
+
throw new Error(
|
|
255
|
+
'Either "files" or "code" must be provided to lint().',
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const { stdout, stderr, exitCode } = await runGale(args, {
|
|
260
|
+
stdinData,
|
|
261
|
+
cwd,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// When allowEmptyInput is true and no files were found, return an
|
|
265
|
+
// empty successful result instead of propagating any error.
|
|
266
|
+
if (options.allowEmptyInput && !stdout.trim()) {
|
|
267
|
+
return {
|
|
268
|
+
cwd,
|
|
269
|
+
results: [],
|
|
270
|
+
errored: false,
|
|
271
|
+
report: "",
|
|
272
|
+
code: undefined,
|
|
273
|
+
maxWarningsExceeded: undefined,
|
|
274
|
+
ruleMetadata: {},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Parse the JSON results
|
|
279
|
+
const results = parseJsonOutput(stdout);
|
|
280
|
+
|
|
281
|
+
// Determine if any result had errors
|
|
282
|
+
const errored = results.some((r) => r.errored);
|
|
283
|
+
|
|
284
|
+
// Build formatted report if a specific formatter was requested
|
|
285
|
+
let report = stdout;
|
|
286
|
+
if (
|
|
287
|
+
options.formatter &&
|
|
288
|
+
typeof options.formatter === "string" &&
|
|
289
|
+
options.formatter !== "json"
|
|
290
|
+
) {
|
|
291
|
+
// Re-run with the requested formatter for the report string
|
|
292
|
+
const reportArgs = [...args];
|
|
293
|
+
const jsonIdx = reportArgs.indexOf("json");
|
|
294
|
+
if (jsonIdx !== -1) {
|
|
295
|
+
reportArgs[jsonIdx] = options.formatter;
|
|
296
|
+
}
|
|
297
|
+
const reportResult = await runGale(reportArgs, { stdinData, cwd });
|
|
298
|
+
report = reportResult.stdout;
|
|
299
|
+
} else if (typeof options.formatter === "function") {
|
|
300
|
+
report = options.formatter(results, { cwd, results, errored });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Determine fixed code (only when fix + code input)
|
|
304
|
+
let fixedCode;
|
|
305
|
+
if (options.fix && options.code != null) {
|
|
306
|
+
// When --fix + --stdin, gale outputs the fixed source to stdout.
|
|
307
|
+
// We need to re-run with fix to get the fixed code, without JSON formatter.
|
|
308
|
+
const fixArgs = [];
|
|
309
|
+
if (tempConfig) {
|
|
310
|
+
fixArgs.push("--config", tempConfig.file);
|
|
311
|
+
} else if (options.configFile) {
|
|
312
|
+
fixArgs.push("--config", resolve(cwd, options.configFile));
|
|
313
|
+
}
|
|
314
|
+
fixArgs.push("--fix", "--stdin");
|
|
315
|
+
if (options.codeFilename) {
|
|
316
|
+
fixArgs.push("--stdin-filename", options.codeFilename);
|
|
317
|
+
}
|
|
318
|
+
const fixResult = await runGale(fixArgs, { stdinData: options.code, cwd });
|
|
319
|
+
fixedCode = fixResult.stdout;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Max warnings check
|
|
323
|
+
let maxWarningsExceeded;
|
|
324
|
+
if (options.maxWarnings != null) {
|
|
325
|
+
const totalWarnings = results.reduce(
|
|
326
|
+
(sum, r) =>
|
|
327
|
+
sum + r.warnings.filter((w) => w.severity === "warning").length,
|
|
328
|
+
0,
|
|
329
|
+
);
|
|
330
|
+
if (totalWarnings > options.maxWarnings) {
|
|
331
|
+
maxWarningsExceeded = {
|
|
332
|
+
maxWarnings: options.maxWarnings,
|
|
333
|
+
foundWarnings: totalWarnings,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
cwd,
|
|
340
|
+
results,
|
|
341
|
+
errored,
|
|
342
|
+
report,
|
|
343
|
+
code: fixedCode,
|
|
344
|
+
maxWarningsExceeded,
|
|
345
|
+
ruleMetadata: {},
|
|
346
|
+
};
|
|
347
|
+
} finally {
|
|
348
|
+
if (tempConfig) {
|
|
349
|
+
cleanupTempConfig(tempConfig);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// formatters
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Create a formatter function that calls gale with the given format name.
|
|
360
|
+
*/
|
|
361
|
+
function createFormatterFn(formatName) {
|
|
362
|
+
return async (results, returnValue) => {
|
|
363
|
+
// Build a minimal JSON array to pipe to gale for re-formatting.
|
|
364
|
+
// Since gale reads files (not a JSON stream), we format client-side
|
|
365
|
+
// for the simple built-in formats.
|
|
366
|
+
if (formatName === "json") {
|
|
367
|
+
return JSON.stringify(
|
|
368
|
+
results.map((r) => ({
|
|
369
|
+
source: r.source,
|
|
370
|
+
warnings: r.warnings,
|
|
371
|
+
})),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// For other formatters, produce a simple text representation
|
|
376
|
+
// matching what gale would output.
|
|
377
|
+
let output = "";
|
|
378
|
+
|
|
379
|
+
if (formatName === "compact") {
|
|
380
|
+
for (const r of results) {
|
|
381
|
+
for (const w of r.warnings) {
|
|
382
|
+
output += `${r.source}: line ${w.line}, col ${w.column}, ${w.severity} - ${w.text}\n`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return output;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (formatName === "tap") {
|
|
389
|
+
output += "TAP version 13\n";
|
|
390
|
+
output += `1..${results.length}\n`;
|
|
391
|
+
results.forEach((r, i) => {
|
|
392
|
+
if (r.warnings.length === 0) {
|
|
393
|
+
output += `ok ${i + 1} - ${r.source}\n`;
|
|
394
|
+
} else {
|
|
395
|
+
output += `not ok ${i + 1} - ${r.source}\n`;
|
|
396
|
+
for (const w of r.warnings) {
|
|
397
|
+
output += ` ---\n`;
|
|
398
|
+
output += ` message: "${w.text}"\n`;
|
|
399
|
+
output += ` severity: ${w.severity}\n`;
|
|
400
|
+
output += ` data:\n`;
|
|
401
|
+
output += ` line: ${w.line}\n`;
|
|
402
|
+
output += ` column: ${w.column}\n`;
|
|
403
|
+
output += ` ruleId: ${w.rule}\n`;
|
|
404
|
+
output += ` ...\n`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
return output;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (formatName === "unix") {
|
|
412
|
+
for (const r of results) {
|
|
413
|
+
for (const w of r.warnings) {
|
|
414
|
+
output += `${r.source}:${w.line}:${w.column}: ${w.text} [${w.severity}]\n`;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const total = results.reduce((s, r) => s + r.warnings.length, 0);
|
|
418
|
+
if (total > 0) {
|
|
419
|
+
output += `\n${total} problem${total === 1 ? "" : "s"}\n`;
|
|
420
|
+
}
|
|
421
|
+
return output;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// "string" / "verbose" / default: human-readable
|
|
425
|
+
for (const r of results) {
|
|
426
|
+
if (r.warnings.length === 0) continue;
|
|
427
|
+
output += `${r.source}\n`;
|
|
428
|
+
for (const w of r.warnings) {
|
|
429
|
+
const icon = w.severity === "error" ? "\u2716" : "\u26A0";
|
|
430
|
+
output += ` ${w.line}:${w.column} ${icon} ${w.text} ${w.rule}\n`;
|
|
431
|
+
}
|
|
432
|
+
output += "\n";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const totalErrors = results.reduce(
|
|
436
|
+
(s, r) => s + r.warnings.filter((w) => w.severity === "error").length,
|
|
437
|
+
0,
|
|
438
|
+
);
|
|
439
|
+
const totalWarnings = results.reduce(
|
|
440
|
+
(s, r) => s + r.warnings.filter((w) => w.severity === "warning").length,
|
|
441
|
+
0,
|
|
442
|
+
);
|
|
443
|
+
const total = totalErrors + totalWarnings;
|
|
444
|
+
if (total > 0) {
|
|
445
|
+
const p = total === 1 ? "problem" : "problems";
|
|
446
|
+
const e = totalErrors === 1 ? "error" : "errors";
|
|
447
|
+
const w = totalWarnings === 1 ? "warning" : "warnings";
|
|
448
|
+
output += `\u2716 ${total} ${p} (${totalErrors} ${e}, ${totalWarnings} ${w})\n`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return output;
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Lazy promise-based formatters matching Stylelint's API.
|
|
457
|
+
* Each getter returns a Promise<Function>.
|
|
458
|
+
*/
|
|
459
|
+
export const formatters = {
|
|
460
|
+
get json() {
|
|
461
|
+
return Promise.resolve(createFormatterFn("json"));
|
|
462
|
+
},
|
|
463
|
+
get string() {
|
|
464
|
+
return Promise.resolve(createFormatterFn("string"));
|
|
465
|
+
},
|
|
466
|
+
get compact() {
|
|
467
|
+
return Promise.resolve(createFormatterFn("compact"));
|
|
468
|
+
},
|
|
469
|
+
get verbose() {
|
|
470
|
+
return Promise.resolve(createFormatterFn("verbose"));
|
|
471
|
+
},
|
|
472
|
+
get tap() {
|
|
473
|
+
return Promise.resolve(createFormatterFn("tap"));
|
|
474
|
+
},
|
|
475
|
+
get unix() {
|
|
476
|
+
return Promise.resolve(createFormatterFn("unix"));
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// resolveConfig()
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Resolve the effective config for a given file path.
|
|
486
|
+
*
|
|
487
|
+
* @param {string} filePath - The file to resolve config for
|
|
488
|
+
* @param {object} [options]
|
|
489
|
+
* @param {string} [options.configFile] - Explicit config file to use
|
|
490
|
+
* @param {string} [options.cwd] - Working directory
|
|
491
|
+
* @returns {Promise<object|undefined>}
|
|
492
|
+
*/
|
|
493
|
+
export async function resolveConfig(filePath, options = {}) {
|
|
494
|
+
const cwd = options.cwd || process.cwd();
|
|
495
|
+
const args = ["--print-config", filePath];
|
|
496
|
+
|
|
497
|
+
if (options.configFile) {
|
|
498
|
+
args.push("--config", resolve(cwd, options.configFile));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const { stdout, exitCode } = await runGale(args, { cwd });
|
|
503
|
+
|
|
504
|
+
if (exitCode !== 0 || !stdout.trim()) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return JSON.parse(stdout);
|
|
509
|
+
} catch {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
// createPlugin() — compatibility stub
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Stub for Stylelint's `createPlugin()` API.
|
|
520
|
+
* Gale uses built-in Rust rules instead of JS plugins.
|
|
521
|
+
*
|
|
522
|
+
* @param {string} ruleName
|
|
523
|
+
* @param {Function} ruleFunction
|
|
524
|
+
* @returns {{ ruleName: string, rule: Function }}
|
|
525
|
+
*/
|
|
526
|
+
export function createPlugin(ruleName, ruleFunction) {
|
|
527
|
+
console.warn(
|
|
528
|
+
`[gale] createPlugin("${ruleName}"): Gale uses built-in rules instead of JS plugins. ` +
|
|
529
|
+
"This plugin will not be executed.",
|
|
530
|
+
);
|
|
531
|
+
return { ruleName, rule: ruleFunction };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
// Default export (Stylelint compat)
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
export default {
|
|
539
|
+
lint,
|
|
540
|
+
formatters,
|
|
541
|
+
resolveConfig,
|
|
542
|
+
createPlugin,
|
|
543
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codebend3r/gale",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "An extremely fast CSS linter, written in Rust. Drop-in replacement for Stylelint.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/LyricalString/gale"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/LyricalString/gale",
|
|
11
|
+
"bugs": "https://github.com/LyricalString/gale/issues",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"css",
|
|
14
|
+
"linter",
|
|
15
|
+
"stylelint",
|
|
16
|
+
"scss",
|
|
17
|
+
"less",
|
|
18
|
+
"rust",
|
|
19
|
+
"fast",
|
|
20
|
+
"lint",
|
|
21
|
+
"gale"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "index.mjs",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./index.mjs",
|
|
28
|
+
"require": "./index.cjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"gale": "bin/gale"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"index.cjs",
|
|
37
|
+
"index.mjs",
|
|
38
|
+
"test.mjs",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=16.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "node test.mjs"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/test.mjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Basic smoke tests for the @codebend3r/gale programmatic API.
|
|
5
|
+
*
|
|
6
|
+
* Run: node test.mjs
|
|
7
|
+
*
|
|
8
|
+
* Requires a working gale binary (either in npm/bin/ or on PATH).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { lint, formatters, resolveConfig, createPlugin } from "./index.mjs";
|
|
12
|
+
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
|
|
16
|
+
function assert(condition, message) {
|
|
17
|
+
if (condition) {
|
|
18
|
+
console.log(` PASS: ${message}`);
|
|
19
|
+
passed++;
|
|
20
|
+
} else {
|
|
21
|
+
console.error(` FAIL: ${message}`);
|
|
22
|
+
failed++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Test 1: lint({ code }) with empty block
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
async function testLintCodeEmptyBlock() {
|
|
31
|
+
console.log("\nTest 1: lint({ code: 'a {}' })");
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await lint({ code: "a {}" });
|
|
35
|
+
|
|
36
|
+
assert(result != null, "result is not null");
|
|
37
|
+
assert(typeof result.cwd === "string", "result.cwd is a string");
|
|
38
|
+
assert(Array.isArray(result.results), "result.results is an array");
|
|
39
|
+
assert(typeof result.errored === "boolean", "result.errored is a boolean");
|
|
40
|
+
assert(typeof result.report === "string", "result.report is a string");
|
|
41
|
+
assert(typeof result.ruleMetadata === "object", "result.ruleMetadata is an object");
|
|
42
|
+
|
|
43
|
+
if (result.results.length > 0) {
|
|
44
|
+
const first = result.results[0];
|
|
45
|
+
assert(typeof first.source === "string", "first result has source");
|
|
46
|
+
assert(Array.isArray(first.warnings), "first result has warnings array");
|
|
47
|
+
assert(Array.isArray(first.deprecations), "first result has deprecations array");
|
|
48
|
+
assert(Array.isArray(first.parseErrors), "first result has parseErrors array");
|
|
49
|
+
assert(typeof first.errored === "boolean", "first result has errored boolean");
|
|
50
|
+
assert(typeof first.ignored === "boolean", "first result has ignored boolean");
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(` ERROR: ${err.message}`);
|
|
54
|
+
failed++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Test 2: lint({ code, config }) with color-named rule
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
async function testLintCodeWithConfig() {
|
|
63
|
+
console.log("\nTest 2: lint({ code: 'a { color: pink; }', config: { rules: { 'color-named': 'never' } } })");
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await lint({
|
|
67
|
+
code: "a { color: pink; }",
|
|
68
|
+
config: { rules: { "color-named": "never" } },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert(result != null, "result is not null");
|
|
72
|
+
assert(Array.isArray(result.results), "result.results is an array");
|
|
73
|
+
|
|
74
|
+
if (result.results.length > 0) {
|
|
75
|
+
const warnings = result.results[0].warnings;
|
|
76
|
+
assert(Array.isArray(warnings), "warnings is an array");
|
|
77
|
+
|
|
78
|
+
if (warnings.length > 0) {
|
|
79
|
+
const w = warnings[0];
|
|
80
|
+
assert(typeof w.line === "number", "warning has line number");
|
|
81
|
+
assert(typeof w.column === "number", "warning has column number");
|
|
82
|
+
assert(typeof w.rule === "string", "warning has rule name");
|
|
83
|
+
assert(typeof w.severity === "string", "warning has severity");
|
|
84
|
+
assert(typeof w.text === "string", "warning has text");
|
|
85
|
+
assert(
|
|
86
|
+
w.rule === "color-named",
|
|
87
|
+
`warning rule is "color-named" (got "${w.rule}")`,
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
console.log(" INFO: No warnings returned (gale may not flag this without config).");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(` ERROR: ${err.message}`);
|
|
95
|
+
failed++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Test 3: resolveConfig
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
async function testResolveConfig() {
|
|
104
|
+
console.log("\nTest 3: resolveConfig('test.css')");
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const config = await resolveConfig("test.css");
|
|
108
|
+
// May be undefined if no config file is found in the directory
|
|
109
|
+
assert(
|
|
110
|
+
config === undefined || typeof config === "object",
|
|
111
|
+
"resolveConfig returns object or undefined",
|
|
112
|
+
);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(` ERROR: ${err.message}`);
|
|
115
|
+
failed++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Test 4: formatters.json resolves to a function
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
async function testFormattersJson() {
|
|
124
|
+
console.log("\nTest 4: formatters.json resolves to a function");
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const jsonFormatter = await formatters.json;
|
|
128
|
+
assert(typeof jsonFormatter === "function", "formatters.json resolves to a function");
|
|
129
|
+
|
|
130
|
+
// Test it works
|
|
131
|
+
const output = await jsonFormatter(
|
|
132
|
+
[
|
|
133
|
+
{
|
|
134
|
+
source: "test.css",
|
|
135
|
+
warnings: [
|
|
136
|
+
{ line: 1, column: 1, rule: "test-rule", severity: "warning", text: "test" },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
{},
|
|
141
|
+
);
|
|
142
|
+
assert(typeof output === "string", "formatter returns a string");
|
|
143
|
+
|
|
144
|
+
const parsed = JSON.parse(output);
|
|
145
|
+
assert(Array.isArray(parsed), "JSON formatter output is parseable as array");
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(` ERROR: ${err.message}`);
|
|
148
|
+
failed++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Test 5: createPlugin stub
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
async function testCreatePlugin() {
|
|
157
|
+
console.log("\nTest 5: createPlugin returns stub");
|
|
158
|
+
|
|
159
|
+
const plugin = createPlugin("my-rule", () => {});
|
|
160
|
+
assert(plugin.ruleName === "my-rule", 'plugin.ruleName is "my-rule"');
|
|
161
|
+
assert(typeof plugin.rule === "function", "plugin.rule is a function");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Test 6: LinterResult shape
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
async function testLinterResultShape() {
|
|
169
|
+
console.log("\nTest 6: LinterResult has correct shape");
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await lint({ code: "a { color: red; }" });
|
|
173
|
+
|
|
174
|
+
assert("cwd" in result, "result has cwd");
|
|
175
|
+
assert("results" in result, "result has results");
|
|
176
|
+
assert("errored" in result, "result has errored");
|
|
177
|
+
assert("report" in result, "result has report");
|
|
178
|
+
assert("ruleMetadata" in result, "result has ruleMetadata");
|
|
179
|
+
assert("maxWarningsExceeded" in result, "result has maxWarningsExceeded key");
|
|
180
|
+
assert("code" in result, "result has code key");
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(` ERROR: ${err.message}`);
|
|
183
|
+
failed++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Run all tests
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
async function main() {
|
|
192
|
+
console.log("=== @codebend3r/gale programmatic API tests ===");
|
|
193
|
+
|
|
194
|
+
await testLintCodeEmptyBlock();
|
|
195
|
+
await testLintCodeWithConfig();
|
|
196
|
+
await testResolveConfig();
|
|
197
|
+
await testFormattersJson();
|
|
198
|
+
await testCreatePlugin();
|
|
199
|
+
await testLinterResultShape();
|
|
200
|
+
|
|
201
|
+
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
|
202
|
+
|
|
203
|
+
if (failed > 0) {
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main();
|