diff --git a/gulpfile.js b/gulpfile.js index 6dac9d40c4..867823c067 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -51,6 +51,8 @@ var regularPlunker = require(path.resolve(TOOLS_PATH, 'plunker-builder/regularPl var embeddedPlunker = require(path.resolve(TOOLS_PATH, 'plunker-builder/embeddedPlunker')); var fsUtils = require(path.resolve(TOOLS_PATH, 'fs-utils/fsUtils')); +var publish = require(path.resolve(EXAMPLES_PATH + '/cb-third-party-lib/hero-profile/publish')); + const WWW = argv.page ? 'www-pages' : 'www' const isSilent = !!argv.silent; @@ -464,7 +466,9 @@ gulp.task('add-example-boilerplate', function(done) { fsUtils.addSymlink(realPath, linkPath); }); - return buildStyles(copyExampleBoilerplate, done); + publish().then(function(){ + return buildStyles(copyExampleBoilerplate, done); + }); }); diff --git a/public/docs/_examples/_boilerplate/package.json b/public/docs/_examples/_boilerplate/package.json index 3f255fcbd6..39d440cbd0 100644 --- a/public/docs/_examples/_boilerplate/package.json +++ b/public/docs/_examples/_boilerplate/package.json @@ -18,6 +18,7 @@ "build:webpack": "rimraf dist && webpack --config config/webpack.prod.js --bail", "build:cli": "ng build", "build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js", + "build:aot:jit": "npm run build:aot && npm run tsc", "copy-dist-files": "node ./copy-dist-files.js", "i18n": "ng-xi18n" }, diff --git a/public/docs/_examples/_boilerplate/systemjs.config.js b/public/docs/_examples/_boilerplate/systemjs.config.js index 457f040fc0..3ae1989388 100644 --- a/public/docs/_examples/_boilerplate/systemjs.config.js +++ b/public/docs/_examples/_boilerplate/systemjs.config.js @@ -26,7 +26,8 @@ // other libraries 'rxjs': 'npm:rxjs', - 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js' + 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js', + 'hero-profile': 'npm:hero-profile/bundles/hero-profile.umd.js' }, // packages tells the System loader how to load when no filename and/or no extension packages: { diff --git a/public/docs/_examples/_boilerplate/tsconfig.json b/public/docs/_examples/_boilerplate/tsconfig.json index d90e457b10..d84cabba2a 100644 --- a/public/docs/_examples/_boilerplate/tsconfig.json +++ b/public/docs/_examples/_boilerplate/tsconfig.json @@ -16,6 +16,7 @@ "compileOnSave": true, "exclude": [ "node_modules/*", - "**/*-aot.ts" + "**/*-aot.ts", + "app-aot" ] } diff --git a/public/docs/_examples/cb-third-party-lib/.gitignore b/public/docs/_examples/cb-third-party-lib/.gitignore new file mode 100644 index 0000000000..b8c17ad953 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/.gitignore @@ -0,0 +1,9 @@ +**/*.ngfactory.ts +**/*.metadata.json +**/*.css.shim.ts +*.js +!/hero-profile/rollup-config.js +!/hero-profile/publish.js +!/hero-profile/inline-resources.js +!/hero-profile/package.json +!ts/rollup-config.js \ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/e2e-spec.ts b/public/docs/_examples/cb-third-party-lib/e2e-spec.ts new file mode 100644 index 0000000000..1bcc69f45d --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/e2e-spec.ts @@ -0,0 +1,22 @@ +'use strict'; // necessary for es6 output in node + +import { browser, element, by } from 'protractor'; + +describe('Third Party Lib Cookbook', function () { + + let expectedMsgAoT = 'Library consumed by AoT application'; + let expectedMsgJiT = 'Library consumed by JiT application'; + + beforeEach(function () { + browser.get(''); + }); + + it(`should load AoT compiled version`, function () { + expect(element(by.css('.aot')).getText()).toEqual(expectedMsgAoT); + }); + + it('should load JiT compiled version', function () { + expect(element(by.css('.jit')).getText()).toEqual(expectedMsgJiT); + }); + +}); diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.css b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.css new file mode 100644 index 0000000000..9f906ea720 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.css @@ -0,0 +1,6 @@ +/*#docregion*/ +.bio { + border: 1px solid black; + padding: 10px; + width: 300px; +} \ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.html b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.html new file mode 100644 index 0000000000..3b7ef9c2c2 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.html @@ -0,0 +1,7 @@ + +
+

Featured Hero

+ +

{{hero.name}}

+
{{hero.bio}}
+
\ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.ts b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.ts new file mode 100644 index 0000000000..82c451d017 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.ts @@ -0,0 +1,14 @@ +// #docregion +import { Component, Input } from '@angular/core'; + +import { Hero } from './hero'; + +@Component({ + moduleId: module.id, + selector: 'hero-profile', + templateUrl: 'hero-profile.component.html', + styleUrls: ['hero-profile.component.css'] +}) +export class HeroProfileComponent { + @Input() hero: Hero; +} diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.module.ts b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.module.ts new file mode 100644 index 0000000000..692bdd6806 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.module.ts @@ -0,0 +1,12 @@ +// #docregion +import { NgModule } from '@angular/core'; + +import { HeroProfileComponent } from './hero-profile.component'; + +@NgModule({ + declarations: [HeroProfileComponent], + exports: [HeroProfileComponent] +}) +export class HeroProfileModule { + +} diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/hero.ts b/public/docs/_examples/cb-third-party-lib/hero-profile/hero.ts new file mode 100644 index 0000000000..09d04629f5 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/hero.ts @@ -0,0 +1,4 @@ +// #docregion +export class Hero { + constructor(public name: string, public bio: string) {} +} diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/index.ts b/public/docs/_examples/cb-third-party-lib/hero-profile/index.ts new file mode 100644 index 0000000000..70556487f5 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/index.ts @@ -0,0 +1,4 @@ +// #docregion +export { HeroProfileComponent } from './hero-profile.component'; +export { HeroProfileModule } from './hero-profile.module'; +export { Hero } from './hero'; diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/inline-resources.js b/public/docs/_examples/cb-third-party-lib/hero-profile/inline-resources.js new file mode 100644 index 0000000000..61b5d4b36e --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/inline-resources.js @@ -0,0 +1,137 @@ +// #docregion +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +/** + * Simple Promiseify function that takes a Node API and return a version that supports promises. + * We use promises instead of synchronized functions to make the process less I/O bound and + * faster. It also simplify the code. + */ +function promiseify(fn) { + return function() { + const args = [].slice.call(arguments, 0); + return new Promise((resolve, reject) => { + fn.apply(this, args.concat([function (err, value) { + if (err) { + reject(err); + } else { + resolve(value); + } + }])); + }); + }; +} + +const readFile = promiseify(fs.readFile); +const writeFile = promiseify(fs.writeFile); + + +function inlineResources(globs) { + if (typeof globs == 'string') { + globs = [globs]; + } + + /** + * For every argument, inline the templates and styles under it and write the new file. + */ + return Promise.all(globs.map(pattern => { + if (pattern.indexOf('*') < 0) { + // Argument is a directory target, add glob patterns to include every files. + pattern = path.join(pattern, '**', '*'); + } + + const files = glob.sync(pattern, {}) + .filter(name => /\.js$/.test(name)); // Matches only JavaScript files. + + // Generate all files content with inlined templates. + return Promise.all(files.map(filePath => { + return readFile(filePath, 'utf-8') + .then(content => inlineResourcesFromString(content, url => { + return path.join(path.dirname(filePath), url); + })) + .then(content => writeFile(filePath, content)) + .catch(err => { + console.error('An error occured: ', err); + }); + })); + })); +} + +/** + * Inline resources from a string content. + * @param content {string} The source file's content. + * @param urlResolver {Function} A resolver that takes a URL and return a path. + * @returns {string} The content with resources inlined. + */ +function inlineResourcesFromString(content, urlResolver) { + // Curry through the inlining functions. + return [ + inlineTemplate, + inlineStyle, + removeModuleId + ].reduce((content, fn) => fn(content, urlResolver), content); +} + +if (require.main === module) { + inlineResources(process.argv.slice(2)); +} + + +/** + * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and + * replace with `template: ...` (with the content of the file included). + * @param content {string} The source file's content. + * @param urlResolver {Function} A resolver that takes a URL and return a path. + * @return {string} The content with all templates inlined. + */ +function inlineTemplate(content, urlResolver) { + return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function(m, templateUrl) { + const templateFile = urlResolver(templateUrl); + const templateContent = fs.readFileSync(templateFile, 'utf-8'); + const shortenedTemplate = templateContent + .replace(/([\n\r]\s*)+/gm, ' ') + .replace(/"/g, '\\"'); + return `template: "${shortenedTemplate}"`; + }); +} + + +/** + * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and + * replace with `styles: [...]` (with the content of the file included). + * @param urlResolver {Function} A resolver that takes a URL and return a path. + * @param content {string} The source file's content. + * @return {string} The content with all styles inlined. + */ +function inlineStyle(content, urlResolver) { + return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function(m, styleUrls) { + const urls = eval(styleUrls); + return 'styles: [' + + urls.map(styleUrl => { + const styleFile = urlResolver(styleUrl); + const styleContent = fs.readFileSync(styleFile, 'utf-8'); + const shortenedStyle = styleContent + .replace(/([\n\r]\s*)+/gm, ' ') + .replace(/"/g, '\\"'); + return `"${shortenedStyle}"`; + }) + .join(',\n') + + ']'; + }); +} + + +/** + * Remove every mention of `moduleId: module.id`. + * @param content {string} The source file's content. + * @returns {string} The content with all moduleId: mentions removed. + */ +function removeModuleId(content) { + return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); +} + +module.exports = inlineResources; +module.exports.inlineResourcesFromString = inlineResourcesFromString; \ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/package.json b/public/docs/_examples/cb-third-party-lib/hero-profile/package.json new file mode 100644 index 0000000000..467a117c77 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/package.json @@ -0,0 +1,14 @@ +{ + "name": "hero-profile", + "version": "0.0.1", + "main": "bundles/hero-profile.umd.js", + "module": "index.js", + "typings": "index.d.ts", + "author": "", + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/angular/angular.io/blob/master/LICENSE" + } + ] +} \ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/publish.js b/public/docs/_examples/cb-third-party-lib/hero-profile/publish.js new file mode 100644 index 0000000000..ec370ec220 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/publish.js @@ -0,0 +1,59 @@ +// #docregion +function publish() { + var inlineResources = require('./inline-resources'); + + // AoT compile + var spawnNgc = require( 'child_process' ).spawnSync; + var ngc = spawnNgc('./public/docs/_examples/node_modules/.bin/ngc', ['-p', './public/docs/_examples/cb-third-party-lib/hero-profile/tsconfig-aot.json']); + + // Copy to node_modules + var fs = require('fs'); + var del = require('del'); + + var node_modules_root = './public/docs/_examples/node_modules/hero-profile/'; + + del.sync(node_modules_root, {force:true}); + + fs.mkdirSync(node_modules_root); + fs.mkdirSync(node_modules_root + 'bundles'); + + var aotFiles = [ + 'hero-profile.component.html', + 'hero-profile.component.css', + + 'hero-profile.component.d.ts', + 'hero-profile.module.d.ts', + 'hero.d.ts', + 'index.d.ts', + + 'hero-profile.component.js', + 'hero-profile.module.js', + 'hero.js', + 'index.js', + + 'index.metadata.json', + 'hero-profile.module.metadata.json', + 'hero-profile.component.metadata.json', + + 'package.json' + ] + + aotFiles.map(function(f) { + var path = f.split('/'); + var release = node_modules_root + path[path.length-1]; + fs.createReadStream('./public/docs/_examples/cb-third-party-lib/hero-profile/' + f).pipe(fs.createWriteStream(release)); + }); + + return inlineResources('./public/docs/_examples/cb-third-party-lib/hero-profile/hero-profile.component.*').then(function(){ + + // Create umd bundle + var spawnRollup = require( 'child_process' ).spawnSync; + var rollup = spawnRollup('./public/docs/_examples/node_modules/.bin/rollup', ['-c', './public/docs/_examples/cb-third-party-lib/hero-profile/rollup-config.js']); + + var umd = './public/docs/_examples/cb-third-party-lib/hero-profile/bundles/hero-profile.umd.js'; + fs.createReadStream(umd).pipe(fs.createWriteStream(node_modules_root + 'bundles/hero-profile.umd.js')); + + }); +} + +module.exports = publish; diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/rollup-config.js b/public/docs/_examples/cb-third-party-lib/hero-profile/rollup-config.js new file mode 100644 index 0000000000..2e02133362 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/rollup-config.js @@ -0,0 +1,10 @@ +// #docregion +export default { + entry: './public/docs/_examples/cb-third-party-lib/hero-profile/index.js', + dest: './public/docs/_examples/cb-third-party-lib/hero-profile/bundles/hero-profile.umd.js', + format: 'umd', + moduleName: 'ng.heroProfile', + globals: { + '@angular/core': 'ng.core' + } +} \ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/hero-profile/tsconfig-aot.json b/public/docs/_examples/cb-third-party-lib/hero-profile/tsconfig-aot.json new file mode 100644 index 0000000000..5e3cca7517 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/hero-profile/tsconfig-aot.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "es2015", + "moduleResolution": "node", + "sourceMap": true, + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noImplicitAny": true, + "typeRoots": [ + "../../node_modules/@types/" + ] + }, + "files": [ + "index.ts" + ], + "angularCompilerOptions": { + "genDir": "aot", + "skipMetadataEmit" : false + } +} \ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/ts/app-aot/app.component.ts b/public/docs/_examples/cb-third-party-lib/ts/app-aot/app.component.ts new file mode 100644 index 0000000000..6fce027213 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/app-aot/app.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +import { Hero } from 'hero-profile'; + +@Component({ + selector: 'my-aot-app', + template: `
+

Library consumed by AoT application

+ +
` +}) +export class AppComponent { + hero = new Hero('Bombasto', 'Bombastic at times'); +} diff --git a/public/docs/_examples/cb-third-party-lib/ts/app-aot/app.module.ts b/public/docs/_examples/cb-third-party-lib/ts/app-aot/app.module.ts new file mode 100644 index 0000000000..bf8970d57c --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/app-aot/app.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { HeroProfileModule } from 'hero-profile'; + +@NgModule({ + imports: [HeroProfileModule, BrowserModule], + declarations: [AppComponent], + bootstrap: [AppComponent] +}) +export class AppModule { +} diff --git a/public/docs/_examples/cb-third-party-lib/ts/app-aot/main-aot.ts b/public/docs/_examples/cb-third-party-lib/ts/app-aot/main-aot.ts new file mode 100644 index 0000000000..ada092b67b --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/app-aot/main-aot.ts @@ -0,0 +1,3 @@ +import { platformBrowser } from '@angular/platform-browser'; +import { AppModuleNgFactory } from '../aot/app-aot/app.module.ngfactory'; +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/public/docs/_examples/cb-third-party-lib/ts/app/app.component.ts b/public/docs/_examples/cb-third-party-lib/ts/app/app.component.ts new file mode 100644 index 0000000000..3a7dadfe65 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/app/app.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +import { Hero } from 'hero-profile'; + +@Component({ + selector: 'my-jit-app', + template: `
+

Library consumed by JiT application

+ +
` +}) +export class AppComponent { + hero = new Hero('Magneta', 'Brave as they come'); +} diff --git a/public/docs/_examples/cb-third-party-lib/ts/app/app.module.ts b/public/docs/_examples/cb-third-party-lib/ts/app/app.module.ts new file mode 100644 index 0000000000..bf8970d57c --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/app/app.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { HeroProfileModule } from 'hero-profile'; + +@NgModule({ + imports: [HeroProfileModule, BrowserModule], + declarations: [AppComponent], + bootstrap: [AppComponent] +}) +export class AppModule { +} diff --git a/public/docs/_examples/cb-third-party-lib/ts/app/main.ts b/public/docs/_examples/cb-third-party-lib/ts/app/main.ts new file mode 100644 index 0000000000..4acf5de663 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/app/main.ts @@ -0,0 +1,5 @@ +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/cb-third-party-lib/ts/example-config.json b/public/docs/_examples/cb-third-party-lib/ts/example-config.json new file mode 100644 index 0000000000..85a9a43ad0 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/example-config.json @@ -0,0 +1,3 @@ +{ + "build": "build:aot:jit" +} \ No newline at end of file diff --git a/public/docs/_examples/cb-third-party-lib/ts/index.html b/public/docs/_examples/cb-third-party-lib/ts/index.html new file mode 100644 index 0000000000..52aaa60641 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/index.html @@ -0,0 +1,32 @@ + + + + + + Third Party Lib + + + + + + + + + + + + + + + + + Loading app... + + Loading app... + + + + + diff --git a/public/docs/_examples/cb-third-party-lib/ts/rollup-config.js b/public/docs/_examples/cb-third-party-lib/ts/rollup-config.js new file mode 100644 index 0000000000..603d33408f --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/rollup-config.js @@ -0,0 +1,23 @@ +// #docregion +import rollup from 'rollup' +import nodeResolve from 'rollup-plugin-node-resolve' +import commonjs from 'rollup-plugin-commonjs'; +import uglify from 'rollup-plugin-uglify' + +//paths are relative to the execution path +export default { + entry: 'app-aot/main-aot.js', + dest: 'dist/build.js', // output a single application bundle + sourceMap: true, + sourceMapFile: 'dist/build.js.map', + format: 'iife', + plugins: [ + // #docregion nodeResolve + nodeResolve({jsnext: true, module: true}), + // #enddocregion nodeResolve + commonjs({ + include: ['node_modules/rxjs/**'] + }), + uglify() + ] +} diff --git a/public/docs/_examples/cb-third-party-lib/ts/tsconfig-aot.json b/public/docs/_examples/cb-third-party-lib/ts/tsconfig-aot.json new file mode 100644 index 0000000000..85ab089d23 --- /dev/null +++ b/public/docs/_examples/cb-third-party-lib/ts/tsconfig-aot.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "es2015", + "moduleResolution": "node", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "removeComments": false, + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true, + "typeRoots": [ + "../../node_modules/@types/" + ] + }, + + "files": [ + "app-aot/app.module.ts", + "app-aot/main-aot.ts" + ], + + "angularCompilerOptions": { + "genDir": "aot", + "skipMetadataEmit" : true + } +} diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index 7a45a91e77..9063c851a4 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -56,6 +56,11 @@ "intro": "Setting the document or window title using the Title service." }, + "third-party-lib": { + "title": "Third Party Library", + "intro": "Create a third party library with support for AoT, JiT and Tree Shaking" + }, + "ts-to-js": { "title": "TypeScript to JavaScript", "intro": "Convert Angular TypeScript examples into ES5 JavaScript" diff --git a/public/docs/ts/latest/cookbook/third-party-lib.jade b/public/docs/ts/latest/cookbook/third-party-lib.jade new file mode 100644 index 0000000000..68200ca119 --- /dev/null +++ b/public/docs/ts/latest/cookbook/third-party-lib.jade @@ -0,0 +1,247 @@ +include ../_util-fns + +:marked + Traditionally, third party JavaScript libraries have been published in the form of a single JavaScript file. + Consumers of the library have then included the library, "as is", somewhere on the page using a `script` tag. + + Modern web development has changed this process. Instead of publishing a "one size fits all" bundle, developers want to only include the parts of the library they actually need. + + This cookbook shows how to publish a third party library in a way that makes it possible to take advantage of techniques like Ahead of Time Compilation (AoT) and Tree Shaking. + + +:marked + ## Table of contents + + [Creating a Third Party Library](#third-party-lib) + + [Supporting AoT](#aot) + + [Preparing the library for Tree Shaking](#tree-shaking) + + [Supporting JiT](#jit) + + [Publish](#publish) + + [Integrate with Application](#integrate-with-app) + + [Final Application](#final-app) + +.l-main-section + +:marked + ## Creating a Third Party Library + + This cookbook shows how to create a simple Hero-Profile library and publish it with support for AoT compilation and Tree Shaking. + + A version of the library intended for JiT compiled applications will also be included. + + The code for the Hero-Profile library can be found below. + + To support both AoT and JiT compilation there are two different tsconfig files. + + `tsconfig.json` for JiT compilation and `tsconfig-aot.json` for AoT compilation. + ++makeTabs( + `cb-third-party-lib/hero-profile/hero-profile.module.ts, + cb-third-party-lib/hero-profile/tsconfig-aot.json, + cb-third-party-lib/ts/tsconfig.json, + cb-third-party-lib/hero-profile/hero-profile.component.html, + cb-third-party-lib/hero-profile/hero-profile.component.ts, + cb-third-party-lib/hero-profile/hero-profile.component.css, + cb-third-party-lib/hero-profile/hero.ts, + cb-third-party-lib/hero-profile/package.json`, + null, + `hero-profile.module.ts, + tsconfig-aot.json, + tsconfig.json, + hero-profile.component.html, + hero-profile.component.ts, + hero-profile.component.css, + hero.ts, + package.json` +)(format='.') + +.l-main-section + +:marked + ## Supporting AoT + + AoT plays an important role in optimizing Angular applications. It's therefore important that third party libraries be published in a format compatible with AoT compilation. Otherwise it will not be possible to include the library in an AoT compiled application. + + Only code written in TypeScript can be AoT compiled. + + Before publishing the library must first be compiled using the `ngc` compiler. + + `ngc` extends the `tsc` compiler by adding extensions to support AoT compilation in addition to regular TypeScript compilation. + +:marked + AoT compilation outputs three files that must be included in order to be compatible with AoT. + + *Transpiled JavaScript* + + As usual the original TypeScript is transpiled to regular JavaScript. + + *Typings files* + + JavaScript has no way of representing typings. In order to preserve the original typings, `ngc` will generate .d.ts typings files. + + *Meta Data JSON files* + + `ngc` outputs a metadata.json file for every `Component` and `NgModule`. These meta data files represent the information in the original `NgModule` and `Component` decorators. + + The meta data may reference external templates or css files. These external files must be included with the library. + + ### NgFactories + + `ngc` generates a series of files with an `.ngfactory` suffix as well. These files represent the AoT compiled source, but should not be included with the published library. + + Instead the `ngc` compiler in the consuming application will generate `.ngfactory` files based on the JavaScript, Typings and meta data shipped with the library. + + ### Why not publish TypeScript? + + Why not ship TypeScript source instead? After all the library will be part of another TypeScript compilation step when the library is imported by the consuming application? + + Generally it's discouraged to ship TypeScript with third party libraries. It would require the consumer to replicate the complete build environment of the library. Not only typings, but potentially a specific version of `ngc` as well. + + Publishing plain JavaScript with typings and meta data allows the consuming application to remain agnostic of the library's build environment. + + +:marked + ## Preparing the library for Tree Shaking + + In addition to supporting AoT, the library code should also be "Tree Shakable". + + Tree Shakers work best with `ES2015` JavaScript. + + `ES2015` `import` and `export` statements make it easier to statically analyse the code to determine which modules are in use by the application. + + By setting the `module` attribute in `tsconfig-aot.json` to `es2015`, the transpiled JavaScript will use `ES2015` modules. + + The library is made up of several independent files. Bundler frameworks like `Rollup` and `Webpack` need a way to determine the entry point to the module. In this example the entry point is `index.js`, the transpiled version of the index.ts TypeScript barrel. + + The entry point to the module is configured using the `module` attribute in `package.json`. In this case `module` is defined as `"module": "index.js"`. + + +:marked + ## Supporting JiT + + AoT compiled code is the prefered format for production builds, but due to the long compilation time, it may not be practical to use AoT during development. + + To create a more flexible developer experience, a JiT compatible build of the library should be published as well. The format of the JiT bundle is `umd`, which stands for Universal Module Definition. Shipping the bundle as `umd` ensures compatibility with most common module loading formats. + + The `umd` bundle will ship as a single file containing the JavaScript and inlined versions of any external templates or css. + + The path to the `umd` file identified in package.json as `"main": "bundles/hero-profile.umd.js"` + + In `tsconfig.json` the module for the `umd` bundle is specified as `commonjs`, not `es2015`. This is done to ensure that the bundle can be executed "as is", without further transpilation or bundling. + + To generate the bundle we will be using a framework called `Rollup`. + + The JiT build assumes the following Rollup and TypeScript configuration. + ++makeTabs( + `cb-third-party-lib/hero-profile/rollup-config.js, + cb-third-party-lib/ts/tsconfig.json`, + null, + `rollup-config.js, + tsconfig.json` +)(format='.') +:marked + Generate the `umd` bundle by running `node_modules/.bin/rollup -c rollup-config.js`. + + +:marked + ## Publish + +:marked + `Rollup` outputs the `umd` bundle, but prior to bundling, all external templates or css files must be inlined. + + There are a few options for how to do inlining. This cookbook uses an approach borrowed from `Angular Material 2`. + + The idea is to create a node script that will walk the component code and replace `templateUrl` and `styleUrls` references with inlined html and css. + + Additionally the script will remove references to `module.id` since it's no longer needed after templates and css have been inlined. + + Angular ships with its own set of `umd` bundles. These bundles can be referenced by Rollup when generating the `umd` bundle. + + In this example there is a dependency on `@angular/core`. + + By listing `@angular/core` as a `global` in the configuration, `Rollup` will point to the `@angular/core` umd bundle. + + Normally third party libraries will be published to `npm`. This cookbook simulates publishing by executing all required steps in the `publish.js` script. + + `publish.js` will do the following: + + 1) AoT compile the Hero-Profile library by running `node_modules/.bin/ngc -p tsconfig-aot.json` + + 2) Inline templates and css using the `inline-resources.js` script + + 3) Create an `umd` bundle by running `node_modules/.bin/rollup -c rollup-config.js` + ++makeTabs( + `cb-third-party-lib/hero-profile/publish.js, + cb-third-party-lib/hero-profile/inline-resources.js`, + null, + `publish.js, + inline-resources.js` +)(format='.') + +.l-main-section + +:marked + ## Integrate with Application + + The library is now ready to be integrated with either AoT compiled applications or JiT compiled applications. The following sections describes how to configure both. + + For the purposes of this demo the JiT and the AoT versions are loaded using the same index.html page. + + ### AoT + + AoT compiled applications will integrate the library in the compilation of the application as a whole. + + As in the AoT compilation Cookbook `ngc` is used in combination with `Rollup` to AoT and Tree Shake the application. + + Run the command `ngc -p tsconfig-aot.json && rollup -c rollup-config.js` to execute the combined steps of AoT compilation and Tree Shaking. + + `tsconfig-aot.json` and `rollup-config.js` contain the necessary configuration. + ++makeTabs( + `cb-third-party-lib/ts/rollup-config.js, + cb-third-party-lib/ts/tsconfig-aot.json`, + null, + `rollup-config.js, + tsconfig-aot.json` +)(format='.') + +:marked + Inside `rollup-config` there is a `nodeResolve` section. This is where the `module` setting from `package.json` comes into play. + + `nodeResolve` will look for either `module` or `jsnext` in `package.json` to determine how to bundle external libraries. + ++makeExample('cb-third-party-lib/ts/rollup-config.js', 'nodeResolve', 'rollup-config.js')(format=".") + +:marked + The combined output of `ngc` and `Rollup` is a single `build.js` JavaScript file. + + `build.js` contains the entire application including all Angular dependencies and the third party Hero-Profile library. + + To run the application in AoT mode, include `build.js` as a script tag and the `my-aot-app` root level component tag in `index.html` + + ### JiT + + JiT applications load the `umd` bundle using `SystemJS`. This requires a minor tweak to `systemjs.config.js` to register the `umd` bundle with `SystemJS`. + + Simply add `'hero-profile': 'npm:hero-profile/bundles/hero-profile.umd.js'` to the map section of `systemjs.config.js`. + +.l-main-section + +:marked + ## Final Application + + If you have cloned the `Angular.io` repo, all the steps described in this cookbook can be executed by running the following command: + + `gulp add-example-boilerplate && npm run build:aot:jit && npm run lite` + + After loading both the JiT and AoT versions of the library the final application looks like this: +figure.image-display + img(src="/resources/images/cookbooks/third-party-lib/third-party-lib.png" alt="Third-Party-Library") \ No newline at end of file diff --git a/public/resources/images/cookbooks/third-party-lib/third-party-lib.png b/public/resources/images/cookbooks/third-party-lib/third-party-lib.png new file mode 100644 index 0000000000..525a27b88d Binary files /dev/null and b/public/resources/images/cookbooks/third-party-lib/third-party-lib.png differ