Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add XO linter #98

Merged
merged 1 commit into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions .eslintrc.yaml

This file was deleted.

14 changes: 5 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,10 @@ jobs:
os:
- ubuntu-latest
node-version:
- 12.22.0
LitoMore marked this conversation as resolved.
Show resolved Hide resolved
- 14.17.0
- 16.0.0
- 18.0.0
- 20.0.0
- 12
- 14
- 16
- 18
- 19
- 20
include:
- os: macos-latest
Expand All @@ -52,9 +46,11 @@ jobs:
with:
cache: npm
node-version: ${{ matrix.node-version }}
- name: Install compatible npm version
run: npm install --global [email protected]
- name: Use specific version of npm for Node.js 12 & 14
if: matrix.node-version < 16
run: npm install -g npm@8
- name: Install dependencies
run: npm ci
# Use `--engine-strict=false` to ignore engine errors from XO dependencies
run: npm ci --engine-strict=false
- name: Test
run: npm test
43 changes: 43 additions & 0 deletions .xo-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"prettier": true,
"space": 4,
"plugins": ["import"],
"ignores": [
"test/projects/broken/broken-svglint-config.js"
],
"rules": {
"unicorn/prefer-event-target": "warn",
"n/file-extension-in-import": "off",
"sort-imports": [
"error",
{
"ignoreCase": false,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
"allowSeparatedGroups": false
}
],
"import/no-named-as-default": "off",
"import/extensions": "off",
"import/order": [
"error",
{
"groups": ["builtin", "external", "parent", "sibling", "index"],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
},
"warnOnUnassignedImports": true,
"newlines-between": "never"
}
],
"no-console": ["error", { "allow": ["warn", "error"] }]
},
"overrides": [
{
"files": ["test/**/*"],
"envs": "mocha"
}
]
}
172 changes: 90 additions & 82 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
* @fileoverview The CLI that is executed from a terminal.
* Acts as an interface to the JS API
*/
import path from "path";
import process from "process";
import gui from "../src/cli/gui.js";
import Logger from "../src/lib/logger.js";
import SVGLint from "../src/svglint.js";
import path from 'node:path';
import process from 'node:process';
import glob from 'glob';
import meow from 'meow';
import {loadConfigurationFile} from '../src/cli/config.js';
import GUI from '../src/cli/gui.js';
import {chalk} from '../src/cli/util.js';
import logging from '../src/lib/logger.js';
import SVGLint from '../src/svglint.js';
// @ts-ignore
import { loadConfigurationFile } from "../src/cli/config.js";
import meow from "meow";
import { chalk } from "../src/cli/util.js";
import glob from "glob";

const GUI = new gui();
const gui = new GUI();

const logger = Logger("");
const logger = logging('');

