-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3288ebe
commit 5a1aa5d
Showing
8 changed files
with
745 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,4 @@ | |
|
||
// Exports | ||
|
||
module.exports = {}; | ||
module.exports = require('./lib/index.js'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
/* -------------------- | ||
* terser-sync module | ||
* Entry point | ||
* ------------------*/ | ||
|
||
'use strict'; | ||
|
||
// Modules | ||
const terser = require('terser'), | ||
{minify} = terser, | ||
hasOwnProp = require('has-own-prop'); | ||
|
||
// Imports | ||
const createSourceMap = require('./sourceMap.js'); | ||
|
||
// Exports | ||
|
||
module.exports = {...terser, minifySync}; | ||
|
||
/** | ||
* Synchronous version of `terser.minify()`. | ||
* Output should be identical to `terser.minify()` and supports all options `terser.minify()` does. | ||
* | ||
* Calls `minify()` and uses some hacks to extract internal state from within `minify()` and fool | ||
* it into using a synchronous implementation of source map parser to get the result synchronously. | ||
* | ||
* @param {string|Array|Object} files - Input files | ||
* @param {Object} [options] - Options | ||
* @returns {Object} - Properties depend on options | ||
* @throws {Error} - If `minify()` errors or injection fails | ||
*/ | ||
function minifySync(files, options) { | ||
let result, err; | ||
|
||
// Conform files | ||
options = {...options}; | ||
if ( | ||
typeof files === 'string' | ||
|| (options.parse && options.parse.spidermonkey && !Array.isArray(files)) | ||
) { | ||
files = [files]; | ||
} | ||
|
||
// First thing Terser does is clone the options object and add defaults to it. | ||
// https://github.com/terser/terser/blob/d225b75e82770d0d7eedf011e4769d18a43de9c0/lib/minify.js#L64 | ||
// Capture the cloned options object via a setter on `Object.prototype.spidermonkey` | ||
// which is triggered when Terser sets the `.spidermonkey` property on the cloned object. | ||
// We need Terser's clone of options object in order to manipulate its `.sourceMap` property later. | ||
const originalSpidermonkeyOpt = !!options.spidermonkey; | ||
delete options.spidermonkey; | ||
|
||
let hasCapturedClonedOptions = false; | ||
captureObjectViaObjectPrototypeSetter('spidermonkey', (clonedOptions) => { | ||
options = clonedOptions; | ||
hasCapturedClonedOptions = true; | ||
options.spidermonkey = originalSpidermonkeyOpt; | ||
}); | ||
|
||
// Terser's code includes `if (options.format.ast) { result.ast = toplevel; }`. | ||
// https://github.com/terser/terser/blob/d225b75e82770d0d7eedf011e4769d18a43de9c0/lib/minify.js#L216 | ||
// `result` is the result object returned by Terser that we want to capture. | ||
// Define a getter on `options.format.ast` which returns `true` and then installs a setter | ||
// on `Object.prototype.ast` to capture `result` in next statement `result.ast = toplevel`. | ||
// This allows us to capture `result` before the promise returned by `minify()` resolves. | ||
const format = options.format = options.format || {}; | ||
let hasSubtitutedSourceMap = false; | ||
|
||
shimGetter(format, 'ast', true, () => { | ||
captureObjectViaObjectPrototypeSetter('ast', (capturedResult, ast) => { | ||
result = capturedResult; | ||
if (format.ast) result.ast = ast; | ||
substituteSourceMapImplementation(); | ||
}); | ||
}); | ||
|
||
function substituteSourceMapImplementation() { | ||
// Skip if source map will not be created | ||
const {sourceMap} = options; | ||
if (!sourceMap || (hasOwnProp(format, 'code') && !format.code)) { | ||
hasSubtitutedSourceMap = true; | ||
return; | ||
} | ||
|
||
// Define getter for `options.sourceMap` that returns `false` to prevent Terser initializing | ||
// source map asynchronously. Then create a source map synchronously instead. | ||
// This short-circuits section of code which includes the async call: | ||
// https://github.com/terser/terser/blob/d225b75e82770d0d7eedf011e4769d18a43de9c0/lib/minify.js#L223 | ||
shimGetter(options, 'sourceMap', false, () => { | ||
hasSubtitutedSourceMap = true; | ||
|
||
try { | ||
createSourceMap(options, files); | ||
} catch (e) { | ||
err = e; | ||
throw err; | ||
} | ||
}); | ||
} | ||
|
||
// Run Terser | ||
const promise = minify(files, options).catch(e => e); | ||
|
||
// If failed, throw error with `.promise` property which resolves to the error | ||
if (!err) { | ||
if (!hasCapturedClonedOptions) { | ||
err = new Error('Failed to capture cloned options'); | ||
} else if (!hasSubtitutedSourceMap) { | ||
err = new Error('Failed to skip async source map initialization'); | ||
} else if (!result) { | ||
err = new Error('Unknown error - await `error.promise` to get error details'); | ||
} | ||
} | ||
|
||
if (err) { | ||
err.promise = promise; | ||
throw err; | ||
} | ||
|
||
// Return result | ||
return result; | ||
} | ||
|
||
/** | ||
* Capture an Object internal to Terser when Terser sets a property on it. | ||
* Works by creating a setter on `Object.prototype` which will be triggered when property is set | ||
* on Object we're interested in. | ||
* @param {string} propName - Property name to create setter for | ||
* @param {Function} cb - Callback, called with object and value which was written to property | ||
* @returns {undefined} | ||
*/ | ||
function captureObjectViaObjectPrototypeSetter(propName, cb) { | ||
shimObject(Object.prototype, propName, 'set', cb); | ||
} | ||
|
||
/** | ||
* Catch when Terser reads a property on an Object. | ||
* This is used to return a desired value to fool Terser into doing something it shouldn't, | ||
* or to be alerted to when it's up to a particular point in execution so we can take some | ||
* other action at that point. | ||
* @param {Object} obj - Object to shim | ||
* @param {string} propName - Property name to create getter/setter on | ||
* @param {*} value - Value to return from getter | ||
* @param {Function} cb - Callback, called with no args | ||
* @returns {undefined} | ||
*/ | ||
function shimGetter(obj, propName, value, cb) { | ||
shimObject(obj, propName, 'get', () => { | ||
cb(); | ||
return value; | ||
}); | ||
} | ||
|
||
/** | ||
* Shim Object property with a getter/setter. | ||
* As soon as getter/setter is called, the shim is removed again. | ||
* Call callback with object and value setter was called with (if it's a setter). | ||
* @param {Object} obj - Object to shim | ||
* @param {string} propName - Property name to create getter/setter on | ||
* @param {string} getOrSet - 'get' or 'set' | ||
* @param {Function} cb - Callback, called with object and value setter was called with | ||
* @returns {undefined} | ||
*/ | ||
function shimObject(obj, propName, getOrSet, cb) { | ||
const descriptor = Object.getOwnPropertyDescriptor(obj, propName); | ||
Object.defineProperty(obj, propName, { | ||
[getOrSet](value) { | ||
// Restore to as it was before | ||
if (descriptor) { | ||
Object.defineProperty(obj, propName, descriptor); | ||
} else { | ||
delete obj[propName]; | ||
} | ||
|
||
return cb(this, value); | ||
}, | ||
configurable: true | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
/* -------------------- | ||
* terser-sync module | ||
* Create source map synchronously | ||
* ------------------*/ | ||
|
||
/* eslint-disable camelcase */ | ||
|
||
'use strict'; | ||
|
||
// Modules | ||
const MOZ_SourceMap = require('source-map'), | ||
HOP = require('has-own-prop'), | ||
{isObject} = require('is-it-type'); | ||
|
||
// Exports | ||
|
||
/** | ||
* Create source map synchronously. | ||
* Logic copied from | ||
* https://github.com/terser/terser/blob/d225b75e82770d0d7eedf011e4769d18a43de9c0/lib/minify.js#L224 | ||
* @param {Object} options - Options object | ||
* @param {Array|Object} files - Files | ||
* @returns {undefined} | ||
* @throws {Error} - If error | ||
*/ | ||
module.exports = function createSourceMap(options, files) { | ||
options.format.source_map = SourceMap({ | ||
file: options.sourceMap.filename, | ||
orig: options.sourceMap.content, | ||
root: options.sourceMap.root | ||
}); | ||
|
||
if (options.sourceMap.includeSources) { | ||
if (isObject(files)) { | ||
throw new Error('original source content unavailable'); | ||
} else { | ||
for (const name in files) { | ||
if (HOP(files, name)) { | ||
options.format.source_map.get().setSourceContent(name, files[name]); | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Create SourceMap interface. | ||
* Copied verbatim from | ||
* https://github.com/terser/terser/blob/d225b75e82770d0d7eedf011e4769d18a43de9c0/lib/sourcemap.js#L52 | ||
* except `await new MOZ_SourceMap.SourceMapConsumer()` substituted for a sync version. | ||
* @param {Object} options - Options object | ||
* @returns {Object} - SourceMap interface | ||
*/ | ||
function SourceMap(options) { | ||
options = defaults(options, { | ||
file: null, | ||
root: null, | ||
orig: null, | ||
|
||
orig_line_diff: 0, | ||
dest_line_diff: 0 | ||
}); | ||
|
||
let orig_map; | ||
const generator = new MOZ_SourceMap.SourceMapGenerator({ | ||
file: options.file, | ||
sourceRoot: options.root | ||
}); | ||
|
||
if (options.orig) { | ||
orig_map = new MOZ_SourceMap.SourceMapConsumer(options.orig); | ||
orig_map.sources.forEach((source) => { | ||
const sourceContent = orig_map.sourceContentFor(source, true); | ||
if (sourceContent) { | ||
generator.setSourceContent(source, sourceContent); | ||
} | ||
}); | ||
} | ||
|
||
function add(source, gen_line, gen_col, orig_line, orig_col, name) { | ||
if (orig_map) { | ||
const info = orig_map.originalPositionFor({ | ||
line: orig_line, | ||
column: orig_col | ||
}); | ||
if (info.source === null) { | ||
return; | ||
} | ||
source = info.source; | ||
orig_line = info.line; | ||
orig_col = info.column; | ||
name = info.name || name; | ||
} | ||
generator.addMapping({ | ||
generated: {line: gen_line + options.dest_line_diff, column: gen_col}, | ||
original: {line: orig_line + options.orig_line_diff, column: orig_col}, | ||
source, | ||
name | ||
}); | ||
} | ||
|
||
return { | ||
add, | ||
get() { return generator; }, | ||
toString() { return generator.toString(); }, | ||
destroy() { | ||
if (orig_map && orig_map.destroy) { | ||
orig_map.destroy(); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Apply defaults to options object. | ||
* Copied verbatim from | ||
* https://github.com/terser/terser/blob/d225b75e82770d0d7eedf011e4769d18a43de9c0/lib/utils/index.js#L64 | ||
* except one section which is not used here commented out. | ||
* @param {Object|boolean} args - Args | ||
* @param {Object} defs - Defaults definition | ||
* @param {boolean} croak - Not used | ||
* @returns {Object} - Conformed object | ||
*/ | ||
function defaults(args, defs, croak) { // eslint-disable-line no-unused-vars | ||
if (args === true) { | ||
args = {}; | ||
} else if (args != null && typeof args === 'object') { | ||
args = {...args}; | ||
} | ||
|
||
const ret = args || {}; | ||
|
||
/* | ||
// No need for this | ||
if (croak) { | ||
for (const i in ret) { | ||
if (HOP(ret, i) && !HOP(defs, i)) { | ||
throw new DefaultsError(`\`${i}\` is not a supported option`, defs); | ||
} | ||
} | ||
} | ||
*/ | ||
|
||
for (const i in defs) { | ||
if (HOP(defs, i)) { | ||
if (!args || !HOP(args, i)) { | ||
ret[i] = defs[i]; | ||
} else if (i === 'ecma') { | ||
let ecma = args[i] | 0; // eslint-disable-line no-bitwise | ||
if (ecma > 5 && ecma < 2015) ecma += 2009; | ||
ret[i] = ecma; | ||
} else { | ||
ret[i] = (args && HOP(args, i)) ? args[i] : defs[i]; | ||
} | ||
} | ||
} | ||
|
||
return ret; | ||
} |
Oops, something went wrong.