UNPKG

@adguard/aglint

Version:

Universal adblock filter list linter.

254 lines (252 loc) 11 kB
#!/usr/bin/env node /* * AGLint v3.0.0 (build date: Wed, 21 May 2025 13:24:14 GMT) * (c) 2025 AdGuard Software Ltd. * Released under the MIT license * https://github.com/AdguardTeam/AGLint#readme */ import { AdblockSyntax } from '@adguard/agtree'; import { program } from 'commander'; import { readdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import yaml from 'js-yaml'; import checkbox from '@inquirer/checkbox'; import select from '@inquirer/select'; import 'superstruct'; import 'clone-deep'; import '@adguard/agtree/parser'; import '@adguard/agtree/generator'; import './linter/config.js'; import './linter/rules/duplicated-hint-platforms.js'; import './linter/rules/duplicated-hints.js'; import './linter/rules/unknown-hints-and-platforms.js'; import './linter/rules/duplicated-modifiers.js'; import './linter/rules/if-closed.js'; import './linter/rules/invalid-domain-list.js'; import './linter/rules/invalid-modifiers.js'; import './linter/rules/inconsistent-hint-platforms.js'; import './linter/rules/single-selector.js'; import './linter/rules/unknown-preprocessor-directives.js'; import './linter/rules/no-short-rules.js'; import './linter/rules/no-excluded-rules.js'; import './linter/rules/no-invalid-css-syntax.js'; import './linter/rules/no-invalid-css-declaration.js'; import './linter/inline-config.js'; import './linter/severity.js'; import { NEWLINE } from './common/constants.js'; import './linter/config-presets/aglint-all.js'; import './linter/config-presets/aglint-recommended.js'; import './linter/helpers/css-tree-types.js'; import '@adguard/ecss-tree'; import { version } from './version.js'; import { LinterCli } from './linter/cli/index.js'; import { CONFIG_FILE_NAMES, JSON_RC_CONFIG_FILE_NAME, YAML_RC_CONFIG_FILE_NAME } from './linter/cli/constants.js'; import { LinterConsoleReporter } from './linter/cli/reporters/console.js'; import './linter/cli/scan.js'; /** * @file AGLint CLI * @todo DEV run: node --no-warnings --loader ts-node/esm --experimental-specifier-resolution=node src/cli.ts */ /** * Represents possible config file formats. */ var ConfigFileFormat; (function (ConfigFileFormat) { ConfigFileFormat["Yaml"] = "yaml"; ConfigFileFormat["Json"] = "json"; })(ConfigFileFormat || (ConfigFileFormat = {})); /** * Print error to the console, also handle unknown errors. * * @param error Error to print */ function printError(error) { const lines = [ 'Oops! Something went wrong! :(', '', `AGLint: ${version}`, '', ]; if (error instanceof Error) { const { message, stack } = error; lines.push(message || 'No error message provided'); lines.push(''); // Very basic stack trace formatting lines.push(...(stack || '').split('\n').map((line) => ` ${line}`)); } else { // Convert any unknown error to string lines.push(String(error)); } // Print generated lines to the console as error // eslint-disable-next-line no-console console.error(lines.join('\n')); } /** * Creates a content of the config file. * * @param chosenFormat Format chosen by the user. * @param chosenSyntaxes Syntaxes chosen by the user. * * @returns Config file content. */ const getConfigFileContent = (chosenFormat, chosenSyntaxes) => { // Prepare config object const preparedConfig = { root: true, extends: [ 'aglint:recommended', ], // set Common syntax as default if nothing is chosen syntax: chosenSyntaxes.length === 0 ? [AdblockSyntax.Common] : chosenSyntaxes, }; // Serialize config object to a string based on the chosen format let serializedConfig; switch (chosenFormat) { case ConfigFileFormat.Yaml: // YAML supports comments, so we can add some useful info to the beginning of the file serializedConfig = [ '# AGLint config file', '# Documentation: https://github.com/AdguardTeam/AGLint#configuration', ].join(NEWLINE); serializedConfig += NEWLINE; // Serialize config object to YAML. This will add the final newline automatically serializedConfig += yaml.dump(preparedConfig); break; case ConfigFileFormat.Json: // Serialize config object to JSON serializedConfig = JSON.stringify(preparedConfig, null, 2); // Add final newline manually serializedConfig += NEWLINE; break; default: throw new Error(`Unsupported config file format "${chosenFormat}"`); } return serializedConfig; }; /** * Creates a config file in the current directory. * * TODO: This is a very basic implementation, we should implement a proper config file generator in the future. * * @param cwd Current working directory. */ const createConfig = async (cwd) => { // Ask user to specify which config file format to use const chosenFormat = await select({ message: 'Select which config file format you want to use.\n', choices: [ { value: ConfigFileFormat.Yaml }, { value: ConfigFileFormat.Json }, ], }); // Ask user to specify which syntaxes to use const chosenSyntaxes = await checkbox({ message: 'Select which adblock syntax(es) you want to use.\n"Common" is to be used if none is chosen.\n', choices: [ { value: AdblockSyntax.Abp }, { value: AdblockSyntax.Adg }, { value: AdblockSyntax.Ubo }, ], }); // Generate config file content based on the chosen format and syntaxes const configContent = getConfigFileContent(chosenFormat, chosenSyntaxes); // Determine the config file name based on the chosen format let configFileName; switch (chosenFormat) { case ConfigFileFormat.Yaml: configFileName = YAML_RC_CONFIG_FILE_NAME; break; case ConfigFileFormat.Json: configFileName = JSON_RC_CONFIG_FILE_NAME; break; default: throw new Error(`Unsupported config file format "${chosenFormat}"`); } // Write the config file to the current working directory await writeFile(join(cwd, configFileName), configContent); // Notify the user that the config file was created successfully // eslint-disable-next-line no-console console.log(`Config file was created successfully in directory "${cwd}" as "${configFileName}"`); // Notify user about root: true option // eslint-disable-next-line no-console, max-len console.log('Note: "root: true" option was added to the config file. Please make sure that the config file is located in the root directory of your project.'); // eslint-disable-next-line no-console console.log('You can learn more at https://github.com/AdguardTeam/AGLint#why-the-root-option-is-important'); }; (async () => { try { // Set-up Commander program // Basic info .name('AGLint') .description('Adblock filter list linter') .version(version, '-v, --version', 'Output the version number') .usage('[options] [file paths...]') // Customized help option .helpOption('-h, --help', 'Display help for command') // Options .option('-f, --fix', 'Enable automatic fix, if possible (BE CAREFUL, this overwrites original files with the fixed ones)', false) .option('-c, --colors', 'Force enabling colors', true) .option('--no-colors', 'Force disabling colors') .option('--no-ignores', 'Force ignoring .aglintignore files') // Parse the arguments .parse(process.argv); // This specifies in which folder the "npx aglint" / "pnpm aglint" command was invoked // and use "process.cwd" as fallback. This is the current working directory (cwd). const cwd = process.env.INIT_CWD || process.cwd(); // "aglint init": initialize config file in the current directory (cwd) if (program.args[0] === 'init') { // TODO: Move config init logic to a separate file: // https://github.com/AdguardTeam/AGLint/issues/119 // Don't allow to initialize config file if another config file already exists const cwdItems = await readdir(cwd); for (const item of cwdItems) { if (CONFIG_FILE_NAMES.has(item)) { // Show which config file is conflicting exactly // eslint-disable-next-line no-console console.error(`Config file already exists in directory "${cwd}" as "${item}"`); process.exit(1); } } await createConfig(cwd); // We should exit the process here, because we don't want to run the linter after // initializing the config file process.exit(0); } // TODO: Custom reporter support with --reporter option const cli = new LinterCli(new LinterConsoleReporter(program.opts().colors), !!program.opts().fix, !!program.opts().ignores); await cli.run(cwd, program.args); // If there are errors, exit with code 1. This is necessary for CI/CD pipelines, // see https://docs.github.com/en/actions/creating-actions/setting-exit-codes-for-actions#about-exit-codes if (cli.hasErrors()) { process.exit(1); } } catch (error) { if (error instanceof Error && error.name === 'NoConfigError') { /* eslint-disable max-len, no-console */ // Show a detailed error message if the config file was not found console.error([ 'AGLint couldn\'t find the config file. To set up a configuration file for this project, please run:', '', ' If you use NPM:\tnpx aglint init', ' If you use PNPM:\tpnpm aglint init', ' If you use Yarn:\tyarn aglint init', '', 'IMPORTANT: The init command creates a root config file, so be sure to run it in the root directory of your project!', '', 'AGLint will try to find the config file in the current directory (cwd), but if the config file is not found', 'there, it will try to find it in the parent directory, and so on until it reaches your OS root directory.', ].join('\n')); /* eslint-enable max-len, no-console */ // Exit with code 1. This is necessary for CI/CD pipelines process.exit(1); } // If any error occurs it means that the linter failed to run // Format and print error to the console printError(error); // Exit with code 2. This is necessary for CI/CD pipelines process.exit(2); } })();