Skip to content
This repository was archived by the owner on May 1, 2020. It is now read-only.

Commit 3a4c42a

Browse files
authored
feature(minification): add basic experimental support for closure compiler and babili
1 parent 6156d52 commit 3a4c42a

19 files changed

+310
-48
lines changed

config/babili.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
// https://www.npmjs.com/package/babili
3+
4+
module.exports = {
5+
/**
6+
* sourceFile: The javascript file to minify
7+
*/
8+
sourceFile: process.env.IONIC_OUTPUT_JS_FILE_NAME,
9+
10+
/**
11+
* destFileName: file name for the minified js in the build dir
12+
*/
13+
destFileName: process.env.IONIC_OUTPUT_JS_FILE_NAME,
14+
}

config/closure.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,9 @@ module.exports = {
99
* `java` will suffice. Verify by running `java --version`
1010
*/
1111
pathToJavaExecutable: 'java',
12-
pathToClosureJar: process.env.IONIC_CLOSURE_JAR
12+
pathToClosureJar: process.env.IONIC_CLOSURE_JAR,
13+
optimization: 'ADVANCED_OPTIMIZATIONS',
14+
languageIn: `ECMASCRIPT6`,
15+
languageOut: 'ECMASCRIPT5',
16+
debug: false
1317
};

