first commit

This commit is contained in:
monjack
2025-06-20 18:01:48 +08:00
commit 6daa6d65c1
24611 changed files with 2512443 additions and 0 deletions

View File

@ -0,0 +1,15 @@
"use strict";
class ESLintError extends Error {
/**
* @param {string=} messages
*/
constructor(messages) {
super(`[eslint] ${messages}`);
this.name = 'ESLintError';
this.stack = '';
}
}
module.exports = ESLintError;

View File

@ -0,0 +1,160 @@
"use strict";
const {
cpus
} = require('os');
const {
Worker: JestWorker
} = require('jest-worker');
const {
getESLintOptions
} = require('./options');
const {
jsonStringifyReplacerSortKeys
} = require('./utils');
/** @type {{[key: string]: any}} */
const cache = {};
/** @typedef {import('eslint').ESLint} ESLint */
/** @typedef {import('eslint').ESLint.LintResult} LintResult */
/** @typedef {import('./options').Options} Options */
/** @typedef {() => Promise<void>} AsyncTask */
/** @typedef {(files: string|string[]) => Promise<LintResult[]>} LintTask */
/** @typedef {{threads: number, ESLint: ESLint, eslint: ESLint, lintFiles: LintTask, cleanup: AsyncTask}} Linter */
/** @typedef {JestWorker & {lintFiles: LintTask}} Worker */
/**
* @param {Options} options
* @returns {Linter}
*/
function loadESLint(options) {
const {
eslintPath
} = options;
const {
ESLint
} = require(eslintPath || 'eslint'); // Filter out loader options before passing the options to ESLint.
const eslint = new ESLint(getESLintOptions(options));
return {
threads: 1,
ESLint,
eslint,
lintFiles: async files => {
const results = await eslint.lintFiles(files); // istanbul ignore else
if (options.fix) {
await ESLint.outputFixes(results);
}
return results;
},
// no-op for non-threaded
cleanup: async () => {}
};
}
/**
* @param {string|undefined} key
* @param {number} poolSize
* @param {Options} options
* @returns {Linter}
*/
function loadESLintThreaded(key, poolSize, options) {
const cacheKey = getCacheKey(key, options);
const {
eslintPath = 'eslint'
} = options;
const source = require.resolve('./worker');
const workerOptions = {
enableWorkerThreads: true,
numWorkers: poolSize,
setupArgs: [{
eslintPath,
eslintOptions: getESLintOptions(options)
}]
};
const local = loadESLint(options);
let worker =
/** @type {Worker?} */
new JestWorker(source, workerOptions);
/** @type {Linter} */
const context = { ...local,
threads: poolSize,
lintFiles: async files => worker && (await worker.lintFiles(files)) ||
/* istanbul ignore next */
[],
cleanup: async () => {
cache[cacheKey] = local;
context.lintFiles = files => local.lintFiles(files);
if (worker) {
worker.end();
worker = null;
}
}
};
return context;
}
/**
* @param {string|undefined} key
* @param {Options} options
* @returns {Linter}
*/
function getESLint(key, {
threads,
...options
}) {
const max = typeof threads !== 'number' ? threads ? cpus().length - 1 : 1 :
/* istanbul ignore next */
threads;
const cacheKey = getCacheKey(key, {
threads,
...options
});
if (!cache[cacheKey]) {
cache[cacheKey] = max > 1 ? loadESLintThreaded(key, max, options) : loadESLint(options);
}
return cache[cacheKey];
}
/**
* @param {string|undefined} key
* @param {Options} options
* @returns {string}
*/
function getCacheKey(key, options) {
return JSON.stringify({
key,
options
}, jsonStringifyReplacerSortKeys);
}
module.exports = {
loadESLint,
loadESLintThreaded,
getESLint
};

View File

