diff --git a/config/uglifyjs.config.js b/config/uglifyjs.config.js index d4d0cd9d..ee5c8dd8 100644 --- a/config/uglifyjs.config.js +++ b/config/uglifyjs.config.js @@ -3,26 +3,6 @@ module.exports = { - /** - * sourceFile: The javascript file to minify - */ - sourceFile: process.env.IONIC_OUTPUT_JS_FILE_NAME, - - /** - * destFileName: file name for the minified js in the build dir - */ - destFileName: process.env.IONIC_OUTPUT_JS_FILE_NAME, - - /** - * inSourceMap: file name for the input source map - */ - inSourceMap: process.env.IONIC_OUTPUT_JS_FILE_NAME + '.map', - - /** - * outSourceMap: file name for the output source map - */ - outSourceMap: process.env.IONIC_OUTPUT_JS_FILE_NAME + '.map', - /** * mangle: uglify 2's mangle option */ diff --git a/src/babili.spec.ts b/src/babili.spec.ts index e34405f7..c53d81f8 100644 --- a/src/babili.spec.ts +++ b/src/babili.spec.ts @@ -4,47 +4,64 @@ import * as crossSpawn from 'cross-spawn'; import { EventEmitter } from 'events'; describe('babili function', () => { - let emitter: EventEmitter; beforeEach(() => { - emitter = new EventEmitter(); - spyOn(configUtil, 'getUserConfigFile').and.returnValue('fileContents'); - spyOn(crossSpawn, 'spawn').and.returnValue(emitter); + + }); - it('should call main babili function', () => { + + + it('should reject promise when non-zero status code', () => { + const spawnMock: any = { + on: () => {} + }; + spyOn(crossSpawn, 'spawn').and.returnValue(spawnMock); + const onSpy = spyOn(spawnMock, 'on'); + const context = { - rootDir: '/Users/noone/Projects/ionic-conference-app' + nodeModulesDir: '/Users/noone/Projects/ionic-conference-app/node_modules' }; - const configFile = 'configFileContents'; + const knownError = 'should never get here'; + + const promise = babili.runBabili(context); + const spawnCallback = onSpy.calls.first().args[1]; + spawnCallback(1); - let pr = babili.babili(context, configFile); - emitter.emit('close', 0); - pr.then(() => { - expect(configUtil.getUserConfigFile).toHaveBeenCalledWith(context, babili.taskInfo, configFile); + return promise.then(() => { + throw new Error(knownError); + }).catch((err: Error) => { + expect(err.message).not.toEqual(knownError); }); }); - it('should throw if context does not have a rootDir', () => { - const context = {}; - const configFile = 'configFileContents'; + it('should resolve promise when zero status code', () => { + const spawnMock: any = { + on: () => {} + }; + spyOn(crossSpawn, 'spawn').and.returnValue(spawnMock); + const onSpy = spyOn(spawnMock, 'on'); - expect(babili.babili(context, configFile)).toThrow(); - }); + const context = { + nodeModulesDir: '/Users/noone/Projects/ionic-conference-app/node_modules' + }; - it('should fail because it does not have a valid build context', () => { - const context: null = null; - const configFile = 'configFileContents'; + const promise = babili.runBabili(context); + const spawnCallback = onSpy.calls.first().args[1]; + spawnCallback(0); - expect(babili.babili(context, configFile)).toThrow(); + return promise; }); - it('should fail because it does not have a valid config file', () => { + it('should throw if context does not have a rootDir', () => { const context = {}; - const configFile: null = null; - - expect(babili.babili(context, configFile)).toThrow(); + const knownError = 'should never get here'; + const promise = babili.runBabili(context); + return promise.then(() => { + throw new Error(knownError); + }).catch((err: Error) => { + expect(err.message).not.toEqual(knownError); + }); }); - }); diff --git a/src/babili.ts b/src/babili.ts index 4ae4de01..fe80a3d9 100644 --- a/src/babili.ts +++ b/src/babili.ts @@ -23,20 +23,16 @@ export function babili(context: BuildContext, configFile?: string) { export function babiliWorker(context: BuildContext, configFile: string) { const babiliConfig: BabiliConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); // TODO - figure out source maps?? - return runBabili(context, babiliConfig); + return runBabili(context); } -function runBabili(context: BuildContext, config: BabiliConfig) { - return runBabiliImpl(context); -} - -function runBabiliImpl(context: BuildContext) { +export function runBabili(context: BuildContext) { // TODO - is there a better way to run this? return new Promise((resolve, reject) => { - if (!context.rootDir) { + if (!context.nodeModulesDir) { return reject(new Error('Babili failed because the context passed did not have a rootDir')); } - const babiliPath = join(context.rootDir, 'node_modules', '.bin', 'babili'); + const babiliPath = join(context.nodeModulesDir, '.bin', 'babili'); const command = spawn(babiliPath, [context.buildDir, '--out-dir', context.buildDir]); command.on('close', (code: number) => { if (code !== 0) { diff --git a/src/optimization.spec.ts b/src/optimization.spec.ts index 458ae52c..3d5381fc 100644 --- a/src/optimization.spec.ts +++ b/src/optimization.spec.ts @@ -1,3 +1,5 @@ +import { join } from 'path'; + import * as optimization from './optimization'; import * as decorators from './optimization/decorators'; import * as treeshake from './optimization/treeshake'; @@ -28,4 +30,35 @@ describe('optimization task', () => { expect(treeshake.calculateUnusedComponents).not.toHaveBeenCalled(); }); }); + + describe('purgeGeneratedFiles', () => { + it('should remove files in buildDir with suffix from the cache', () => { + const buildDir = '/some/fake/dir/myApp/www/build'; + const context = { + fileCache: new FileCache(), + buildDir: buildDir + }; + const suffix = 'deptree.js'; + const filePathOne = join(buildDir, `0.${suffix}`); + const filePathTwo = join(buildDir, `1.${suffix}`); + const filePathThree = join(buildDir, `main.js`); + const filePathFour = join(buildDir, `main.css`); + const filePathFive = join('some', 'fake', 'dir', 'myApp', 'src', `app.ts`); + const filePathSix = join('some', 'fake', 'dir', 'myApp', 'src', `app.js`); + const filePathSeven = join('some', 'fake', 'dir', 'myApp', 'src', 'pages', `1.${suffix}`); + context.fileCache.set(filePathOne, { path: filePathOne, content: filePathOne}); + context.fileCache.set(filePathTwo, { path: filePathTwo, content: filePathTwo}); + context.fileCache.set(filePathThree, { path: filePathThree, content: filePathThree}); + context.fileCache.set(filePathFour, { path: filePathFour, content: filePathFour}); + context.fileCache.set(filePathFive, { path: filePathFive, content: filePathFive}); + context.fileCache.set(filePathSix, { path: filePathSix, content: filePathSix}); + context.fileCache.set(filePathSeven, { path: filePathSeven, content: filePathSeven}); + + optimization.purgeGeneratedFiles(context, suffix); + + expect(context.fileCache.getAll().length).toEqual(5); + expect(context.fileCache.get(filePathOne)).toBeFalsy(); + expect(context.fileCache.get(filePathTwo)).toBeFalsy(); + }); + }); }); diff --git a/src/optimization.ts b/src/optimization.ts index b75e8ee7..55369b6d 100644 --- a/src/optimization.ts +++ b/src/optimization.ts @@ -3,7 +3,7 @@ import { Logger } from './logger/logger'; import { fillConfigDefaults, getUserConfigFile, replacePathVars } from './util/config'; import * as Constants from './util/constants'; import { BuildError } from './util/errors'; -import { getBooleanPropertyValue, webpackStatsToDependencyMap, printDependencyMap, unlinkAsync } from './util/helpers'; +import { getBooleanPropertyValue, webpackStatsToDependencyMap, printDependencyMap } from './util/helpers'; import { BuildContext, TaskInfo } from './util/interfaces'; import { runWebpackFullBuild, WebpackConfig } from './webpack'; import { purgeDecorators } from './optimization/decorators'; @@ -32,7 +32,8 @@ function optimizationWorker(context: BuildContext, configFile: string): Promise< printDependencyMap(dependencyMap); Logger.debug('Original Dependency Map End'); } - return deleteOptimizationJsFile(join(webpackConfig.output.path, webpackConfig.output.filename)); + + purgeGeneratedFiles(context, webpackConfig.output.filename); }).then(() => { return doOptimizations(context, dependencyMap); }); @@ -41,8 +42,9 @@ function optimizationWorker(context: BuildContext, configFile: string): Promise< } } -export function deleteOptimizationJsFile(fileToDelete: string) { - return unlinkAsync(fileToDelete); +export function purgeGeneratedFiles(context: BuildContext, fileNameSuffix: string) { + const buildFiles = context.fileCache.getAll().filter(file => file.path.indexOf(context.buildDir) >= 0 && file.path.indexOf(fileNameSuffix) >= 0); + buildFiles.forEach(buildFile => context.fileCache.remove(buildFile.path)); } export function doOptimizations(context: BuildContext, dependencyMap: Map>) { diff --git a/src/uglifyjs.spec.ts b/src/uglifyjs.spec.ts index 511fd174..73374f6c 100644 --- a/src/uglifyjs.spec.ts +++ b/src/uglifyjs.spec.ts @@ -1,44 +1,71 @@ -import * as uglifyjs from './uglifyjs'; -import * as configUtil from './util/config'; -import * as workerClient from './worker-client'; - -describe('uglifyjs function', () => { - beforeEach(() => { - spyOn(configUtil, 'getUserConfigFile').and.returnValue('fileContents'); - spyOn(workerClient, 'runWorker').and.returnValue(Promise.resolve()); - }); +import * as fs from 'fs'; +import { join } from 'path'; - it('should call workerClient function', () => { - const context = {}; - const configFile = 'configFileContents'; +import * as uglifyLib from 'uglify-js'; - return uglifyjs.uglifyjs(context, configFile).then(() => { - expect(configUtil.getUserConfigFile).toHaveBeenCalledWith(context, uglifyjs.taskInfo, configFile); - expect(workerClient.runWorker).toHaveBeenCalledWith('uglifyjs', 'uglifyjsWorker', context, 'fileContents'); - }); - }); +import * as helpers from './util/helpers'; +import * as uglifyTask from './uglifyjs'; - it('should fail because it does not have a valid build context', () => { - const context: null = null; - const configFile = 'configFileContents'; - expect(uglifyjs.uglifyjs(context, configFile)).toThrow(); - }); +describe('uglifyjs', () => { + describe('uglifyjsWorkerImpl', () => { + it('should call uglify for the appropriate files', () => { + const buildDir = join('some', 'fake', 'dir', 'myApp', 'www', 'build'); + const context = { + buildDir: buildDir + }; + const fileNames = ['polyfills.js', 'sw-toolbox.js', '0.main.js', '0.main.js.map', '1.main.js', '1.main.js.map', 'main.js', 'main.js.map']; + const mockMinfiedResponse = { + code: 'code', + map: 'map' + }; + const mockUglifyConfig = { + mangle: true, + compress: true + }; - it('should fail because it does not have a valid config file', () => { - const context = {}; - const configFile: null = null; + spyOn(fs, 'readdirSync').and.returnValue(fileNames); + const uglifySpy = spyOn(uglifyLib, 'minify').and.returnValue(mockMinfiedResponse); + const writeFileSpy = spyOn(helpers, helpers.writeFileAsync.name).and.returnValue(Promise.resolve()); - expect(uglifyjs.uglifyjs(context, configFile)).toThrow(); - }); + const promise = uglifyTask.uglifyjsWorkerImpl(context, mockUglifyConfig); + + return promise.then(() => { + expect(uglifyLib.minify).toHaveBeenCalledTimes(3); + expect(uglifySpy.calls.all()[0].args[0]).toEqual(join(buildDir, '0.main.js')); + expect(uglifySpy.calls.all()[0].args[1].compress).toEqual(true); + expect(uglifySpy.calls.all()[0].args[1].mangle).toEqual(true); + expect(uglifySpy.calls.all()[0].args[1].inSourceMap).toEqual(join(buildDir, '0.main.js.map')); + expect(uglifySpy.calls.all()[0].args[1].outSourceMap).toEqual(join(buildDir, '0.main.js.map')); + + expect(uglifySpy.calls.all()[1].args[0]).toEqual(join(buildDir, '1.main.js')); + expect(uglifySpy.calls.all()[1].args[1].compress).toEqual(true); + expect(uglifySpy.calls.all()[1].args[1].mangle).toEqual(true); + expect(uglifySpy.calls.all()[1].args[1].inSourceMap).toEqual(join(buildDir, '1.main.js.map')); + expect(uglifySpy.calls.all()[1].args[1].outSourceMap).toEqual(join(buildDir, '1.main.js.map')); + + expect(uglifySpy.calls.all()[2].args[0]).toEqual(join(buildDir, 'main.js')); + expect(uglifySpy.calls.all()[2].args[1].compress).toEqual(true); + expect(uglifySpy.calls.all()[2].args[1].mangle).toEqual(true); + expect(uglifySpy.calls.all()[2].args[1].inSourceMap).toEqual(join(buildDir, 'main.js.map')); + expect(uglifySpy.calls.all()[2].args[1].outSourceMap).toEqual(join(buildDir, 'main.js.map')); + + expect(writeFileSpy).toHaveBeenCalledTimes(6); + expect(writeFileSpy.calls.all()[0].args[0]).toEqual(join(buildDir, '0.main.js')); + expect(writeFileSpy.calls.all()[0].args[1]).toEqual(mockMinfiedResponse.code); + expect(writeFileSpy.calls.all()[1].args[0]).toEqual(join(buildDir, '0.main.js.map')); + expect(writeFileSpy.calls.all()[1].args[1]).toEqual(mockMinfiedResponse.map); - it('should not fail if a config is not passed', () => { - const context = {}; - let configFile: any; + expect(writeFileSpy.calls.all()[2].args[0]).toEqual(join(buildDir, '1.main.js')); + expect(writeFileSpy.calls.all()[2].args[1]).toEqual(mockMinfiedResponse.code); + expect(writeFileSpy.calls.all()[3].args[0]).toEqual(join(buildDir, '1.main.js.map')); + expect(writeFileSpy.calls.all()[3].args[1]).toEqual(mockMinfiedResponse.map); - return uglifyjs.uglifyjs(context).then(() => { - expect(configUtil.getUserConfigFile).toHaveBeenCalledWith(context, uglifyjs.taskInfo, configFile); - expect(workerClient.runWorker).toHaveBeenCalledWith('uglifyjs', 'uglifyjsWorker', context, 'fileContents'); + expect(writeFileSpy.calls.all()[4].args[0]).toEqual(join(buildDir, 'main.js')); + expect(writeFileSpy.calls.all()[4].args[1]).toEqual(mockMinfiedResponse.code); + expect(writeFileSpy.calls.all()[5].args[0]).toEqual(join(buildDir, 'main.js.map')); + expect(writeFileSpy.calls.all()[5].args[1]).toEqual(mockMinfiedResponse.map); + }); }); }); }); diff --git a/src/uglifyjs.ts b/src/uglifyjs.ts index 2b0cc906..7d8e19e0 100644 --- a/src/uglifyjs.ts +++ b/src/uglifyjs.ts @@ -1,11 +1,11 @@ -import { join } from 'path'; +import { readdirSync } from 'fs'; +import { extname, join } from 'path'; + import * as uglify from 'uglify-js'; import { Logger } from './logger/logger'; -import { extname } from 'path'; -import { readdirSync, writeFileSync } from 'fs'; import { fillConfigDefaults, generateContext, getUserConfigFile } from './util/config'; -import { BuildError } from './util/errors'; +import { writeFileAsync } from './util/helpers'; import { BuildContext, TaskInfo } from './util/interfaces'; import { runWorker } from './worker-client'; @@ -25,39 +25,37 @@ export function uglifyjs(context: BuildContext, configFile?: string) { }); } - export function uglifyjsWorker(context: BuildContext, configFile: string): Promise { - return new Promise((resolve, reject) => { - try { - // provide a full path for the config options - context = generateContext(context); - const files = readdirSync(context.buildDir); - - files.forEach((file) => { - if (extname(file) === '.js' && file.indexOf('polyfills') === -1 && file.indexOf('sw-toolbox') === -1) { - const uglifyJsConfig: UglifyJsConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); - uglifyJsConfig.sourceFile = join(context.buildDir, file); - uglifyJsConfig.inSourceMap = join(context.buildDir, uglifyJsConfig.inSourceMap); - uglifyJsConfig.destFileName = join(context.buildDir, file); - - const minifiedOutputPath = join(context.buildDir, uglifyJsConfig.outSourceMap); - const minifyOutput: uglify.MinifyOutput = runUglifyInternal(uglifyJsConfig); - - - writeFileSync(uglifyJsConfig.destFileName, minifyOutput.code); - writeFileSync(minifiedOutputPath, minifyOutput.map); - - resolve(); - } - }); + const uglifyJsConfig: UglifyJsConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); + if (!context) { + context = generateContext(context); + } + return uglifyjsWorkerImpl(context, uglifyJsConfig); +} - } catch (e) { - reject(new BuildError(e)); +export function uglifyjsWorkerImpl(context: BuildContext, uglifyJsConfig: UglifyJsConfig) { + return Promise.resolve().then(() => { + // provide a full path for the config options + const files = readdirSync(context.buildDir); + const promises: Promise[] = []; + for (const file of files) { + if (extname(file) === '.js' && file.indexOf('polyfills') === -1 && file.indexOf('sw-toolbox') === -1 && file.indexOf('.map') === -1) { + uglifyJsConfig.sourceFile = join(context.buildDir, file); + uglifyJsConfig.inSourceMap = join(context.buildDir, file + '.map'); + uglifyJsConfig.destFileName = join(context.buildDir, file); + uglifyJsConfig.outSourceMap = join(context.buildDir, file + '.map'); + + const minifyOutput: uglify.MinifyOutput = runUglifyInternal(uglifyJsConfig); + + promises.push(writeFileAsync(uglifyJsConfig.destFileName, minifyOutput.code.toString())); + promises.push(writeFileAsync(uglifyJsConfig.outSourceMap, minifyOutput.map.toString())); + } } + + return Promise.all(promises); }); } - function runUglifyInternal(uglifyJsConfig: UglifyJsConfig): uglify.MinifyOutput { return uglify.minify(uglifyJsConfig.sourceFile, { compress: uglifyJsConfig.compress, @@ -79,11 +77,11 @@ export const taskInfo: TaskInfo = { export interface UglifyJsConfig { // https://www.npmjs.com/package/uglify-js - sourceFile: string; - destFileName: string; - inSourceMap: string; - outSourceMap: string; - mangle: boolean; - compress: boolean; - comments: boolean; + sourceFile?: string; + destFileName?: string; + inSourceMap?: string; + outSourceMap?: string; + mangle?: boolean; + compress?: boolean; + comments?: boolean; }