@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 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
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
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();