@ -0,0 +1,195 @@
"use strict";
const {
isAbsolute,
join
} = require('path');
const {
isMatch
} = require('micromatch');
const {
getOptions
} = require('./options');
const linter = require('./linter');
const {
arrify,
parseFiles,
parseFoldersToGlobs
} = require('./utils');
/** @typedef {import('webpack').Compiler} Compiler */
/** @typedef {import('./options').Options} Options */
const ESLINT_PLUGIN = 'ESLintWebpackPlugin';
let counter = 0;
class ESLintWebpackPlugin {
/**
* @param {Options} options
*/
constructor(options = {}) {
this.key = ESLINT_PLUGIN;
this.options = getOptions(options);
this.run = this.run.bind(this);
}
/**
* @param {Compiler} compiler
* @returns {void}
*/
apply(compiler) {
// Generate key for each compilation,
// this differentiates one from the other when being cached.
this.key = compiler.name || `${this.key}_${counter += 1}`;
const options = { ...this.options,
exclude: parseFiles(this.options.exclude || [], this.getContext(compiler)),
extensions: arrify(this.options.extensions),
resourceQueryExclude: arrify(this.options.resourceQueryExclude || []).map(item => item instanceof RegExp ? item : new RegExp(item)),
files: parseFiles(this.options.files || '', this.getContext(compiler))
};
const wanted = parseFoldersToGlobs(options.files, options.extensions);
const exclude = parseFoldersToGlobs(this.options.exclude ? options.exclude : '**/node_modules/**', []); // If `lintDirtyModulesOnly` is disabled,
// execute the linter on the build
if (!this.options.lintDirtyModulesOnly) {
compiler.hooks.run.tapPromise(this.key, c => this.run(c, options, wanted, exclude));
}
let isFirstRun = this.options.lintDirtyModulesOnly;
compiler.hooks.watchRun.tapPromise(this.key, c => {
if (isFirstRun) {
isFirstRun = false;
return Promise.resolve();
}
return this.run(c, options, wanted, exclude);
});
}
/**
* @param {Compiler} compiler
* @param {Omit<Options, 'resourceQueryExclude'> & {resourceQueryExclude: RegExp[]}} options
* @param {string[]} wanted
* @param {string[]} exclude
*/
async run(compiler, options, wanted, exclude) {
// Do not re-hook
if ( // @ts-ignore
compiler.hooks.compilation.taps.find(({
name
}) => name === this.key)) {
return;
}
compiler.hooks.compilation.tap(this.key, compilation => {
/** @type {import('./linter').Linter} */
let lint;
/** @type {import('./linter').Reporter} */
let report;
/** @type number */
let threads;
try {
({
lint,
report,
threads
} = linter(this.key, options, compilation));
} catch (e) {
compilation.errors.push(e);
return;
}
/** @type {string[]} */
const files = []; // @ts-ignore
// Add the file to be linted
compilation.hooks.succeedModule.tap(this.key, ({
resource
}) => {
if (resource) {
const [file, query] = resource.split('?');
if (file && !files.includes(file) && isMatch(file, wanted, {
dot: true
}) && !isMatch(file, exclude, {
dot: true
}) && options.resourceQueryExclude.every(reg => !reg.test(query))) {
files.push(file);
if (threads > 1) {
lint(file);
}
}
}
}); // Lint all files added
compilation.hooks.finishModules.tap(this.key, () => {
if (files.length > 0 && threads <= 1) {
lint(files);
}
}); // await and interpret results
compilation.hooks.additionalAssets.tapPromise(this.key, processResults);
async function processResults() {
const {
errors,
warnings,
generateReportAsset
} = await report();
if (warnings && !options.failOnWarning) {
// @ts-ignore
compilation.warnings.push(warnings);
} else if (warnings && options.failOnWarning) {
// @ts-ignore
compilation.errors.push(warnings);
}
if (errors && options.failOnError) {
// @ts-ignore
compilation.errors.push(errors);
} else if (errors && !options.failOnError) {
// @ts-ignore
compilation.warnings.push(errors);
}
if (generateReportAsset) {
await generateReportAsset(compilation);
}
}
});
}
/**
*
* @param {Compiler} compiler
* @returns {string}
*/
getContext(compiler) {
if (!this.options.context) {
return String(compiler.options.context);
}
if (!isAbsolute(this.options.context)) {
return join(String(compiler.options.context), this.options.context);
}
return this.options.context;
}
}
module.exports = ESLintWebpackPlugin;

View File

