diff --git a/package.json b/package.json index 54b30298..6a995cb6 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "source-map": "^0.5.6", - "uglify-js": "^2.8.29", + "uglify-es": "^3.0.19", "webpack-sources": "^1.0.1" }, "devDependencies": { @@ -51,7 +51,6 @@ "nsp": "^2.6.3", "pre-commit": "^1.2.2", "standard-version": "^4.1.0", - "uglify-js": "^2.8.18", "webpack": "^3.0.0", "webpack-defaults": "^1.4.0" }, diff --git a/src/index.js b/src/index.js index 4ea3f161..ef1034ad 100644 --- a/src/index.js +++ b/src/index.js @@ -3,247 +3,268 @@ Author Tobias Koppers @sokra */ -// TODO: temporarily disabled rules -/* eslint-disable - no-undefined, - no-param-reassign, - no-underscore-dangle, - import/order -*/ import { SourceMapConsumer } from 'source-map'; -import { ConcatSource, RawSource, SourceMapSource } from 'webpack-sources'; - +import { SourceMapSource, RawSource, ConcatSource } from 'webpack-sources'; import RequestShortener from 'webpack/lib/RequestShortener'; import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers'; +import uglify from 'uglify-es'; -import uglify from 'uglify-js'; +const defaultUglifyOptions = { + output: { + comments: /^\**!|@preserve|@license|@cc_on/, + beautify: false, + semicolons: true, + shebang: true, + }, +}; class UglifyJsPlugin { constructor(options) { if (typeof options !== 'object' || Array.isArray(options)) { - options = {}; + this.options = {}; + } else { + this.options = options || {}; } - if (typeof options.compressor !== 'undefined') { - options.compress = options.compressor; + + this.options.test = this.options.test || /\.js($|\?)/i; + this.options.warningsFilter = this.options.warningsFilter || (() => true); + + this.uglifyOptions = this.options.uglifyOptions || {}; + } + + static buildDefaultUglifyOptions({ ecma, warnings, parse, compress, mangle, output, toplevel, ie8 }) { + return { + ecma, + warnings, + parse: parse || {}, + compress: compress || {}, + /* eslint-disable no-undefined */ + mangle: mangle === undefined || mangle === null ? true : mangle, + /* eslint-enable no-undefined */ + // Ignoring sourcemap from options + sourceMap: null, + output: { ...defaultUglifyOptions.output, ...output }, + toplevel, + ie8, + }; + } + + static buildError(err, file, sourceMap, requestShortener) { + // Handling error which should have line, col, filename and message + if (err.line) { + const original = sourceMap && sourceMap.originalPositionFor({ + line: err.line, + column: err.col, + }); + if (original && original.source) { + return new Error(`${file} from UglifyJs\n${err.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${err.line},${err.col}]`); + } + return new Error(`${file} from UglifyJs\n${err.message} [${file}:${err.line},${err.col}]`); + } else if (err.msg) { + return new Error(`${file} from UglifyJs\n${err.msg}`); } + return new Error(`${file} from UglifyJs\n${err.stack}`); + } - this.options = options; + static buildWarnings(warnings, file, sourceMap, warningsFilter, requestShortener) { + return warnings.reduce((accWarnings, warning) => { + if (!sourceMap) { + accWarnings.push(warning); + } else { + const match = /\[.+:([0-9]+),([0-9]+)\]/.exec(warning); + const line = +match[1]; + const column = +match[2]; + const original = sourceMap.originalPositionFor({ + line, + column, + }); + + if (original && original.source && original.source !== file && warningsFilter(original.source)) { + accWarnings.push(`${warning.replace(/\[.+:([0-9]+),([0-9]+)\]/, '')}[${requestShortener.shorten(original.source)}:${original.line},${original.column}]`); + } + } + return accWarnings; + }, []); } - apply(compiler) { - const { options } = this; - options.test = options.test || /\.js($|\?)/i; - const warningsFilter = options.warningsFilter || (() => true); + static buildCommentsFunction(options, uglifyOptions, extractedComments) { + const condition = {}; + const commentsOpts = uglifyOptions.output.comments; + if (typeof options.extractComments === 'string' || options.extractComments instanceof RegExp) { + // extractComments specifies the extract condition and commentsOpts specifies the preserve condition + condition.preserve = commentsOpts; + condition.extract = options.extractComments; + } else if (Object.prototype.hasOwnProperty.call(options.extractComments, 'condition')) { + // Extract condition is given in extractComments.condition + condition.preserve = commentsOpts; + condition.extract = options.extractComments.condition; + } else { + // No extract condition is given. Extract comments that match commentsOpts instead of preserving them + condition.preserve = false; + condition.extract = commentsOpts; + } + + // Ensure that both conditions are functions + ['preserve', 'extract'].forEach((key) => { + let regexStr; + let regex; + switch (typeof (condition[key])) { + case 'boolean': + condition[key] = condition[key] ? () => true : () => false; + break; + case 'function': + break; + case 'string': + if (condition[key] === 'all') { + condition[key] = () => true; + break; + } + if (condition[key] === 'some') { + condition[key] = (astNode, comment) => comment.type === 'comment2' && /@preserve|@license|@cc_on/i.test(comment.value); + break; + } + regexStr = condition[key]; + condition[key] = (astNode, comment) => new RegExp(regexStr).test(comment.value); + break; + default: + regex = condition[key]; + condition[key] = (astNode, comment) => (regex.test(comment.value)); + } + }); + + // Redefine the comments function to extract and preserve + // comments according to the two conditions + return (astNode, comment) => { + if (condition.extract(astNode, comment)) { + extractedComments.push( + comment.type === 'comment2' ? `/*${comment.value}*/` : `//${comment.value}`, + ); + } + return condition.preserve(astNode, comment); + }; + } + apply(compiler) { const requestShortener = new RequestShortener(compiler.context); - compiler.plugin('compilation', (compilation) => { - if (options.sourceMap) { - compilation.plugin('build-module', (module) => { + compiler.plugin('compilation', (compilationArg) => { + const compilation = compilationArg; + + if (this.options.sourceMap) { + compilation.plugin('build-module', (moduleArg) => { // to get detailed location info about errors - module.useSourceMap = true; + const moduleVar = moduleArg; + moduleVar.useSourceMap = true; }); } + compilation.plugin('optimize-chunk-assets', (chunks, callback) => { - const files = []; - // eslint-disable-next-line prefer-spread - chunks.forEach(chunk => files.push.apply(files, chunk.files)); - // eslint-disable-next-line prefer-spread - files.push.apply(files, compilation.additionalChunkAssets); - const filteredFiles = files.filter(ModuleFilenameHelpers.matchObject.bind(undefined, options)); const uglifiedAssets = new WeakSet(); - filteredFiles.forEach((file) => { - const oldWarnFunction = uglify.AST_Node.warn_function; - const warnings = []; - let sourceMap; - try { + chunks.reduce((acc, chunk) => acc.concat(chunk.files || []), []) + .concat(compilation.additionalChunkAssets || []) + .filter(ModuleFilenameHelpers.matchObject.bind(null, this.options)) + .forEach((file) => { + // Copy uglify options + const uglifyOptions = UglifyJsPlugin.buildDefaultUglifyOptions(this.uglifyOptions); + let sourceMap; const asset = compilation.assets[file]; if (uglifiedAssets.has(asset)) { return; } - let input; - let inputSourceMap; - if (options.sourceMap) { - if (asset.sourceAndMap) { - const sourceAndMap = asset.sourceAndMap(); - inputSourceMap = sourceAndMap.map; - input = sourceAndMap.source; + + try { + let input; + let inputSourceMap; + if (this.options.sourceMap) { + if (asset.sourceAndMap) { + const sourceAndMap = asset.sourceAndMap(); + inputSourceMap = sourceAndMap.map; + input = sourceAndMap.source; + } else { + inputSourceMap = asset.map(); + input = asset.source(); + } + sourceMap = new SourceMapConsumer(inputSourceMap); + // Add source map data + uglifyOptions.sourceMap = { + content: inputSourceMap, + }; } else { - inputSourceMap = asset.map(); input = asset.source(); } - sourceMap = new SourceMapConsumer(inputSourceMap); - uglify.AST_Node.warn_function = (warning) => { // eslint-disable-line camelcase - const match = /\[.+:([0-9]+),([0-9]+)\]/.exec(warning); - const line = +match[1]; - const column = +match[2]; - const original = sourceMap.originalPositionFor({ - line, - column, - }); - if (!original || !original.source || original.source === file) return; - if (!warningsFilter(original.source)) return; - warnings.push(`${warning.replace(/\[.+:([0-9]+),([0-9]+)\]/, '') - }[${requestShortener.shorten(original.source)}:${original.line},${original.column}]`); - }; - } else { - input = asset.source(); - uglify.AST_Node.warn_function = (warning) => { // eslint-disable-line camelcase - warnings.push(warning); - }; - } - uglify.base54.reset(); - let ast = uglify.parse(input, { - filename: file, - }); - if (options.compress !== false) { - ast.figure_out_scope(); - const compress = uglify.Compressor(options.compress || { - warnings: false, - }); // eslint-disable-line new-cap - ast = compress.compress(ast); - } - if (options.mangle !== false) { - ast.figure_out_scope(options.mangle || {}); - ast.compute_char_frequency(options.mangle || {}); - ast.mangle_names(options.mangle || {}); - if (options.mangle && options.mangle.props) { - uglify.mangle_properties(ast, options.mangle.props); + + // Handling comment extraction + const extractedComments = []; + let commentsFile = false; + if (this.options.extractComments) { + uglifyOptions.output = uglifyOptions.output || {}; + uglifyOptions.output.comments = UglifyJsPlugin.buildCommentsFunction(this.options, uglifyOptions, extractedComments); + + commentsFile = this.options.extractComments.filename || `${file}.LICENSE`; + if (typeof commentsFile === 'function') { + commentsFile = commentsFile(file); + } } - } - const output = {}; - output.comments = Object.prototype.hasOwnProperty.call(options, 'comments') ? options.comments : /^\**!|@preserve|@license/; - output.beautify = options.beautify; - for (const k in options.output) { // eslint-disable-line guard-for-in - output[k] = options.output[k]; - } - const extractedComments = []; - if (options.extractComments) { - const condition = {}; - if (typeof options.extractComments === 'string' || options.extractComments instanceof RegExp) { - // extractComments specifies the extract condition and output.comments specifies the preserve condition - condition.preserve = output.comments; - condition.extract = options.extractComments; - } else if (Object.prototype.hasOwnProperty.call(options.extractComments, 'condition')) { - // Extract condition is given in extractComments.condition - condition.preserve = output.comments; - condition.extract = options.extractComments.condition; + + // Calling uglify + const { error, map, code, warnings } = uglify.minify({ [file]: input }, uglifyOptions); + + // Handling results + if (error) { + throw error; + } + + let outputSource; + if (map) { + outputSource = new SourceMapSource(code, file, JSON.parse(map), input, inputSourceMap); } else { - // No extract condition is given. Extract comments that match output.comments instead of preserving them - condition.preserve = false; - condition.extract = output.comments; + outputSource = new RawSource(code); } - // Ensure that both conditions are functions - ['preserve', 'extract'].forEach((key) => { - switch (typeof condition[key]) { - case 'boolean': { - const b = condition[key]; - condition[key] = () => b; - break; - } - case 'function': - break; - case 'string': { - if (condition[key] === 'all') { - condition[key] = () => true; - break; - } - const regex = new RegExp(condition[key]); - condition[key] = (astNode, comment) => regex.test(comment.value); - break; + // Write extracted comments to commentsFile + if (commentsFile && extractedComments.length > 0) { + // Add a banner to the original file + if (this.options.extractComments.banner !== false) { + let banner = this.options.extractComments.banner || `For license information please see ${commentsFile}`; + if (typeof banner === 'function') { + banner = banner(commentsFile); } - default: { - const defaultRegex = condition[key]; - condition[key] = (astNode, comment) => defaultRegex.test(comment.value); + if (banner) { + outputSource = new ConcatSource( + `/*! ${banner} */\n`, outputSource, + ); } } - }); - - // Redefine the comments function to extract and preserve - // comments according to the two conditions - output.comments = (astNode, comment) => { - if (condition.extract(astNode, comment)) { - extractedComments.push( - comment.type === 'comment2' ? `/*${comment.value}*/` : `//${comment.value}`, - ); - } - return condition.preserve(astNode, comment); - }; - } - let map; - if (options.sourceMap) { - map = uglify.SourceMap({ // eslint-disable-line new-cap - file, - root: '', - }); - output.source_map = map; // eslint-disable-line camelcase - } - const stream = uglify.OutputStream(output); // eslint-disable-line new-cap - ast.print(stream); - if (map) map += ''; - const stringifiedStream = `${stream}`; - let outputSource = map ? new SourceMapSource( - stringifiedStream, file, JSON.parse(map), input, inputSourceMap // eslint-disable-line comma-dangle - ) : new RawSource(stringifiedStream); - if (extractedComments.length > 0) { - let commentsFile = options.extractComments.filename || `${file}.LICENSE`; - if (typeof commentsFile === 'function') { - commentsFile = commentsFile(file); - } - // Write extracted comments to commentsFile - const commentsSource = new RawSource(`${extractedComments.join('\n\n')}\n`); - if (commentsFile in compilation.assets) { - // commentsFile already exists, append new comments... - if (compilation.assets[commentsFile] instanceof ConcatSource) { - compilation.assets[commentsFile].add('\n'); - compilation.assets[commentsFile].add(commentsSource); + const commentsSource = new RawSource(`${extractedComments.join('\n\n')}\n`); + if (commentsFile in compilation.assets) { + // commentsFile already exists, append new comments... + if (compilation.assets[commentsFile] instanceof ConcatSource) { + compilation.assets[commentsFile].add('\n'); + compilation.assets[commentsFile].add(commentsSource); + } else { + compilation.assets[commentsFile] = new ConcatSource( + compilation.assets[commentsFile], '\n', commentsSource, + ); + } } else { - compilation.assets[commentsFile] = new ConcatSource( - compilation.assets[commentsFile], '\n', commentsSource, - ); + compilation.assets[commentsFile] = commentsSource; } - } else { - compilation.assets[commentsFile] = commentsSource; } - // Add a banner to the original file - if (options.extractComments.banner !== false) { - let banner = options.extractComments.banner || `For license information please see ${commentsFile}`; - if (typeof banner === 'function') { - banner = banner(commentsFile); - } - if (banner) { - outputSource = new ConcatSource( - `/*! ${banner} */\n`, outputSource, - ); + // Updating assets + uglifiedAssets.add(compilation.assets[file] = outputSource); + + // Handling warnings + if (warnings) { + const warnArr = UglifyJsPlugin.buildWarnings(warnings, file, sourceMap, this.options.warningsFilter, requestShortener); + if (warnArr.length > 0) { + compilation.warnings.push(new Error(`${file} from UglifyJs\n${warnArr.join('\n')}`)); } } + } catch (error) { + compilation.errors.push(UglifyJsPlugin.buildError(error, file, sourceMap, compilation, requestShortener)); } - uglifiedAssets.add(compilation.assets[file] = outputSource); - if (warnings.length > 0) { - compilation.warnings.push(new Error(`${file} from UglifyJs\n${warnings.join('\n')}`)); - } - } catch (err) { - if (err.line) { - const original = sourceMap && sourceMap.originalPositionFor({ - line: err.line, - column: err.col, - }); - if (original && original.source) { - compilation.errors.push(new Error(`${file} from UglifyJs\n${err.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${err.line},${err.col}]`)); - } else { - compilation.errors.push(new Error(`${file} from UglifyJs\n${err.message} [${file}:${err.line},${err.col}]`)); - } - } else if (err.msg) { - compilation.errors.push(new Error(`${file} from UglifyJs\n${err.msg}`)); - } else { - compilation.errors.push(new Error(`${file} from UglifyJs\n${err.stack}`)); - } - } finally { - uglify.AST_Node.warn_function = oldWarnFunction; // eslint-disable-line camelcase - } - }); + }); callback(); }); }); diff --git a/test/all-options.test.js b/test/all-options.test.js index 13a79a2c..db8f36be 100644 --- a/test/all-options.test.js +++ b/test/all-options.test.js @@ -18,12 +18,6 @@ describe('when applied with all options', () => { const plugin = new UglifyJsPlugin({ sourceMap: true, - compress: { - warnings: true, - }, - mangle: false, - beautify: true, - comments: false, extractComments: { condition: 'should be extracted', filename(file) { @@ -33,6 +27,14 @@ describe('when applied with all options', () => { return `License information can be found in ${licenseFile}`; }, }, + uglifyOptions: { + warnings: true, + mangle: false, + output: { + beautify: true, + comments: false, + }, + }, }); plugin.apply(compilerEnv); eventBindings = pluginEnvironment.getEventBindings(); @@ -42,12 +44,14 @@ describe('when applied with all options', () => { const compiler = createCompiler(); new UglifyJsPlugin({ sourceMap: true, - compress: { + uglifyOptions: { + mangle: false, + output: { + beautify: true, + comments: false, + }, warnings: true, }, - mangle: false, - beautify: true, - comments: false, extractComments: { condition: 'should be extracted', filename(file) { @@ -273,12 +277,14 @@ describe('when applied with all options', () => { const plugin = new UglifyJsPlugin({ warningsFilter: () => true, sourceMap: true, - compress: { + uglifyOptions: { warnings: true, + mangle: false, + output: { + beautify: true, + comments: false, + }, }, - mangle: false, - beautify: true, - comments: false, }); plugin.apply(compilerEnv); eventBindings = pluginEnvironment.getEventBindings(); @@ -325,12 +331,14 @@ describe('when applied with all options', () => { const plugin = new UglifyJsPlugin({ warningsFilter: () => false, sourceMap: true, - compressor: { + uglifyOptions: { warnings: true, + mangle: false, + output: { + beautify: true, + comments: false, + }, }, - mangle: false, - beautify: true, - comments: false, }); plugin.apply(compilerEnv); eventBindings = pluginEnvironment.getEventBindings(); diff --git a/test/extract-comments-options.test.js b/test/extract-comments-options.test.js index 3ea84b27..54d6386c 100644 --- a/test/extract-comments-options.test.js +++ b/test/extract-comments-options.test.js @@ -11,16 +11,18 @@ describe('when options.extractComments', () => { compilerEnv.context = ''; const plugin = new UglifyJsPlugin({ - compress: { + uglifyOptions: { warnings: true, - }, - comments: false, - extractComments: 1, - mangle: { - props: { - builtins: true, + output: { + comments: false, + }, + mangle: { + properties: { + builtins: true, + }, }, }, + extractComments: 1, }); plugin.apply(compilerEnv); @@ -75,7 +77,11 @@ describe('when options.extractComments', () => { compilerEnv.context = ''; const plugin = new UglifyJsPlugin({ - comments: false, + uglifyOptions: { + output: { + comments: false, + }, + }, extractComments: /foo/, }); plugin.apply(compilerEnv); @@ -105,7 +111,11 @@ describe('when options.extractComments', () => { compilerEnv.context = ''; const plugin = new UglifyJsPlugin({ - comments: false, + uglifyOptions: { + output: { + comments: false, + }, + }, extractComments: { condition: true, filename(file) { diff --git a/test/extract-option-set-to-a-single-file.test.js b/test/extract-option-set-to-a-single-file.test.js index ad7ee987..1db7af40 100644 --- a/test/extract-option-set-to-a-single-file.test.js +++ b/test/extract-option-set-to-a-single-file.test.js @@ -11,7 +11,11 @@ describe('when applied with extract option set to a single file', () => { compilerEnv.context = ''; const plugin = new UglifyJsPlugin({ - comments: 'all', + uglifyOptions: { + output: { + comments: 'all', + }, + }, extractComments: { condition: /.*/, filename: 'extracted-comments.js', diff --git a/test/invalid-options.test.js b/test/invalid-options.test.js index ed2ccb10..f14c018a 100644 --- a/test/invalid-options.test.js +++ b/test/invalid-options.test.js @@ -10,8 +10,10 @@ describe('when applied with invalid options', () => { it('matches snapshot', () => { const compiler = createCompiler(); new UglifyJsPlugin({ - output: { - 'invalid-option': true, + uglifyOptions: { + output: { + 'invalid-option': true, + }, }, }).apply(compiler); @@ -36,8 +38,10 @@ describe('when applied with invalid options', () => { compilerEnv.context = ''; const plugin = new UglifyJsPlugin({ - output: { - 'invalid-option': true, + uglifyOptions: { + output: { + 'invalid-option': true, + }, }, }); plugin.apply(compilerEnv);