@softvisio/cli
Version:
Softisio CLI Tool
1,240 lines (1,015 loc) • 34 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import PrettierPluginOxc from "@prettier/plugin-oxc";
import PrettierPluginXml from "@prettier/plugin-xml";
import editorconfig from "editorconfig";
import { ESLint as EsLint } from "eslint";
import prettier from "prettier";
import PrettierPluginPackageJson from "prettier-plugin-packagejson";
import * as PrettierPluginSh from "prettier-plugin-sh";
import ansi from "#core/ansi";
import File from "#core/file";
import * as json5 from "#core/json5";
import mime from "#core/mime";
import Table from "#core/text/table";
import * as utils from "#core/utils";
import uuid from "#core/uuid";
import Markdown from "#lib/markdown";
// NOTE: status codes:
const STATUSES = {
"200": "OK",
"201": "File ignored",
"210": "Lint warnings",
"400": "Lint errors",
"500": "Fatal error",
};
var terser, postcss, cssnano, htmlMinifier;
const customMime = mime.clone();
customMime.get( "application/json" ).extnames.add( [
//
".gyp",
] );
customMime.get( "application/x-sh" ).shebangs.add( [
//
"#!/bin/sh",
"#!/bin/bash",
"#!/usr/bin/sh",
"#!/usr/bin/bash",
"#!/usr/bin/env sh",
"#!/usr/bin/env bash",
] );
customMime.get( "application/node" ).shebangs.add( [
//
"#!/usr/bin/env node",
] );
customMime.get( "text/plain" ).filenames.add( [
//
".editorconfig",
".gitattributes",
".npmrc",
] );
customMime.get( "application/x-sh" ).filenames.add( [
//
".gitignore",
".dockerignore",
".lintignore",
] );
// NOTE: ".ts" conflicted with the "video/mp2t"
customMime.add( {
"essence": "application/x-typescript",
"compressible": true,
"extnames": [ ".ts", ".tsx", ".mts", ".cts" ],
} );
customMime.add( {
"essence": "application/x-vue",
"compressible": true,
"extnames": [ ".vue" ],
} );
customMime.add( {
"essence": "text/x-vim",
"compressible": true,
"extnames": [ ".vim" ],
} );
customMime.get( "text/x-lua" ).shebangs.add( [
//
"#!/usr/bin/env lua",
] );
customMime.add( {
"essence": "text/x-dockerfile",
"compressible": true,
"filenames": [ "dockerfile", "*.dockerfile" ],
} );
const DEFAULT_ESLINT_CONFIG_JAVASCRIPT = import.meta.resolve( "#resources/eslint.config.javascript.js" ),
DEFAULT_ESLINT_CONFIG_VUE = import.meta.resolve( "#resources/eslint.config.vue.js" ),
DEFAULT_ESLINT_CONFIG_TYPESCRIPT = import.meta.resolve( "#resources/eslint.config.typescript.js" );
const USE_PRETTIER_FOR_JS = true,
TYPES = {
"text/javascript": {
"prettier": {
"parser": USE_PRETTIER_FOR_JS
? "oxc"
: null,
},
"eslint": {
"config": DEFAULT_ESLINT_CONFIG_JAVASCRIPT,
},
"terser": true,
},
"application/node": {
"prettier": {
"parser": USE_PRETTIER_FOR_JS
? "babel"
: null,
},
"eslint": {
"config": DEFAULT_ESLINT_CONFIG_JAVASCRIPT,
},
"terser": true,
},
"application/x-typescript": {
"prettier": {
"parser": USE_PRETTIER_FOR_JS
? "oxc-ts"
: null,
},
"eslint": {
"config": DEFAULT_ESLINT_CONFIG_TYPESCRIPT,
},
"terser": false,
},
"application/x-vue": {
"prettier": {
"parser": USE_PRETTIER_FOR_JS
? "vue"
: null,
},
"eslint": {
"config": DEFAULT_ESLINT_CONFIG_VUE,
},
},
"application/json": {
"prettier": {
"parser": "json-stringify",
},
"json": true,
},
"application/json5": {
"prettier": {
"parser": "jsonc",
},
"json": true,
},
"text/yaml": {
"prettier": {
"parser": "yaml",
},
},
"text/css": {
"prettier": {
"parser": "css",
},
"cssnano": true,
},
"text/x-scss": {
"prettier": {
"parser": "scss",
},
},
"text/less": {
"prettier": {
"parser": "less",
},
},
"text/markdown": {
"prettier": {
"parser": "markdown",
},
},
"text/html": {
"prettier": {
"parser": "html",
},
"html": true,
},
"application/x-sh": {
"prettier": {
"parser": "sh",
},
},
"text/x-dockerfile": {
// "prettier": {
// "parser": "dockerfile",
// },
},
"text/xml": {
"prettier": {
"parser": "xml",
},
},
"application/wsdl+xml": {
"prettier": {
"parser": "xml",
},
},
"image/svg+xml": {
"prettier": {
"parser": "xml",
},
},
"text/x-lua": {},
"text/x-vim": {},
"text/plain": {},
};
// prepare types
for ( const [ type, spec ] of Object.entries( TYPES ) ) {
spec.type = type;
}
const RESOURCES_ROOT = path.dirname( utils.resolve( "#resources/.prettierrc.yaml", import.meta.url ) );
const DEFAULT_PRETTIER_CONFIG = await prettier.resolveConfig( "fake-config", {
"editorconfig": false,
"config": RESOURCES_ROOT + "/.prettierrc.yaml",
} );
// NOTE: https://eslint.org/docs/latest/integrate/nodejs-api#parameters
const ESLINT_OPTIONS = {
// files
"cwd": undefined,
// lint
"allowInlineConfig": true,
"stats": false,
// cache
"cache": false,
"cacheLocation": undefined,
"cacheStrategy": "metadata",
};
const CSSNANO_PRESETS = {
"compress": [
"default",
{
"discardComments": {
"removeAll": true,
},
},
],
"obfuscate": [
"default",
{
"discardComments": {
"removeAll": true,
},
},
],
};
const POSTCSS_CACHE = {};
const EOL = {
"cr": "\r",
"lf": "\n",
"crlf": "\r\n",
};
export class LintFile {
#file;
#processUnsupportedTypes;
#write;
#cache;
#fix;
#log;
#cwd;
#path;
#fullPath;
#useDefaults;
#originalData;
#data;
#type;
constructor ( file, { type, cwd, processUnsupportedTypes, write, fix = true, log = true, cache } = {} ) {
this.#file = File.new( file );
this.#type = type;
this.#cwd = cwd;
this.#processUnsupportedTypes = Boolean( processUnsupportedTypes );
this.#write = Boolean( write );
this.#cache = cache || {};
this.#fix = Boolean( fix );
this.#log = Boolean( log );
}
// static
static getStatusText ( status ) {
return STATUSES[ status ];
}
// public
async run ( action ) {
if ( !this.#file.path ) {
return result( [ 500, "File path is required" ] );
}
if ( path.isAbsolute( this.#file.path ) ) {
this.#path = path.normalize( this.#file.path );
}
else {
if ( !this.#cwd ) {
return result( [ 500, "Cwd is required when file path is relative" ] );
}
this.#path = path.normalize( path.join( this.#cwd, this.#file.path ) );
}
this.#cwd = path.dirname( this.#path );
if ( this.#write && !this.#path ) {
return result( [ 500, "File path is required to write content" ] );
}
this.#fullPath = this.#path;
// detect type
{
let type = this.#type;
type ||= customMime.findByFilename( this.#fullPath )?.essence;
type ||= (
await customMime.findByShebang( {
"path": this.#fullPath,
"content": await this.#file.slice( 0, 50 ).text( "latin1" ),
} )
)?.essence;
type ||= this.#file.type;
this.#type = TYPES[ type ];
}
// file type is not supported
if ( !this.#type && !this.#processUnsupportedTypes ) {
return result( [ 201, "File ignored" ], null, {
"isIgnored": true,
} );
}
// read file content
this.#originalData = this.#data = await this.#file.text( "utf8" );
// add default extension
if ( this.#type ) {
const extnames = customMime.get( this.#type.type )?.extnames;
if ( extnames?.default && !extnames.has( path.extname( this.#path ) ) ) {
this.#fullPath += extnames.default;
}
}
let res;
if ( action === "lint" ) res = await this.#lint();
else if ( action === "format" ) res = await this.#format();
else if ( action === "compress" ) res = await this.#compress();
else if ( action === "obfuscate" ) res = await this.#obfuscate();
res.meta.type = this.#type?.type;
res.meta.isIgnored = false;
res.meta.isFatalError ??= false;
res.meta.hasErrors = false;
res.meta.hasWarnings = false;
if ( res.status >= 500 ) {
res.meta.isFatalError = true;
}
else if ( res.status >= 300 ) {
res.meta.hasErrors = true;
}
else if ( res.status >= 201 ) {
res.meta.hasWarnings = true;
}
// data was modified
if ( this.#data !== this.#originalData ) {
res.meta.isModified = true;
res.meta.bytesDelta = this.#data.length - this.#originalData.length;
if ( this.#write ) {
await fs.promises.writeFile( this.#path, this.#data );
}
}
else {
res.meta.isModified = false;
res.meta.bytesDelta = 0;
}
res.data = this.#data;
return res;
}
// private
async #lint () {
const configs = await this.#getConfigs( this.#cwd ),
editorConfig = await this.#getEditorConfig( {
"fallbackToDefault": configs.package
? Boolean( configs.cli )
: true,
} );
var res;
// markdown
if ( this.#type?.type === "text/markdown" ) {
res = await this.#runMarkdown( {
configs,
editorConfig,
} );
}
// prettier
else {
res = await this.#runPrettier( {
configs,
editorConfig,
} );
}
// eslint
if ( this.#type?.eslint ) {
res = await this.#runEslint( {
configs,
editorConfig,
} );
}
// apply editor config
if ( editorConfig ) {
// replace tabs
if ( editorConfig.indent_style === "space" ) {
this.#replaceTabs( editorConfig.tab_width );
}
let eol = editorConfig.end_of_line;
// detect EOL
if ( !eol ) {
if ( this.#data.includes( "\r\n" ) ) {
eol = "crlf";
}
else if ( this.#data.includes( "\r" ) ) {
eol = "cr";
}
else if ( this.#data.includes( "\n" ) ) {
eol = "lf";
}
}
if ( eol ) {
// replace EOL
this.#replaceEndOfLine( eol );
// trim trailing whitespaces
if ( editorConfig.trim_trailing_whitespace ) {
this.#data = this.#data.replaceAll( new RegExp( ` +${ EOL[ eol ] }`, "g" ), EOL[ eol ] );
}
// trim
if ( editorConfig.trim ) {
this.#data = this.#data.trim();
}
// insert final new line
if ( editorConfig.insert_final_newline && this.#data.length && !this.#data.endsWith( EOL[ eol ] ) ) {
this.#data += EOL[ eol ];
}
}
}
return res;
}
async #format () {
this.#useDefaults = true;
return this.#lint();
}
async #compress () {
if ( this.#type ) {
if ( this.#type.terser ) {
return await this.#runTerser( {
"compress": true,
"mangle": false,
} );
}
else if ( this.#type.cssnano ) {
return this.#runCssnano( "compress" );
}
else if ( this.#type.json ) {
return this.#runJson( "compress" );
}
else if ( this.#type.html ) {
return this.#runHtmlMinifier( {
"cleanCssOptions": {
"level": 1,
},
"terserOptions": {
"compress": true,
"mangle": false,
},
} );
}
}
return result( 200 );
}
async #obfuscate () {
if ( this.#type ) {
if ( this.#type.terser ) {
return await this.#runTerser( {
"compress": true,
"mangle": true,
} );
}
else if ( this.#type.cssnano ) {
return this.#runCssnano( "obfuscate" );
}
else if ( this.#type.json ) {
return this.#runJson( "obfuscate" );
}
else if ( this.#type.html ) {
return this.#runHtmlMinifier( {
"cleanCssOptions": {
"level": 2,
},
"terserOptions": {
"compress": true,
"mangle": true,
},
} );
}
}
return result( 200 );
}
async #runPrettier ( { configs, editorConfig } ) {
if ( !this.#type?.prettier?.parser ) return result( 200 );
var prettierConfig;
// use default prettier config
if ( this.#useDefaults ) {
prettierConfig = DEFAULT_PRETTIER_CONFIG;
}
// has custom prettier config
else if ( configs.prettier ) {
prettierConfig = await prettier.resolveConfig( this.#path, {
"config": configs.prettier,
"editorconfig": false,
} );
}
// covered by "cli.config.yaml"
else if ( configs.cli ) {
prettierConfig = DEFAULT_PRETTIER_CONFIG;
}
// not covered by "package.json" or ".git"
else if ( !configs.package && !configs.git ) {
prettierConfig = DEFAULT_PRETTIER_CONFIG;
}
if ( !prettierConfig ) return result( 200 );
prettierConfig = { ...prettierConfig };
prettierConfig.plugins ??= [];
prettierConfig.plugins.push( PrettierPluginSh, PrettierPluginPackageJson, PrettierPluginXml, PrettierPluginOxc );
prettierConfig.parser = this.#type.prettier.parser;
prettierConfig.filepath = this.#fullPath;
// merge prettier config with editor config
if ( editorConfig ) {
prettierConfig.useTabs ??= editorConfig.indent_style === "tab";
prettierConfig.tabWidth ??= editorConfig.tab_width;
prettierConfig.printWidth ??= editorConfig.max_line_length ?? Infinity;
prettierConfig.endOfLine ??= editorConfig.end_of_line;
}
// run prettier
try {
// pre-parse package.json
if ( path.basename( this.#path ) === "package.json" ) {
const res = this.#runJson( "compress" );
if ( !res.ok ) return res;
}
this.#data = await prettier.format( this.#data, prettierConfig );
return result( 200 );
}
catch ( e ) {
const diagnostic = {};
// sh parser
if ( e.Pos ) {
diagnostic.message = e.message;
diagnostic.lnum = e.Pos.Line - 1;
diagnostic.col = e.Pos.Col - 1;
}
// other parser
else {
// diagnostic.message = ansi.remove( e.message );
diagnostic.message = ansi.remove( e.message.split( "\n" )[ 0 ] );
diagnostic.lnum = e.loc.start.line - 1;
diagnostic.col = e.loc.start.column - 1;
diagnostic.source = e.cause.code;
diagnostic.code = ansi.remove( e.codeFrame );
}
const res = result( [ 500, "Parsing error" ], null, {
"isFatalError": true,
"diagnostic": [
{
"severity": "ERROR",
...diagnostic,
},
],
} );
return res;
}
}
async #runMarkdown ( { configs, editorConfig } = {} ) {
var process;
// use default config
if ( this.#useDefaults ) {
process = true;
}
// has custom prettier config
else if ( configs.prettier ) {
process = true;
}
// covered by "cli.config.yaml"
else if ( configs.cli ) {
process = true;
}
// not covered by "package.json" or ".git"
else if ( !configs.package && !configs.git ) {
process = true;
}
if ( !process ) return result( 200 );
var replaceCodeLanguage;
if ( configs.cli ) {
replaceCodeLanguage = true;
}
else {
replaceCodeLanguage = false;
}
var res;
// pre-process markdown
const markdown = new Markdown( this.#data ),
nodes = [];
markdown.traverse(
( node, index, parent ) => {
nodes.push( {
"id": null,
"originalLanguage": node.lang,
"language": null,
node,
} );
},
{
"test": "code",
}
);
for ( const node of nodes ) {
const language = markdown.getCodeLanguage( node.originalLanguage );
// language is not supported
if ( !language ) continue;
node.language = language.language;
// language type is not supported
if ( !language.type ) {
if ( replaceCodeLanguage ) {
node.node.lang = node.language;
}
}
else {
node.id = node.node.lang = uuid();
// lint code block
res = await lintFile(
{
"path": "markdown-code-block",
"type": language.type,
"buffer": markdown.nodeToString( node.node ),
},
{
"cwd": this.#cwd,
"log": false,
"cache": this.#cache,
}
);
if ( res.data ) {
node.node.value = res.data.trim();
}
}
}
this.#data = markdown.toMarkdown();
// prettier
res = await this.#runPrettier( {
configs,
editorConfig,
} );
if ( !res.ok ) return res;
// post-process markdown
for ( const node of nodes ) {
if ( !node.id ) continue;
if ( replaceCodeLanguage ) {
this.#data = this.#data.replace( node.id, node.language );
}
else {
this.#data = this.#data.replace( node.id, node.originalLanguage );
}
}
return result( 200 );
}
async #runEslint ( { configs, editorConfig, repeat } ) {
if ( !this.#type?.eslint ) return result( 200 );
const fullPath = this.#fullPath;
var eslintConfig;
// use default eslint config
if ( this.#useDefaults ) {
eslintConfig = true;
}
// custom eslint config
else if ( configs.eslint ) {
eslintConfig = configs.eslint;
}
// covered by "cli.config.yaml"
else if ( configs.cli ) {
eslintConfig = true;
}
// not covered by "package.json" or ".git"
else if ( !configs.package && !configs.git ) {
eslintConfig = true;
}
if ( !eslintConfig ) return result( 200 );
const options = {
...ESLINT_OPTIONS,
"fix": this.#fix,
};
// use default config
if ( eslintConfig === true ) {
options.cwd = path.dirname( fullPath );
options.overrideConfigFile = true;
const { Config } = await import( this.#type.eslint.config );
options.overrideConfig = new Config().create( editorConfig );
}
// use custom config
else {
options.cwd = path.dirname( eslintConfig );
options.overrideConfigFile = eslintConfig;
}
const eslint = new EsLint( options );
// file ignored by eslint
const ignored = await eslint.isPathIgnored( fullPath );
if ( ignored ) return result( 200 );
// NOTE https://eslint.org/docs/latest/integrate/nodejs-api#-eslintlinttextcode-options
const [ report ] = await eslint.lintText( this.#data, {
"filePath": fullPath,
"warnIgnored": true,
} );
const isFatalError = Boolean( report.fatalErrorCount ),
hasErrors = Boolean( report.errorCount ),
hasWarnings = Boolean( report.warningCount );
// repeat on fatal error
if ( !repeat && isFatalError ) {
// repeat with the original data
this.#data = this.#originalData;
return this.#runEslint( {
configs,
editorConfig,
"repeat": true,
} );
}
else if ( report.output != null ) {
this.#data = report.output;
}
var log = "";
// append log
const table = new Table( {
"width": 100,
"columns": {
"severity": { "title": "Severity", "width": 10, "margin": [ 1, 0 ] },
"line": {
"title": "Line",
"width": 10,
"align": "end",
"margin": [ 0, 1 ],
},
"description": { "title": "Desctiption", "margin": [ 1, 0 ] },
},
} );
let diagnostic = [];
for ( const msg of report.messages.sort( ( a, b ) => b.severity - a.severity || a.line - b.line || a.column - b.column ) ) {
let severity;
if ( msg.severity === 1 ) {
severity = "WARN";
}
else {
severity = "ERROR";
}
table.add( {
severity,
"line": `${ msg.line }:${ msg.column }`,
"description": this.#escapeLog( msg.ruleId + ", " + msg.message ),
} );
diagnostic.push( {
"lnum": msg.line - 1,
"col": msg.column - 1,
"end_lnum": msg.endLine - 1,
"end_col": msg.endColumn - 1,
severity,
"source": msg.ruleId,
"message": msg.message,
// "code": "",
} );
}
table.end();
if ( !diagnostic.length ) diagnostic = null;
log = table.content;
// vue
if ( this.#type.type === "application/x-vue" ) {
this.#data = this.#data.replace( /^\s*<!-{2} -{5}BEGIN LINT LOG-{5}[\S\s]+?-{5}END LINT LOG-{5} -{2}>/m, "" );
this.#data = this.#data.trim();
if ( this.#log && log ) {
log = log.trim().replaceAll( /^/gm, "<!-- " ).replaceAll( /$/gm, " -->" );
this.#data += "\n\n<!-- -----BEGIN LINT LOG----- -->\n";
this.#data += log + "\n";
this.#data += "<!-- -----END LINT LOG----- -->";
}
}
// javascript
else {
this.#data = this.#data.replace( /^\s*\/{2} -{5}BEGIN LINT LOG-{5}[\S\s]+?-{5}END LINT LOG-{5}/m, "" );
this.#data = this.#data.trim();
if ( this.#log && log ) {
log = log.trim().replaceAll( /^/gm, "// " );
this.#data += "\n\n// -----BEGIN LINT LOG-----\n";
this.#data += log + "\n";
this.#data += "// -----END LINT LOG-----";
}
}
// fatal error
if ( isFatalError ) {
return result( [ 500, "Parsing error" ], null, {
"isFatalError": true,
diagnostic,
} );
}
// errors
else if ( hasErrors ) {
return result( [ 400, "Errors" ], null, {
hasErrors,
diagnostic,
} );
}
// warnings
else if ( hasWarnings ) {
return result( [ 210, "Warnings" ], null, {
hasWarnings,
diagnostic,
} );
}
// ok
else {
return result( 200, null, {
diagnostic,
} );
}
}
async #runTerser ( options ) {
terser ??= await import( "terser" );
var res;
try {
res = await terser.minify( this.#data, options );
}
catch ( e ) {
return result( [ 500, e.message ] );
}
if ( res.error ) {
return result( [ 500, res.error.message ] );
}
else {
this.#data = res.code;
return result( 200 );
}
}
async #runCssnano ( preset ) {
if ( !POSTCSS_CACHE[ preset ] ) {
postcss ??= ( await import( "postcss" ) ).default;
cssnano ??= ( await import( "Cssnano" ) ).default;
POSTCSS_CACHE[ preset ] = postcss( [ cssnano( { "preset": CSSNANO_PRESETS[ preset ] } ) ] );
}
const res = await POSTCSS_CACHE[ preset ].process( this.#data, {
"from": null,
} );
this.#data = res.toString();
return result( 200 );
}
async #runHtmlMinifier ( { cleanCssOptions, terserOptions } = {} ) {
htmlMinifier ??= await import( "html-minifier-terser" );
// https://github.com/terser/html-minifier-terser?tab=readme-ov-file#options-quick-reference
this.#data = await htmlMinifier.minify( this.#data, {
"caseSensitive": true,
"collapseBooleanAttributes": true,
"collapseInlineTagWhitespace": true,
"collapseWhitespace": true,
"decodeEntities": true,
"html5": true,
"includeAutoGeneratedTags": false,
"keepClosingSlash": true,
"minifyCSS": cleanCssOptions,
"minifyJS": terserOptions,
"preventAttributesEscaping": true,
"quoteCharacter": '"',
"removeAttributeQuotes": false,
"removeComments": true,
"useShortDoctype": true,
} );
// htmlMinifier ??= ( await import( "@minify-html/node" ) ).default;
// this.#data = htmlMinifier
// .minify( Buffer.from( this.#data ), {
// "do_not_minify_doctype": true,
// "ensure_spec_compliant_unquoted_attribute_values": true,
// "keep_html_and_head_opening_tags": true,
// "minify_css": true,
// "minify_js": true,
// "keep_spaces_between_attributes": true,
// "keep_comments": false,
// "keep_closing_tags": true,
// "preserve_chevron_percent_template_syntax": true,
// } )
// .toString();
return result( 200 );
}
#runJson ( action, { indent } = {} ) {
try {
if ( action === "lint" ) {
this.#data = JSON.stringify( json5.fromJson5( this.#data ), null, indent );
}
else {
this.#data = JSON.stringify( json5.fromJson5( this.#data ) );
}
return result( 200 );
}
catch ( e ) {
const res = result( [ 500, "Parsing error" ], null, {
"isFatalError": true,
"diagnostic": [
{
"lnum": e.lineNumber - 1,
"col": e.columnNumber - 1,
"severity": "ERROR",
"source": "SyntaxError",
"message": e.message,
},
],
} );
return res;
}
}
#replaceTabs ( tabWidth ) {
this.#data = this.#data.replaceAll( "\t", " ".repeat( tabWidth ) );
}
#replaceEndOfLine ( endOfLine ) {
const eol = EOL[ endOfLine ];
if ( !eol ) return;
this.#data = this.#data.replaceAll( /\r\n|\r|\n/g, eol );
}
async #getConfigs ( dirname ) {
this.#cache.configs ||= {};
const cache = this.#cache.configs;
if ( !cache[ dirname ] ) {
const parent = path.dirname( dirname );
let configs;
if ( parent === dirname ) {
configs = {};
}
else {
configs = await this.#getConfigs( parent );
}
var fullPath;
// .git
fullPath = await this.#fileExists( dirname, ".git" );
if ( fullPath ) {
configs.git = fullPath;
}
// package.json
fullPath = await this.#fileExists( dirname, "package.json" );
if ( fullPath ) {
configs.package = fullPath;
// cli.config.yaml
fullPath = await this.#fileExists( dirname, "cli.config.yaml" );
if ( fullPath ) {
configs.cli = fullPath;
}
else {
configs.cli = null;
}
}
// prettier config
fullPath = await this.#fileExists( dirname, [
//
".prettierrc",
".prettierrc.json",
".prettierrc.yml",
".prettierrc.yaml",
".prettierrc.json5",
".prettierrc.js",
"prettier.config.js",
".prettierrc.mjs",
"prettier.config.mjs",
".prettierrc.cjs",
"prettier.config.cjs",
".prettierrc.toml",
] );
if ( fullPath ) {
configs.prettier = fullPath;
}
// eslint config
fullPath = await this.#fileExists( dirname, [
//
"/eslint.config.js",
"/eslint.config.mjs",
"/eslint.config.cjs",
] );
if ( fullPath ) {
configs.eslint = fullPath;
}
cache[ dirname ] = configs;
}
return { ...cache[ dirname ] };
}
async #fileExists ( cwd, basenames ) {
if ( !Array.isArray( basenames ) ) basenames = [ basenames ];
for ( const basename of basenames ) {
const fullPath = path.join( cwd, basename );
try {
await fs.promises.access( fullPath );
return fullPath;
}
catch {}
}
}
async #getEditorConfig ( { fallbackToDefault } = {} ) {
this.#cache.editorConfigs ||= new Map();
// use default config
// if ( this.#useDefaults ) {
// return editorconfig.parse( path.join( RESOURCES_ROOT, path.basename( this.#fullPath ) ), {
// "cache": this.#cache.editorConfigs,
// "unset": true,
// } );
// }
const config = await editorconfig.parse( this.#fullPath, {
"cache": this.#cache.editorConfigs,
"unset": true,
} );
if ( Object.keys( config ).length ) {
return config;
}
// fallback to the default config
else if ( this.#useDefaults || fallbackToDefault ) {
return editorconfig.parse( path.join( RESOURCES_ROOT, path.basename( this.#fullPath ) ), {
"cache": this.#cache.editorConfigs,
"unset": true,
} );
}
}
#escapeLog ( string ) {
if ( this.#type.type === "application/x-vue" ) {
string = string.replaceAll( "-->", "--\\>" );
}
return string;
}
}
export default async function lintFile ( file, { action = "lint", ...options } = {} ) {
const lintFile = new LintFile( file, options );
return lintFile.run( action );
}