@ -0,0 +1,342 @@
"use strict";
const {
dirname,
isAbsolute,
join
} = require('path');
const ESLintError = require('./ESLintError');
const {
getESLint
} = require('./getESLint');
/** @typedef {import('eslint').ESLint} ESLint */
/** @typedef {import('eslint').ESLint.Formatter} Formatter */
/** @typedef {import('eslint').ESLint.LintResult} LintResult */
/** @typedef {import('webpack').Compiler} Compiler */
/** @typedef {import('webpack').Compilation} Compilation */
/** @typedef {import('./options').Options} Options */
/** @typedef {import('./options').FormatterFunction} FormatterFunction */
/** @typedef {(compilation: Compilation) => Promise<void>} GenerateReport */
/** @typedef {{errors?: ESLintError, warnings?: ESLintError, generateReportAsset?: GenerateReport}} Report */
/** @typedef {() => Promise<Report>} Reporter */
/** @typedef {(files: string|string[]) => void} Linter */
/** @typedef {{[files: string]: LintResult}} LintResultMap */
/** @type {WeakMap<Compiler, LintResultMap>} */
const resultStorage = new WeakMap();
/**
* @param {string|undefined} key
* @param {Options} options
* @param {Compilation} compilation
* @returns {{lint: Linter, report: Reporter, threads: number}}
*/
function linter(key, options, compilation) {
/** @type {ESLint} */
let eslint;
/** @type {(files: string|string[]) => Promise<LintResult[]>} */
let lintFiles;
/** @type {() => Promise<void>} */
let cleanup;
/** @type number */
let threads;
/** @type {Promise<LintResult[]>[]} */
const rawResults = [];
const crossRunResultStorage = getResultStorage(compilation);
try {
({
eslint,
lintFiles,
cleanup,
threads
} = getESLint(key, options));
} catch (e) {
throw new ESLintError(e.message);
}
return {
lint,
report,
threads
};
/**
* @param {string | string[]} files
*/
function lint(files) {
for (const file of asList(files)) {
delete crossRunResultStorage[file];
}
rawResults.push(lintFiles(files).catch(e => {
// @ts-ignore
compilation.errors.push(new ESLintError(e.message));
return [];
}));
}
async function report() {
// Filter out ignored files.
let results = await removeIgnoredWarnings(eslint, // Get the current results, resetting the rawResults to empty
await flatten(rawResults.splice(0, rawResults.length)));
await cleanup();
for (const result of results) {
crossRunResultStorage[result.filePath] = result;
}
results = Object.values(crossRunResultStorage); // do not analyze if there are no results or eslint config
if (!results || results.length < 1) {
return {};
}
const formatter = await loadFormatter(eslint, options.formatter);
const {
errors,
warnings
} = await formatResults(formatter, parseResults(options, results));
return {
errors,
warnings,
generateReportAsset
};
/**
* @param {Compilation} compilation
* @returns {Promise<void>}
*/
async function generateReportAsset({
compiler
}) {
const {
outputReport
} = options;
/**
* @param {string} name
* @param {string | Buffer} content
*/
const save = (name, content) =>
/** @type {Promise<void>} */
new Promise((finish, bail) => {
const {
mkdir,
writeFile
} = compiler.outputFileSystem; // ensure directory exists
// @ts-ignore - the types for `outputFileSystem` are missing the 3 arg overload
mkdir(dirname(name), {
recursive: true
}, err => {
/* istanbul ignore if */
if (err) bail(err);else writeFile(name, content, err2 => {
/* istanbul ignore if */
if (err2) bail(err2);else finish();
});
});
});
if (!outputReport || !outputReport.filePath) {
return;
}
const content = await (outputReport.formatter ? (await loadFormatter(eslint, outputReport.formatter)).format(results) : formatter.format(results));
let {
filePath
} = outputReport;
if (!isAbsolute(filePath)) {
filePath = join(compiler.outputPath, filePath);
}
await save(filePath, content);
}
}
}
/**
* @param {Formatter} formatter
* @param {{ errors: LintResult[]; warnings: LintResult[]; }} results
* @returns {Promise<{errors?: ESLintError, warnings?: ESLintError}>}
*/
async function formatResults(formatter, results) {
let errors;
let warnings;
if (results.warnings.length > 0) {
warnings = new ESLintError(await formatter.format(results.warnings));
}
if (results.errors.length > 0) {
errors = new ESLintError(await formatter.format(results.errors));
}
return {
errors,
warnings
};
}
/**
* @param {Options} options
* @param {LintResult[]} results
* @returns {{errors: LintResult[], warnings: LintResult[]}}
*/
function parseResults(options, results) {
/** @type {LintResult[]} */
const errors = [];
/** @type {LintResult[]} */
const warnings = [];
results.forEach(file => {
if (fileHasErrors(file)) {
const messages = file.messages.filter(message => options.emitError && message.severity === 2);
if (messages.length > 0) {
errors.push({ ...file,
messages
});
}
}
if (fileHasWarnings(file)) {
const messages = file.messages.filter(message => options.emitWarning && message.severity === 1);
if (messages.length > 0) {
warnings.push({ ...file,
messages
});
}
}
});
return {
errors,
warnings
};
}
/**
* @param {LintResult} file
* @returns {boolean}
*/
function fileHasErrors(file) {
return file.errorCount > 0;
}
/**
* @param {LintResult} file
* @returns {boolean}
*/
function fileHasWarnings(file) {
return file.warningCount > 0;
}
/**
* @param {ESLint} eslint
* @param {string|FormatterFunction=} formatter
* @returns {Promise<Formatter>}
*/
async function loadFormatter(eslint, formatter) {
if (typeof formatter === 'function') {
return {
format: formatter
};
}
if (typeof formatter === 'string') {
try {
return eslint.loadFormatter(formatter);
} catch (_) {// Load the default formatter.
}
}
return eslint.loadFormatter();
}
/**
* @param {ESLint} eslint
* @param {LintResult[]} results
* @returns {Promise<LintResult[]>}
*/
async function removeIgnoredWarnings(eslint, results) {
const filterPromises = results.map(async result => {
// Short circuit the call to isPathIgnored.
// fatal is false for ignored file warnings.
// ruleId is unset for internal ESLint errors.
// line is unset for warnings not involving file contents.
const ignored = result.messages.length === 0 || result.warningCount === 1 && result.errorCount === 0 && !result.messages[0].fatal && !result.messages[0].ruleId && !result.messages[0].line && (await eslint.isPathIgnored(result.filePath));
return ignored ? false : result;
}); // @ts-ignore
return (await Promise.all(filterPromises)).filter(result => !!result);
}
/**
* @param {Promise<LintResult[]>[]} results
* @returns {Promise<LintResult[]>}
*/
async function flatten(results) {
/**
* @param {LintResult[]} acc
* @param {LintResult[]} list
*/
const flat = (acc, list) => [...acc, ...list];
return (await Promise.all(results)).reduce(flat, []);
}
/**
* @param {Compilation} compilation
* @returns {LintResultMap}
*/
function getResultStorage({
compiler
}) {
let storage = resultStorage.get(compiler);
if (!storage) {
resultStorage.set(compiler, storage = {});
}
return storage;
}
/**
* @param {string | string[]} x
*/
function asList(x) {
/* istanbul ignore next */
return Array.isArray(x) ? x : [x];
}
module.exports = linter;

