diff --git a/.travis.yml b/.travis.yml index dc3f4e01dfe6..9e50dedf9fb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,9 +43,12 @@ env: - MODE=browserstack_required - MODE=saucelabs_optional - MODE=browserstack_optional + - MODE=plunker matrix: + fast_finish: true allow_failures: + - env: "MODE=plunker" - env: "MODE=saucelabs_optional" - env: "MODE=browserstack_optional" diff --git a/package.json b/package.json index 08895eb3d8b9..fad8865388e4 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,13 @@ "scripts": { "ci:forbidden-identifiers": "node ./scripts/ci/forbidden-identifiers.js", "build": "ng build", + "build:production": "ng build -prod", "demo-app": "ng serve", "test": "karma start test/karma.conf.js", "tslint": "tslint -c tslint.json 'src/**/*.ts'", "stylelint": "stylelint 'src/**/*.scss' --config stylelint-config.json --syntax scss", "check-circular-deps": "madge --circular ./dist", + "update-plunker": "node ./scripts/ci/create-plunker-bundle.js", "typings": "typings install --global", "postinstall": "npm run typings", "e2e": "protractor", diff --git a/scripts/ci/build-and-test.sh b/scripts/ci/build-and-test.sh index 969cba5a68ed..eb523220c3d3 100755 --- a/scripts/ci/build-and-test.sh +++ b/scripts/ci/build-and-test.sh @@ -45,6 +45,11 @@ elif is_extract_metadata; then # Run `tsc` first so that the directory structure in dist/ matches what ngc expects. ./node_modules/.bin/tsc -p ./src/demo-app/ ./node_modules/.bin/ngc -p ./src/demo-app/ +elif is_plunker; then + # Only update the plunker when the firebase access token is available. + if [ "$MATERIAL2_PLNKR_TOKEN" = "false" ]; then + npm run update-plunker + fi else # Unit tests npm run build diff --git a/scripts/ci/create-plunker-bundle.js b/scripts/ci/create-plunker-bundle.js new file mode 100644 index 000000000000..a1ab5b3ca41a --- /dev/null +++ b/scripts/ci/create-plunker-bundle.js @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +'use strict'; + +/* + * This script creates a bundle of all components and publishes it to Firebase. + * The bundle will be used to load a Plunker Demo of Angular Material 2. + */ + +const globSync = require('glob').sync; +const spawnSync = require('child_process').spawnSync; +const execSync = require('child_process').execSync; +const firebase = require('firebase-tools'); +const path = require('path'); +const fse = require('fs-extra'); +const inlineResources = require('../../tools/inline-resources-tools'); + +const ROOT = path.join(__dirname, '..', '..'); +const DIST_ROOT = path.join(ROOT, 'dist'); +const DEPLOY_ROOT = path.join(DIST_ROOT, 'plunker-deploy/'); + +const mainFile = path.join(DIST_ROOT, 'main.js'); +const latestTag = getLatestTag(); +const isRelease = getShaFromTag(latestTag) === getLatestSha(); +const baseName = isRelease ? latestTag : 'HEAD'; + +// Remove the distribution folder. +fse.removeSync(DIST_ROOT); + +if (!buildProject()) { + console.error('An error occurred while building the project.'); + process.exit(1); +} + +// Inline the resources into the bundle file. +inlineBundle(); + +// Create distribution folder. +fse.mkdirp(DEPLOY_ROOT); + +// Copy the bundle to the deploy folder. +fse.copySync(mainFile, path.join(DEPLOY_ROOT, `${baseName}_bundle.js`)); + +firebase.deploy({ + firebase: 'material2-plnkr', + token: process.env.MATERIAL2_PLNKR_TOKEN, + public: 'dist/plunker-deploy' +}).then(() => { + console.log('Firebase: Successfully deployed bundle to firebase.'); + process.exit(0); +}).catch(err => { + console.error('Firebase: An error occurred while deploying to firebase.'); + console.error(err); + process.exit(1); +}); + +function inlineBundle() { + let filePathFn = (sourceFile) => { + let sourceFiles = globSync(`**/${sourceFile}`, { cwd: DIST_ROOT }); + return path.resolve(DIST_ROOT, sourceFiles[0]); + }; + + executeInline(inlineResources.inlineStyle); + executeInline(inlineResources.inlineTemplate); + + function executeInline(inlineFn) { + fse.writeFileSync(mainFile, inlineFn(filePathFn, fse.readFileSync(mainFile).toString())); + } +} + +function buildProject() { + // Note: We can't use spawnSync here, because on some environments the Angular CLI + // is not added to the System Paths and is only available in the locals. + let out = execSync('npm run build:production').toString(); + + return out.indexOf('successfully') !== -1; +} + +function getLatestTag() { + let tagSHA = spawnSync('git', ['rev-list', '--tags', '--max-count=1']).stdout.toString().trim(); + return spawnSync('git', ['describe', '--tags', tagSHA]).stdout.toString().trim(); +} + +function getLatestSha() { + return spawnSync('git', ['rev-parse', 'master']) +} + +function getShaFromTag(tag) { + return spawnSync('git', ['rev-list', '-1', tag]).stdout.toString().trim(); +} \ No newline at end of file diff --git a/scripts/ci/sources/mode.sh b/scripts/ci/sources/mode.sh index a6903ccf3d7f..6410f8ed4540 100644 --- a/scripts/ci/sources/mode.sh +++ b/scripts/ci/sources/mode.sh @@ -16,3 +16,7 @@ is_circular_deps_check() { is_extract_metadata() { [[ "$MODE" = extract_metadata ]] } + +is_plunker() { + [[ "$MODE" = plunker ]] +} \ No newline at end of file diff --git a/scripts/release/inline-resources.js b/scripts/release/inline-resources.js index 42cfd23f8ca1..04aee74f9a72 100644 --- a/scripts/release/inline-resources.js +++ b/scripts/release/inline-resources.js @@ -1,9 +1,10 @@ #!/usr/bin/env node 'use strict'; -const fs = require('fs'); +const fs = require('fs-extra'); const path = require('path'); const glob = require('glob'); +const inlineResources = require('../../tools/inline-resources-tools'); /** * Simple Promiseify function that takes a Node API and return a version that supports promises. @@ -28,7 +29,6 @@ function promiseify(fn) { const readFile = promiseify(fs.readFile); const writeFile = promiseify(fs.writeFile); - /** * For every argument, inline the templates and styles under it and write the new file. */ @@ -44,66 +44,13 @@ for (let arg of process.argv.slice(2)) { // Generate all files content with inlined templates. files.forEach(filePath => { readFile(filePath, 'utf-8') - .then(content => inlineTemplate(filePath, content)) - .then(content => inlineStyle(filePath, content)) - .then(content => removeModuleIds(content)) + .then(content => inlineResources.inlineTemplate(filePath, content)) + .then(content => inlineResources.inlineStyle(filePath, content)) + .then(content => inlineResources.removeModuleIds(content)) .then(content => writeFile(filePath, content)) .catch(err => { console.error('An error occured: ', err); }); }); -} - - -/** - * 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 filePath {string} The path of the source file. - * @param content {string} The source file's content. - * @return {string} The content with all templates inlined. - */ -function inlineTemplate(filePath, content) { - return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function(m, templateUrl) { - const templateFile = path.join(path.dirname(filePath), 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 filePath {string} The path of the source file. - * @param content {string} The source file's content. - * @return {string} The content with all styles inlined. - */ -function inlineStyle(filePath, content) { - return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function(m, styleUrls) { - const urls = eval(styleUrls); - return 'styles: [' - + urls.map(styleUrl => { - const styleFile = path.join(path.dirname(filePath), styleUrl); - const styleContent = fs.readFileSync(styleFile, 'utf-8'); - const shortenedStyle = styleContent - .replace(/([\n\r]\s*)+/gm, ' ') - .replace(/"/g, '\\"'); - return `"${shortenedStyle}"`; - }) - .join(',\n') - + ']'; - }); -} -/** - * Removes the module ids of the component metadata. - * Since the templates and styles are now inlined, the module id has become unnecessary and - * can cause unexpected issues. - */ -function removeModuleIds(content) { - // Match the line feeds as well, because we want to get rid of that line. - return content.replace(/^\W+moduleId:\W+module\.id,?[\n|\r]+/gm, ''); } diff --git a/tools/inline-resources-tools.js b/tools/inline-resources-tools.js new file mode 100644 index 000000000000..7ac46cb208a4 --- /dev/null +++ b/tools/inline-resources-tools.js @@ -0,0 +1,80 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs-extra'); + +/** + * 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 filePath {string} The path of the source file. + * @param content {string} The source file's content. + * @return {string} The content with all templates inlined. + */ +function inlineTemplate(filePath, content) { + + // Transform the filePath into a function, to be able to customize the path. + let fileRootFn = typeof filePath !== 'function' ? () => filePath : filePath; + + return content.replace(/templateUrl:\s*(?:'|")(.+?\.html)(?:"|')/g, function(m, templateUrl) { + const templateFile = path.join(path.dirname(fileRootFn(templateUrl)), templateUrl); + + if (!fs.existsSync(templateFile)) { + return; + } + + 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 filePath {string} The path of the source file. + * @param content {string} The source file's content. + * @return {string} The content with all styles inlined. + */ +function inlineStyle(filePath, content) { + + // Transform the filePath into a function, to be able to customize the path. + let fileRootFn = typeof filePath !== 'function' ? () => filePath : filePath; + + return content.replace(/styleUrls:\s*(\[[\s\S]*?])/gm, function(m, styleUrls) { + const urls = eval(styleUrls); + + let inlineStyles = urls + .map(styleUrl => path.join(path.dirname(fileRootFn(styleUrl)), styleUrl)) + .filter(styleUrl => fs.existsSync(styleUrl)) + .map(styleFile => { + const styleContent = fs.readFileSync(styleFile, 'utf-8'); + const shortenedStyle = styleContent + .replace(/([\n\r]\s*)+/gm, ' ') + .replace(/"/g, '\\"'); + + return `"${shortenedStyle}"`; + }); + + return 'styles: [' + inlineStyles.join(',\n') + ']'; + }); +} + +/** + * Removes the module ids of the component metadata. + * Since the templates and styles are now inlined, the module id has become unnecessary and + * can cause unexpected issues. + */ +function removeModuleIds(content) { + // Match the line feeds as well, because we want to get rid of that line. + return content.replace(/^\W+moduleId:\W+module\.id,?[\n|\r]+/gm, ''); +} + +module.exports = { + inlineStyle: inlineStyle, + inlineTemplate: inlineTemplate, + removeModuleIds: removeModuleIds +}; \ No newline at end of file