From e6dc4c98e8e17cc11afd1c8e84f09eec06b26fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rokas=20Brazd=C5=BEionis?= Date: Thu, 28 Jul 2016 16:55:44 +0300 Subject: [PATCH 1/2] feat: implement --base-href argument Implement --base-href argument for build and github-pages:deploy commands Closes #1064 --- README.md | 11 ++++ addon/ng2/commands/build.ts | 4 +- addon/ng2/commands/github-pages-deploy.ts | 21 +++++++- addon/ng2/models/webpack-build-common.ts | 6 ++- addon/ng2/models/webpack-config.ts | 5 +- addon/ng2/tasks/build-webpack-watch.ts | 3 +- addon/ng2/tasks/build-webpack.ts | 5 +- .../ng2/utilities/base-href-webpack-plugin.ts | 32 ++++++++++++ .../base-href-webpack-plugin.spec.js | 50 +++++++++++++++++++ tests/e2e/e2e_workflow.spec.js | 10 ++++ 10 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 addon/ng2/utilities/base-href-webpack-plugin.ts create mode 100644 tests/acceptance/base-href-webpack-plugin.spec.js diff --git a/README.md b/README.md index 1acd5f83412c..ce0178c2d371 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ The generated project has dependencies that require **Node 4.x.x and NPM 3.x.x** * [Generating a Route](#generating-a-route) * [Creating a Build](#creating-a-build) * [Build Targets and Environment Files](#build-targets-and-environment-files) +* [Base tag handling in index.html](#base-tag-handling-in-indexhtml) * [Adding extra files to the build](#adding-extra-files-to-the-build) * [Running Unit Tests](#running-unit-tests) * [Running End-to-End Tests](#running-end-to-end-tests) @@ -152,6 +153,16 @@ You can also add your own env files other than `dev` and `prod` by doing the fol - add `{ NAME: 'src/environments/environment.NAME.ts' }` to the the `apps[0].environments` object in `angular-cli.json` - use them by using the `--env=NAME` flag on the build/serve commands. +### Base tag handling in index.html + +When building you can modify base tag (``) in your index.html with `--base-href your-url` option. + +```bash +# Sets base tag href to /myUrl/ in your index.html +ng build --base-href /myUrl/ +ng build --bh /myUrl/ +``` + ### Bundling All builds make use of bundling, and using the `--prod` flag in `ng build --prod` diff --git a/addon/ng2/commands/build.ts b/addon/ng2/commands/build.ts index 423788459318..8efc10b2c36d 100644 --- a/addon/ng2/commands/build.ts +++ b/addon/ng2/commands/build.ts @@ -9,6 +9,7 @@ interface BuildOptions { watch?: boolean; watcher?: string; supressSizes: boolean; + baseHref?: string; } module.exports = Command.extend({ @@ -27,7 +28,8 @@ module.exports = Command.extend({ { name: 'output-path', type: 'Path', default: 'dist/', aliases: ['o'] }, { name: 'watch', type: Boolean, default: false, aliases: ['w'] }, { name: 'watcher', type: String }, - { name: 'suppress-sizes', type: Boolean, default: false } + { name: 'suppress-sizes', type: Boolean, default: false }, + { name: 'base-href', type: String, default: null, aliases: ['bh'] }, ], run: function (commandOptions: BuildOptions) { diff --git a/addon/ng2/commands/github-pages-deploy.ts b/addon/ng2/commands/github-pages-deploy.ts index c0420298f83b..bd374d8aa531 100644 --- a/addon/ng2/commands/github-pages-deploy.ts +++ b/addon/ng2/commands/github-pages-deploy.ts @@ -16,6 +16,17 @@ const fsWriteFile = Promise.denodeify(fs.writeFile); const fsReadDir = Promise.denodeify(fs.readdir); const fsCopy = Promise.denodeify(fse.copy); +interface GithubPagesDeployOptions { + message?: string; + target?: string; + environment?: string; + userPage?: boolean; + skipBuild?: boolean; + ghToken?: string; + ghUsername?: string; + baseHref?: string; +} + module.exports = Command.extend({ name: 'github-pages:deploy', aliases: ['gh-pages:deploy'], @@ -61,9 +72,14 @@ module.exports = Command.extend({ type: String, default: '', description: 'Github username' + }, { + name: 'base-href', + type: String, + default: null, + aliases: ['bh'] }], - run: function(options, rawArgs) { + run: function(options: GithubPagesDeployOptions, rawArgs) { const ui = this.ui; const root = this.project.root; const execOptions = { @@ -102,7 +118,8 @@ module.exports = Command.extend({ const buildOptions = { target: options.target, environment: options.environment, - outputPath: outDir + outputPath: outDir, + baseHref: options.baseHref, }; const createGithubRepoTask = new CreateGithubRepo({ diff --git a/addon/ng2/models/webpack-build-common.ts b/addon/ng2/models/webpack-build-common.ts index 7bc30b3eef3e..31ba23372981 100644 --- a/addon/ng2/models/webpack-build-common.ts +++ b/addon/ng2/models/webpack-build-common.ts @@ -5,8 +5,9 @@ import * as webpack from 'webpack'; import * as atl from 'awesome-typescript-loader'; import { findLazyModules } from './find-lazy-modules'; +import { BaseHrefWebpackPlugin } from '../utilities/base-href-webpack-plugin'; -export function getWebpackCommonConfig(projectRoot: string, environment: string, appConfig: any) { +export function getWebpackCommonConfig(projectRoot: string, environment: string, appConfig: any, baseHref: string) { const appRoot = path.resolve(projectRoot, appConfig.root); const appMain = path.resolve(appRoot, appConfig.main); @@ -118,6 +119,9 @@ export function getWebpackCommonConfig(projectRoot: string, environment: string, template: path.resolve(appRoot, appConfig.index), chunksSortMode: 'dependency' }), + new BaseHrefWebpackPlugin({ + baseHref: baseHref + }), new webpack.NormalModuleReplacementPlugin( // This plugin is responsible for swapping the environment files. // Since it takes a RegExp as first parameter, we need to escape the path. diff --git a/addon/ng2/models/webpack-config.ts b/addon/ng2/models/webpack-config.ts index 2da873f98df5..fecbbeb258e0 100644 --- a/addon/ng2/models/webpack-config.ts +++ b/addon/ng2/models/webpack-config.ts @@ -20,14 +20,15 @@ export class NgCliWebpackConfig { public ngCliProject: any, public target: string, public environment: string, - outputDir?: string + outputDir?: string, + baseHref?: string ) { const config: CliConfig = CliConfig.fromProject(); const appConfig = config.config.apps[0]; appConfig.outDir = outputDir || appConfig.outDir; - this.baseConfig = getWebpackCommonConfig(this.ngCliProject.root, environment, appConfig); + this.baseConfig = getWebpackCommonConfig(this.ngCliProject.root, environment, appConfig, baseHref); this.devConfigPartial = getWebpackDevConfigPartial(this.ngCliProject.root, appConfig); this.prodConfigPartial = getWebpackProdConfigPartial(this.ngCliProject.root, appConfig); diff --git a/addon/ng2/tasks/build-webpack-watch.ts b/addon/ng2/tasks/build-webpack-watch.ts index af350d17e0ba..c1a7536b2223 100644 --- a/addon/ng2/tasks/build-webpack-watch.ts +++ b/addon/ng2/tasks/build-webpack-watch.ts @@ -20,7 +20,8 @@ module.exports = Task.extend({ project, runTaskOptions.target, runTaskOptions.environment, - runTaskOptions.outputPath + runTaskOptions.outputPath, + runTaskOptions.baseHref ).config; const webpackCompiler = webpack(config); diff --git a/addon/ng2/tasks/build-webpack.ts b/addon/ng2/tasks/build-webpack.ts index e0a3316a5cd2..a4df60af3a2d 100644 --- a/addon/ng2/tasks/build-webpack.ts +++ b/addon/ng2/tasks/build-webpack.ts @@ -11,7 +11,7 @@ let lastHash: any = null; module.exports = Task.extend({ // Options: String outputPath - run: function(runTaskOptions: ServeTaskOptions) { + run: function (runTaskOptions: ServeTaskOptions) { const project = this.cliProject; @@ -20,7 +20,8 @@ module.exports = Task.extend({ project, runTaskOptions.target, runTaskOptions.environment, - runTaskOptions.outputPath + runTaskOptions.outputPath, + runTaskOptions.baseHref ).config; const webpackCompiler = webpack(config); diff --git a/addon/ng2/utilities/base-href-webpack-plugin.ts b/addon/ng2/utilities/base-href-webpack-plugin.ts new file mode 100644 index 000000000000..a5f5ae1556cb --- /dev/null +++ b/addon/ng2/utilities/base-href-webpack-plugin.ts @@ -0,0 +1,32 @@ +interface BaseHrefWebpackPluginOptions { + baseHref: string; +} + +export class BaseHrefWebpackPlugin { + constructor(private options: BaseHrefWebpackPluginOptions) { } + + apply(compiler): void { + // Ignore if baseHref is not passed + if (!this.options.baseHref) { + return; + } + + compiler.plugin('compilation', (compilation) => { + compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, callback) => { + // Check if base tag already exists + const baseTagRegex = //i; + const baseTagMatches = htmlPluginData.html.match(baseTagRegex); + if (!baseTagMatches) { + // Insert it in top of the head if not exist + htmlPluginData.html = htmlPluginData.html.replace(//i, '$&' + ``); + } else { + // Replace only href attribute if exists + const modifiedBaseTag = baseTagMatches[0].replace(/href="\S+"/i, `href="${this.options.baseHref}"`); + htmlPluginData.html = htmlPluginData.html.replace(baseTagRegex, modifiedBaseTag); + } + + callback(null, htmlPluginData); + }); + }); + } +} \ No newline at end of file diff --git a/tests/acceptance/base-href-webpack-plugin.spec.js b/tests/acceptance/base-href-webpack-plugin.spec.js new file mode 100644 index 000000000000..58bf73442e25 --- /dev/null +++ b/tests/acceptance/base-href-webpack-plugin.spec.js @@ -0,0 +1,50 @@ +/*eslint-disable no-console */ +'use strict'; + +var expect = require('chai').expect; +var BaseHrefWebpackPlugin = require('../../addon/ng2/utilities/base-href-webpack-plugin').BaseHrefWebpackPlugin; + +function mockCompiler(indexHtml, callback) { + return { + plugin: function (event, compilerCallback) { + var compilation = { + plugin: function (hook, compilationCallback) { + var htmlPluginData = { + html: indexHtml + }; + compilationCallback(htmlPluginData, callback); + } + }; + compilerCallback(compilation); + } + }; +} + +describe('base href webpack plugin', function () { + it('should do nothing when baseHref is null', function () { + var plugin = new BaseHrefWebpackPlugin({ baseHref: null }); + + var compiler = mockCompiler('', function (x, htmlPluginData) { + expect(htmlPluginData.html).to.equal(''); + }); + plugin.apply(compiler); + }); + + it('should insert base tag when not exist', function () { + var plugin = new BaseHrefWebpackPlugin({ baseHref: '/' }); + + var compiler = mockCompiler('', function (x, htmlPluginData) { + expect(htmlPluginData.html).to.equal(''); + }); + plugin.apply(compiler); + }); + + it('should replace href attribute when base tag already exists', function () { + var plugin = new BaseHrefWebpackPlugin({ baseHref: '/myUrl/' }); + + var compiler = mockCompiler('', function (x, htmlPluginData) { + expect(htmlPluginData.html).to.equal(''); + }); + plugin.apply(compiler); + }); +}); diff --git a/tests/e2e/e2e_workflow.spec.js b/tests/e2e/e2e_workflow.spec.js index a92ec8cc6501..814c39f9428f 100644 --- a/tests/e2e/e2e_workflow.spec.js +++ b/tests/e2e/e2e_workflow.spec.js @@ -128,6 +128,16 @@ describe('Basic end-to-end Workflow', function () { }); }); + it('Supports base tag modifications via `ng build --base-href`', function() { + this.timeout(420000); + + sh.exec(`${ngBin} build --base-href /myUrl/`); + const indexHtmlPath = path.join(process.cwd(), 'dist/index.html'); + const indexHtml = fs.readFileSync(indexHtmlPath, { encoding: 'utf8' }); + + expect(indexHtml).to.match(/ Date: Thu, 25 Aug 2016 11:21:35 +0300 Subject: [PATCH 2/2] refactor(github-pages-deploy): reuse webpack build --base-href logic Reuse basehref handling logic from webpack build task --- addon/ng2/commands/build.ts | 2 +- addon/ng2/commands/github-pages-deploy.ts | 27 +++++++------- addon/ng2/models/webpack-build-common.ts | 7 +++- addon/ng2/models/webpack-config.ts | 7 +++- addon/ng2/tasks/build-webpack-watch.ts | 4 +- addon/ng2/tasks/build-webpack.ts | 4 +- .../ng2/utilities/base-href-webpack-plugin.ts | 37 +++++++++++-------- tests/acceptance/github-pages-deploy.spec.js | 30 ++------------- 8 files changed, 57 insertions(+), 61 deletions(-) diff --git a/addon/ng2/commands/build.ts b/addon/ng2/commands/build.ts index 8efc10b2c36d..a976372531ff 100644 --- a/addon/ng2/commands/build.ts +++ b/addon/ng2/commands/build.ts @@ -2,7 +2,7 @@ import * as Command from 'ember-cli/lib/models/command'; import * as WebpackBuild from '../tasks/build-webpack'; import * as WebpackBuildWatch from '../tasks/build-webpack-watch'; -interface BuildOptions { +export interface BuildOptions { target?: string; environment?: string; outputPath?: string; diff --git a/addon/ng2/commands/github-pages-deploy.ts b/addon/ng2/commands/github-pages-deploy.ts index bd374d8aa531..c8c3ec400038 100644 --- a/addon/ng2/commands/github-pages-deploy.ts +++ b/addon/ng2/commands/github-pages-deploy.ts @@ -11,8 +11,6 @@ import * as CreateGithubRepo from '../tasks/create-github-repo'; import { CliConfig } from '../models/config'; import { oneLine } from 'common-tags'; -const fsReadFile = Promise.denodeify(fs.readFile); -const fsWriteFile = Promise.denodeify(fs.writeFile); const fsReadDir = Promise.denodeify(fs.readdir); const fsCopy = Promise.denodeify(fse.copy); @@ -115,11 +113,19 @@ module.exports = Command.extend({ outputPath: outDir }); + /** + * BaseHref tag setting logic: + * First, use --base-href flag value if provided. + * Else if --user-page is true, then keep baseHref default as declared in index.html. + * Otherwise auto-replace with `/${projectName}/`. + */ + const baseHref = options.baseHref || (options.userPage ? null : `/${projectName}/`); + const buildOptions = { target: options.target, environment: options.environment, outputPath: outDir, - baseHref: options.baseHref, + baseHref: baseHref, }; const createGithubRepoTask = new CreateGithubRepo({ @@ -140,7 +146,7 @@ module.exports = Command.extend({ .then(createGitHubRepoIfNeeded) .then(checkoutGhPages) .then(copyFiles) - .then(updateBaseHref) + .then(createNotFoundPage) .then(addAndCommit) .then(returnStartingBranch) .then(pushToGitRepo) @@ -208,15 +214,10 @@ module.exports = Command.extend({ }))); } - function updateBaseHref() { - if (options.userPage) { return Promise.resolve(); } - let indexHtml = path.join(root, 'index.html'); - return fsReadFile(indexHtml, 'utf8') - .then((data) => data.replace(//g, ``)) - .then((data) => { - fsWriteFile(indexHtml, data, 'utf8'); - fsWriteFile(path.join(root, '404.html'), data, 'utf8'); - }); + function createNotFoundPage() { + const indexHtml = path.join(root, 'index.html'); + const notFoundPage = path.join(root, '404.html'); + return fsCopy(indexHtml, notFoundPage); } function addAndCommit() { diff --git a/addon/ng2/models/webpack-build-common.ts b/addon/ng2/models/webpack-build-common.ts index 31ba23372981..2abd5822e5b1 100644 --- a/addon/ng2/models/webpack-build-common.ts +++ b/addon/ng2/models/webpack-build-common.ts @@ -7,7 +7,12 @@ import * as atl from 'awesome-typescript-loader'; import { findLazyModules } from './find-lazy-modules'; import { BaseHrefWebpackPlugin } from '../utilities/base-href-webpack-plugin'; -export function getWebpackCommonConfig(projectRoot: string, environment: string, appConfig: any, baseHref: string) { +export function getWebpackCommonConfig( + projectRoot: string, + environment: string, + appConfig: any, + baseHref: string +) { const appRoot = path.resolve(projectRoot, appConfig.root); const appMain = path.resolve(appRoot, appConfig.main); diff --git a/addon/ng2/models/webpack-config.ts b/addon/ng2/models/webpack-config.ts index fecbbeb258e0..fd292f9aaeaa 100644 --- a/addon/ng2/models/webpack-config.ts +++ b/addon/ng2/models/webpack-config.ts @@ -28,7 +28,12 @@ export class NgCliWebpackConfig { appConfig.outDir = outputDir || appConfig.outDir; - this.baseConfig = getWebpackCommonConfig(this.ngCliProject.root, environment, appConfig, baseHref); + this.baseConfig = getWebpackCommonConfig( + this.ngCliProject.root, + environment, + appConfig, + baseHref + ); this.devConfigPartial = getWebpackDevConfigPartial(this.ngCliProject.root, appConfig); this.prodConfigPartial = getWebpackProdConfigPartial(this.ngCliProject.root, appConfig); diff --git a/addon/ng2/tasks/build-webpack-watch.ts b/addon/ng2/tasks/build-webpack-watch.ts index c1a7536b2223..c25f8a7d80a5 100644 --- a/addon/ng2/tasks/build-webpack-watch.ts +++ b/addon/ng2/tasks/build-webpack-watch.ts @@ -5,12 +5,12 @@ import * as webpack from 'webpack'; import * as ProgressPlugin from 'webpack/lib/ProgressPlugin'; import { NgCliWebpackConfig } from '../models/webpack-config'; import { webpackOutputOptions } from '../models/'; -import { ServeTaskOptions } from '../commands/serve'; +import { BuildOptions } from '../commands/build'; let lastHash: any = null; module.exports = Task.extend({ - run: function(runTaskOptions: ServeTaskOptions) { + run: function(runTaskOptions: BuildOptions) { const project = this.cliProject; diff --git a/addon/ng2/tasks/build-webpack.ts b/addon/ng2/tasks/build-webpack.ts index a4df60af3a2d..b8cf8fa83d7c 100644 --- a/addon/ng2/tasks/build-webpack.ts +++ b/addon/ng2/tasks/build-webpack.ts @@ -2,7 +2,7 @@ import * as rimraf from 'rimraf'; import * as path from 'path'; import * as Task from 'ember-cli/lib/models/task'; import * as webpack from 'webpack'; -import { ServeTaskOptions } from '../commands/serve'; +import { BuildOptions } from '../commands/build'; import { NgCliWebpackConfig } from '../models/webpack-config'; import { webpackOutputOptions } from '../models/'; @@ -11,7 +11,7 @@ let lastHash: any = null; module.exports = Task.extend({ // Options: String outputPath - run: function (runTaskOptions: ServeTaskOptions) { + run: function (runTaskOptions: BuildOptions) { const project = this.cliProject; diff --git a/addon/ng2/utilities/base-href-webpack-plugin.ts b/addon/ng2/utilities/base-href-webpack-plugin.ts index a5f5ae1556cb..8466882adc53 100644 --- a/addon/ng2/utilities/base-href-webpack-plugin.ts +++ b/addon/ng2/utilities/base-href-webpack-plugin.ts @@ -12,21 +12,28 @@ export class BaseHrefWebpackPlugin { } compiler.plugin('compilation', (compilation) => { - compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, callback) => { - // Check if base tag already exists - const baseTagRegex = //i; - const baseTagMatches = htmlPluginData.html.match(baseTagRegex); - if (!baseTagMatches) { - // Insert it in top of the head if not exist - htmlPluginData.html = htmlPluginData.html.replace(//i, '$&' + ``); - } else { - // Replace only href attribute if exists - const modifiedBaseTag = baseTagMatches[0].replace(/href="\S+"/i, `href="${this.options.baseHref}"`); - htmlPluginData.html = htmlPluginData.html.replace(baseTagRegex, modifiedBaseTag); - } + compilation.plugin( + 'html-webpack-plugin-before-html-processing', + (htmlPluginData, callback) => { + // Check if base tag already exists + const baseTagRegex = //i; + const baseTagMatches = htmlPluginData.html.match(baseTagRegex); + if (!baseTagMatches) { + // Insert it in top of the head if not exist + htmlPluginData.html = htmlPluginData.html.replace( + //i, '$&' + `` + ); + } else { + // Replace only href attribute if exists + const modifiedBaseTag = baseTagMatches[0].replace( + /href="\S+"/i, `href="${this.options.baseHref}"` + ); + htmlPluginData.html = htmlPluginData.html.replace(baseTagRegex, modifiedBaseTag); + } - callback(null, htmlPluginData); - }); + callback(null, htmlPluginData); + } + ); }); } -} \ No newline at end of file +} diff --git a/tests/acceptance/github-pages-deploy.spec.js b/tests/acceptance/github-pages-deploy.spec.js index 3098a2cf4083..28358cc40a3a 100644 --- a/tests/acceptance/github-pages-deploy.spec.js +++ b/tests/acceptance/github-pages-deploy.spec.js @@ -14,7 +14,6 @@ var https = require('https'); var SilentError = require('silent-error'); const expect = chai.expect; -const fsReadFile = Promise.denodeify(fs.readFile); const fsWriteFile = Promise.denodeify(fs.writeFile); const fsMkdir = Promise.denodeify(fs.mkdir); @@ -67,7 +66,7 @@ describe('Acceptance: ng github-pages:deploy', function() { }); }); - it('should deploy with defaults to existing remote', function() { + it('should deploy with defaults to existing remote', function () { execStub.addExecSuccess('git status --porcelain') .addExecSuccess('git rev-parse --abbrev-ref HEAD', initialBranch) .addExecSuccess('git remote -v', remote) @@ -78,12 +77,7 @@ describe('Acceptance: ng github-pages:deploy', function() { .addExecSuccess(`git push origin ${ghPagesBranch}:${ghPagesBranch}`) .addExecSuccess('git remote -v', remote); - return ng(['github-pages:deploy', '--skip-build']) - .then(() => { - let indexHtml = path.join(process.cwd(), 'index.html'); - return fsReadFile(indexHtml, 'utf8'); - }) - .then((data) => expect(data.search(``)).to.not.equal(-1)); + return ng(['github-pages:deploy', '--skip-build']); }); it('should deploy with changed defaults', function() { @@ -100,13 +94,7 @@ describe('Acceptance: ng github-pages:deploy', function() { .addExecSuccess(`git push origin ${ghPagesBranch}:${userPageBranch}`) .addExecSuccess('git remote -v', remote); - return ng(['github-pages:deploy', '--skip-build', `--message=${message}`, - '--user-page']) - .then(() => { - let indexHtml = path.join(process.cwd(), 'index.html'); - return fsReadFile(indexHtml, 'utf8'); - }) - .then((data) => expect(data.search('')).to.not.equal(-1)); + return ng(['github-pages:deploy', '--skip-build', `--message=${message}`, '--user-page']); }); it('should create branch if needed', function() { @@ -125,12 +113,7 @@ describe('Acceptance: ng github-pages:deploy', function() { .addExecSuccess(`git push origin ${ghPagesBranch}:${ghPagesBranch}`) .addExecSuccess('git remote -v', remote); - return ng(['github-pages:deploy', '--skip-build']) - .then(() => { - let indexHtml = path.join(process.cwd(), 'index.html'); - return fsReadFile(indexHtml, 'utf8'); - }) - .then((data) => expect(data.search(``)).to.not.equal(-1)); + return ng(['github-pages:deploy', '--skip-build']); }); it('should create repo if needed', function() { @@ -183,11 +166,6 @@ describe('Acceptance: ng github-pages:deploy', function() { return ng(['github-pages:deploy', '--skip-build', `--gh-token=${token}`, `--gh-username=${username}`]) - .then(() => { - let indexHtml = path.join(process.cwd(), 'index.html'); - return fsReadFile(indexHtml, 'utf8'); - }) - .then((data) => expect(data.search(``)).to.not.equal(-1)) .then(() => httpsStub.restore()); });