View File

@ -0,0 +1,103 @@
"use strict";
const {
validate
} = require('schema-utils');
const schema = require('./options.json');
/** @typedef {import("eslint").ESLint.Options} ESLintOptions */
/** @typedef {import('eslint').ESLint.LintResult} LintResult */
/** @typedef {import('eslint').ESLint.LintResultData} LintResultData */
/**
* @callback FormatterFunction
* @param {LintResult[]} results
* @param {LintResultData=} data
* @returns {string}
*/
/**
* @typedef {Object} OutputReport
* @property {string=} filePath
* @property {string|FormatterFunction=} formatter
*/
/**
* @typedef {Object} PluginOptions
* @property {string=} context
* @property {boolean=} emitError
* @property {boolean=} emitWarning
* @property {string=} eslintPath
* @property {string|string[]=} exclude
* @property {string|string[]=} extensions
* @property {boolean=} failOnError
* @property {boolean=} failOnWarning
* @property {string|string[]=} files
* @property {boolean=} fix
* @property {string|FormatterFunction=} formatter
* @property {boolean=} lintDirtyModulesOnly
* @property {boolean=} quiet
* @property {OutputReport=} outputReport
* @property {number|boolean=} threads
* @property {RegExp|RegExp[]=} resourceQueryExclude
*/
/** @typedef {PluginOptions & ESLintOptions} Options */
/**
* @param {Options} pluginOptions
* @returns {PluginOptions}
*/
function getOptions(pluginOptions) {
const options = {
extensions: 'js',
emitError: true,
emitWarning: true,
failOnError: true,
resourceQueryExclude: [],
...pluginOptions,
...(pluginOptions.quiet ? {
emitError: true,
emitWarning: false
} : {})
}; // @ts-ignore
validate(schema, options, {
name: 'ESLint Webpack Plugin',
baseDataPath: 'options'
});
return options;
}
/**
* @param {Options} loaderOptions
* @returns {ESLintOptions}
*/
function getESLintOptions(loaderOptions) {
const eslintOptions = { ...loaderOptions
}; // Keep the fix option because it is common to both the loader and ESLint.
const {
fix,
extensions,
...eslintOnlyOptions
} = schema.properties; // No need to guard the for-in because schema.properties has hardcoded keys.
// eslint-disable-next-line guard-for-in
for (const option in eslintOnlyOptions) {
// @ts-ignore
delete eslintOptions[option];
}
return eslintOptions;
}
module.exports = {
getOptions,
getESLintOptions
};

