diff --git a/README.md b/README.md index ef4b9014c3c8..28b73f38c992 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ The generated project has dependencies that require **Node 4 or greater**. * [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) * [Bundling](#bundling) * [Running Unit Tests](#running-unit-tests) * [Running End-to-End Tests](#running-end-to-end-tests) @@ -141,6 +142,20 @@ You can also add your own env files other than `dev` and `prod` by creating a `src/app/environments/environment.{NAME}.ts` and use them by using the `--env=NAME` flag on the build/serve commands. +### Base tag handling in index.html + +You can modify base tag (``) in your index.html by using `--base-href your-url` option. It's useful when building or serving for different environments. + +```bash +# Sets base tag href to /myUrl/ in your index.html +ng build --base-href /myUrl/ +ng serve --base-href /myUrl/ + +# Does nothing +ng build --base-href +ng serve --base-href +``` + ### Bundling Builds created with the `-prod` flag via `ng build -prod` or `ng serve -prod` bundle diff --git a/addon/ng2/commands/build.ts b/addon/ng2/commands/build.ts index 935102fa8484..a3749004dd3c 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({ @@ -22,7 +23,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 }, ], run: function (commandOptions: BuildOptions) { @@ -43,7 +45,8 @@ module.exports = Command.extend({ ui: ui, outputPath: commandOptions.outputPath, target: commandOptions.target, - environment: commandOptions.environment + environment: commandOptions.environment, + baseHref: commandOptions.baseHref }) : new WebpackBuild({ cliProject: project, @@ -51,6 +54,7 @@ module.exports = Command.extend({ outputPath: commandOptions.outputPath, target: commandOptions.target, environment: commandOptions.environment, + baseHref: commandOptions.baseHref }); return buildTask.run(commandOptions); diff --git a/addon/ng2/commands/serve.ts b/addon/ng2/commands/serve.ts index 7e048aff2a39..7ea94b23e3f9 100644 --- a/addon/ng2/commands/serve.ts +++ b/addon/ng2/commands/serve.ts @@ -28,6 +28,7 @@ export interface ServeTaskOptions { ssl?: boolean; sslKey?: string; sslCert?: string; + baseHref?: string; } module.exports = Command.extend({ @@ -51,7 +52,8 @@ module.exports = Command.extend({ { name: 'output-path', type: 'Path', default: 'dist/', aliases: ['op', 'out'] }, { name: 'ssl', type: Boolean, default: false }, { name: 'ssl-key', type: String, default: 'ssl/server.key' }, - { name: 'ssl-cert', type: String, default: 'ssl/server.crt' } + { name: 'ssl-cert', type: String, default: 'ssl/server.crt' }, + { name: 'base-href', type: String, default: null }, ], run: function(commandOptions: ServeTaskOptions) { @@ -85,6 +87,7 @@ module.exports = Command.extend({ ui: this.ui, analytics: this.analytics, project: this.project, + baseHref: commandOptions.baseHref }); return serve.run(commandOptions); diff --git a/addon/ng2/models/webpack-build-common.ts b/addon/ng2/models/webpack-build-common.ts index 4871410f9be7..4765e0e78b1a 100644 --- a/addon/ng2/models/webpack-build-common.ts +++ b/addon/ng2/models/webpack-build-common.ts @@ -4,8 +4,9 @@ import * as HtmlWebpackPlugin from 'html-webpack-plugin'; import * as webpack from 'webpack'; import { ForkCheckerPlugin } from 'awesome-typescript-loader'; import { CliConfig } from './config'; +import { BaseHrefWebpackPlugin } from '../utilities/base-href-webpack-plugin'; -export function getWebpackCommonConfig(projectRoot: string, sourceDir: string) { +export function getWebpackCommonConfig(projectRoot: string, sourceDir: string, baseHref: string) { return { devtool: 'inline-source-map', resolve: { @@ -64,6 +65,9 @@ export function getWebpackCommonConfig(projectRoot: string, sourceDir: string) { template: path.resolve(projectRoot, `./${sourceDir}/index.html`), chunksSortMode: 'dependency' }), + new BaseHrefWebpackPlugin({ + baseHref: baseHref + }), new webpack.optimize.CommonsChunkPlugin({ name: ['polyfills'] }), diff --git a/addon/ng2/models/webpack-config.ts b/addon/ng2/models/webpack-config.ts index a215217c0afa..8aa148b8dd83 100644 --- a/addon/ng2/models/webpack-config.ts +++ b/addon/ng2/models/webpack-config.ts @@ -23,12 +23,12 @@ export class NgCliWebpackConfig { private webpackMobileConfigPartial: any; private webpackMobileProdConfigPartial: any; - constructor(public ngCliProject: any, public target: string, public environment: string) { + constructor(public ngCliProject: any, public target: string, public environment: string, public baseHref: string) { const sourceDir = CliConfig.fromProject().defaults.sourceDir; const environmentPath = `./${sourceDir}/app/environments/environment.${environment}.ts`; - this.webpackBaseConfig = getWebpackCommonConfig(this.ngCliProject.root, sourceDir); + this.webpackBaseConfig = getWebpackCommonConfig(this.ngCliProject.root, sourceDir, baseHref); this.webpackDevConfigPartial = getWebpackDevConfigPartial(this.ngCliProject.root, sourceDir); this.webpackProdConfigPartial = getWebpackProdConfigPartial(this.ngCliProject.root, sourceDir); diff --git a/addon/ng2/tasks/build-webpack-watch.ts b/addon/ng2/tasks/build-webpack-watch.ts index 7e6514c6513f..e0144395d80b 100644 --- a/addon/ng2/tasks/build-webpack-watch.ts +++ b/addon/ng2/tasks/build-webpack-watch.ts @@ -16,7 +16,7 @@ module.exports = Task.extend({ rimraf.sync(path.resolve(project.root, runTaskOptions.outputPath)); - const config = new NgCliWebpackConfig(project, runTaskOptions.target, runTaskOptions.environment).config; + const config = new NgCliWebpackConfig(project, runTaskOptions.target, runTaskOptions.environment, runTaskOptions.baseHref).config; const webpackCompiler = webpack(config); webpackCompiler.apply(new ProgressPlugin({ diff --git a/addon/ng2/tasks/build-webpack.ts b/addon/ng2/tasks/build-webpack.ts index 8d98d76a6812..aef91f480d31 100644 --- a/addon/ng2/tasks/build-webpack.ts +++ b/addon/ng2/tasks/build-webpack.ts @@ -11,12 +11,12 @@ let lastHash: any = null; module.exports = Task.extend({ // Options: String outputPath - run: function(runTaskOptions: ServeTaskOptions) { + run: function (runTaskOptions: ServeTaskOptions) { var project = this.cliProject; rimraf.sync(path.resolve(project.root, runTaskOptions.outputPath)); - var config = new NgCliWebpackConfig(project, runTaskOptions.target, runTaskOptions.environment).config; + var config = new NgCliWebpackConfig(project, runTaskOptions.target, runTaskOptions.environment, runTaskOptions.baseHref).config; const webpackCompiler = webpack(config); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); diff --git a/addon/ng2/tasks/serve-webpack.ts b/addon/ng2/tasks/serve-webpack.ts index 60589e48c011..a798e4773fc6 100644 --- a/addon/ng2/tasks/serve-webpack.ts +++ b/addon/ng2/tasks/serve-webpack.ts @@ -15,7 +15,7 @@ module.exports = Task.extend({ let lastHash = null; let webpackCompiler: any; - var config: NgCliWebpackConfig = new NgCliWebpackConfig(this.project, commandOptions.target, commandOptions.environment).config; + var config: NgCliWebpackConfig = new NgCliWebpackConfig(this.project, commandOptions.target, commandOptions.environment, commandOptions.baseHref).config; // This allows for live reload of page when changes are made to repo. // https://webpack.github.io/docs/webpack-dev-server.html#inline-mode config.entry.main.unshift(`webpack-dev-server/client?http://${commandOptions.host}:${commandOptions.port}/`); 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 1aaa5159f3b7..a4cb903d3e75 100644 --- a/tests/e2e/e2e_workflow.spec.js +++ b/tests/e2e/e2e_workflow.spec.js @@ -125,6 +125,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(/