/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
validator/js/nodejs/index.js
495 строк
16 KB
Kyle Niewiada
Fix deprecated default program import with named import (#39932)
01 апр 2024, 22:36
Не верифицирован
01 апр 2024, 22:36
f2bc788
Код
Авторство
О чём код?
'use strict'; const colors = require('colors/safe'); const fs = require('fs'); const http = require('http'); const https = require('https'); const path = require('path'); const { program } = require('commander'); const Promise = require('promise'); const util = require('util'); const vm = require('vm'); const DEFAULT_USER_AGENT = 'amphtml-validator'; /** * Determines if str begins with prefix. * @param {string} str * @param {string} prefix * @return {boolean} */ function hasPrefix(str, prefix) { return str.indexOf(prefix) == 0; } /** * Convenience function to detect whether an argument is a URL. If not, * it may be a local file. * @param {string} url * @return {boolean} */ function isHttpOrHttpsUrl(url) { return hasPrefix(url, 'http://') || hasPrefix(url, 'https://'); } /** * Creates a promise which reads from a file. * @param {string} name * @return {Promise<string>} */ function readFromFile(name) { return new Promise(function(resolve, reject) { fs.readFile(name, 'utf8', function(err, data) { if (err) { reject(err); } else { resolve(data.trim()); } }); }); } /** * Creates a promise which reads from a stream. * @param {string} name * @param {!stream.Readable} readable * @return {Promise<string>} */ function readFromReadable(name, readable) { return new Promise(function(resolve, reject) { const chunks = []; readable.setEncoding('utf8'); readable.on('data', function(chunk) { chunks.push(chunk); }); readable.on('end', function() { resolve(chunks.join('')); }); readable.on('error', function(error) { reject(new Error('Could not read from ' + name + ' - ' + error.message)); }); }); } /** * Creates a promise which reads from standard input. Even though it would * be easy to make a function that just returns the data, we return a promise * for consistency with readFromUrl and readFromFile. * @return {Promise<string>} */ function readFromStdin() { return readFromReadable('stdin', process.stdin).then(function(data) { process.stdin.resume(); return data; }); } /** * Creates a promise which reads from a URL or more precisely, fetches * the contents located at the URL by using the 'http' or 'https' module. * Any HTTP status other than 200 is interpreted as an error. * @param {string} url * @param {string} userAgent * @return {Promise<string>} */ function readFromUrl(url, userAgent) { return new Promise(function(resolve, reject) { const clientModule = hasPrefix(url, 'http://') ? http : https; const req = clientModule.request(url, function(response) { if (response.statusCode !== 200) { // https://nodejs.org/api/http.html says: "[...] However, if // you add a 'response' event handler, then you must consume // the data from the response object, either by calling // response.read() whenever there is a 'readable' event, or by // adding a 'data' handler, or by calling the .resume() // method." response.resume(); reject(new Error( 'Unable to fetch ' + url + ' - HTTP Status ' + response.statusCode)); } else { resolve(response); } }); req.setHeader('User-Agent', userAgent); req.on('error', function(error) { // E.g., DNS resolution errors. reject( new Error('Unable to fetch ' + url + ' - ' + error.message)); }); req.end(); }) .then(readFromReadable.bind(null, url)); } /** * ValidationResult is the record computed by the validator for each * document. It contains an overall status (PASS/FAIL) and the list of * errors, if any. This class corresponds to the ValidationResult * message in validator.proto in this directory. * @export * @constructor */ function ValidationResult() { /** * Possible values are 'UNKNOWN', 'PASS', and 'FAIL'. * @type {string} */ this.status = 'UNKNOWN'; /** @type {!Array<!ValidationError>} */ this.errors = []; } /** * Each validation error describes a specific problem in a validated * document. This class corresponds to the ValidationError message in * validator.proto in this directory. * @export * @constructor */ function ValidationError() { /** * The severity of the error - possible values are 'UNKNOWN_SEVERITY', * 'ERROR', and 'WARNING'. */ this.severity = 'UNKNOWN_SEVERITY'; /** * The line number at which the error was seen (1 is the first line). */ this.line = 1; /** * The column number at which the error was seen (0 is the first column). */ this.col = 0; /** * A human-readable error message for the validation error. * If you find yourself trying to write a parser against this string * to scrape out some detail, consider looking at the code and params * fields below. * @type {string} */ this.message = ''; /** * The spec URL is often added by the validator to provide additional * context for the error. In a user interface this would be shown * as a "Learn more" link. * @type {string} */ this.specUrl = null; /** * This field is only useful when scripting against the validator, * it should not be displayed in a user interface as it adds nothing * for humans to read over the message field (see above). * Possible values are the codes listed in ValidationError.Code - see * validator.proto. Examples: 'UNKNOWN_CODE', 'MANDATORY_TAG_MISSING', * 'TAG_REQUIRED_BY_MISSING'. For each of these codes there is a * format string in validator-main.protoascii (look for error_formats), * which is used to assemble the message from the strings in params. * @type {string} */ this.code = 'UNKNOWN_CODE'; /** * This field is only useful when scripting against the validator, * it should not be displayed in a user interface as it adds nothing * for humans to read over the message field (see above). * @type {!Array<string>} */ this.params = []; } /** * The validator instance is a proxy object to a precompiled * validator_wasm.js script - in practice the script was either downloaded * from 'https://cdn.ampproject.org/v0/validator_wasm.js' or read from a * local file. * @param {string} scriptContents * @throws {!Error} * @constructor */ function Validator(scriptContents) { // The 'sandbox' is a Javascript object (dictionary) which holds // the results of evaluating the validatorJs / scriptContents, so // basically, it holds functions, prototypes, etc. As a // side-effect of evaluating, the VM will compile this code and // it's worth holding onto it. Hence, this validate function is // reached via 2 codepaths - either the sandbox came from the // cache, precompiledByValidatorJs - or we just varructed it // after downloading and evaluating the script. The API is fancier // here, vm.Script / vm.createContext / vm.runInContext and all // that, but it's quite similar to a Javascript eval. this.sandbox = vm.createContext(); try { new vm.Script(scriptContents).runInContext(this.sandbox); } catch (error) { throw new Error('Could not instantiate validator_wasm.js - ' + error.message); } } /** * Initialize the validator. * @return {Promise<undefined>!} */ Validator.prototype.init = function() { if (this.sandbox.amp.validator.init) { return this.sandbox.amp.validator.init(); } else { return Promise.resolve(undefined); } }; /** * Validates the provided inputString; the htmlFormat can be 'AMP' or * 'AMP4ADS'; it defaults to 'AMP' if not specified. * @param {string} inputString * @param {string=} htmlFormat * @return {!ValidationResult} * @export */ Validator.prototype.validateString = function(inputString, htmlFormat) { const internalResult = this.sandbox.amp.validator.validateString(inputString, htmlFormat); const result = new ValidationResult(); result.status = internalResult.status; for (let ii = 0; ii < internalResult.errors.length; ii++) { const internalError = internalResult.errors[ii]; const error = new ValidationError(); error.severity = internalError.severity; error.line = internalError.line; error.col = internalError.col; error.message = this.sandbox.amp.validator.renderErrorMessage(internalError); error.specUrl = internalError.specUrl; error.code = internalError.code; error.params = internalError.params; result.errors.push(error); } return result; }; /** * A global static map used by the getInstance function to avoid loading * AMP Validators more than once. * @type {!Object<string, Validator>} */ const instanceByValidatorJs = {}; /** * Provided a URL or a filename from which to fetch the validator_wasm.js * file, fetches, instantiates, and caches the validator instance * asynchronously. If you prefer to implement your own fetching / * caching logic, you may want to consider newInstance() instead, * which is synchronous and much simpler. * * @param {string=} opt_validatorJs * @param {string=} opt_userAgent * @return {!Promise<Validator>} * @export */ function getInstance(opt_validatorJs, opt_userAgent) { const validatorJs = opt_validatorJs || 'https://cdn.ampproject.org/v0/validator_wasm.js'; const userAgent = opt_userAgent || DEFAULT_USER_AGENT; if (instanceByValidatorJs.hasOwnProperty(validatorJs)) { return Promise.resolve(instanceByValidatorJs[validatorJs]); } const validatorJsPromise = isHttpOrHttpsUrl(validatorJs) ? readFromUrl(validatorJs, userAgent) : readFromFile(validatorJs); return validatorJsPromise.then(function(scriptContents) { let instance; try { instance = new Validator(scriptContents); } catch (error) { // It may be useful to cache errors and exceptions encountered // here, but for now we don't do this for e.g. http errors when // fetching the validator, so we shouldn't do it for syntax // errors etc. either (which lead to the varructor throwing an error). throw error; } instanceByValidatorJs[validatorJs] = instance; return instance; }).then(function(instance) { return instance.init().then(() => instance); }); } exports.getInstance = getInstance; /** * Provided the contents of the validator_wasm.js file, e.g. as downloaded from * 'https://cdn.ampproject.org/v0/validator_wasm.js', returns a new validator * instance. The tradeoff between this function and getInstance() is that this * function is synchronous but requires the contents of the validator_wasm.js * file as a parameter, while getInstance is asynchronous, fetches files * from disk or the web, and caches them. * * @param {string} validatorJsContents * @return {!Validator} * @export */ function newInstance(validatorJsContents) { return new Validator(validatorJsContents); } exports.newInstance = newInstance; // A note on emitting output to the console and process exit status: // Node.js prior to 0.11.8 did not support process.exitCode // (https://nodejs.org/api/process.html#process_process_exitcode), which // makes it difficult to emit output and errors from multiple callbacks // and set the appropriate exit code. We use the following workaround: // process.<<stream>>(<<some output>>, function() { process.exit(<<code>>); }); // This will flush the appropriate stream (stdout or stderr) and then // exit with the provided code. For now, this makes the CLI work with // Node.js versions as old as v0.10.25. /** * Logs a validation result to the console using process.stdout and * process.stderr as is appropriate. * @param {string} filename * @param {!ValidationResult} validationResult * @param {boolean} color */ function logValidationResult(filename, validationResult, color) { if (validationResult.status === 'PASS') { process.stdout.write( filename + ': ' + (color ? colors.green('PASS') : 'PASS') + '\n'); } for (let ii = 0; ii < validationResult.errors.length; ii++) { const error = validationResult.errors[ii]; let msg = filename + ':' + error.line + ':' + error.col + ' '; if (color) { msg += (error.severity === 'ERROR' ? colors.red : colors.magenta)( error.message); } else { msg += error.message; } if (error.specUrl) { msg += ' (see ' + error.specUrl + ')'; } // TODO(powdercloud): Should we distinguish error.severity === 'WARNING' ? process.stderr.write(msg + '\n'); } } /** * Main entry point into the command line tool. */ function main() { program .usage( '[options] <fileOrUrlOrMinus...>\n\n' + ' Validates the files or urls provided as arguments. If "-" is\n' + ' specified, reads from stdin instead.') .option( '--validator_js <fileOrUrl>', 'The Validator Javascript.\n' + ' Latest published version by default, or\n' + ' dist/validator_minified.js (built with build.py)\n' + ' for development.', 'https://cdn.ampproject.org/v0/validator_wasm.js') .option( '--user-agent <userAgent>', 'User agent string to use in requests.', DEFAULT_USER_AGENT) .option( '--html_format <AMP|AMP4ADS|AMP4EMAIL>', 'The input format to be validated.\n' + ' AMP by default.', 'AMP') .option( '--format <color|text|json>', 'How to format the output.\n' + ' "color" displays errors/warnings/success in\n' + ' red/orange/green.\n' + ' "text" avoids color (e.g., useful in terminals not\n' + ' supporting color).\n' + ' "json" emits json corresponding to the ValidationResult\n' + ' message in validator.proto.', 'color') .parse(process.argv); const opts = program.opts(); if (opts.length === 0) { program.outputHelp(); process.exit(1); } if (opts.html_format !== 'AMP' && opts.html_format !== 'AMP4ADS' && opts.html_format !== 'AMP4EMAIL') { process.stderr.write( '--html_format must be set to "AMP", "AMP4ADS", or "AMP4EMAIL".\n', function() { process.exit(1); }); } if (opts.format !== 'color' && opts.format !== 'text' && opts.format !== 'json') { process.stderr.write( '--format must be set to "color", "text", or "json".\n', function() { process.exit(1); }); } const inputs = []; for (let ii = 0; ii < program.args.length; ii++) { const item = program.args[ii]; if (item === '-') { inputs.push(readFromStdin()); } else if (isHttpOrHttpsUrl(item)) { inputs.push(readFromUrl(item, opts.userAgent)); } else { inputs.push(readFromFile(item)); } } getInstance(opts.validator_js, opts.userAgent) .then(function(validator) { Promise.all(inputs) .then(function(resolvedInputs) { const jsonOut = {}; let hasError = false; for (let ii = 0; ii < resolvedInputs.length; ii++) { const validationResult = validator.validateString( resolvedInputs[ii], opts.html_format); if (opts.format === 'json') { jsonOut[program.args[ii]] = validationResult; } else { logValidationResult( program.args[ii], validationResult, opts.format === 'color' ? true : false); } if (validationResult.status !== 'PASS') { hasError = true; } } if (opts.format === 'json') { process.stdout.write( JSON.stringify(jsonOut) + '\n', function() { process.exit(hasError ? 1 : 0); }); } else if (hasError) { process.stderr.write('', function() { process.exit(1); }); } else { process.stdout.write('', function() { process.exit(0); }); } }) .catch(function(error) { process.stderr.write( (opts.format == 'color' ? colors.red(error.message) : error.message) + '\n', function() { process.exit(1); }); }); }) .catch(function(error) { process.stderr.write( (opts.format == 'color' ? colors.red(error.message) : error.message) + '\n', function() { process.exit(1); }); }); } exports.main = main;