config/rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var rollupConfig = {
1919
* sourceMap: If true, a separate sourcemap file will
2020
* be created.
2121
*/
22-
sourceMap: process.env.IONIC_GENERATE_SOURCE_MAP ? true : false,
22+
sourceMap: process.env.IONIC_GENERATE_SOURCE_MAP === 'true' ? true : false,
2323

2424
/**
2525
* format: The format of the generated bundle

config/webpack.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module.exports = {
1010
filename: '[name].js',
1111
devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(),
1212
},
13-
devtool: process.env.IONIC_GENERATE_SOURCE_MAP ? process.env.IONIC_SOURCE_MAP_TYPE : '',
13+
devtool: process.env.IONIC_GENERATE_SOURCE_MAP === 'true' ? process.env.IONIC_SOURCE_MAP_TYPE : '',
1414

1515
resolve: {
1616
extensions: ['.ts', '.js', '.json'],
@@ -33,8 +33,8 @@ module.exports = {
3333

3434
plugins: [
3535
ionicWebpackFactory.getIonicEnvironmentPlugin(),
36-
ionicWebpackFactory.getNonIonicCommonChunksPlugin(),
37-
ionicWebpackFactory.getIonicCommonChunksPlugin()
36+
// ionicWebpackFactory.getNonIonicCommonChunksPlugin(),
37+
// ionicWebpackFactory.getIonicCommonChunksPlugin()
3838
],
3939

4040
// Some libraries import Node modules but don't use them in the browser.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"@types/uglify-js": "^2.0.27",
8484
"@types/webpack": "^1.12.35",
8585
"@types/ws": "^0.0.34",
86+
"babili": "0.0.10",
8687
"conventional-changelog-cli": "1.2.0",
8788
"github": "0.2.4",
8889
"ionic-cz-conventional-changelog": "1.0.0",

src/aot/aot-compiler.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mkdirpSync, readFileSync, writeFileSync } from 'fs-extra';
22
import { basename, dirname, extname, join, normalize, relative, resolve } from 'path';
33

44
import 'reflect-metadata';
5-
import { CallExpression, CompilerOptions, createProgram, createSourceFile, Decorator, Identifier, ParsedCommandLine, Program, ScriptTarget, SyntaxKind, transpileModule, TranspileOptions, TranspileOutput } from 'typescript';
5+
import { CompilerOptions, createProgram, ParsedCommandLine, Program, transpileModule, TranspileOptions, TranspileOutput } from 'typescript';
66
import { CodeGenerator, NgcCliOptions, NodeReflectorHostContext, ReflectorHost, StaticReflector }from '@angular/compiler-cli';
77
import { tsc } from '@angular/tsc-wrapped/src/tsc';
88
import AngularCompilerOptions from '@angular/tsc-wrapped/src/options';
@@ -12,13 +12,11 @@ import { getInstance as getHybridFileSystem } from '../util/hybrid-file-system-f
1212
import { getInstance } from './compiler-host-factory';
1313
import { NgcCompilerHost } from './compiler-host';
1414
import { patchReflectorHost } from './reflector-host';
15-
import { findNodes, getNodeStringContent, getTypescriptSourceFile, removeDecorators } from '../util/typescript-utils';
1615
import { getFallbackMainContent, replaceBootstrap } from './utils';
1716
import { Logger } from '../logger/logger';
1817
import { printDiagnostics, clearDiagnostics, DiagnosticsType } from '../logger/logger-diagnostics';
1918
import { runTypeScriptDiagnostics } from '../logger/logger-typescript';
2019
import { isDebugMode } from '../util/config';
21-
import * as Constants from '../util/constants';
2220
import { BuildError } from '../util/errors';
2321
import { changeExtension } from '../util/helpers';
2422
import { BuildContext } from '../util/interfaces';
@@ -32,7 +30,6 @@ export class AotCompiler {
3230
private reflectorHost: ReflectorHost;
3331
private compilerHost: NgcCompilerHost;
3432
private fileSystem: HybridFileSystem;
35-
private appLevelNgModuleFilePath: string;
3633
private lazyLoadedModuleDictionary: any;
3734

3835
constructor(private context: BuildContext, private options: AotOptions) {
@@ -96,9 +93,7 @@ export class AotCompiler {
9693
if (!mainFile) {
9794
throw new BuildError(new Error(`Could not find entry point (bootstrap file) ${this.options.entryPoint}`));
9895
}
99-
const mainSourceFile = getTypescriptSourceFile(mainFile.path, mainFile.content, ScriptTarget.Latest, false);
10096
Logger.debug('[AotCompiler] compile: Resolving NgModule from entry point');
101-
this.appLevelNgModuleFilePath = this.options.appNgModulePath
10297
let modifiedFileContent: string = null;
10398
try {
10499
Logger.debug('[AotCompiler] compile: Dynamically changing entry point content to AOT mode content');

src/babili.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { join } from 'path';
2+
import { spawn } from 'cross-spawn';
3+
4+
import { fillConfigDefaults, getUserConfigFile } from './util/config';
5+
import { BuildContext, TaskInfo } from './util/interfaces';
6+
import { Logger } from './logger/logger';
7+
import { writeFileAsync } from './util/helpers';
8+
9+
export function babili(context: BuildContext, configFile?: string) {
10+
11+
configFile = getUserConfigFile(context, taskInfo, configFile);
12+
const logger = new Logger('babili - experimental');
13+
14+
return babiliWorker(context, configFile).then(() => {
15+
logger.finish();
16+
})
17+
.catch(err => {
18+
throw logger.fail(err);
19+
});
20+
}
21+
22+
23+
export function babiliWorker(context: BuildContext, configFile: string): Promise<any> {
24+
const babiliConfig: BabiliConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile);
25+
// TODO - figure out source maps??
26+
return runBabili(context, babiliConfig).then((minifiedCode: string) => {
27+
// write the file back to disk
28+
const fileToWrite = join(context.buildDir, babiliConfig.destFileName);
29+
return writeFileAsync(fileToWrite, minifiedCode);
30+
});
31+
}
32+
33+
function runBabili(context: BuildContext, config: BabiliConfig) {
34+
const babiliPath = join(context.rootDir, 'node_modules', '.bin', 'babili');
35+
const bundlePath = join(context.buildDir, config.sourceFile);
36+
return runBabiliImpl(babiliPath, bundlePath);
37+
}
38+
39+
function runBabiliImpl(pathToBabili: string, pathToBundle: string) {
40+
// TODO - is there a better way to run this?
41+
let chunks: string[] = [];
42+
return new Promise((resolve, reject) => {
43+
const command = spawn(pathToBabili, [pathToBundle]);
44+
command.stdout.on('data', (buffer: Buffer) => {
45+
const stringRepresentation = buffer.toString();
46+
Logger.debug(`[Babili] ${stringRepresentation}`);
47+
chunks.push(stringRepresentation);
48+
});
49+
50+
command.stderr.on('data', (buffer: Buffer) => {
51+
Logger.warn(`[Babili] ${buffer.toString()}`);
52+
});
53+
54+
command.on('close', (code: number) => {
55+
if ( code !== 0) {
56+
return reject(new Error('Babili failed with a non-zero status code'));
57+
}
58+
return resolve(chunks.join(''));
59+
});
60+
61+
62+
/*exec(`${pathToBabili} ${pathToBundle}`, (err: Error, stdout: string, stderr: string) => {
63+
console.log('err: ', err.message);
64+
console.log('stdout: ', stdout);
65+
console.log('stderr: ', stderr);
66+
if (err) {
67+
reject(err);
68+
} else {
69+
resolve(stdout);
70+
}
71+
});
72+
*/
73+
});
74+
}
75+
76+
export const taskInfo: TaskInfo = {
77+
fullArg: '--babili',
78+
shortArg: null,
79+
envVar: 'IONIC_EXP_BABILI',
80+
packageConfig: 'ionic_exp_babili',
81+
defaultConfigFile: 'babili.config'
82+
};
83+
84+
85+
export interface BabiliConfig {
86+
// https://www.npmjs.com/package/uglify-js
87+
sourceFile: string;
88+
destFileName: string;
89+
}

