diff --git a/packages/angular_devkit/build_angular/builders.json b/packages/angular_devkit/build_angular/builders.json index 9ad029858017..835bc088185f 100644 --- a/packages/angular_devkit/build_angular/builders.json +++ b/packages/angular_devkit/build_angular/builders.json @@ -14,6 +14,7 @@ }, "dev-server": { "class": "./src/dev-server", + "implementation": "./src/dev-server/index2", "schema": "./src/dev-server/schema.json", "description": "Serve a browser app." }, diff --git a/packages/angular_devkit/build_angular/src/dev-server/index2.ts b/packages/angular_devkit/build_angular/src/dev-server/index2.ts new file mode 100644 index 000000000000..a838882f9f8b --- /dev/null +++ b/packages/angular_devkit/build_angular/src/dev-server/index2.ts @@ -0,0 +1,487 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + BuilderContext, + createBuilder, + targetFromTargetString, +} from '@angular-devkit/architect/src/index2'; +import { + DevServerBuildOutput, + runWebpackDevServer, +} from '@angular-devkit/build-webpack/src/webpack-dev-server/index2'; +import { WebpackLoggingCallback } from '@angular-devkit/build-webpack/src/webpack/index2'; +import { experimental, json, logging, tags } from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { existsSync, readFileSync } from 'fs'; +import * as path from 'path'; +import { Observable, from } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import * as url from 'url'; +import * as webpack from 'webpack'; +import * as WebpackDevServer from 'webpack-dev-server'; +import { checkPort } from '../angular-cli-files/utilities/check-port'; +import { + BrowserConfigTransformFn, + buildBrowserWebpackConfigFromContext, + createBrowserLoggingCallback, +} from '../browser/index2'; +import { Schema as BrowserBuilderSchema } from '../browser/schema'; +import { normalizeOptimization } from '../utils'; +import { Schema } from './schema'; +const opn = require('opn'); + +export type DevServerBuilderSchema = Schema & json.JsonObject; + +export const devServerBuildOverriddenKeys: (keyof DevServerBuilderSchema)[] = [ + 'watch', + 'optimization', + 'aot', + 'sourceMap', + 'vendorSourceMap', + 'evalSourceMap', + 'vendorChunk', + 'commonChunk', + 'baseHref', + 'progress', + 'poll', + 'verbose', + 'deployUrl', +]; + + +export type DevServerBuilderOutput = DevServerBuildOutput & { + baseUrl: string; +}; + +export type ServerConfigTransformFn = ( + workspace: experimental.workspace.Workspace, + config: WebpackDevServer.Configuration, +) => Observable; + +/** + * Reusable implementation of the build angular webpack dev server builder. + * @param options Dev Server options. + * @param context The build context. + * @param transforms A map of transforms that can be used to hook into some logic (such as + * transforming webpack configuration before passing it to webpack). + */ +export function serveWebpackBrowser( + options: DevServerBuilderSchema, + context: BuilderContext, + transforms: { + browserConfig?: BrowserConfigTransformFn, + serverConfig?: ServerConfigTransformFn, + logging?: WebpackLoggingCallback, + } = {}, +): Observable { + const browserTarget = targetFromTargetString(options.browserTarget); + const root = context.workspaceRoot; + let first = true; + let opnAddress: string; + const host = new NodeJsSyncHost(); + + const loggingFn = transforms.logging + || createBrowserLoggingCallback(!!options.verbose, context.logger); + + async function setup(): Promise<{ + browserOptions: json.JsonObject & BrowserBuilderSchema, + webpackConfig: webpack.Configuration, + webpackDevServerConfig: WebpackDevServer.Configuration, + port: number, + }> { + // Get the browser configuration from the target name. + const rawBrowserOptions = await context.getTargetOptions(browserTarget); + + // Override options we need to override, if defined. + const overrides = (Object.keys(options) as (keyof DevServerBuilderSchema)[]) + .filter(key => options[key] !== undefined && devServerBuildOverriddenKeys.includes(key)) + .reduce>((previous, key) => ({ + ...previous, + [key]: options[key], + }), {}); + + const browserName = await context.getBuilderNameForTarget(browserTarget); + const browserOptions = await context.validateOptions( + { ...rawBrowserOptions, ...overrides }, + browserName, + ); + + const webpackConfigResult = await buildBrowserWebpackConfigFromContext( + browserOptions, + context, + host, + ); + let webpackConfig = webpackConfigResult.config; + const workspace = webpackConfigResult.workspace; + + if (transforms.browserConfig) { + webpackConfig = await transforms.browserConfig(workspace, webpackConfig).toPromise(); + } + + const port = await checkPort(options.port || 0, options.host || 'localhost', 4200); + let webpackDevServerConfig = buildServerConfig( + root, + options, + browserOptions, + context.logger, + ); + + if (transforms.serverConfig) { + webpackDevServerConfig = await transforms.serverConfig(workspace, webpackConfig).toPromise(); + } + + return { browserOptions, webpackConfig, webpackDevServerConfig, port }; + } + + return from(setup()).pipe( + switchMap(({ browserOptions, webpackConfig, webpackDevServerConfig, port }) => { + options.port = port; + + // Resolve public host and client address. + let clientAddress = `${options.ssl ? 'https' : 'http'}://0.0.0.0:0`; + if (options.publicHost) { + let publicHost = options.publicHost; + if (!/^\w+:\/\//.test(publicHost)) { + publicHost = `${options.ssl ? 'https' : 'http'}://${publicHost}`; + } + const clientUrl = url.parse(publicHost); + options.publicHost = clientUrl.host; + clientAddress = url.format(clientUrl); + } + + // Resolve serve address. + const serverAddress = url.format({ + protocol: options.ssl ? 'https' : 'http', + hostname: options.host === '0.0.0.0' ? 'localhost' : options.host, + // Port cannot be undefined here since we have a step that sets it back in options above. + // tslint:disable-next-line:no-non-null-assertion + port: options.port !.toString(), + }); + + // Add live reload config. + if (options.liveReload) { + _addLiveReload(options, browserOptions, webpackConfig, clientAddress, context.logger); + } else if (options.hmr) { + context.logger.warn('Live reload is disabled. HMR option ignored.'); + } + + if (!options.watch) { + // There's no option to turn off file watching in webpack-dev-server, but + // we can override the file watcher instead. + webpackConfig.plugins = [...(webpackConfig.plugins || []), { + // tslint:disable-next-line:no-any + apply: (compiler: any) => { + compiler.hooks.afterEnvironment.tap('angular-cli', () => { + compiler.watchFileSystem = { watch: () => { } }; + }); + }, + }]; + } + + const normalizedOptimization = normalizeOptimization(browserOptions.optimization); + if (normalizedOptimization.scripts || normalizedOptimization.styles) { + context.logger.error(tags.stripIndents` + **************************************************************************************** + This is a simple server for use in testing or debugging Angular applications locally. + It hasn't been reviewed for security issues. + + DON'T USE IT FOR PRODUCTION! + **************************************************************************************** + `); + } + + context.logger.info(tags.oneLine` + ** + Angular Live Development Server is listening on ${options.host}:${options.port}, + open your browser on ${serverAddress}${webpackDevServerConfig.publicPath} + ** + `); + + opnAddress = serverAddress + webpackDevServerConfig.publicPath; + webpackConfig.devServer = webpackDevServerConfig; + + return runWebpackDevServer(webpackConfig, context, { logging: loggingFn }); + }), + map(buildEvent => { + if (first && options.open) { + first = false; + opn(opnAddress); + } + + return { ...buildEvent, baseUrl: opnAddress } as DevServerBuilderOutput; + }), + ); +} + + +/** + * Create a webpack configuration for the dev server. + * @param workspaceRoot The root of the workspace. This comes from the context. + * @param serverOptions DevServer options, based on the dev server input schema. + * @param browserOptions Browser builder options. See the browser builder from this package. + * @param logger A generic logger to use for showing warnings. + * @returns A webpack dev-server configuration. + */ +export function buildServerConfig( + workspaceRoot: string, + serverOptions: DevServerBuilderSchema, + browserOptions: BrowserBuilderSchema, + logger: logging.LoggerApi, +): WebpackDevServer.Configuration { + if (serverOptions.host) { + // Check that the host is either localhost or prints out a message. + if (!/^127\.\d+\.\d+\.\d+/g.test(serverOptions.host) && serverOptions.host !== 'localhost') { + logger.warn(tags.stripIndent` + WARNING: This is a simple server for use in testing or debugging Angular applications + locally. It hasn't been reviewed for security issues. + + Binding this server to an open connection can result in compromising your application or + computer. Using a different host than the one passed to the "--host" flag might result in + websocket connection issues. You might need to use "--disableHostCheck" if that's the + case. + `); + } + } + if (serverOptions.disableHostCheck) { + logger.warn(tags.oneLine` + WARNING: Running a server with --disable-host-check is a security risk. + See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a + for more information. + `); + } + + const servePath = buildServePath(serverOptions, browserOptions, logger); + const { styles, scripts } = normalizeOptimization(browserOptions.optimization); + + const config: WebpackDevServer.Configuration = { + host: serverOptions.host, + port: serverOptions.port, + headers: { 'Access-Control-Allow-Origin': '*' }, + historyApiFallback: { + index: `${servePath}/${path.basename(browserOptions.index)}`, + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + } as WebpackDevServer.HistoryApiFallbackConfig, + stats: false, + compress: styles || scripts, + watchOptions: { + poll: browserOptions.poll, + }, + https: serverOptions.ssl, + overlay: { + errors: !(styles || scripts), + warnings: false, + }, + public: serverOptions.publicHost, + disableHostCheck: serverOptions.disableHostCheck, + publicPath: servePath, + hot: serverOptions.hmr, + contentBase: false, + }; + + if (serverOptions.ssl) { + _addSslConfig(workspaceRoot, serverOptions, config); + } + + if (serverOptions.proxyConfig) { + _addProxyConfig(workspaceRoot, serverOptions, config); + } + + return config; +} + +/** + * Resolve and build a URL _path_ that will be the root of the server. This resolved base href and + * deploy URL from the browser options and returns a path from the root. + * @param serverOptions The server options that were passed to the server builder. + * @param browserOptions The browser options that were passed to the browser builder. + * @param logger A generic logger to use for showing warnings. + */ +export function buildServePath( + serverOptions: DevServerBuilderSchema, + browserOptions: BrowserBuilderSchema, + logger: logging.LoggerApi, +): string { + let servePath = serverOptions.servePath; + if (!servePath && servePath !== '') { + const defaultPath = _findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl); + const showWarning = serverOptions.servePathDefaultWarning; + if (defaultPath == null && showWarning) { + logger.warn(tags.oneLine` + WARNING: --deploy-url and/or --base-href contain unsupported values for ng serve. Default + serve path of '/' used. Use --serve-path to override. + `); + } + servePath = defaultPath || ''; + } + if (servePath.endsWith('/')) { + servePath = servePath.substr(0, servePath.length - 1); + } + if (!servePath.startsWith('/')) { + servePath = `/${servePath}`; + } + + return servePath; +} + +/** + * Private method to enhance a webpack config with live reload configuration. + * @private + */ +function _addLiveReload( + options: DevServerBuilderSchema, + browserOptions: BrowserBuilderSchema, + webpackConfig: webpack.Configuration, + clientAddress: string, + logger: logging.LoggerApi, +) { + if (webpackConfig.plugins === undefined) { + webpackConfig.plugins = []; + } + + // This allows for live reload of page when changes are made to repo. + // https://webpack.js.org/configuration/dev-server/#devserver-inline + let webpackDevServerPath; + try { + webpackDevServerPath = require.resolve('webpack-dev-server/client'); + } catch { + throw new Error('The "webpack-dev-server" package could not be found.'); + } + const entryPoints = [`${webpackDevServerPath}?${clientAddress}`]; + if (options.hmr) { + const webpackHmrLink = 'https://webpack.js.org/guides/hot-module-replacement'; + + logger.warn( + tags.oneLine`NOTICE: Hot Module Replacement (HMR) is enabled for the dev server.`); + + const showWarning = options.hmrWarning; + if (showWarning) { + logger.info(tags.stripIndents` + The project will still live reload when HMR is enabled, + but to take advantage of HMR additional application code is required' + (not included in an Angular CLI project by default).' + See ${webpackHmrLink} + for information on working with HMR for Webpack.`, + ); + logger.warn( + tags.oneLine`To disable this warning use "hmrWarning: false" under "serve" + options in "angular.json".`, + ); + } + entryPoints.push('webpack/hot/dev-server'); + webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); + if (browserOptions.extractCss) { + logger.warn(tags.oneLine`NOTICE: (HMR) does not allow for CSS hot reload + when used together with '--extract-css'.`); + } + } + if (typeof webpackConfig.entry !== 'object' || Array.isArray(webpackConfig.entry)) { + webpackConfig.entry = {} as webpack.Entry; + } + if (!Array.isArray(webpackConfig.entry.main)) { + webpackConfig.entry.main = []; + } + webpackConfig.entry.main.unshift(...entryPoints); +} + +/** + * Private method to enhance a webpack config with SSL configuration. + * @private + */ +function _addSslConfig( + root: string, + options: DevServerBuilderSchema, + config: WebpackDevServer.Configuration, +) { + let sslKey: string | undefined = undefined; + let sslCert: string | undefined = undefined; + if (options.sslKey) { + const keyPath = path.resolve(root, options.sslKey); + if (existsSync(keyPath)) { + sslKey = readFileSync(keyPath, 'utf-8'); + } + } + if (options.sslCert) { + const certPath = path.resolve(root, options.sslCert); + if (existsSync(certPath)) { + sslCert = readFileSync(certPath, 'utf-8'); + } + } + + config.https = true; + if (sslKey != null && sslCert != null) { + config.https = { + key: sslKey, + cert: sslCert, + }; + } +} + +/** + * Private method to enhance a webpack config with Proxy configuration. + * @private + */ +function _addProxyConfig( + root: string, + options: DevServerBuilderSchema, + config: WebpackDevServer.Configuration, +) { + let proxyConfig = {}; + const proxyPath = path.resolve(root, options.proxyConfig as string); + if (existsSync(proxyPath)) { + proxyConfig = require(proxyPath); + } else { + const message = 'Proxy config file ' + proxyPath + ' does not exist.'; + throw new Error(message); + } + config.proxy = proxyConfig; +} + +/** + * Find the default server path. We don't want to expose baseHref and deployUrl as arguments, only + * the browser options where needed. This method should stay private (people who want to resolve + * baseHref and deployUrl should use the buildServePath exported function. + * @private + */ +function _findDefaultServePath(baseHref?: string, deployUrl?: string): string | null { + if (!baseHref && !deployUrl) { + return ''; + } + + if (/^(\w+:)?\/\//.test(baseHref || '') || /^(\w+:)?\/\//.test(deployUrl || '')) { + // If baseHref or deployUrl is absolute, unsupported by ng serve + return null; + } + + // normalize baseHref + // for ng serve the starting base is always `/` so a relative + // and root relative value are identical + const baseHrefParts = (baseHref || '') + .split('/') + .filter(part => part !== ''); + if (baseHref && !baseHref.endsWith('/')) { + baseHrefParts.pop(); + } + const normalizedBaseHref = baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`; + + if (deployUrl && deployUrl[0] === '/') { + if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) { + // If baseHref and deployUrl are root relative and not equivalent, unsupported by ng serve + return null; + } + + return deployUrl; + } + + // Join together baseHref and deployUrl + return `${normalizedBaseHref}${deployUrl || ''}`; +} + + +export default createBuilder(serveWebpackBrowser); diff --git a/packages/angular_devkit/build_angular/test/dev-server/deploy-url_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/deploy-url_spec_large.ts index e2137e8af356..05490741bf85 100644 --- a/packages/angular_devkit/build_angular/test/dev-server/deploy-url_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/dev-server/deploy-url_spec_large.ts @@ -5,26 +5,35 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { request, runTargetSpec } from '@angular-devkit/architect/testing'; -import { from } from 'rxjs'; -import { concatMap, take, tap } from 'rxjs/operators'; -import { Schema as DevServerBuilderOptions } from '../../src/dev-server/schema'; -import { devServerTargetSpec, host } from '../utils'; +import { Architect, BuilderRun } from '@angular-devkit/architect/src/index2'; +import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies +import { DevServerBuilderOutput } from '../../src/dev-server/index2'; +import { createArchitect, host } from '../utils'; describe('Dev Server Deploy Url', () => { - beforeEach(done => host.initialize().toPromise().then(done, done.fail)); - afterEach(done => host.restore().toPromise().then(done, done.fail)); + const target = { project: 'app', target: 'serve' }; + let architect: Architect; + let runs: BuilderRun[] = []; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + runs = []; + }); + afterEach(async () => { + await host.restore().toPromise(); + await Promise.all(runs.map(r => r.stop())); + }); - it('works', (done) => { - const overrides: Partial = { deployUrl: 'test/' }; + it('works', async () => { + const run = await architect.scheduleTarget(target, { deployUrl: 'test/' }); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('http://localhost:4200/test'); - runTargetSpec(host, devServerTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('http://localhost:4200/test/polyfills.js'))), - tap(response => expect(response).toContain('window["webpackJsonp"]')), - take(1), - ).toPromise().then(done, done.fail); + const response = await fetch(`${output.baseUrl}/polyfills.js`); + expect(await response.text()).toContain('window["webpackJsonp"]'); }, 30000); }); diff --git a/packages/angular_devkit/build_angular/test/dev-server/proxy_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/proxy_spec_large.ts index e73f93df4dd0..3b7b74e731f9 100644 --- a/packages/angular_devkit/build_angular/test/dev-server/proxy_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/dev-server/proxy_spec_large.ts @@ -5,27 +5,38 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { request, runTargetSpec } from '@angular-devkit/architect/testing'; +import { Architect, BuilderRun } from '@angular-devkit/architect/src/index2'; import * as express from 'express'; // tslint:disable-line:no-implicit-dependencies import * as http from 'http'; import { AddressInfo } from 'net'; -import { from } from 'rxjs'; -import { concatMap, take, tap } from 'rxjs/operators'; +import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies +import { DevServerBuilderOutput } from '../../src/dev-server/index2'; import { Schema as DevServerBuilderOptions } from '../../src/dev-server/schema'; -import { devServerTargetSpec, host } from '../utils'; +import { createArchitect, host } from '../utils'; + describe('Dev Server Builder proxy', () => { - beforeEach(done => host.initialize().toPromise().then(done, done.fail)); - afterEach(done => host.restore().toPromise().then(done, done.fail)); + const target = { project: 'app', target: 'serve' }; + let architect: Architect; + // We use runs like this to ensure it WILL stop the servers at the end of each tests. + let runs: BuilderRun[]; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + runs = []; + }); + afterEach(async () => { + await host.restore().toPromise(); + await Promise.all(runs.map(r => r.stop())); + }); - it('works', (done) => { + it('works', async () => { // Create an express app that serves as a proxy. const app = express(); const server = http.createServer(app); server.listen(0); - // cast is safe, the HTTP server is not using a pipe or UNIX domain socket app.set('port', (server.address() as AddressInfo).port); app.get('/api/test', function (_req, res) { res.send('TEST_API_RETURN'); @@ -40,23 +51,27 @@ describe('Dev Server Builder proxy', () => { 'proxy.config.json': `{ "/api/*": { "target": "${proxyServerUrl}" } }`, }); - const overrides: Partial = { proxyConfig: 'proxy.config.json' }; - - runTargetSpec(host, devServerTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('http://localhost:4200/api/test'))), - tap(response => { - expect(response).toContain('TEST_API_RETURN'); - server.close(); - }), - take(1), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(target, { proxyConfig: 'proxy.config.json' }); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('http://localhost:4200/'); + + const response = await fetch('http://localhost:4200/api/test'); + expect(await response.text()).toContain('TEST_API_RETURN'); + server.close(); }, 30000); - it('errors out with a missing proxy file', (done) => { + it('errors out with a missing proxy file', async () => { const overrides: Partial = { proxyConfig: '../proxy.config.json' }; + const run = await architect.scheduleTarget(target, { proxyConfig: 'INVALID.json' }); + runs.push(run); - runTargetSpec(host, devServerTargetSpec, overrides) - .subscribe(undefined, () => done(), done.fail); + try { + await run.result; + expect('THE ABOVE LINE SHOULD THROW').toBe('true'); + } catch { + // Success! + } }, 30000); }); diff --git a/packages/angular_devkit/build_angular/test/dev-server/public-host_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/public-host_spec_large.ts index 851258b26e53..342b488e1c55 100644 --- a/packages/angular_devkit/build_angular/test/dev-server/public-host_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/dev-server/public-host_spec_large.ts @@ -5,12 +5,10 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { request, runTargetSpec } from '@angular-devkit/architect/testing'; -import { from } from 'rxjs'; -import { concatMap, take, tap } from 'rxjs/operators'; -import { Schema as DevServerBuilderOptions } from '../../src/dev-server/schema'; -import { devServerTargetSpec, host } from '../utils'; +import { Architect, BuilderRun } from '@angular-devkit/architect/src/index2'; +import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies +import { DevServerBuilderOutput } from '../../src/dev-server/index2'; +import { createArchitect, host } from '../utils'; describe('Dev Server Builder public host', () => { @@ -18,37 +16,51 @@ describe('Dev Server Builder public host', () => { // check the hosts anymore when requests come from numeric IP addresses. const headers = { host: 'http://spoofy.mcspoofface' }; - beforeEach(done => host.initialize().toPromise().then(done, done.fail)); - afterEach(done => host.restore().toPromise().then(done, done.fail)); + const target = { project: 'app', target: 'serve' }; + let architect: Architect; + // We use runs like this to ensure it WILL stop the servers at the end of each tests. + let runs: BuilderRun[]; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + runs = []; + }); + afterEach(async () => { + await host.restore().toPromise(); + await Promise.all(runs.map(r => r.stop())); + }); + + it('works', async () => { + const run = await architect.scheduleTarget(target); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('http://localhost:4200/'); - it('works', (done) => { - runTargetSpec(host, devServerTargetSpec).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('http://localhost:4200/', headers))), - tap(response => expect(response).toContain('Invalid Host header')), - take(1), - ).toPromise().then(done, done.fail); + const response = await fetch(`${output.baseUrl}`, { headers }); + expect(await response.text()).toContain('Invalid Host header'); }, 30000); - it('works', (done) => { - const overrides: Partial = { publicHost: headers.host }; + it('works', async () => { + const run = await architect.scheduleTarget(target, { publicHost: headers.host }); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('http://localhost:4200/'); - runTargetSpec(host, devServerTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('http://localhost:4200/', headers))), - tap(response => expect(response).toContain('HelloWorldApp')), - take(1), - ).toPromise().then(done, done.fail); + const response = await fetch(`${output.baseUrl}`, { headers }); + expect(await response.text()).toContain('HelloWorldApp'); }, 30000); - it('works', (done) => { - const overrides: Partial = { disableHostCheck: true }; + it('works', async () => { + const run = await architect.scheduleTarget(target, { disableHostCheck: true }); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('http://localhost:4200/'); - runTargetSpec(host, devServerTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('http://localhost:4200/', headers))), - tap(response => expect(response).toContain('HelloWorldApp')), - take(1), - ).toPromise().then(done, done.fail); + const response = await fetch(`${output.baseUrl}`, { headers }); + expect(await response.text()).toContain('HelloWorldApp'); }, 30000); }); diff --git a/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts index 6bde940c3c78..0fd8c3998d24 100644 --- a/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts @@ -5,26 +5,36 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { request, runTargetSpec } from '@angular-devkit/architect/testing'; -import { from } from 'rxjs'; -import { concatMap, take, tap } from 'rxjs/operators'; -import { Schema as DevServerBuilderOptions } from '../../src/dev-server/schema'; -import { devServerTargetSpec, host } from '../utils'; +import { Architect, BuilderRun } from '@angular-devkit/architect/src/index2'; +import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies +import { DevServerBuilderOutput } from '../../src/dev-server/index2'; +import { createArchitect, host } from '../utils'; describe('Dev Server Builder serve path', () => { - beforeEach(done => host.initialize().toPromise().then(done, done.fail)); - afterEach(done => host.restore().toPromise().then(done, done.fail)); + const target = { project: 'app', target: 'serve' }; + let architect: Architect; + // We use runs like this to ensure it WILL stop the servers at the end of each tests. + let runs: BuilderRun[]; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + runs = []; + }); + afterEach(async () => { + await host.restore().toPromise(); + await Promise.all(runs.map(r => r.stop())); + }); - it('works', (done) => { - const overrides: Partial = { servePath: 'test/' }; + it('works', async () => { + const run = await architect.scheduleTarget(target, { servePath: 'test/' }); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('http://localhost:4200/test'); - runTargetSpec(host, devServerTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('http://localhost:4200/test/polyfills.js'))), - tap(response => expect(response).toContain('window["webpackJsonp"]')), - take(1), - ).toPromise().then(done, done.fail); + const response = await fetch(`${output.baseUrl}/polyfills.js`); + expect(await response.text()).toContain('window["webpackJsonp"]'); }, 30000); }); diff --git a/packages/angular_devkit/build_angular/test/dev-server/ssl_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/ssl_spec_large.ts index 26ca8657d1f2..317c1d5a6872 100644 --- a/packages/angular_devkit/build_angular/test/dev-server/ssl_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/dev-server/ssl_spec_large.ts @@ -5,31 +5,44 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { request, runTargetSpec } from '@angular-devkit/architect/testing'; +import { Architect, BuilderRun } from '@angular-devkit/architect/src/index2'; import { tags } from '@angular-devkit/core'; -import { from } from 'rxjs'; -import { concatMap, take, tap } from 'rxjs/operators'; -import { Schema as DevServerBuilderOptions } from '../../src/dev-server/schema'; -import { devServerTargetSpec, host } from '../utils'; +import * as https from 'https'; +import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies +import { DevServerBuilderOutput } from '../../src/dev-server/index2'; +import { createArchitect, host } from '../utils'; describe('Dev Server Builder ssl', () => { - beforeEach(done => host.initialize().toPromise().then(done, done.fail)); - afterEach(done => host.restore().toPromise().then(done, done.fail)); + const target = { project: 'app', target: 'serve' }; + let architect: Architect; + // We use runs like this to ensure it WILL stop the servers at the end of each tests. + let runs: BuilderRun[]; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + runs = []; + }); + afterEach(async () => { + await host.restore().toPromise(); + await Promise.all(runs.map(r => r.stop())); + }); - it('works', (done) => { - const overrides: Partial = { ssl: true }; + it('works', async () => { + const run = await architect.scheduleTarget(target, { ssl: true }); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('https://localhost:4200/'); - runTargetSpec(host, devServerTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('https://localhost:4200/index.html'))), - tap(response => expect(response).toContain('HelloWorldApp')), - take(1), - ).toPromise().then(done, done.fail); + const response = await fetch('https://localhost:4200/index.html', { + agent: new https.Agent({ rejectUnauthorized: false }), + }); + expect(await response.text()).toContain('HelloWorldApp'); }, 30000); - it('supports key and cert', (done) => { + it('supports key and cert', async () => { host.writeMultipleFiles({ 'ssl/server.key': tags.stripIndents` -----BEGIN RSA PRIVATE KEY----- @@ -87,17 +100,21 @@ describe('Dev Server Builder ssl', () => { `, }); - const overrides: Partial = { + const overrides = { ssl: true, sslKey: '../ssl/server.key', sslCert: '../ssl/server.crt', }; - runTargetSpec(host, devServerTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('https://localhost:4200/index.html'))), - tap(response => expect(response).toContain('HelloWorldApp')), - take(1), - ).toPromise().then(done, done.fail); + const run = await architect.scheduleTarget(target, overrides); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('https://localhost:4200/'); + + const response = await fetch('https://localhost:4200/index.html', { + agent: new https.Agent({ rejectUnauthorized: false }), + }); + expect(await response.text()).toContain('HelloWorldApp'); }, 30000); }); diff --git a/packages/angular_devkit/build_angular/test/dev-server/works_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/works_spec_large.ts index 9907d35cbfce..bc8019d5cb03 100644 --- a/packages/angular_devkit/build_angular/test/dev-server/works_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/dev-server/works_spec_large.ts @@ -5,44 +5,65 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { TestLogger, request, runTargetSpec } from '@angular-devkit/architect/testing'; -import { from } from 'rxjs'; -import { concatMap, take, tap } from 'rxjs/operators'; -import { Schema as DevServerBuilderOptions } from '../../src/dev-server/schema'; -import { devServerTargetSpec, host } from '../utils'; +import { Architect, BuilderRun } from '@angular-devkit/architect/src/index2'; +import { logging } from '@angular-devkit/core'; +import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies +import { DevServerBuilderOutput } from '../../src/dev-server/index2'; +import { createArchitect, host } from '../utils'; describe('Dev Server Builder', () => { - beforeEach(done => host.initialize().toPromise().then(done, done.fail)); - afterEach(done => host.restore().toPromise().then(done, done.fail)); - - it('works', (done) => { - runTargetSpec(host, devServerTargetSpec).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - concatMap(() => from(request('http://localhost:4200/index.html'))), - tap(response => expect(response).toContain('HelloWorldApp')), - take(1), - ).toPromise().then(done, done.fail); + const target = { project: 'app', target: 'serve' }; + let architect: Architect; + let runs: BuilderRun[] = []; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + runs = []; + }); + afterEach(async () => { + await host.restore().toPromise(); + await Promise.all(runs.map(r => r.stop())); + }); + + it('works', async () => { + const run = await architect.scheduleTarget(target); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(output.baseUrl).toBe('http://localhost:4200/'); + + const response = await fetch('http://localhost:4200/index.html'); + expect(await response.text()).toContain('HelloWorldApp'); }, 30000); - it('works with verbose', (done) => { - const overrides: Partial = { verbose: true }; - const logger = new TestLogger('verbose-serve'); - runTargetSpec(host, devServerTargetSpec, overrides, undefined, logger).pipe( - tap(() => expect(logger.includes('Built at')).toBe(true)), - take(1), - ).toPromise().then(done, done.fail); + it('works with verbose', async () => { + const logger = new logging.Logger('verbose-serve'); + let logs = ''; + logger.subscribe(event => logs += event.message); + + const run = await architect.scheduleTarget(target, { verbose: true }, { logger }); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + expect(logs).toContain('Built at'); }, 30000); it(`doesn't serve files on the cwd directly`, async () => { - const res = await runTargetSpec(host, devServerTargetSpec).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - // When webpack-dev-server doesn't have `contentBase: false`, this will serve the repo README. - concatMap(() => from(request('http://localhost:4200/README.md'))), - take(1), - ).toPromise(); + const run = await architect.scheduleTarget(target); + runs.push(run); + const output = await run.result as DevServerBuilderOutput; + expect(output.success).toBe(true); + + // When webpack-dev-server doesn't have `contentBase: false`, this will serve the repo README. + const response = await fetch('http://localhost:4200/README.md', { + headers: { + 'Accept': 'text/html', + }, + }); + const res = await response.text(); expect(res).not.toContain('This file is automatically generated during release.'); expect(res).toContain('HelloWorldApp'); });