View File

@ -0,0 +1,88 @@
{
"type": "object",
"additionalProperties": true,
"properties": {
"context": {
"description": "A string indicating the root of your files.",
"type": "string"
},
"emitError": {
"description": "The errors found will always be emitted, to disable set to `false`.",
"type": "boolean"
},
"emitWarning": {
"description": "The warnings found will always be emitted, to disable set to `false`.",
"type": "boolean"
},
"eslintPath": {
"description": "Path to `eslint` instance that will be used for linting. If the `eslintPath` is a folder like a official eslint, or specify a `formatter` option. now you dont have to install `eslint`.",
"type": "string"
},
"exclude": {
"description": "Specify the files and/or directories to exclude. Must be relative to `options.context`.",
"anyOf": [{ "type": "string" }, { "type": "array" }]
},
"resourceQueryExclude": {
"description": "Specify the resource query to exclude.",
"anyOf": [{ "instanceof": "RegExp" }, { "type": "array" }]
},
"failOnError": {
"description": "Will cause the module build to fail if there are any errors, to disable set to `false`.",
"type": "boolean"
},
"failOnWarning": {
"description": "Will cause the module build to fail if there are any warnings, if set to `true`.",
"type": "boolean"
},
"files": {
"description": "Specify the files and/or directories to traverse. Must be relative to `options.context`.",
"anyOf": [{ "type": "string" }, { "type": "array" }]
},
"extensions": {
"description": "Specify extensions that should be checked.",
"anyOf": [{ "type": "string" }, { "type": "array" }]
},
"fix": {
"description": "Will enable ESLint autofix feature",
"type": "boolean"
},
"formatter": {
"description": "Accepts a function that will have one argument: an array of eslint messages (object). The function must return the output as a string.",
"anyOf": [{ "type": "string" }, { "instanceof": "Function" }]
},
"lintDirtyModulesOnly": {
"description": "Lint only changed files, skip lint on start.",
"type": "boolean"
},
"quiet": {
"description": "Will process and report errors only and ignore warnings, if set to `true`.",
"type": "boolean"
},
"outputReport": {
"description": "Write the output of the errors to a file, for example a checkstyle xml file for use for reporting on Jenkins CI",
"anyOf": [
{
"type": "boolean"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"filePath": {
"description": "The `filePath` is relative to the webpack config: output.path",
"anyOf": [{ "type": "string" }]
},
"formatter": {
"description": "You can pass in a different formatter for the output file, if none is passed in the default/configured formatter will be used",
"anyOf": [{ "type": "string" }, { "instanceof": "Function" }]
}
}
}
]
},
"threads": {
"description": "Default is false. Set to true for an auto-selected pool size based on number of cpus. Set to a number greater than 1 to set an explicit pool size. Set to false, 1, or less to disable and only run in main process.",
"anyOf": [{ "type": "number" }, { "type": "boolean" }]
}
}
}

