UNPKG

hint

Version:

The linting tool for the web

190 lines (189 loc) 7.59 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Analyzer = void 0; const path = require("path"); const url_1 = require("url"); const config_1 = require("./config"); const types_1 = require("./types"); const engine_1 = require("./engine"); const error_status_1 = require("./enums/error-status"); const resource_loader_1 = require("./utils/resource-loader"); const utils_1 = require("@hint/utils"); const utils_string_1 = require("@hint/utils-string"); const utils_fs_1 = require("@hint/utils-fs"); const initFormatters = (formatters) => { const result = formatters.map((FormatterConstructor) => { return new FormatterConstructor(); }); return result; }; const validateResources = (resources) => { if (resources.missing.length > 0 || resources.incompatible.length > 0) { throw new types_1.AnalyzerError('Missing or incompatible dependencies', error_status_1.AnalyzerErrorStatus.ResourceError, resources); } }; const validateHints = (configuration) => { const hintsValidation = config_1.Configuration.validateHintsConfig(configuration); if (hintsValidation.invalid.length > 0) { throw new types_1.AnalyzerError('Invalid Hints', error_status_1.AnalyzerErrorStatus.HintError, hintsValidation.invalid); } }; const validateConnector = (configuration) => { const connectorCofigurationValid = config_1.Configuration.validateConnectorConfig(configuration); if (!connectorCofigurationValid) { throw new types_1.AnalyzerError('Invalid connector configuration', error_status_1.AnalyzerErrorStatus.ConnectorError); } }; class Analyzer { constructor(configuration, resources, formatters) { this.messages = { 'fetch::end': '%url% downloaded', 'fetch::start': 'Downloading %url%', 'scan::end': 'Finishing...', 'scan::start': 'Analyzing %url%', 'traverse::down': 'Traversing the DOM', 'traverse::end': 'Traversing finished', 'traverse::start': 'Traversing the DOM', 'traverse::up': 'Traversing the DOM' }; this.configuration = configuration; this._resources = resources; this.formatters = formatters; this.watch = this.configuration.connector && this.configuration.connector.options && this.configuration.connector.options.watch; } static create(userConfiguration, options = {}) { let configuration; if (!userConfiguration) { throw new types_1.AnalyzerError('Missed configuration', error_status_1.AnalyzerErrorStatus.ConfigurationError); } try { configuration = config_1.Configuration.fromConfig(userConfiguration, options); } catch (e) { throw new types_1.AnalyzerError(`Invalid configuration. ${e.message}.`, error_status_1.AnalyzerErrorStatus.ConfigurationError); } const resources = (0, resource_loader_1.loadResources)(configuration); const formatters = initFormatters(resources.formatters); validateResources(resources); validateConnector(configuration); validateHints(configuration); return new Analyzer(configuration, resources, formatters); } static getUserConfig(filePath) { const isDirectory = !(0, utils_fs_1.isFile)(filePath); const configPath = isDirectory ? config_1.Configuration.getFilenameForDirectory(filePath || (0, utils_fs_1.cwd)()) : filePath; if (!configPath) { return null; } try { const resolvedPath = path.resolve(isDirectory ? (filePath || (0, utils_fs_1.cwd)()) : (0, utils_fs_1.cwd)(), configPath); return config_1.Configuration.loadConfigFile(resolvedPath); } catch (_a) { return null; } } normalizeTarget(inputUrl) { if (inputUrl.url) { const target = inputUrl; const url = target.url instanceof url_1.URL ? target.url : new url_1.URL(target.url); return { content: target.content, url }; } const url = inputUrl instanceof url_1.URL ? inputUrl : new url_1.URL(inputUrl); return { content: undefined, url }; } normalizeEvent(event) { if (event.startsWith('fetch::end')) { return 'fetch::end'; } return event; } configureEngine(engine, url, options) { if (options.updateCallback) { engine.prependAny(((event, value) => { const message = this.messages[this.normalizeEvent(event)]; if (!message) { return; } options.updateCallback({ message: message.replace('%url%', (0, utils_string_1.cutString)(value.resource)), resource: value.resource, url }); })); } if (this.watch) { engine.on('print', async (event) => { await this.format(event.problems); }); } } async analyze(endpoints, options = {}) { let targets; const results = []; if (Array.isArray(endpoints)) { targets = endpoints.map(this.normalizeTarget); } else { targets = [this.normalizeTarget(endpoints)]; } for (const target of targets) { const url = target.url; if (target.content && this.configuration.connector.name !== 'local') { throw new types_1.AnalyzerError(`Property 'content' is only supported in formatter local. Webhint will analyze the url ${url.href}`, error_status_1.AnalyzerErrorStatus.AnalyzeError); } this.engine = new engine_1.Engine(this.configuration, this._resources); this.configureEngine(this.engine, url.href, options); let problems = null; try { if (options.targetStartCallback) { await options.targetStartCallback({ url: url.href }); } problems = await this.engine.executeOn(url, { content: target.content }); } catch (e) { throw new types_1.AnalyzerError(e, error_status_1.AnalyzerErrorStatus.AnalyzeError); } finally { await this.engine.close(); } if (options.targetEndCallback) { await options.targetEndCallback({ problems: problems, url: url.href }); } results.push({ problems: problems, url: target.url.href }); } if (this.watch && this.configuration.connector.name !== 'local') { utils_1.logger.warn(`WARNING: The option 'watch' is not supported in connector '${this.configuration.connector.name}'`); } return results; } async format(problems, options = {}) { options.language = options.language || this.configuration.language; options.resources = options.resources || this.resources; for (const formatter of this.formatters) { await formatter.format(problems, options); } } close() { if (this.engine) { return this.engine.close(); } return Promise.resolve(); } get resources() { return this._resources; } } exports.Analyzer = Analyzer;