closure-builder
Version:
Simple Closure, Soy and JavaScript Build system
547 lines (488 loc) • 15.3 kB
JavaScript
/**
* @fileoverview Closure Builder - Closure compilers (online/local)
*
* @license Copyright 2016 Google Inc. All Rights Reserved.
*
* 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.
*
* @author mbordihn@google.com (Markus Bordihn)
*/
let dnsSync = require('dns-sync');
let fs = require('fs-extra');
let https = require('https');
let querystring = require('querystring');
let javaTools = require('../../tools/java.js');
let pathTools = require('../../tools/path.js');
let textTools = require('../../tools/text.js');
/**
* @constructor
* @struct
* @final
*/
let ClosureCompiler = function() {};
/**
* @type {boolean}
*/
ClosureCompiler.DEBUG = false;
/**
* @type {string}
*/
ClosureCompiler.REMOTE_SERVICE = 'closure-compiler.appspot.com';
/**
* @type {!array}
*/
ClosureCompiler.IGNORED_WARNINGS = [
'Java HotSpot(TM) Client VM warning',
'com.google.javascript.jscomp.PhaseOptimizer$NamedPass',
'Skipping pass ambiguateProperties',
'Skipping pass checkAccessControls',
'Skipping pass checkConformance',
'Skipping pass checkTypes',
'Skipping pass devirtualizePrototypeMethods',
'Skipping pass disambiguateProperties',
'Skipping pass inferTypes',
'Skipping pass inlineProperties',
'Skipping pass resolveTypes',
];
/**
* @param {!array} files
* @param {Object=} opt_options
* @param {string=} opt_target_file
* @param {function=} opt_callback
* @param {boolean=} opt_remote_service
*/
ClosureCompiler.compile = function(files, opt_options, opt_target_file,
opt_callback, opt_remote_service) {
if (!files || files.length == 0) {
ClosureCompiler.error('No valid files are provided!', opt_callback);
return;
}
if (javaTools.hasJava() && !opt_remote_service) {
ClosureCompiler.localCompile(files, opt_options, opt_target_file,
opt_callback);
} else if (dnsSync.resolve(ClosureCompiler.REMOTE_SERVICE)) {
ClosureCompiler.remoteCompile(files, opt_options, opt_target_file,
opt_callback);
} else {
ClosureCompiler.localCompileJs(files, opt_options, opt_target_file,
opt_callback);
}
};
/**
* @param {!string} files
* @param {Object=} opt_options
* @param {string=} opt_target_file
* @param {function=} opt_callback
*/
ClosureCompiler.localCompile = function(files, opt_options, opt_target_file,
opt_callback) {
if (!files) {
ClosureCompiler.error('No valid files are provided!', opt_callback);
return;
}
if (!javaTools.hasJava()) {
ClosureCompiler.error('Java (JRE) is needed!', opt_callback);
return;
}
let compilerOptions = [];
let options = opt_options || {};
let showWarnings = true;
// Compilation level
if (!options.compilation_level) {
options.compilation_level = 'SIMPLE_OPTIMIZATIONS';
}
// Handling warnings
if (options.no_warnings) {
showWarnings = false;
delete options.jscomp_warnings;
delete options.no_warnings;
} else {
// Compiler warnings
if (!options.jscomp_warning) {
options.jscomp_warning = ['checkVars', 'conformanceViolations',
'deprecated', 'externsValidation', 'fileoverviewTags', 'globalThis',
'misplacedTypeAnnotation', 'missingProvide', 'missingRequire',
'missingReturn', 'nonStandardJsDocs', 'typeInvalidation',
'uselessCode'];
}
}
// Handling compiler error
if (options.jscomp_error) {
for (let i = 0; i < options.jscomp_error.length; i++) {
compilerOptions.push('--jscomp_error', options.jscomp_error[i]);
}
delete options.jscomp_warning;
}
// Handling compiler off
if (options.jscomp_off) {
for (let i = 0; i < options.jscomp_off.length; i++) {
compilerOptions.push('--jscomp_off', options.jscomp_off[i]);
}
delete options.jscomp_warning;
}
// Handling compiler warnings
if (options.jscomp_warning) {
for (let i = 0; i < options.jscomp_warning.length; i++) {
compilerOptions.push('--jscomp_warning', options.jscomp_warning[i]);
}
delete options.jscomp_warning;
}
// Handling files
let dupFile = {};
for (let i = 0; i < files.length; i++) {
if (!dupFile[files[i]]) {
compilerOptions.push('--js', files[i]);
}
dupFile[files[i]] = true;
}
// Handling externs files
if (options.externs) {
for (let i = 0; i < options.externs.length; i++) {
compilerOptions.push('--externs', options.externs[i]);
}
delete options.externs;
}
// Handling generate_exports
if (options.generate_exports && !options.use_closure_basefile) {
options.use_closure_basefile = true;
}
// Closure templates
if (options.use_closure_templates) {
compilerOptions.push('--js=' + pathTools.getClosureSoyChecksFile());
compilerOptions.push('--js=' + pathTools.getClosureSoyUtilsFile());
if (!options.use_closure_library) {
options.use_closure_library = true;
}
delete options.use_closure_templates;
}
let closureLibraryFolder = undefined;
if (typeof options.use_closure_library === 'string') {
closureLibraryFolder = options.use_closure_library;
}
// Include Closure base file
if (options.use_closure_basefile || options.use_closure_library) {
let baseFile = pathTools.getClosureBaseFile(closureLibraryFolder);
if (baseFile) {
compilerOptions.push('--js', baseFile);
}
delete options.use_closure_basefile;
}
// Include Closure library files
if (options.use_closure_library) {
let ignoreList = [];
if (options.use_closure_library_ui) {
delete options.use_closure_library_ui;
} else {
ignoreList.push('ui');
}
let closureLibraryFolders = pathTools.getClosureLibraryFolders(
ignoreList, closureLibraryFolder);
for (let i = 0; i < closureLibraryFolders.length; i++) {
compilerOptions.push('--js=' + closureLibraryFolders[i]);
}
delete options.use_closure_library;
}
// Handling options
for (let option in options) {
if (Object.prototype.hasOwnProperty.call(options, option)) {
if (Array.isArray(options[option])) {
options[option].forEach((option_val) => {
compilerOptions.push('--' + option, option_val);
});
} else {
compilerOptions.push('--' + option, options[option]);
}
}
}
let compilerEvent = (error, stdout, stderr) => {
let code = stdout;
let errorMsg = stderr || error;
let errors = null;
let warnings = null;
let numErrors = 0;
let numWarnings = 0;
// Handling Error messages
if (errorMsg) {
errorMsg = textTools.filterStrings(errorMsg,
ClosureCompiler.IGNORED_WARNINGS);
let parsedErrorMessage = ClosureCompiler.parseErrorMessage(errorMsg);
numErrors = parsedErrorMessage.errors;
numWarnings = parsedErrorMessage.warnings;
}
if (numErrors == 0 && numWarnings > 0 && showWarnings) {
warnings = errorMsg;
ClosureCompiler.warn(warnings);
} else if (numErrors > 0) {
errors = errorMsg;
ClosureCompiler.error(errors);
code = null;
}
if (opt_callback) {
opt_callback(errors, warnings, opt_target_file, code);
}
};
javaTools.execJar(
pathTools.getClosureCompilerJar(),
compilerOptions,
compilerEvent,
null,
ClosureCompiler.DEBUG);
};
/**
* @param {!string} files
* @param {Object=} opt_options
* @param {string=} opt_target_file
* @param {function=} opt_callback
*/
ClosureCompiler.localCompileJs = function(files, opt_options, opt_target_file,
opt_callback) {
if (!files) {
ClosureCompiler.error('No valid files are provided!', opt_callback);
return;
}
console.error('Not supported yet ...');
};
/**
* @param {!string} files
* @param {Object=} opt_options
* @param {string=} opt_target_file
* @param {function=} opt_callback
* @deprecated
*/
ClosureCompiler.remoteCompile = function(files,
opt_options, opt_target_file, opt_callback) {
if (!files) {
ClosureCompiler.error('No valid files are provided!', opt_callback);
return;
}
if (!dnsSync.resolve(ClosureCompiler.REMOTE_SERVICE)) {
ClosureCompiler.error('No network connection to remote service!');
return;
}
// Handling options (true = critical / false = ignore)
let unsupportedOptions = {
'entry_point': true,
'language_in': false,
'language_out': false,
'generate_exports': true,
};
let option;
for (option in opt_options) {
if (option in unsupportedOptions) {
if (unsupportedOptions[option]) {
ClosureCompiler.error(option + ' is unsupported by the ' +
'closure-compiler webservice!', opt_callback);
return;
} else {
ClosureCompiler.warn(option + ' is unsupported by the ' +
'closure-compiler webservice!');
delete opt_options[option];
}
}
}
let options = opt_options || {};
let data = {
'compilation_level': 'SIMPLE_OPTIMIZATIONS',
'output_format': 'json',
'output_info': ['compiled_code', 'warnings', 'errors', 'statistics'],
'js_code': [],
};
let showWarnings = true;
// Closure templates
if (options.use_closure_templates) {
let closureSoyChecksFile = pathTools.getClosureSoyChecksFile();
if (closureSoyChecksFile) {
data['js_code'].push(fs.readFileSync(closureSoyChecksFile).toString());
}
let closureSoyUtilsFile = pathTools.getClosureSoyUtilsFile();
if (closureSoyUtilsFile) {
data['js_code'].push(fs.readFileSync(closureSoyUtilsFile).toString());
if (!options.use_closure_library) {
options.use_closure_library = true;
}
}
delete options.use_closure_templates;
}
// Handling files
for (let i = 0; i < files.length; i++) {
let fileContent = fs.readFileSync(files[i]).toString();
if (fileContent) {
data['js_code'].push(fileContent);
}
}
// Handling externs files
if (options.externs) {
let externsCode = '';
for (let i = 0; i < options.externs.length; i++) {
externsCode += fs.readFileSync(options.externs[i]).toString();
}
if (externsCode) {
data['js_externs'] = externsCode;
}
delete options.externs;
}
// Handling warnings
if (options.no_warnings) {
showWarnings = false;
delete options.no_warnings;
}
// Handling options
for (option in options) {
if (Object.prototype.hasOwnProperty.call(options, option)) {
data[option] = options[option];
}
}
let dataString = querystring.stringify(data);
let httpOptions = {
host: ClosureCompiler.REMOTE_SERVICE,
path: '/compile',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': dataString.length,
},
};
let request = https.request(httpOptions, function(response) {
let data = '';
response.setEncoding('utf8');
response.on('data', function(chunk) {
data += chunk;
});
response.on('end', function() {
let result = JSON.parse(data);
let code = result.compiledCode;
let errorMsg = result.errors;
let warningMsg = result.warnings;
let serverErrorMsg = result.serverErrors;
let errors = null;
let warnings = null;
if (serverErrorMsg) {
errors = ClosureCompiler.parseJsonError(serverErrorMsg);
ClosureCompiler.error(errors || errorMsg);
code = '';
} else if (errorMsg) {
errors = ClosureCompiler.parseJsonError(errorMsg);
ClosureCompiler.error(errors);
code = '';
} else if (warningMsg && showWarnings) {
warnings = ClosureCompiler.parseJsonError(warningMsg);
ClosureCompiler.warn(warnings);
}
if (code) {
code += '\n';
}
if (opt_callback) {
opt_callback(errors, warnings, opt_target_file, code);
}
});
});
request.on('error', function(e) {
ClosureCompiler.error('HTTP request error:' + e.message, opt_callback);
});
request.write(dataString);
request.end();
};
/**
* @param {string} data
* @return {!string}
*/
ClosureCompiler.parseJsonError = function(data) {
let message = '';
for (let i=0; i<data.length; i++) {
let msg = data[i].error || data[i].warning;
let type = (data[i].error) ? 'ERROR' : 'WARNING';
if (data.file && data.file !== 'Input_0') {
message += data.file + ':' + data.lineno + ': ' + type + ' - ' +
msg + '\n';
} else if (data[i].line) {
message += type + ' - ' + msg + ' : ' + data[i].line + '\n';
} else {
message += type + ' - ' + msg + '\n';
}
}
return message;
};
/**
* @param {string} message
* @return {Object} with number of detected errors and warnings
*/
ClosureCompiler.parseErrorMessage = function(message) {
let errors = 0;
let warnings = 0;
if (message && message.match) {
let message_reg = /([0-9]+) error\(s\), ([0-9]+) warning\(s\)/;
let messageInfo = message.match(message_reg);
if (messageInfo) {
errors = messageInfo[1];
warnings = messageInfo[2];
} else if (message.includes('INTERNAL COMPILER ERROR') ||
message.includes('NullPointerException') ||
message.includes('java.lang.NoSuchMethodError')) {
errors = 1;
} else if (message.toLowerCase().includes('exception')) {
errors = 1;
} else if (message.toLowerCase().includes('error')) {
errors = message.toLowerCase().split('error').length - 1;
} else if (message.toLowerCase().includes('warning')) {
if (message.includes('Java HotSpot(TM) Client VM warning') &&
message.toLowerCase().split('warning').length <= 2) {
warnings = 0;
} else if (message.includes('Illegal reflective access by com.google.') &&
message.toLowerCase().split('warning').length == 7) {
warnings = 0;
} else {
warnings = message.toLowerCase().split('warning').length - 1;
}
}
} else if (message) {
errors = 1;
}
// Ignore closure library specific warnings.
if (warnings == 1 && errors == 0 &&
message.includes('third_party') &&
message.includes('closure-library') &&
message.includes('closure') &&
message.includes('goog') &&
message.includes('deprecated:')) {
warnings = 0;
}
return {
errors: errors,
warnings: warnings,
};
};
/**
* @param {string} msg
*/
ClosureCompiler.info = function(msg) {
if (msg) {
console.info('[Closure Compiler]', msg);
}
};
/**
* @param {string} msg
*/
ClosureCompiler.warn = function(msg) {
console.error('\x1b[1m\x1b[33m[Closure Compiler Warn]\x1b[0m', msg);
};
/**
* @param {string} msg
* @param {function=} opt_callback
*/
ClosureCompiler.error = function(msg, opt_callback) {
console.error('\x1b[1m\x1b[31m[Closure Compiler Error]\x1b[0m', msg);
if (opt_callback) {
opt_callback(msg);
}
};
module.exports = ClosureCompiler;