const EXIT_CODES = Object.freeze({
success: 0,
Expand All @@ -26,147 +26,155 @@ const EXIT_CODES = Object.freeze({
configuration: 4,
});

// used by meow's loud reject
// eslint-disable-next-line no-console
// Used by meow's loud reject

console.error = logger.error.bind(logger);

// Pretty logs all errors, then exits
process.on("uncaughtException", err => {
logger.error(err);
process.on('uncaughtException', (error) => {
logger.error(error);
process.exit(EXIT_CODES.unexpected);
});

// Handle SIGINT
process.on("SIGINT", () => {
process.on('SIGINT', () => {
process.exit(EXIT_CODES.interrupted);
});

// Generates the CLI binding using meow
const cli = meow(`
${chalk.yellow("Usage:")}
${chalk.bold("svglint")} [--config config.js] [--ci] [--debug] ${chalk.bold("file1.svg file2.svg")}
${chalk.bold("svglint")} --stdin [--config config.js] [--ci] [--debug] < ${chalk.bold("file1.svg")}

${chalk.yellow("Options:")}
${chalk.bold("--help")} Display this help text
${chalk.bold("--version")} Show the current SVGLint version
${chalk.bold("--config, -c")} Specify the config file. Defaults to '.svglintrc.js'
${chalk.bold("--debug, -d")} Show debug logs
${chalk.bold("--ci, -C")} Only output to stdout once, when linting is finished
${chalk.bold("--stdin")} Read an SVG from stdin`, {
importMeta: import.meta,
flags: {
config: { type: "string", alias: "c", },
debug: { type: "boolean", alias: "d" },
ci: { type: "boolean", alias: "C" },
stdin: { type: "boolean" }
}
});

process.on("exit", () => {
GUI.finish();
const cli = meow(
`
${chalk.yellow('Usage:')}
${chalk.bold('svglint')} [--config config.js] [--ci] [--debug] ${chalk.bold('file1.svg file2.svg')}
${chalk.bold('svglint')} --stdin [--config config.js] [--ci] [--debug] < ${chalk.bold('file1.svg')}

${chalk.yellow('Options:')}
${chalk.bold('--help')} Display this help text
${chalk.bold('--version')} Show the current SVGLint version
${chalk.bold('--config, -c')} Specify the config file. Defaults to '.svglintrc.js'
${chalk.bold('--debug, -d')} Show debug logs
${chalk.bold('--ci, -C')} Only output to stdout once, when linting is finished
${chalk.bold('--stdin')} Read an SVG from stdin`,
{
importMeta: import.meta,
flags: {
config: {type: 'string', alias: 'c'},
debug: {type: 'boolean', alias: 'd'},
ci: {type: 'boolean', alias: 'C'},
stdin: {type: 'boolean'},
},
},
);

process.on('exit', () => {
gui.finish();
});

/** CLI main function */
(async function(){
// eslint-disable-next-line unicorn/prefer-top-level-await
(async function () {
if (cli.flags.debug) {
Logger.setLevel(Logger.LEVELS.debug);
logging.setLevel(logging.LEVELS.debug);
}
GUI.setCI(cli.flags.ci);

// load the config
let configObj;
gui.setCI(cli.flags.ci);

// Load the config
let configObject;
try {
configObj = await loadConfigurationFile(cli.flags.config);
if (configObj === null) {
logger.debug("No configuration file found");
configObject = await loadConfigurationFile(cli.flags.config);
if (configObject === null) {
logger.debug('No configuration file found');
if (cli.flags.config) {
logger.error("Configuration file not found");
logger.error('Configuration file not found');
process.exit(EXIT_CODES.configuration);
} else {
configObj = {};
configObject = {};
}
} else if (configObj === undefined) {
logger.error("Default export missing from configuration file (use `export default {...}` or `module.exports = {...}`)");
} else if (configObject === undefined) {
logger.error(
'Default export missing from configuration file (use `export default {...}` or `module.exports = {...}`)',
);
process.exit(EXIT_CODES.configuration);
}
} catch (e) {
logger.error(`Failed to parse config: ${e.stack}`);
} catch (error) {
logger.error(`Failed to parse config: ${error.stack}`);
process.exit(EXIT_CODES.configuration);
}

if (cli.flags.stdin) {
// lint what's provided on stdin
// Lint what's provided on stdin
const chunks = [];

process.stdin.on("readable", () => {
process.stdin.on('readable', () => {
let chunk;
while (null !== (chunk = process.stdin.read())) {
while ((chunk = process.stdin.read()) !== null) {
chunks.push(chunk);
}
});

process.stdin.on("end", () => {
SVGLint.lintSource(chunks.join(""), configObj)
.then(linting => {
// handle case where linting failed (e.g. invalid file)
process.stdin.on('end', () => {
SVGLint.lintSource(chunks.join(''), configObject)
.then((linting) => {
// Handle case where linting failed (e.g. invalid file)
if (!linting) {
process.exit(EXIT_CODES.success);
}

// otherwise add it to GUI and wait for it to finish
GUI.addLinting(linting);
linting.on("done", () => {
// Otherwise add it to GUI and wait for it to finish
gui.addLinting(linting);
linting.on('done', () => {
if (linting.state === linting.STATES.error) {
process.exit(EXIT_CODES.violations);
} else {
process.exit(EXIT_CODES.success);
}
});
})
.catch(e => {
logger.error("Failed to lint\n", e);
.catch((error) => {
logger.error('Failed to lint\n', error);
});
});
} else {
// lint all the CLI specified files
// Lint all the CLI specified files
const files = cli.input
.map(v => glob.sync(v))
.reduce((a, v) => a.concat(v), [])
.map(v => path.resolve(process.cwd(), v));
// keep track so we know when every linting has finished
.flatMap((v) => glob.sync(v))
.map((v) => path.resolve(process.cwd(), v));
// Keep track so we know when every linting has finished
let hasErrors = false;
let activeLintings = files.length;
const onLintingDone = () => {
--activeLintings;
logger.debug("Linting done,", activeLintings, "to go");
logger.debug('Linting done,', activeLintings, 'to go');
if (activeLintings <= 0) {
process.exit(
hasErrors ? EXIT_CODES.violations : EXIT_CODES.success
hasErrors ? EXIT_CODES.violations : EXIT_CODES.success,
);
}
};
files.forEach(filePath => {
SVGLint.lintFile(filePath, configObj)
.then(linting => {
// handle case where linting failed (e.g. invalid file)

for (const filePath of files) {
SVGLint.lintFile(filePath, configObject)
.then((linting) => {
// Handle case where linting failed (e.g. invalid file)
if (!linting) {
onLintingDone();
return;
}

// otherwise add it to GUI and wait for it to finish
GUI.addLinting(linting);
linting.on("done", () => {
// Otherwise add it to GUI and wait for it to finish
gui.addLinting(linting);
linting.on('done', () => {
if (linting.state === linting.STATES.error) {
hasErrors = true;
}

onLintingDone();
});
})
.catch(e => {
logger.error("Failed to lint file", filePath, "\n", e);
.catch((error) => {
logger.error('Failed to lint file', filePath, '\n', error);
});
});
}
}
})();
Loading