diff --git a/aio/package.json b/aio/package.json
index d96b867730..54a5b54aee 100644
--- a/aio/package.json
+++ b/aio/package.json
@@ -24,7 +24,8 @@
"pre~~deploy": "yarn build",
"~~deploy": "firebase deploy --message \"Commit: $TRAVIS_COMMIT\" --non-interactive --token \"$FIREBASE_TOKEN\"",
"boilerplate:add": "node ./tools/examples/add-example-boilerplate add",
- "boilerplate:remove": "node ./tools/examples/add-example-boilerplate remove"
+ "boilerplate:remove": "node ./tools/examples/add-example-boilerplate remove",
+ "generate-plunkers": "node ./tools/plunker-builder/generatePlunkers"
},
"private": true,
"dependencies": {
@@ -62,6 +63,7 @@
"html": "^1.0.0",
"jasmine-core": "~2.5.2",
"jasmine-spec-reporter": "~3.2.0",
+ "jsdom": "^9.12.0",
"karma": "~1.4.1",
"karma-chrome-launcher": "~2.0.0",
"karma-cli": "~1.0.1",
diff --git a/aio/tools/examples/add-example-boilerplate.js b/aio/tools/examples/add-example-boilerplate.js
index 09dafcbdce..263071415f 100644
--- a/aio/tools/examples/add-example-boilerplate.js
+++ b/aio/tools/examples/add-example-boilerplate.js
@@ -12,6 +12,7 @@ const EXAMPLES_TESTING_PATH = path.join(EXAMPLES_PATH, 'testing');
const files = {
exampleBoilerplate: [
'src/styles.css',
+ 'src/systemjs-angular-loader.js',
'src/systemjs.config.js',
'src/tsconfig.json',
'bs-config.json',
diff --git a/aio/tools/plunker-builder/builder.js b/aio/tools/plunker-builder/builder.js
new file mode 100644
index 0000000000..59e246024a
--- /dev/null
+++ b/aio/tools/plunker-builder/builder.js
@@ -0,0 +1,309 @@
+'use strict';
+
+// Canonical path provides a consistent path (i.e. always forward slashes) across different OSes
+var path = require('canonical-path');
+var Q = require('q');
+var _ = require('lodash');
+var jsdom = require("jsdom");
+var fs = require("fs-extra");
+var globby = require('globby');
+
+var fileTranslator = require('./translator/fileTranslator');
+var indexHtmlRules = require('./translator/rules/indexHtml');
+var systemjsConfigExtrasRules = require('./translator/rules/systemjsConfigExtras');
+var regionExtractor = require('../../transforms/examples-package/services/region-parser');
+
+class PlunkerBuilder {
+ constructor(basePath, destPath, options) {
+ this.basePath = basePath;
+ this.destPath = destPath;
+ this.options = options;
+ this.boilerplate = path.join(__dirname, '../examples/shared/boilerplate');
+ this.copyrights = {};
+
+ this._buildCopyrightStrings();
+ }
+
+ buildPlunkers() {
+ this._getPlunkerFiles();
+ var errFn = this.options.errFn || function(e) { console.log(e); };
+ var plunkerPaths = path.join(this.basePath, '**/*plnkr.json');
+ var fileNames = globby.sync(plunkerPaths, { ignore: ['**/node_modules/**'] });
+ fileNames.forEach((configFileName) => {
+ try {
+ this._buildPlunkerFrom(configFileName);
+ } catch (e) {
+ errFn(e);
+ }
+ });
+ }
+
+ _addPlunkerFiles(postData) {
+ this.options.addField(postData, 'systemjs.config.js', this.systemjsConfig);
+ this.options.addField(postData, 'systemjs-angular-loader.js', this.systemjsModulePlugin);
+ }
+
+ _buildCopyrightStrings() {
+ var copyright = 'Copyright 2017 Google Inc. All Rights Reserved.\n'
+ + 'Use of this source code is governed by an MIT-style license that\n'
+ + 'can be found in the LICENSE file at http://angular.io/license';
+ var pad = '\n\n';
+ this.copyrights.jsCss = `${pad}/*\n${copyright}\n*/`;
+ this.copyrights.html = `${pad}`;
+ }
+
+ // Build plunker from JSON configuration file (e.g., plnkr.json):
+ // all properties are optional
+ // files: string[] - array of globs - defaults to all js, ts, html, json, css and md files (with certain files removed)
+ // description: string - description of this plunker - defaults to the title in the index.html page.
+ // tags: string[] - optional array of plunker tags (for searchability)
+ // main: string - name of file that will become index.html in the plunker - defaults to index.html
+ // open: string - name of file to display within the plunker as in "open": "app/app.module.ts"
+ _buildPlunkerFrom(configFileName) {
+ // replace ending 'plnkr.json' with 'plnkr.no-link.html' to create output file name;
+ var outputFileName = `${this.options.plunkerFileName}.no-link.html`;
+ outputFileName = configFileName.replace(/plnkr\.json$/, outputFileName);
+ var altFileName;
+ if (this.destPath && this.destPath.length > 0) {
+ var partPath = path.dirname(path.relative(this.basePath, outputFileName));
+ var altFileName = path.join(this.destPath, partPath, path.basename(outputFileName)).replace('.no-link.', '.');
+ }
+ try {
+ var config = this._initConfigAndCollectFileNames(configFileName);
+ var postData = this._createPostData(config);
+ this._addPlunkerFiles(postData);
+ var html = this._createPlunkerHtml(config, postData);
+ if (this.options.writeNoLink) {
+ fs.writeFileSync(outputFileName, html, 'utf-8');
+ }
+ if (altFileName) {
+ var altDirName = path.dirname(altFileName);
+ fs.ensureDirSync(altDirName);
+ fs.writeFileSync(altFileName, html, 'utf-8');
+ }
+ } catch (e) {
+ // if we fail delete the outputFile if it exists because it is an old one.
+ if (this._existsSync(outputFileName)) {
+ fs.unlinkSync(outputFileName);
+ }
+ if (altFileName && this._existsSync(altFileName)) {
+ fs.unlinkSync(altFileName);
+ }
+ throw e;
+ }
+ }
+
+ _createBasePlunkerHtml(config, embedded) {
+ var open = '';
+
+ if (config.open) {
+ open = embedded ? `&show=${config.open}` : `&open=${config.open}`;
+ }
+ var action = `${this.options.url}${open}`;
+ var html = '
';
+ html += `'
+ html += '';
+ html += '';
+ return html;
+ }
+
+ _createPostData(config) {
+ var postData = {};
+ config.fileNames.forEach((fileName) => {
+ var content;
+ var extn = path.extname(fileName);
+ if (extn == '.png') {
+ content = this._encodeBase64(fileName);
+ fileName = fileName.substr(0, fileName.length - 4) + '.base64.png'
+ } else if (-1 < fileName.indexOf('systemjs.config.extras')) {
+ content = this._getSystemjsConfigExtras(config);
+ } else {
+ content = fs.readFileSync(fileName, 'utf-8');
+ }
+
+ if (extn == '.js' || extn == '.ts' || extn == '.css') {
+ content = content + this.copyrights.jsCss;
+ } else if (extn == '.html') {
+ content = content + this.copyrights.html;
+ }
+ // var escapedValue = escapeHtml(content);
+
+ var relativeFileName = path.relative(config.basePath, fileName);
+
+ if (relativeFileName == config.main) {
+ relativeFileName = 'index.html';
+ }
+
+ if (relativeFileName == 'index.html') {
+ content = fileTranslator.translate(content, indexHtmlRules);
+ if (config.description == null) {
+ // set config.description to title from index.html
+ var matches = /(.*)<\/title>/.exec(content);
+ if (matches) {
+ config.description = matches[1];
+ }
+ }
+ }
+
+ if (relativeFileName == 'systemjs.config.extras.js') {
+ content = fileTranslator.translate(content, systemjsConfigExtrasRules);
+ }
+
+ content = regionExtractor()(content, extn.substr(1)).contents;
+
+ this.options.addField(postData, relativeFileName, content);
+ });
+
+ var tags = ['angular', 'example'].concat(config.tags || []);
+ tags.forEach(function(tag,ix) {
+ postData['tags[' + ix + ']'] = tag;
+ });
+
+ if (!this.options.embedded) {
+ postData.private = true;
+
+ postData.description = "Angular Example - " + config.description;
+ } else {
+ postData.title = "Angular Example - " + config.description;
+ }
+
+ // Embedded needs to add more content, so if the callback is available, we call it
+ if (this.options.extraData) {
+ this.options.extraData(postData, config);
+ }
+ return postData;
+ }
+
+ _createPlunkerHtml(config, postData) {
+ var baseHtml = this._createBasePlunkerHtml(config, this.options.embedded);
+ var doc = jsdom.jsdom(baseHtml);
+ var form = doc.querySelector('form');
+ _.forEach(postData, (value, key) => {
+ var ele = this._htmlToElement(doc, '');
+ ele.setAttribute('value', value);
+ form.appendChild(ele)
+ });
+ var html = doc.documentElement.outerHTML;
+
+ return html;
+ }
+
+ _encodeBase64(file) {
+ // read binary data
+ var bitmap = fs.readFileSync(file);
+ // convert binary data to base64 encoded string
+ return Buffer(bitmap).toString('base64');
+ }
+
+ _existsSync(filename) {
+ try {
+ fs.accessSync(filename);
+ return true;
+ } catch(ex) {
+ return false;
+ }
+ }
+
+ _getPlunkerFiles() {
+ var systemJsModulePlugin = '/src/systemjs-angular-loader.js';
+ var systemJsConfigPath = '/src/systemjs.config.web.js';
+ if (this.options.build) {
+ systemJsConfigPath = '/src/systemjs.config.web.build.js';
+ }
+ this.systemjsConfig = fs.readFileSync(this.boilerplate + systemJsConfigPath, 'utf-8');
+ this.systemjsModulePlugin = fs.readFileSync(this.boilerplate + systemJsModulePlugin, 'utf-8');
+
+ // Copyright already added to web versions of systemjs.config
+ // this.systemjsConfig += this.copyrights.jsCss;
+ }
+
+ // Try to replace `systemjs.config.extras.js` with the
+ // `systemjs.config.extras.web.js` web version that
+ // should default SystemJS barrels to `.ts` files rather than `.js` files
+ // Example: see docs `testing`.
+ // HACK-O-MATIC!
+ _getSystemjsConfigExtras(config) {
+ var extras = config.basePath + '/systemjs.config.extras.js';
+ var webExtras = config.basePath + '/systemjs.config.extras.web.js';
+ if (this._existsSync(webExtras)) {
+ // console.log('** Substituted "' + webExtras + '" for "' + extras + '".');
+ return fs.readFileSync(webExtras, 'utf-8');
+ } else if (this._existsSync(extras)){
+ console.log('** WARNING: no "' + webExtras + '" replacement for "' + extras + '".');
+ return fs.readFileSync(extras, 'utf-8');
+ } else {
+ console.log('** WARNING: no "' + extras + '" file; returning empty content.');
+ return '';
+ }
+ }
+
+ _htmlToElement(document, html) {
+ var div = document.createElement('div');
+ div.innerHTML = html;
+ return div.firstChild;
+ }
+
+ _initConfigAndCollectFileNames(configFileName) {
+ var configDir = path.dirname(configFileName);
+ var configSrc = fs.readFileSync(configFileName, 'utf-8');
+ try {
+ var config = (configSrc && configSrc.trim().length) ? JSON.parse(configSrc) : {};
+ config.basePath = config.basePath ? path.resolve(configDir, config.basePath) : configDir;
+ } catch (e) {
+ throw new Error(`Plunker config - unable to parse json file: ${configFileName}\n${e}`);
+ }
+
+ var defaultIncludes = ['**/*.ts', '**/*.js', '**/*.css', '**/*.html', '**/*.md', '**/*.json', '**/*.png'];
+ if (config.files) {
+ if (config.files.length > 0) {
+ if (config.files[0].substr(0, 1) == '!') {
+ config.files = defaultIncludes.concat(config.files);
+ }
+ }
+ } else {
+ config.files = defaultIncludes;
+ }
+ var includeSpec = false;
+ var gpaths = config.files.map(function(fileName) {
+ fileName = fileName.trim();
+ if (fileName.substr(0,1) == '!') {
+ return "!" + path.join(config.basePath, fileName.substr(1));
+ } else {
+ includeSpec = includeSpec || /\.spec\.(ts|js)$/.test(fileName);
+ return path.join(config.basePath, fileName);
+ }
+ });
+
+ var defaultExcludes = [
+ '!**/tsconfig.json',
+ '!**/*plnkr.*',
+ '!**/package.json',
+ '!**/example-config.json',
+ '!**/tslint.json',
+ '!**/.editorconfig',
+ '!**/systemjs.config.js',
+ '!**/wallaby.js',
+ '!**/karma-test-shim.js',
+ '!**/karma.conf.js',
+ // AoT related files
+ '!**/aot/**/*.*',
+ '!**/*-aot.*'
+ ];
+
+ // exclude all specs if no spec is mentioned in `files[]`
+ if (!includeSpec) {
+ defaultExcludes.push('!**/*.spec.*','!**/spec.js');
+ }
+
+ gpaths.push(...defaultExcludes);
+
+ config.fileNames = globby.sync(gpaths, { ignore: ["**/node_modules/**"] });
+
+ return config;
+ }
+}
+
+module.exports = PlunkerBuilder;
diff --git a/aio/tools/plunker-builder/embeddedPlunker.js b/aio/tools/plunker-builder/embeddedPlunker.js
new file mode 100644
index 0000000000..f1819e7f84
--- /dev/null
+++ b/aio/tools/plunker-builder/embeddedPlunker.js
@@ -0,0 +1,34 @@
+var PlunkerBuilder = require('./builder');
+
+function buildPlunkers(basePath, destPath, options = {}) {
+ configureBuilder(options);
+ var builder = new PlunkerBuilder(basePath, destPath, options);
+ builder.buildPlunkers();
+}
+
+function configureBuilder(options) {
+ options.addField = addField;
+ options.plunkerFileName = 'eplnkr';
+ options.url = 'https://embed.plnkr.co?show=preview';
+ options.writeNoLink = false;
+ options.embedded = true;
+ options.extraData = extraData;
+}
+
+function extraData(postData, config) {
+ postData['source[type]'] = config.description || 'Angular example';
+ postData['source[url]'] = 'https://angular.io'
+}
+
+function addField(postData, name, content) {
+ var encoding = 'utf8';
+ if (name.split('.').pop() === 'png') {
+ encoding = 'base64';
+ }
+ postData[`entries[${name}][content]`] = content;
+ postData[`entries[${name}][encoding]`] = encoding;
+}
+
+module.exports = {
+ buildPlunkers: buildPlunkers
+};
diff --git a/aio/tools/plunker-builder/generatePlunkers.js b/aio/tools/plunker-builder/generatePlunkers.js
new file mode 100644
index 0000000000..b48dd51615
--- /dev/null
+++ b/aio/tools/plunker-builder/generatePlunkers.js
@@ -0,0 +1,9 @@
+const path = require('path');
+const regularPlunker = require('./regularPlunker');
+const embeddedPlunker = require('./embeddedPlunker');
+
+const EXAMPLES_PATH = path.join(__dirname, '../../content/examples');
+const LIVE_EXAMPLES_PATH = path.join(__dirname, '../../src/content/live-examples');
+
+regularPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH);
+embeddedPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH);
diff --git a/aio/tools/plunker-builder/regularPlunker.js b/aio/tools/plunker-builder/regularPlunker.js
new file mode 100644
index 0000000000..6b7ad1b308
--- /dev/null
+++ b/aio/tools/plunker-builder/regularPlunker.js
@@ -0,0 +1,23 @@
+var PlunkerBuilder = require('./builder');
+
+function buildPlunkers(basePath, destPath, options = {}) {
+ configureBuilder(options);
+ var builder = new PlunkerBuilder(basePath, destPath, options);
+ builder.buildPlunkers();
+}
+
+function configureBuilder(options) {
+ options.addField = addField;
+ options.plunkerFileName = 'plnkr';
+ options.url = 'http://plnkr.co/edit/?p=preview';
+ options.writeNoLink = true;
+ options.embedded = false;
+}
+
+function addField(postData, name, content) {
+ postData[`files[${name}]`] = content;
+}
+
+module.exports = {
+ buildPlunkers: buildPlunkers
+};
diff --git a/aio/tools/plunker-builder/translator/fileTranslator.js b/aio/tools/plunker-builder/translator/fileTranslator.js
new file mode 100644
index 0000000000..8a97ad2c53
--- /dev/null
+++ b/aio/tools/plunker-builder/translator/fileTranslator.js
@@ -0,0 +1,47 @@
+// var first_time = true; // DIAGNOSTIC
+
+function translate(html, rulesFile) {
+ rulesFile.rulesToApply.forEach(function(rxDatum) {
+ var rxRule = rulesFile.rules[rxDatum.pattern];
+ // rxFrom is a rexexp
+ var rxFrom = rxRule.from;
+ if (rxDatum.from) {
+ var from = rxDatum.from.replace('/', '\/');
+ var rxTemp = rxFrom.toString();
+ rxTemp = rxTemp.replace('%tag%', from);
+ rxFrom = rxFromString(rxTemp);
+ }
+ // rxTo is a string
+ var rxTo = rxRule.to;
+ if (rxDatum.to) {
+ var to = rxDatum.to;
+ to = Array.isArray(to) ? to : [to];
+ to = to.map(function (toItem) {
+ return rxTo.replace("%tag%", toItem);
+ });
+ rxTo = to.join("\n ");
+ }
+
+ /* DIAGNOSTIC
+ if (first_time && rxDatum.pattern === 'zone_pkg') {
+ first_time = false;
+
+ console.log('zone_pkg');
+ console.log(' rxFrom: '+rxFrom);
+ console.log(' rxTo: '+rxTo);
+ console.log(' replace: ' + html.replace(rxFrom, rxTo ));
+ }
+ */
+ html = html.replace(rxFrom, rxTo);
+ });
+
+ return html;
+}
+
+function rxFromString(rxString) {
+ var rx = /^\/(.*)\/(.*)/;
+ var pieces = rx.exec(rxString);
+ return RegExp(pieces[1], pieces[2]);
+}
+
+module.exports = {translate: translate};
diff --git a/aio/tools/plunker-builder/translator/rules/indexHtml.js b/aio/tools/plunker-builder/translator/rules/indexHtml.js
new file mode 100644
index 0000000000..68263885d1
--- /dev/null
+++ b/aio/tools/plunker-builder/translator/rules/indexHtml.js
@@ -0,0 +1,115 @@
+var rules = {
+ basehref: {
+ from: //,
+ to: ''
+ },
+ angular_pkg: {
+ from: /src=".?node_modules\/@angular/g,
+ to: 'src="https://unpkg.com/@angular'
+ },
+ script: {
+ from: /.*<\/script>/,
+ to: ''
+ },
+ link: {
+ from: '//',
+ to: ''
+ },
+ // Clear script like this:
+ //
+ system_strip_import_app: {
+ from: /