src/build.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { lint, lintUpdate } from './lint';
1010
import { Logger } from './logger/logger';
1111
import { minifyCss, minifyJs } from './minify';
1212
import { ngc } from './ngc';
13-
import { preprocess } from './preprocess';
13+
import { preprocess, preprocessUpdate } from './preprocess';
1414
import { sass, sassUpdate } from './sass';
1515
import { templateUpdate } from './template';
1616
import { transpile, transpileUpdate, transpileDiagnosticsOnly } from './transpile';
@@ -211,9 +211,10 @@ function buildUpdateTasks(changedFiles: ChangedFile[], context: BuildContext) {
211211
changedFiles: []
212212
};
213213

214-
return Promise.resolve()
214+
return loadFiles(changedFiles, context)
215215
.then(() => {
216-
return loadFiles(changedFiles, context);
216+
// PREPROCESS
217+
return preprocessUpdate(changedFiles, context);
217218
})
218219
.then(() => {
219220
// TEMPLATE

src/closure.ts

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { execSync } from 'child_process';
1+
import { join } from 'path';
2+
import { spawn } from 'cross-spawn';
3+
4+
import * as Constants from './util/constants';
5+
import { copyFileAsync, getBooleanPropertyValue, generateRandomHexString, unlinkAsync } from './util/helpers';
26
import { BuildContext, TaskInfo } from './util/interfaces';
37
import { fillConfigDefaults, generateContext, getUserConfigFile } from './util/config';
48
import { Logger } from './logger/logger';
59
import { runWorker } from './worker-client';
610

11+
712
export function closure(context: BuildContext, configFile?: string) {
813
configFile = getUserConfigFile(context, taskInfo, configFile);
914

@@ -14,33 +19,110 @@ export function closure(context: BuildContext, configFile?: string) {
1419
logger.finish();
1520
})
1621
.catch(err => {
22+
console.log('err: ', err);
1723
throw logger.fail(err);
1824
});
1925
}
2026

2127
export function closureWorker(context: BuildContext, configFile: string): Promise<any> {
28+
context = generateContext(context);
29+
const tempFileName = generateRandomHexString(10) + '.js';
30+
const tempFilePath = join(context.buildDir, tempFileName);
31+
const closureConfig = getClosureConfig(context, configFile);
32+
const bundleFilePath = join(context.buildDir, process.env[Constants.ENV_OUTPUT_JS_FILE_NAME]);
33+
return runClosure(closureConfig, bundleFilePath, tempFilePath, context.buildDir, closureConfig.debug)
34+
.then(() => {
35+
const promises: Promise<any>[] = [];
36+
promises.push(copyFileAsync(tempFilePath, bundleFilePath));
37+
promises.push(copyFileAsync(tempFilePath + '.map', bundleFilePath + '.map'));
38+
return Promise.all(promises);
39+
}).then(() => {
40+
// delete the temp bundle either way
41+
const promises: Promise<any>[] = [];
42+
promises.push(unlinkAsync(tempFilePath));
43+
promises.push(unlinkAsync(tempFilePath + '.map'));
44+
return Promise.all(promises);
45+
}).catch(err => {
46+
// delete the temp bundle either way
47+
unlinkAsync(tempFilePath);
48+
unlinkAsync(tempFilePath + '.map');
49+
throw err;
50+
});
51+
}
52+
53+
function checkIfJavaIsAvailable(closureConfig: ClosureConfig) {
2254
return new Promise((resolve, reject) => {
23-
context = generateContext(context);
24-
Logger.warn('Closer Compiler unsupported at this time.');
25-
resolve();
55+
const command = spawn(`${closureConfig.pathToJavaExecutable}`, ['-version']);
56+
57+
command.stdout.on('data', (buffer: Buffer) => {
58+
Logger.debug(`[Closure]: ${buffer.toString()}`);
59+
});
60+
61+
command.stderr.on('data', (buffer: Buffer) => {
62+
Logger.warn(`[Closure]: ${buffer.toString()}`);
63+
});
64+
65+
command.on('close', (code: number) => {
66+
if (code === 0) {
67+
return resolve();
68+
}
69+
reject();
70+
});
2671
});
2772
}
2873

74+
function runClosure(closureConfig: ClosureConfig, nonMinifiedBundlePath: string, minifiedBundleFileName: string, outputDir: string, isDebug: boolean) {
75+
return new Promise((resolve, reject) => {
76+
const closureArgs = ['-jar', `${closureConfig.pathToClosureJar}`,
77+
'--js', `${nonMinifiedBundlePath}`,
78+
'--js_output_file', `${minifiedBundleFileName}`,
79+
`--language_out=${closureConfig.languageOut}`,
80+
'--language_in', `${closureConfig.languageIn}`,
81+
'--compilation_level', `${closureConfig.optimization}`,
82+
`--create_source_map=%outname%.map`,
83+
`--variable_renaming_report=${outputDir}/variable_renaming_report`,
84+
`--property_renaming_report=${outputDir}/property_renaming_report`,
85+
`--rewrite_polyfills=false`,
86+
];
2987

30-
export function isClosureSupported(context: BuildContext): boolean{
31-
/*const config = getClosureConfig(context, '');
32-
try {
33-
execSync(`${config.pathToJavaExecutable} --version`);
34-
return true;
35-
} catch (ex) {
36-
Logger.debug('[Closure] isClosureSupported: Failed to execute java command');
37-
return false;
88+
if (isDebug) {
89+
closureArgs.push('--debug');
90+
}
91+
const closureCommand = spawn(`${closureConfig.pathToJavaExecutable}`, closureArgs);
92+
93+
closureCommand.stdout.on('data', (buffer: Buffer) => {
94+
Logger.debug(`[Closure] ${buffer.toString()}`);
95+
});
96+
97+
closureCommand.stderr.on('data', (buffer: Buffer) => {
98+
Logger.debug(`[Closure] ${buffer.toString()}`);
99+
});
100+
101+
closureCommand.on('close', (code: number) => {
102+
if (code === 0) {
103+
return resolve();
104+
}
105+
reject(new Error('Closure failed with a non-zero status code'));
106+
});
107+
});
108+
}
109+
110+
111+
export function isClosureSupported(context: BuildContext): Promise<boolean> {
112+
if (!getBooleanPropertyValue(Constants.ENV_USE_EXPERIMENTAL_CLOSURE)) {
113+
return Promise.resolve(false);
38114
}
39-
*/
40-
return false;
115+
Logger.debug('[Closure] isClosureSupported: Checking if Closure Compiler is available');
116+
const config = getClosureConfig(context);
117+
return checkIfJavaIsAvailable(config).then(() => {
118+
return Promise.resolve(true);
119+
}).catch(() => {
120+
Logger.warn(`Closure Compiler support is enabled but Java cannot be started. Try running the build again with the "--debug" argument for more information.`);
121+
return Promise.resolve(false);
122+
});
41123
}
42124

43-
function getClosureConfig(context: BuildContext, configFile: string): ClosureConfig {
125+
function getClosureConfig(context: BuildContext, configFile?: string): ClosureConfig {
44126
configFile = getUserConfigFile(context, taskInfo, configFile);
45127

46128
return fillConfigDefaults(configFile, taskInfo.defaultConfigFile);
@@ -59,4 +141,8 @@ export interface ClosureConfig {
59141
// https://developers.google.com/closure/compiler/docs/gettingstarted_app
60142
pathToJavaExecutable: string;
61143
pathToClosureJar: string;
144+
optimization: string;
145+
languageOut: string;
146+
languageIn: string;
147+
debug: boolean;
62148
}

src/copy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars
55
import * as Constants from './util/constants';
66
import { emit, EventType } from './util/events';
77
import { generateGlobTasks, globAll, GlobObject, GlobResult } from './util/glob-util';
8-
import { copyFileAsync, rimRafAsync, unlinkAsync } from './util/helpers';
8+
import { copyFileAsync, getBooleanPropertyValue, rimRafAsync, unlinkAsync } from './util/helpers';
99
import { BuildContext, ChangedFile, TaskInfo } from './util/interfaces';
1010
import { Watcher, copyUpdate as watchCopyUpdate } from './watch';
1111

@@ -41,7 +41,7 @@ export function copyWorker(context: BuildContext, configFile: string) {
4141
// basically, we've got a stew goin'
4242
return populateFileAndDirectoryInfo(resultMap, copyConfig, toCopyList, directoriesToCreate);
4343
}).then(() => {
44-
if (process.env[Constants.ENV_CLEAN_BEFORE_COPY]) {
44+
if (getBooleanPropertyValue(Constants.ENV_CLEAN_BEFORE_COPY)) {
4545
cleanDirectories(context, directoriesToCreate);
4646
}
4747
}).then(() => {

src/declarations.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
declare module 'autoprefixer';
2+
declare module 'cross-spawn';
23
declare module 'mime-types';
34
declare module 'proxyquire';
45
declare module 'os-name';

0 commit comments

Comments
 (0)