From a0589969f5f0833477fd3a3ebe26586f16322c97 Mon Sep 17 00:00:00 2001 From: Ciro Nunes Date: Sun, 20 Mar 2016 17:33:58 +0100 Subject: [PATCH] feat(build production): introduce the production build Closes #41 --- lib/broccoli/angular2-app.js | 363 +++++++++++++++++++++------------ lib/broccoli/is-production.js | 1 + package.json | 2 + tests/e2e/e2e_workflow.spec.js | 16 ++ 4 files changed, 255 insertions(+), 127 deletions(-) create mode 100644 lib/broccoli/is-production.js diff --git a/lib/broccoli/angular2-app.js b/lib/broccoli/angular2-app.js index 22927ca5782d..3d3bdb181069 100644 --- a/lib/broccoli/angular2-app.js +++ b/lib/broccoli/angular2-app.js @@ -1,128 +1,74 @@ var path = require('path'); +var isProduction = require('./is-production'); var configReplace = require('./broccoli-config-replace'); var compileWithTypescript = require('./broccoli-typescript'); var SwManifest = require('./service-worker-manifest').default; var fs = require('fs'); var Funnel = require('broccoli-funnel'); var mergeTrees = require('broccoli-merge-trees'); +var uglify = require('broccoli-uglify-js'); var Project = require('ember-cli/lib/models/project'); +var sourceDir = 'src'; module.exports = Angular2App; function Angular2App(defaults, options) { this._initProject(); this._notifyAddonIncluded(); - this.options = options; + this.options = options || {}; } -Angular2App.prototype.toTree = function () { - var sourceDir = 'src'; - - var sourceTree = new Funnel('src', { - include: ['*.ts', '**/*.ts', '**/*.d.ts'], - destDir: 'src' - }); - var typingsTree = new Funnel('typings', { - include: ['browser.d.ts', 'browser/**'], - destDir: 'typings' - }); +/** + * Create and return the app build system tree that: + * - Get the `assets` tree + * - Get the TS tree + * - Get the TS src tree + * - Get the index.html tree + * - Get the NPM modules tree + * - Apply/remove stuff based on the environment (dev|prod) + * - Return the app trees to be extended + * + * @public + * @method toTree + * @return {Array} The app trees that can be used to extend the build. + */ +Angular2App.prototype.toTree = function () { + var assetTree = this._getAssetsTree(); + var tsTree = this._getTsTree(); + var tsSrcTree = this._getTsSrcTree(); + var indexTree = this._getIndexTree(); + var vendorNpmTree = this._getVendorNpmTree(); + var excludeDotfilesTree = this._getPublicTree(); - var vendorNpmFiles = [ - 'systemjs/dist/system-polyfills.js', - 'systemjs/dist/system.src.js', - 'es6-shim/es6-shim.js', - 'angular2/bundles/angular2-polyfills.js', - 'rxjs/bundles/Rx.js', - 'angular2/bundles/angular2.dev.js', - 'angular2/bundles/http.dev.js', - 'angular2/bundles/router.dev.js', - 'angular2/bundles/upgrade.dev.js' - ]; + var buildTrees = [assetTree, tsTree, indexTree, vendorNpmTree]; - if (this.options && this.options.vendorNpmFiles) { - vendorNpmFiles = vendorNpmFiles.concat(this.options.vendorNpmFiles); + if (!isProduction) { + buildTrees.push(tsSrcTree); } - var tsconfig = JSON.parse(fs.readFileSync('src/tsconfig.json', 'utf-8')); - // Add all spec files to files. We need this because spec files are their own entry - // point. - fs.readdirSync(sourceDir).forEach(function addPathRecursive(name) { - const filePath = path.join(sourceDir, name); - if (filePath.match(/\.spec\.[jt]s$/)) { - tsconfig.files.push(name); - } else if (fs.statSync(filePath).isDirectory()) { - // Recursively call this function with the full sub-path. - fs.readdirSync(filePath).forEach(function (n) { - addPathRecursive(path.join(name, n)); - }); - } - }); - - // Because the tsconfig does not include the source directory, add this as the first path - // element. - tsconfig.files = tsconfig.files.map(name => path.join(sourceDir, name)); - - var srcAndTypingsTree = mergeTrees([sourceTree, typingsTree]); - var tsTree = new compileWithTypescript(srcAndTypingsTree, tsconfig); - - tsTree = new Funnel(tsTree, { - srcDir: 'src', - exclude: ['*.d.ts', 'tsconfig.json'] - }); - - var jsTree = new Funnel(sourceDir, { - include: ['**/*.js'], - allowEmpty: true - }); - - var assetTree = new Funnel(sourceDir, { - include: ['**/*.*'], - exclude: ['**/*.ts', '**/*.js'], - allowEmpty: true - }); - - var vendorNpmTree = new Funnel('node_modules', { - include: vendorNpmFiles, - destDir: 'vendor' - }); - - var allTrees = [ - assetTree, - tsTree, - jsTree, - this.index(), - vendorNpmTree - ]; - if (fs.existsSync('public')) { - allTrees.push(new Funnel('public', { - exclude: ['**/.*'], // Remove dot files. - allowEmpty: true - })); + buildTrees.push(excludeDotfilesTree); } - var merged = mergeTrees(allTrees, { overwrite: true }); + var merged = mergeTrees(buildTrees, { overwrite: true }); return mergeTrees([merged, new SwManifest([merged])]); }; + /** - @private - @method _initProject - @param {Object} options + * @private + * @method _initProject + * @param {Object} options */ Angular2App.prototype._initProject = function () { this.project = Project.closestSync(process.cwd()); - - /*if (options.configPath) { - this.project.configPath = function() { return options.configPath; }; - }*/ }; /** - @private - @method _notifyAddonIncluded + * @private + * @method _notifyAddonIncluded */ Angular2App.prototype._notifyAddonIncluded = function () { this.initializeAddons(); @@ -139,39 +85,41 @@ Angular2App.prototype._notifyAddonIncluded = function () { }, this); }; -/** - Loads and initializes addons for this project. - Calls initializeAddons on the Project. - @private - @method initializeAddons +/** + * Loads and initializes addons for this project. + * Calls initializeAddons on the Project. + * + * @private + * @method initializeAddons */ Angular2App.prototype.initializeAddons = function () { this.project.initializeAddons(); }; + /** - Returns the content for a specific type (section) for index.html. - - Currently supported types: - - 'head' - //- 'config-module' - //- 'app' - //- 'head-footer' - //- 'test-header-footer' - //- 'body-footer' - //- 'test-body-footer' - - Addons can also implement this method and could also define additional - types (eg. 'some-addon-section'). - - @private - @method contentFor - @param {RegExP} match Regular expression to match against - @param {String} type Type of content - @return {String} The content. + * Returns the content for a specific type (section) for index.html. + * + * Currently supported types: + * - 'head' + * //- 'config-module' + * //- 'app' + * //- 'head-footer' + * //- 'test-header-footer' + * //- 'body-footer' + * //- 'test-body-footer' + * + * Addons can also implement this method and could also define additional + * types (eg. 'some-addon-section'). + * + * @private + * @method _contentFor + * @param {RegExP} match Regular expression to match against + * @param {String} type Type of content + * @return {String} The content. */ -Angular2App.prototype.contentFor = function (match, type) { +Angular2App.prototype._contentFor = function (match, type) { var content = []; /*switch (type) { @@ -193,27 +141,28 @@ Angular2App.prototype.contentFor = function (match, type) { return content.join('\n'); }; + /** - @private - @method _configReplacePatterns - @return + * @private + * @method _getReplacePatterns + * @return Array Replace patterns. */ -Angular2App.prototype._configReplacePatterns = function () { +Angular2App.prototype._getReplacePatterns = function () { return [{ match: /\{\{content-for ['"](.+)["']\}\}/g, - replacement: this.contentFor.bind(this) + replacement: isProduction ? '' : this._contentFor.bind(this) }]; }; /** - Returns the tree for app/index.html - - @private - @method index - @return {Tree} Tree for app/index.html + * Returns the tree for app/index.html. + * + * @private + * @method _getIndexTree + * @return {Tree} Tree for app/index.html. */ -Angular2App.prototype.index = function () { +Angular2App.prototype._getIndexTree = function () { var htmlName = 'index.html'; var files = [ 'index.html' @@ -227,6 +176,166 @@ Angular2App.prototype.index = function () { return configReplace(index, { files: [htmlName], - patterns: this._configReplacePatterns() + patterns: this._getReplacePatterns() + }); +}; + + +/** + * Returns the source root dir tree. + * + * @private + * @method _getSourceTree + * @return {Tree} Tree for the src dir. + */ +Angular2App.prototype._getSourceTree = function () { + return new Funnel('src', { + include: ['*.ts', '**/*.ts', '**/*.d.ts'], + destDir: 'src' + }); +}; + + +/** + * Returns the typings tree. + * + * @private + * @method _getTypingsTree + * @return {Tree} Tree for the src dir. + */ +Angular2App.prototype._getTypingsTree = function () { + return new Funnel('typings', { + include: ['browser.d.ts', 'browser/**'], + destDir: 'typings' + }); +}; + + +/** + * Returns the TS tree. + * + * @private + * @method _getTsTree + * @return {Tree} Tree for TypeScript files. + */ +Angular2App.prototype._getTsTree = function () { + var typingsTree = this._getTypingsTree(); + var sourceTree = this._getSourceTree(); + + var tsconfig = JSON.parse(fs.readFileSync('src/tsconfig.json', 'utf-8')); + // Add all spec files to files. We need this because spec files are their own entry + // point. + fs.readdirSync(sourceDir).forEach(function addPathRecursive(name) { + const filePath = path.join(sourceDir, name); + if (filePath.match(/\.spec\.[jt]s$/)) { + tsconfig.files.push(name); + } else if (fs.statSync(filePath).isDirectory()) { + // Recursively call this function with the full sub-path. + fs.readdirSync(filePath).forEach(function (n) { + addPathRecursive(path.join(name, n)); + }); + } + }); + + // Because the tsconfig does not include the source directory, add this as the first path + // element. + tsconfig.files = tsconfig.files.map(name => path.join(sourceDir, name)); + + var srcAndTypingsTree = mergeTrees([sourceTree, typingsTree]); + var tsTree = new compileWithTypescript(srcAndTypingsTree, tsconfig); + + var tsTreeExcludes = ['*.d.ts', 'tsconfig.json']; + var excludeSpecFiles = '**/*.spec.*'; + + if (isProduction) { + tsTreeExcludes.push(excludeSpecFiles); + tsTree = uglify(tsTree); + } + + tsTree = new Funnel(tsTree, { + srcDir: 'src', + exclude: tsTreeExcludes + }); + + return tsTree; +}; + + +/** + * Returns the `vendorNpm` tree by merging the CLI dependencies plus the ones + * passed by the user. + * + * @private + * @method _getVendorNpmTree + * @return {Tree} The NPM tree. + */ +Angular2App.prototype._getVendorNpmTree = function () { + var vendorNpmFiles = [ + 'systemjs/dist/system-polyfills.js', + 'systemjs/dist/system.src.js', + 'es6-shim/es6-shim.js', + 'angular2/bundles/angular2-polyfills.js', + 'rxjs/bundles/Rx.js', + 'angular2/bundles/angular2.dev.js', + 'angular2/bundles/http.dev.js', + 'angular2/bundles/router.dev.js', + 'angular2/bundles/upgrade.dev.js' + ]; + + if (this.options.vendorNpmFiles) { + vendorNpmFiles = vendorNpmFiles.concat(this.options.vendorNpmFiles); + } + + var vendorNpmTree = new Funnel('node_modules', { + include: vendorNpmFiles, + destDir: 'vendor' + }); + + return vendorNpmTree; +}; + + +/** + * Returns the `assets` tree. + * + * @private + * @method _getAssetsTree + * @return {Tree} The assets tree. + */ +Angular2App.prototype._getAssetsTree = function () { + return new Funnel(sourceDir, { + include: ['**/*.*'], + exclude: ['**/*.ts', '**/*.js'], + allowEmpty: true + }); +}; + + +/** + * Returns the `tsSrc` tree. + * + * @private + * @method _getTsSrcTree + * @return {Tree} The TS src tree. + */ +Angular2App.prototype._getTsSrcTree = function () { + return new Funnel(sourceDir, { + include: ['**/*.ts'], + allowEmpty: true + }); +}; + + +/** + * Returns the `excludeDotfiles` tree. + * + * @private + * @method _getPublicTree + * @return {Tree} The dotfiles exclusion tree. + */ +Angular2App.prototype._getPublicTree = function () { + return new Funnel('public', { + exclude: ['**/.*'], + allowEmpty: true }); }; diff --git a/lib/broccoli/is-production.js b/lib/broccoli/is-production.js new file mode 100644 index 000000000000..64885fd576ae --- /dev/null +++ b/lib/broccoli/is-production.js @@ -0,0 +1 @@ +module.exports = (/(^production$|^prod$)/).test(process.env.EMBER_ENV); diff --git a/package.json b/package.json index cb7c3b048b10..573af3e3b9b8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "broccoli-concat": "^2.2.0", "broccoli-funnel": "^1.0.1", "broccoli-merge-trees": "^1.1.1", + "broccoli-uglify-js": "^0.1.3", "broccoli-writer": "^0.1.1", "chalk": "^1.1.1", "ember-cli": "2.4.2", @@ -64,6 +65,7 @@ "glob": "^7.0.3", "minimatch": "^3.0.0", "mocha": "^2.4.5", + "object-assign": "^4.0.1", "rewire": "^2.5.1", "through": "^2.3.8", "tslint": "^3.6.0", diff --git a/tests/e2e/e2e_workflow.spec.js b/tests/e2e/e2e_workflow.spec.js index bd980e5fee7f..926120862ac6 100644 --- a/tests/e2e/e2e_workflow.spec.js +++ b/tests/e2e/e2e_workflow.spec.js @@ -73,6 +73,22 @@ describe('Basic end-to-end Workflow', function () { }); }); + it('Supports production builds via `ng build --envinroment=production`', function() { + this.timeout(420000); + + return ng(['build', '--environment=production', '--silent']) + .then(function () { + expect(existsSync(path.join(process.cwd(), 'dist'))).to.be.equal(true); + }) + .then(function () { + // Also does not create new things in GIT. + expect(sh.exec('git status --porcelain').output).to.be.equal(''); + }) + .catch(() => { + throw new Error('Build failed.'); + }); + }); + it('Produces a service worker manifest after initial build', function () { var manifestPath = path.join(process.cwd(), 'dist', 'manifest.appcache'); expect(existsSync(manifestPath)).to.be.equal(true);