com.google.javascript.jscomp.js.build_polyfill_table.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-linter Show documentation
Show all versions of closure-compiler-linter Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
This binary checks for style issues such as incorrect or missing JSDoc
usage, and missing goog.require() statements. It does not do more advanced
checks such as typechecking.
#!/usr/bin/env node
/*
* Copyright 2016 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const fs = require('fs');
/**
* Provides an ordering to ensure lower-versioned polyfills don't
* depend on higher versions.
*/
const ORDER = ['es3', 'es5', 'es6', 'es7', 'es8', 'es9', 'es_2019'];
/**
* Prints to stderr and exits.
* @param {string} message
*/
function fail(message) {
console.error(message);
process.exit(1);
}
/**
* Builds up a table of polyfills.
*/
class PolyfillTable {
constructor() {
/** @const {!Map>} */
this.symbolToFile = new Map();
/** @const {!Map>} */
this.deps = new Map();
/** @const {!Map} */
this.versions = new Map();
/** @const {!Array>} */
this.rows = [];
}
/**
* Returns a shim for $jscomp.polyfill.
* @param {string} lib Library currently being scanned.
* @return {function(string, ?Function, string, string)}
*/
polyfill(lib) {
return (polyfill, impl, fromLang, toLang) => {
if (!ORDER.includes(fromLang)) {
throw new Error(
`Unknown language version ${fromLang} for ${polyfill}`);
}
if (!ORDER.includes(toLang)) {
throw new Error(
`Unknown language version ${toLang} for ${polyfill}`);
}
this.symbolToFile.set(polyfill, this.symbolToFile.get(polyfill) || []);
this.symbolToFile.get(polyfill).push(lib);
const row = [polyfill, fromLang, toLang];
if (impl) {
row.push(lib);
this.versions.set(lib, maxVersion(this.versions.get(lib), toLang));
}
this.rows.push(row);
};
}
/**
* Reads a JS file and adds it to the table.
* @param {string} lib Name of the library.
* @param {string} data Contents of the file.
*/
readFile(lib, data) {
// Look for 'require' directives and add it to the dependency map.
const deps = new Set();
this.deps.set(lib, deps);
const re = /'require ([^']+)'/g;
let match;
while (match = re.exec(data)) {
match[1].split(' ').forEach(dep => deps.add(dep));
}
// Now run the file.
try {
new Function('$jscomp', data)({
global: global,
polyfill: this.polyfill(lib, table),
});
} catch (err) {
throw new Error('Failed to parse file: ' + lib + ': ' + err);
}
}
/**
* Concatenates the table into a string. Throws an error if
* there are any symbols provided by multiple files.
* @return {string}
*/
build() {
const errors = new Set();
try {
// First check for duplicate provided symbols.
for (const entry of this.symbolToFile.entries()) {
if (entry[1].length != 1) {
errors.add(
`ERROR - ${entry[0]} provided by multiple files:${
entry[1].map(f => '\n ' + f).join('')}`);
}
}
// Next ensure all deps have nonincreasing versions.
checkDeps(errors, this.deps, this.versions);
// If there are any errors, we should fail; otherwise concatenate.
} catch (err) {
errors.add('ERROR - uncaught exception: ' + err.stack);
}
if (errors.size) {
fail(Array.from(errors).join('\n\n'));
}
return this.rows.sort().map(row => row.join(' ')).join('\n');
}
}
/**
* Checks dependencies for the following issues:
* (1) cyclic dependencies
* (2) missing dependencies
* (3) version mismatches
* @param {!Set} errors
* @param {!Map>} deps
* @param {!Map} versions
*/
function checkDeps(errors, deps, versions) {
for (const file of deps.keys()) {
const seen = new Set([file]);
const queue = [file];
const version = versions.get(file);
while (queue.length) {
const next = queue.shift();
for (const dep of deps.get(next) || []) {
if (dep == file) errors.add('ERROR - Cyclic dependency:\n ' + dep);
if (seen.has(dep)) continue;
seen.add(dep);
queue.push(dep);
if (!deps.has(dep)) {
errors.add(
'ERROR - missing dependency:\n ' + dep +
' required from\n ' + file);
}
const depVersion = versions.get(dep);
if (version && maxVersion(depVersion, version) != version) {
errors.add(
'ERROR - lower version depends on higher version:\n ' +
version + ': ' + file + '\n ' + depVersion + ': ' + dep);
}
}
}
}
}
/**
* Returns the higher order of the given versions.
* @param {string} version1
* @param {string} version2
* @return {string} The max version.
*/
function maxVersion(version1, version2) {
return ORDER[Math.max(ORDER.indexOf(version1), ORDER.indexOf(version2))];
}
const table = new PolyfillTable();
const reads = process.argv.slice(2).map(filename =>
new Promise((fulfill, reject) =>
fs.readFile(filename, 'utf8', (err, data) => {
try {
if (err) {
reject(err);
} else {
const lib = filename.replace(/^.*?\/js\/|\.js$/g, '');
table.readFile(lib, data);
fulfill('');
}
} catch (err) {
reject(err);
}
})));
Promise.all(reads).then(
success => console.log(table.build()),
failure => fail(failure.stack));