diff --git a/.prettierignore b/.prettierignore index b4a2634..81a9499 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ bin/baler -src/__tests__/ +src/__tests__/__fixtures__ diff --git a/package-lock.json b/package-lock.json index 3ae16fa..9512695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -313,6 +313,14 @@ "slash": "^2.0.0", "source-map": "^0.6.0", "string-length": "^2.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "@jest/source-map": { @@ -324,6 +332,14 @@ "callsites": "^3.0.0", "graceful-fs": "^4.1.15", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "@jest/test-result": { @@ -370,6 +386,14 @@ "slash": "^2.0.0", "source-map": "^0.6.1", "write-file-atomic": "2.4.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "@jest/types": { @@ -1395,6 +1419,15 @@ "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } } }, "esprima": { @@ -1787,7 +1820,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1808,12 +1842,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1828,17 +1864,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1955,7 +1994,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1967,6 +2007,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1981,6 +2022,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1988,12 +2030,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2012,6 +2056,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2092,7 +2137,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2104,6 +2150,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2189,7 +2236,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2225,6 +2273,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2244,6 +2293,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2287,12 +2337,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2382,6 +2434,14 @@ "optimist": "^0.6.1", "source-map": "^0.6.1", "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "har-schema": { @@ -2854,6 +2914,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -3230,6 +3296,14 @@ "mkdirp": "^0.5.1", "slash": "^2.0.0", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "jest-validate": { @@ -4583,9 +4657,10 @@ } }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true }, "source-map-resolve": { "version": "0.5.2", @@ -4607,6 +4682,13 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "source-map-url": { @@ -4834,6 +4916,13 @@ "commander": "^2.20.0", "source-map": "~0.6.1", "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "test-exclude": { @@ -5012,6 +5101,15 @@ "requires": { "commander": "~2.20.0", "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } } }, "union-value": { diff --git a/package.json b/package.json index 1f91658..6c86f73 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@types/node": "^12.6.8", "jest": "^24.8.0", "prettier": "^1.18.2", + "source-map": "^0.7.3", "tempy": "^0.3.0", "ts-jest": "^24.0.2", "typescript": "^3.5.3" diff --git a/src/__tests__/minifyWorker.unit.js b/src/__tests__/minifyWorker.unit.js index 1636f3f..5cae727 100644 --- a/src/__tests__/minifyWorker.unit.js +++ b/src/__tests__/minifyWorker.unit.js @@ -1,8 +1,9 @@ const { join, basename } = require('path'); const { copyFile } = require('../fsPromises'); -const { minify } = require('../minifyWorker'); +const { minifyFromFilepath, minifyFromString } = require('../minifyWorker'); const { readFile } = require('../fsPromises'); const tempy = require('tempy'); +const { SourceMapConsumer } = require('source-map'); const moveFileToTmpDir = async path => { const tmpDir = tempy.directory(); @@ -14,11 +15,69 @@ const moveFileToTmpDir = async path => { const getFixturePath = (fixtureName, path) => join(__dirname, '__fixtures__', fixtureName, path); -test('Minifies JS file', async () => { +test('minifyFromString minifies JS file without chained sourcemap', async () => { + const js = ` + var abc = () => { + var c = console; + c.log('test'); + }; + abc(); + `; + const result = await minifyFromString(js, 'file.js'); + expect(result.code).toMatchInlineSnapshot(` + "var abc=()=>{console.log(\\"test\\")};abc(); + //# sourceMappingURL=file.js.map" + `); + expect(result.map).toBeTruthy(); +}); + +test('minifyFromString minifies JS file with chained sourcemap', async () => { + // Take code pre-compiled with Babel. Imagine someone runs Babel in static + // content deployment, before Baler has been run + const js = `"use strict"; + + var b = function b() { + console.log(undefined.heh); + }; + "use strict"; + + var a = function a() { + console.log(undefined.lol); + };`; + // sourcemap generated by babel + const map = { + version: 3, + sources: ['file.js', 'index.js'], + names: [], + mappings: + ';;AAAA,IAAM,IAAI,SAAJ,CAAI,GAAM;AACZ,YAAQ,GAAR,CAAY,UAAK,GAAjB;AACH,CAFD;;;ACAA,IAAM,IAAI,SAAJ,CAAI,GAAM;AACZ,YAAQ,GAAR,CAAY,UAAK,GAAjB;AACH,CAFD', + file: 'bundle.js', + sourcesContent: [ + 'const b = () => {\n console.log(this.heh);\n};', + 'const a = () => {\n console.log(this.lol);\n};', + ], + }; + const result = await minifyFromString(js, 'bundle.js', JSON.stringify(map)); + expect(result.code).toMatchInlineSnapshot(` + "\\"use strict\\";var b=function(){console.log((void 0).heh)},a=function(){console.log((void 0).lol)}; + //# sourceMappingURL=bundle.js.map" + `); + + const consumer = await SourceMapConsumer.fromSourceMap(result.map); + // Verify the original code, from before babel, made it to the sourcemap + expect(consumer.sourceContentFor('file.js')).toMatchInlineSnapshot(` + "const b = () => { + console.log(this.heh); + };" + `); + consumer.destroy(); +}); + +test('minifyFromFilepath minifies JS file', async () => { const srcPath = getFixturePath('basic-minify', 'bundle.js'); const { tmpDir, tmpPath } = await moveFileToTmpDir(srcPath); - const result = await minify(tmpPath); + const result = await minifyFromFilepath(tmpPath); const resultFilePath = join(tmpDir, result.minFilename); const minifiedCode = await readFile(resultFilePath, 'utf8'); @@ -27,11 +86,11 @@ test('Minifies JS file', async () => { ); }); -test('Minfies JS file and chains sourcemap', async () => { +test('minifyFromFilepath minifies JS file and chains sourcemap', async () => { const srcPath = getFixturePath('source-mapped-minify', 'bundle.js'); const { tmpDir, tmpPath } = await moveFileToTmpDir(srcPath); - const result = await minify(tmpPath); + const result = await minifyFromFilepath(tmpPath); const resultFilePath = join(tmpDir, result.minFilename); const minifiedCode = await readFile(resultFilePath, 'utf8'); diff --git a/src/minifyWorker.ts b/src/minifyWorker.ts index e782f7b..26960ab 100644 --- a/src/minifyWorker.ts +++ b/src/minifyWorker.ts @@ -1,15 +1,51 @@ import { join, parse, dirname } from 'path'; import { readFile, writeFile } from './fsPromises'; -import terser from 'terser'; -import { RawSourceMap } from 'source-map'; +import terser, { MinifyOptions, SourceMapOptions } from 'terser'; import { wrapP } from './wrapP'; -export type MinificationResult = { +type FileMinificationResult = { totalBytes: number; minFilename: string; }; -export async function minify(path: string): Promise { +type StringMinificationResult = { + code: string; + map?: string; +}; + +/** + * @summary Minifies JS code, optionally chaining from + * a provided source-map + */ +export async function minifyFromString( + code: string, + filename: string, + map?: string, +): Promise { + const opts: MinifyOptions = { + sourceMap: { + filename, + url: `${filename}.map`, + }, + }; + + if (map) { + try { + const parsedMap = JSON.parse(map) as SourceMapOptions['content']; + // @ts-ignore + opts.sourceMap.content = parsedMap; + } catch {} + } + + const result = terser.minify(code, opts); + if (result.error) throw result.error; + + return { code: result.code as string, map: result.map }; +} + +export async function minifyFromFilepath( + path: string, +): Promise { const source = await readFile(path, 'utf8'); const { 1: sourceMapName } = source.match(/\/\/#\ssourceMappingURL=(.+\.js\.map)/) || []; @@ -59,7 +95,7 @@ async function minifyWithInputMap( return minifyWithoutInputMap(source, targetFilename, targetFilePath); } - const map = JSON.parse(mapSrc as string) as RawSourceMap; + const map = JSON.parse(mapSrc as string) as SourceMapOptions['content']; const result = terser.minify(source, { sourceMap: { content: map,