diff --git a/modules/builders/BUILD.bazel b/modules/builders/BUILD.bazel index 070fc4085..c36b0eb8d 100644 --- a/modules/builders/BUILD.bazel +++ b/modules/builders/BUILD.bazel @@ -33,7 +33,9 @@ ts_library( "@npm//@angular-devkit/architect", "@npm//@angular-devkit/core", "@npm//@types/browser-sync", + "@npm//@types/http-proxy-middleware", "@npm//browser-sync", + "@npm//http-proxy-middleware", "@npm//rxjs", "@npm//tree-kill", ], diff --git a/modules/builders/package.json b/modules/builders/package.json index 12ca4ddc3..682211ca9 100644 --- a/modules/builders/package.json +++ b/modules/builders/package.json @@ -17,6 +17,7 @@ "@angular-devkit/architect": "DEVKIT_ARCHITECT_VERSION", "@angular-devkit/core": "DEVKIT_CORE_VERSION", "browser-sync": "^2.26.7", + "http-proxy-middleware": "^0.20.0", "rxjs": "RXJS_VERSION", "tree-kill": "^1.2.1" }, diff --git a/modules/builders/src/ssr-dev-server/index.ts b/modules/builders/src/ssr-dev-server/index.ts index cd7a58255..e5f95cd7a 100644 --- a/modules/builders/src/ssr-dev-server/index.ts +++ b/modules/builders/src/ssr-dev-server/index.ts @@ -37,6 +37,8 @@ import { } from 'rxjs/operators'; import * as browserSync from 'browser-sync'; import { join } from 'path'; +import * as url from 'url'; +import * as proxy from 'http-proxy-middleware'; import { getAvailablePort, spawnAsObservable, waitUntilServerIsListening } from './utils'; @@ -198,31 +200,75 @@ async function initBrowserSync( return browserSyncInstance; } - const { port, open, host } = options; - const bsPort = port || await getAvailablePort(); + const { port: browserSyncPort, open, host, publicHost } = options; + const bsPort = browserSyncPort || await getAvailablePort(); + const bsOptions: browserSync.Options = { + proxy: { + target: `localhost:${nodeServerPort}`, + proxyRes: [ + proxyRes => { + if ('headers' in proxyRes) { + proxyRes.headers['cache-control'] = undefined; + } + }, + ] + }, + host, + port: bsPort, + ui: false, + server: false, + notify: false, + ghostMode: false, + logLevel: 'silent', + open, + // Remove leading slash + scriptPath: path => path.substring(1), + }; + + const publicHostNormalized = publicHost && publicHost.endsWith('/') + ? publicHost.substring(0, publicHost.length - 1) + : publicHost; + + if (publicHostNormalized) { + const { protocol, hostname, port, pathname } = url.parse(publicHostNormalized); + const defaultSocketIoPath = '/browser-sync/socket.io'; + const defaultNamespace = '/browser-sync'; + const hasPathname = !!(pathname && pathname !== '/'); + const namespace = hasPathname ? pathname + defaultNamespace : defaultNamespace; + const path = hasPathname ? pathname + defaultSocketIoPath : defaultSocketIoPath; + + bsOptions.socket = { + namespace, + path, + domain: url.format({ + protocol, + hostname, + port, + }), + }; + + // When having a pathname we also need to create a reverse proxy because socket.io + // will be listening on: 'http://localhost:4200/ssr/browser-sync/socket.io' + // However users will typically have a reverse proxy that will redirect all matching requests + // ex: http://testinghost.com/ssr -> http://localhost:4200 which will result in a 404. + if (hasPathname) { + bsOptions.middleware = [ + proxy(defaultSocketIoPath, { + target: url.format({ + protocol: 'http', + hostname: host, + port: bsPort, + pathname: path, + }), + ws: true, + logLevel: 'silent', + }), + ]; + } + } return new Promise((resolve, reject) => { - browserSyncInstance - .init({ - proxy: { - target: `localhost:${nodeServerPort}`, - proxyRes: [ - proxyRes => { - if ('headers' in proxyRes) { - proxyRes.headers['cache-control'] = undefined; - } - }, - ] - }, - host, - port: bsPort, - ui: false, - server: false, - notify: false, - ghostMode: false, - logLevel: 'silent', - open, - }, (error, bs) => { + browserSyncInstance.init(bsOptions, (error, bs) => { if (error) { reject(error); } else { diff --git a/modules/builders/src/ssr-dev-server/schema.json b/modules/builders/src/ssr-dev-server/schema.json index 06f2618de..d84229988 100644 --- a/modules/builders/src/ssr-dev-server/schema.json +++ b/modules/builders/src/ssr-dev-server/schema.json @@ -24,6 +24,10 @@ "default": 4200, "description": "Port to start the development server at. Default is 4200. Pass 0 to get a dynamically assigned port." }, + "publicHost": { + "type": "string", + "description": "The URL that the browser client should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies." + }, "open": { "type": "boolean", "description": "Opens the url in default browser.", diff --git a/modules/builders/src/ssr-dev-server/schema.ts b/modules/builders/src/ssr-dev-server/schema.ts index 5cd1fe2b6..fa0b5ea8b 100644 --- a/modules/builders/src/ssr-dev-server/schema.ts +++ b/modules/builders/src/ssr-dev-server/schema.ts @@ -27,4 +27,10 @@ export interface Schema { /** Opens the url in default browser. */ open?: boolean; + + /** + * The URL that the browser client should use to connect to the development server. + * Use for a complex dev server setup, such as one with reverse proxies. + */ + publicHost?: string; } diff --git a/package.json b/package.json index 2f35678b8..f6ce18fc3 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,12 @@ }, "devDependencies": { "@angular-devkit/architect": "^0.900.0-rc.6", - "@angular/cli": "^9.0.0-rc.6", "@angular-devkit/build-angular": "^0.900.0-rc.6", "@angular-devkit/core": "^9.0.0-rc.6", "@angular-devkit/schematics": "^9.0.0-rc.6", "@angular/animations": "^9.0.0-rc.6", "@angular/bazel": "^9.0.0-rc.6", + "@angular/cli": "^9.0.0-rc.6", "@angular/common": "^9.0.0-rc.6", "@angular/compiler": "^9.0.0-rc.6", "@angular/compiler-cli": "^9.0.0-rc.6", @@ -65,12 +65,14 @@ "@types/fs-extra": "^8.0.0", "@types/hapi__hapi": "^18.2.5", "@types/hapi__inert": "^5.2.0", + "@types/http-proxy-middleware": "^0.19.3", "@types/jasmine": "^3.4.4", "@types/node": "^12.11.1", "@types/shelljs": "^0.8.6", "browser-sync": "^2.26.7", "domino": "^2.1.2", "express": "^4.15.2", + "http-proxy-middleware": "^0.20.0", "jasmine-core": "^3.0.0", "karma": "^4.1.0", "karma-chrome-launcher": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index be5c6c057..67bee1b7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1508,6 +1508,22 @@ dependencies: "@types/node" "*" +"@types/http-proxy-middleware@^0.19.3": + version "0.19.3" + resolved "https://registry.yarnpkg.com/@types/http-proxy-middleware/-/http-proxy-middleware-0.19.3.tgz#b2eb96fbc0f9ac7250b5d9c4c53aade049497d03" + integrity sha512-lnBTx6HCOUeIJMLbI/LaL5EmdKLhczJY5oeXZpX/cXE4rRqb3RmV7VcMpiEfYkmTjipv3h7IAyIINe4plEv7cA== + dependencies: + "@types/connect" "*" + "@types/http-proxy" "*" + "@types/node" "*" + +"@types/http-proxy@*": + version "1.17.2" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.2.tgz#3b7fb5365a00d47129967b0b2da51c2123692314" + integrity sha512-Qfb7batJJBlI8wcrd48vHpgsOOYzQQa+OZcaIz33jkJPe8A7KktAJFmRAiR42s5BfnErdlFnOyQucq2BKy/98g== + dependencies: + "@types/node" "*" + "@types/jasmine@^3.4.4": version "3.5.0" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.0.tgz#2ad2006c8a937d20df20a8fee86071d0f730ef99" @@ -2415,7 +2431,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -4732,6 +4748,16 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" +http-proxy-middleware@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.20.0.tgz#5b128f7207985c4ea91b53fab8ad897a48c690d6" + integrity sha512-dNJAk71nEJhPiAczQH9hGvE/MT9kEs+zn2Dh+Hi94PGZe1GluQirC7mw5rdREUtWx6qGS1Gu0bZd4qEAg+REgw== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.1" + lodash "^4.17.14" + micromatch "^4.0.2" + http-proxy@1.15.2: version "1.15.2" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.15.2.tgz#642fdcaffe52d3448d2bda3b0079e9409064da31" @@ -6097,6 +6123,14 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -7020,7 +7054,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.7: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7: version "2.1.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.1.1.tgz#ecdfbea7704adb5fe6fb47f9866c4c0e15e905c5" integrity sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==