View File

@ -0,0 +1,124 @@
"use strict";
const {
resolve
} = require('path');
const {
statSync
} = require('fs');
const normalizePath = require('normalize-path');
/**
* @template T
* @param {T} value
* @return {
T extends (null | undefined)
? []
: T extends string
? [string]
: T extends readonly unknown[]
? T
: T extends Iterable<infer T>
? T[]
: [T]
}
*/
/* istanbul ignore next */
function arrify(value) {
// eslint-disable-next-line no-undefined
if (value === null || value === undefined) {
// @ts-ignore
return [];
}
if (Array.isArray(value)) {
// @ts-ignore
return value;
}
if (typeof value === 'string') {
// @ts-ignore
return [value];
} // @ts-ignore
if (typeof value[Symbol.iterator] === 'function') {
// @ts-ignore
return [...value];
} // @ts-ignore
return [value];
}
/**
* @param {string|string[]} files
* @param {string} context
* @returns {string[]}
*/
function parseFiles(files, context) {
return arrify(files).map((
/** @type {string} */
file) => normalizePath(resolve(context, file)));
}
/**
* @param {string|string[]} patterns
* @param {string|string[]} extensions
* @returns {string[]}
*/
function parseFoldersToGlobs(patterns, extensions = []) {
const extensionsList = arrify(extensions);
const [prefix, postfix] = extensionsList.length > 1 ? ['{', '}'] : ['', ''];
const extensionsGlob = extensionsList.map((
/** @type {string} */
extension) => extension.replace(/^\./u, '')).join(',');
return arrify(patterns).map((
/** @type {string} */
pattern) => {
try {
// The patterns are absolute because they are prepended with the context.
const stats = statSync(pattern);
/* istanbul ignore else */
if (stats.isDirectory()) {
return pattern.replace(/[/\\]*?$/u, `/**${extensionsGlob ? `/*.${prefix + extensionsGlob + postfix}` : ''}`);
}
} catch (_) {// Return the pattern as is on error.
}
return pattern;
});
}
/**
* @param {string} _ key, but unused
* @param {any} value
*/
const jsonStringifyReplacerSortKeys = (_, value) => {
/**
* @param {{ [x: string]: any; }} sorted
* @param {string | number} key
*/
const insert = (sorted, key) => {
// eslint-disable-next-line no-param-reassign
sorted[key] = value[key];
return sorted;
};
return value instanceof Object && !(value instanceof Array) ? Object.keys(value).sort().reduce(insert, {}) : value;
};
module.exports = {
arrify,
parseFiles,
parseFoldersToGlobs,
jsonStringifyReplacerSortKeys
};

View File

@ -0,0 +1,50 @@
"use strict";
/** @typedef {import('eslint').ESLint} ESLint */
/** @typedef {import('eslint').ESLint.Options} ESLintOptions */
Object.assign(module.exports, {
lintFiles,
setup
});
/** @type {{ new (arg0: import("eslint").ESLint.Options): import("eslint").ESLint; outputFixes: (arg0: import("eslint").ESLint.LintResult[]) => any; }} */
let ESLint;
/** @type {ESLint} */
let eslint;
/** @type {boolean} */
let fix;
/**
* @typedef {object} setupOptions
* @property {string=} eslintPath - import path of eslint
* @property {ESLintOptions=} eslintOptions - linter options
*
* @param {setupOptions} arg0 - setup worker
*/
function setup({
eslintPath,
eslintOptions = {}
}) {
fix = !!(eslintOptions && eslintOptions.fix);
({
ESLint
} = require(eslintPath || 'eslint'));
eslint = new ESLint(eslintOptions);
}
/**
* @param {string | string[]} files
*/
async function lintFiles(files) {
const result = await eslint.lintFiles(files); // if enabled, use eslint autofixing where possible
if (fix) {
await ESLint.outputFixes(result);
}
return result;
}