feat(react): migrate @nrwl/web:dev-server to devkit (#4682)

This commit is contained in:
Jason Jean 2021-02-03 16:33:51 -05:00 committed by GitHub
parent a941961bd4
commit 34781a11c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 334 additions and 188 deletions

View File

@ -14,7 +14,7 @@ export {
} from '@nrwl/tao/src/shared/nx'; } from '@nrwl/tao/src/shared/nx';
export { logger } from '@nrwl/tao/src/shared/logger'; export { logger } from '@nrwl/tao/src/shared/logger';
export { getPackageManagerCommand } from '@nrwl/tao/src/shared/package-manager'; export { getPackageManagerCommand } from '@nrwl/tao/src/shared/package-manager';
export { runExecutor } from '@nrwl/tao/src/commands/run'; export { runExecutor, Target } from '@nrwl/tao/src/commands/run';
export { formatFiles } from './src/generators/format-files'; export { formatFiles } from './src/generators/format-files';
export { generateFiles } from './src/generators/generate-files'; export { generateFiles } from './src/generators/generate-files';
@ -30,6 +30,9 @@ export {
export { toJS } from './src/generators/to-js'; export { toJS } from './src/generators/to-js';
export { visitNotIgnoredFiles } from './src/generators/visit-not-ignored-files'; export { visitNotIgnoredFiles } from './src/generators/visit-not-ignored-files';
export { parseTargetString } from './src/executors/parse-target-string';
export { readTargetOptions } from './src/executors/read-target-options';
export { readJson, writeJson, updateJson } from './src/utils/json'; export { readJson, writeJson, updateJson } from './src/utils/json';
export { addDependenciesToPackageJson } from './src/utils/package-json'; export { addDependenciesToPackageJson } from './src/utils/package-json';
export { installPackagesTask } from './src/tasks/install-packages-task'; export { installPackagesTask } from './src/tasks/install-packages-task';

View File

@ -0,0 +1,11 @@
export function parseTargetString(targetString: string) {
const [project, target, configuration] = targetString.split(':');
if (!project || !target) {
throw new Error(`Invalid Target String: ${targetString}`);
}
return {
project,
target,
configuration,
};
}

View File

@ -0,0 +1,29 @@
import { Target } from '@nrwl/tao/src/commands/run';
import { ExecutorContext, Workspaces } from '@nrwl/tao/src/shared/workspace';
import { combineOptionsForExecutor } from '@nrwl/tao/src/shared/params';
export function readTargetOptions<T = any>(
{ project, target, configuration }: Target,
context: ExecutorContext
): T {
const projectConfiguration = context.workspace.projects[project];
const targetConfiguration = projectConfiguration.targets[target];
const ws = new Workspaces(context.root);
const [nodeModule, executorName] = targetConfiguration.executor.split(':');
const { schema } = ws.readExecutor(nodeModule, executorName);
const defaultProject = ws.calculateDefaultProjectName(
context.cwd,
context.workspace
);
return combineOptionsForExecutor(
{},
configuration,
targetConfiguration,
schema,
defaultProject,
ws.relativeCwd(context.cwd)
) as T;
}

View File

@ -19,10 +19,13 @@ import * as chalk from 'chalk';
import { logger } from '../shared/logger'; import { logger } from '../shared/logger';
import { eachValueFrom } from 'rxjs-for-await'; import { eachValueFrom } from 'rxjs-for-await';
export interface RunOptions { export interface Target {
project: string; project: string;
target: string; target: string;
configuration: string; configuration?: string;
}
export interface RunOptions extends Target {
help: boolean; help: boolean;
runOptions: Options; runOptions: Options;
} }

View File

@ -12,7 +12,7 @@
"description": "Package a library" "description": "Package a library"
}, },
"dev-server": { "dev-server": {
"implementation": "./src/builders/dev-server/dev-server.impl", "implementation": "./src/builders/dev-server/compat",
"schema": "./src/builders/dev-server/schema.json", "schema": "./src/builders/dev-server/schema.json",
"description": "Serve a web application" "description": "Serve a web application"
}, },

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import devServerExecutor from './dev-server.impl';
export default convertNxExecutor(devServerExecutor);

View File

@ -1,37 +1,57 @@
import { MockBuilderContext } from '@nrwl/workspace/testing';
import { getMockContext } from '../../utils/testing';
import { EMPTY } from 'rxjs';
import * as normalizeUtils from '../../utils/normalize'; import * as normalizeUtils from '../../utils/normalize';
import { WebBuildBuilderOptions } from '../build/build.impl'; import { WebBuildBuilderOptions } from '../build/build.impl';
import { run, WebDevServerOptions } from './dev-server.impl'; import webDevServerImpl, { WebDevServerOptions } from './dev-server.impl';
jest.mock('@angular-devkit/build-webpack', () => ({ jest.mock('@nrwl/devkit');
runWebpackDevServer: () => EMPTY, import { readTargetOptions, ExecutorContext } from '@nrwl/devkit';
}));
jest.mock('../../utils/devserver.config', () => ({ jest.mock('../../utils/devserver.config', () => ({
getDevServerConfig: jest.fn().mockReturnValue({}), getDevServerConfig: jest.fn().mockReturnValue({}),
})); }));
describe('Web Server Builder', () => { describe('Web Server Builder', () => {
let context: MockBuilderContext; let context: ExecutorContext;
let options: WebDevServerOptions; let options: WebDevServerOptions;
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks(); jest.clearAllMocks();
context = await getMockContext(); context = {
context.getProjectMetadata = jest root: '/root',
.fn() cwd: '/root',
.mockReturnValue({ sourceRoot: '/root/app/src' }); projectName: 'proj',
targetName: 'serve',
context.getTargetOptions = jest.fn().mockReturnValue({}); workspace: {
version: 2,
projects: {
proj: {
root: 'proj',
sourceRoot: 'proj/src',
targets: {
serve: {
executor: '@nrwl/web:dev-server',
options: {
buildTarget: 'proj:build',
},
},
build: {
executor: 'build',
options: {},
},
},
},
},
},
isVerbose: false,
};
options = { options = {
buildTarget: 'app:build', buildTarget: 'proj:build',
port: 4200, port: 4200,
} as WebDevServerOptions; } as WebDevServerOptions;
(readTargetOptions as any).mockImplementation(() => {});
jest jest
.spyOn(normalizeUtils, 'normalizeWebBuildOptions') .spyOn(normalizeUtils, 'normalizeWebBuildOptions')
.mockReturnValue({} as WebBuildBuilderOptions); .mockReturnValue({} as WebBuildBuilderOptions);
@ -39,14 +59,14 @@ describe('Web Server Builder', () => {
it('should pass `baseHref` to build', async () => { it('should pass `baseHref` to build', async () => {
const baseHref = '/my-domain'; const baseHref = '/my-domain';
await run({ ...options, baseHref }, context).toPromise(); await webDevServerImpl({ ...options, baseHref }, context);
expect(normalizeUtils.normalizeWebBuildOptions).toHaveBeenCalledWith( expect(normalizeUtils.normalizeWebBuildOptions).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
baseHref, baseHref,
}), }),
'/root', '/root',
'/root/app/src' 'proj/src'
); );
}); });
}); });

View File

@ -1,27 +1,19 @@
import { import {
BuilderContext, ExecutorContext,
createBuilder, parseTargetString,
targetFromTargetString, readTargetOptions,
} from '@angular-devkit/architect'; } from '@nrwl/devkit';
import { JsonObject } from '@angular-devkit/core';
import { Observable, from, forkJoin } from 'rxjs';
import { normalizeWebBuildOptions } from '../../utils/normalize';
import { map, switchMap } from 'rxjs/operators';
import { WebBuildBuilderOptions } from '../build/build.impl';
import { Configuration } from 'webpack'; import { Configuration } from 'webpack';
import * as opn from 'opn';
import * as url from 'url';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { getDevServerConfig } from '../../utils/devserver.config';
import { buildServePath } from '../../utils/serve-path';
import { getSourceRoot } from '../../utils/source-root';
import {
runWebpackDevServer,
DevServerBuildOutput,
} from '@angular-devkit/build-webpack';
export interface WebDevServerOptions extends JsonObject { import { eachValueFrom } from 'rxjs-for-await';
import { map, tap } from 'rxjs/operators';
import { runWebpackDevServer } from '@nrwl/workspace/src/utilities/run-webpack';
import { normalizeWebBuildOptions } from '../../utils/normalize';
import { WebBuildBuilderOptions } from '../build/build.impl';
import { getDevServerConfig } from '../../utils/devserver.config';
export interface WebDevServerOptions {
host: string; host: string;
port: number; port: number;
publicHost?: string; publicHost?: string;
@ -39,28 +31,21 @@ export interface WebDevServerOptions extends JsonObject {
baseHref?: string; baseHref?: string;
} }
export default createBuilder<WebDevServerOptions>(run); export default function devServerExecutor(
export function run(
serveOptions: WebDevServerOptions, serveOptions: WebDevServerOptions,
context: BuilderContext context: ExecutorContext
): Observable<DevServerBuildOutput> { ) {
return forkJoin( const sourceRoot = context.workspace.projects[context.projectName].sourceRoot;
const buildOptions = normalizeWebBuildOptions(
getBuildOptions(serveOptions, context), getBuildOptions(serveOptions, context),
from(getSourceRoot(context)) context.root,
).pipe(
map(([buildOptions, sourceRoot]) => {
buildOptions = normalizeWebBuildOptions(
buildOptions,
context.workspaceRoot,
sourceRoot sourceRoot
); );
let webpackConfig: Configuration = getDevServerConfig( let webpackConfig: Configuration = getDevServerConfig(
context.workspaceRoot, context.root,
sourceRoot, sourceRoot,
buildOptions, buildOptions,
serveOptions, serveOptions
context.logger
); );
if (buildOptions.webpackConfig) { if (buildOptions.webpackConfig) {
webpackConfig = require(buildOptions.webpackConfig)(webpackConfig, { webpackConfig = require(buildOptions.webpackConfig)(webpackConfig, {
@ -68,82 +53,45 @@ export function run(
configuration: serveOptions.buildTarget.split(':')[2], configuration: serveOptions.buildTarget.split(':')[2],
}); });
} }
return [webpackConfig, buildOptions] as [
Configuration,
WebBuildBuilderOptions
];
}),
map(([_, options]) => {
const path = buildServePath(options);
const serverUrl = url.format({
protocol: serveOptions.ssl ? 'https' : 'http',
hostname: serveOptions.host,
port: serveOptions.port.toString(),
pathname: path,
});
context.logger.info(stripIndents` return eachValueFrom(
** runWebpackDevServer(webpackConfig).pipe(
Web Development Server is listening at ${serverUrl} tap(({ stats }) => {
** console.info(stats.toString(webpackConfig.stats));
`);
if (serveOptions.open) {
opn(serverUrl, {
wait: false,
});
}
return [_, options, serverUrl] as [
Configuration,
WebBuildBuilderOptions,
string
];
}), }),
switchMap(([config, options, serverUrl]) => { map(({ baseUrl, stats }) => {
return runWebpackDevServer(config, context, { return {
logging: (stats) => { stats,
context.logger.info(stats.toString(config.stats)); baseUrl,
}, success: !stats.hasErrors(),
webpackFactory: require('webpack'), };
webpackDevServerFactory: require('webpack-dev-server'),
}).pipe(
map((output) => {
output.baseUrl = serverUrl;
return output;
})
);
}) })
)
); );
} }
function getBuildOptions( function getBuildOptions(
options: WebDevServerOptions, options: WebDevServerOptions,
context: BuilderContext context: ExecutorContext
): Observable<WebBuildBuilderOptions> { ): WebBuildBuilderOptions {
const target = targetFromTargetString(options.buildTarget); const target = parseTargetString(options.buildTarget);
const overrides: Partial<WebBuildBuilderOptions> = {}; const overrides: Partial<WebBuildBuilderOptions> = {
watch: false,
};
if (options.maxWorkers) { if (options.maxWorkers) {
overrides.maxWorkers = options.maxWorkers; overrides.maxWorkers = options.maxWorkers;
} }
if (options.memoryLimit) { if (options.memoryLimit) {
overrides.memoryLimit = options.memoryLimit; overrides.memoryLimit = options.memoryLimit;
} }
return from(
Promise.all([
context.getTargetOptions(target),
context.getBuilderNameForTarget(target),
])
.then(([targetOptions, builderName]) => {
if (options.baseHref) { if (options.baseHref) {
targetOptions.baseHref = options.baseHref; overrides.baseHref = options.baseHref;
} }
return context.validateOptions<WebBuildBuilderOptions & JsonObject>(
targetOptions, const buildOptions = readTargetOptions(target, context);
builderName
); return {
}) ...buildOptions,
.then((options) => ({
...options,
...overrides, ...overrides,
})) };
);
} }

View File

@ -1,6 +1,7 @@
{ {
"title": "Web Dev Server", "title": "Web Dev Server",
"description": "Web Dev Server", "description": "Web Dev Server",
"cli": "nx",
"type": "object", "type": "object",
"properties": { "properties": {
"buildTarget": { "buildTarget": {

View File

@ -1,5 +1,4 @@
import { getDevServerConfig } from './devserver.config'; import { getDevServerConfig } from './devserver.config';
import { Logger } from '@angular-devkit/core/src/logger';
import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import * as ts from 'typescript'; import * as ts from 'typescript';
import * as fs from 'fs'; import * as fs from 'fs';
@ -9,12 +8,14 @@ import { join } from 'path';
jest.mock('tsconfig-paths-webpack-plugin'); jest.mock('tsconfig-paths-webpack-plugin');
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import { logger } from '@nrwl/devkit';
jest.mock('opn');
import * as opn from 'opn';
describe('getDevServerConfig', () => { describe('getDevServerConfig', () => {
let buildInput: WebBuildBuilderOptions; let buildInput: WebBuildBuilderOptions;
let serveInput: WebDevServerOptions; let serveInput: WebDevServerOptions;
let mockCompilerOptions: any; let mockCompilerOptions: any;
let logger: Logger;
let root: string; let root: string;
let sourceRoot: string; let sourceRoot: string;
@ -78,8 +79,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.headers['Access-Control-Allow-Origin']).toEqual('*'); expect(result.headers['Access-Control-Allow-Origin']).toEqual('*');
@ -90,8 +90,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.overlay.warnings).toEqual(false); expect(result.overlay.warnings).toEqual(false);
@ -102,8 +101,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.stats).toEqual(false); expect(result.stats).toEqual(false);
@ -114,12 +112,76 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.contentBase).toEqual(false); expect(result.contentBase).toEqual(false);
}); });
describe('onListening', () => {
let mockServer;
beforeEach(() => {
mockServer = {
options: {
https: false,
},
hostname: 'example.com',
listeningApp: {
address: () => ({
port: 9999,
}),
},
};
spyOn(logger, 'info');
opn.mockImplementation(() => {});
});
it('should print out the URL of the server', () => {
const { devServer: result } = getDevServerConfig(
root,
sourceRoot,
buildInput,
serveInput
) as any;
result.onListening(mockServer);
expect(logger.info).toHaveBeenCalledWith(
jasmine.stringMatching(new RegExp('http://example.com:9999/'))
);
});
it('should not open the url by default', () => {
const { devServer: result } = getDevServerConfig(
root,
sourceRoot,
buildInput,
serveInput
) as any;
result.onListening(mockServer);
expect(opn).not.toHaveBeenCalled();
});
it('should open the url if --open is passed', () => {
mockServer.options.open = true;
const { devServer: result } = getDevServerConfig(
root,
sourceRoot,
buildInput,
serveInput
) as any;
result.onListening(mockServer);
expect(opn).toHaveBeenCalledWith('http://example.com:9999/', {
wait: false,
});
});
});
}); });
describe('host option', () => { describe('host option', () => {
@ -128,8 +190,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.host).toEqual('localhost'); expect(result.host).toEqual('localhost');
@ -142,8 +203,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.port).toEqual(4200); expect(result.port).toEqual(4200);
@ -156,8 +216,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.historyApiFallback).toEqual({ expect(result.historyApiFallback).toEqual({
@ -173,8 +232,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.compress).toEqual(false); expect(result.compress).toEqual(false);
@ -191,8 +249,7 @@ describe('getDevServerConfig', () => {
styles: false, styles: false,
}, },
}, },
serveInput, serveInput
logger
) as any; ) as any;
expect(result.compress).toEqual(true); expect(result.compress).toEqual(true);
@ -209,8 +266,7 @@ describe('getDevServerConfig', () => {
styles: true, styles: true,
}, },
}, },
serveInput, serveInput
logger
) as any; ) as any;
expect(result.compress).toEqual(true); expect(result.compress).toEqual(true);
@ -227,8 +283,7 @@ describe('getDevServerConfig', () => {
styles: true, styles: true,
}, },
}, },
serveInput, serveInput
logger
) as any; ) as any;
expect(result.compress).toEqual(true); expect(result.compress).toEqual(true);
@ -245,8 +300,7 @@ describe('getDevServerConfig', () => {
styles: false, styles: false,
}, },
}, },
serveInput, serveInput
logger
) as any; ) as any;
expect(result.overlay.errors).toEqual(true); expect(result.overlay.errors).toEqual(true);
@ -263,8 +317,7 @@ describe('getDevServerConfig', () => {
styles: true, styles: true,
}, },
}, },
serveInput, serveInput
logger
) as any; ) as any;
expect(result.overlay.errors).toEqual(false); expect(result.overlay.errors).toEqual(false);
@ -277,8 +330,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
); );
expect(result.liveReload).toEqual(true); expect(result.liveReload).toEqual(true);
@ -289,8 +341,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
{ ...serveInput, liveReload: false }, { ...serveInput, liveReload: false }
logger
); );
expect(result.liveReload).toEqual(false); expect(result.liveReload).toEqual(false);
@ -309,8 +360,7 @@ describe('getDevServerConfig', () => {
styles: true, styles: true,
}, },
}, },
serveInput, serveInput
logger
) as any; ) as any;
expect(result.https).toEqual(false); expect(result.https).toEqual(false);
@ -334,8 +384,7 @@ describe('getDevServerConfig', () => {
ssl: true, ssl: true,
sslKey: 'ssl.key', sslKey: 'ssl.key',
sslCert: 'ssl.cert', sslCert: 'ssl.cert',
}, }
logger
) as any; ) as any;
expect(result.https).toEqual({ expect(result.https).toEqual({
@ -364,8 +413,7 @@ describe('getDevServerConfig', () => {
{ {
...serveInput, ...serveInput,
proxyConfig: 'proxy.conf', proxyConfig: 'proxy.conf',
}, }
logger
) as any; ) as any;
expect(result.proxy).toEqual({ expect(result.proxy).toEqual({
@ -383,8 +431,7 @@ describe('getDevServerConfig', () => {
{ {
...serveInput, ...serveInput,
allowedHosts: 'host.com,subdomain.host.com', allowedHosts: 'host.com,subdomain.host.com',
}, }
logger
) as any; ) as any;
expect(result.allowedHosts).toEqual(['host.com', 'subdomain.host.com']); expect(result.allowedHosts).toEqual(['host.com', 'subdomain.host.com']);
@ -398,8 +445,7 @@ describe('getDevServerConfig', () => {
{ {
...serveInput, ...serveInput,
allowedHosts: 'host.com', allowedHosts: 'host.com',
}, }
logger
) as any; ) as any;
expect(result.allowedHosts).toEqual(['host.com']); expect(result.allowedHosts).toEqual(['host.com']);
@ -410,8 +456,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
buildInput, buildInput,
serveInput, serveInput
logger
) as any; ) as any;
expect(result.allowedHosts).toEqual([]); expect(result.allowedHosts).toEqual([]);
@ -423,8 +468,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
{ ...buildInput, maxWorkers: 1 }, { ...buildInput, maxWorkers: 1 },
serveInput, serveInput
logger
) as any; ) as any;
const typeCheckerPlugin = result.plugins.find( const typeCheckerPlugin = result.plugins.find(
@ -440,8 +484,7 @@ describe('getDevServerConfig', () => {
root, root,
sourceRoot, sourceRoot,
{ ...buildInput, memoryLimit: 1024 }, { ...buildInput, memoryLimit: 1024 },
serveInput, serveInput
logger
) as any; ) as any;
const typeCheckerPlugin = result.plugins.find( const typeCheckerPlugin = result.plugins.find(

View File

@ -1,9 +1,13 @@
import { logger } from '@nrwl/devkit';
import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'; import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
import * as opn from 'opn';
import * as url from 'url';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import * as path from 'path'; import * as path from 'path';
import { getWebConfig } from './web.config'; import { getWebConfig } from './web.config';
import { Configuration } from 'webpack'; import { Configuration } from 'webpack';
import { LoggerApi } from '@angular-devkit/core/src/logger';
import { WebBuildBuilderOptions } from '../builders/build/build.impl'; import { WebBuildBuilderOptions } from '../builders/build/build.impl';
import { WebDevServerOptions } from '../builders/dev-server/dev-server.impl'; import { WebDevServerOptions } from '../builders/dev-server/dev-server.impl';
import { buildServePath } from './serve-path'; import { buildServePath } from './serve-path';
@ -13,8 +17,7 @@ export function getDevServerConfig(
root: string, root: string,
sourceRoot: string, sourceRoot: string,
buildOptions: WebBuildBuilderOptions, buildOptions: WebBuildBuilderOptions,
serveOptions: WebDevServerOptions, serveOptions: WebDevServerOptions
logger: LoggerApi
) { ) {
const webpackConfig: Configuration = getWebConfig( const webpackConfig: Configuration = getWebConfig(
root, root,
@ -53,6 +56,23 @@ function getDevServerPartial(
disableDotRule: true, disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
}, },
noInfo: true,
onListening: function (server: any) {
// Depend on the info in the server for this function because the user might adjust the webpack config
const serverUrl = url.format({
protocol: server.options.https ? 'https' : 'http',
hostname: server.hostname,
port: server.listeningApp.address().port,
pathname: buildServePath(buildOptions),
});
logger.info(`NX Web Development Server is listening at ${serverUrl}`);
if (server.options.open) {
opn(serverUrl, {
wait: false,
});
}
},
stats: false, stats: false,
compress: scriptsOptimization || stylesOptimization, compress: scriptsOptimization || stylesOptimization,
https: options.ssl, https: options.ssl,

View File

@ -1,7 +1,12 @@
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import { Observable } from 'rxjs';
import { Stats, Configuration } from 'webpack'; import { Stats, Configuration } from 'webpack';
import * as WebpackDevServer from 'webpack-dev-server';
import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
import { Observable } from 'rxjs';
import { extname } from 'path'; import { extname } from 'path';
import * as url from 'url';
export function runWebpack(config: Configuration): Observable<Stats> { export function runWebpack(config: Configuration): Observable<Stats> {
return new Observable((subscriber) => { return new Observable((subscriber) => {
@ -16,7 +21,11 @@ export function runWebpack(config: Configuration): Observable<Stats> {
if (config.watch) { if (config.watch) {
const watchOptions = config.watchOptions || {}; const watchOptions = config.watchOptions || {};
webpackCompiler.watch(watchOptions, callback); const watching = webpackCompiler.watch(watchOptions, callback);
return () => {
watching.close(() => {});
};
} else { } else {
webpackCompiler.run((err, stats) => { webpackCompiler.run((err, stats) => {
callback(err, stats); callback(err, stats);
@ -26,6 +35,59 @@ export function runWebpack(config: Configuration): Observable<Stats> {
}); });
} }
export function runWebpackDevServer(
config: Configuration
): Observable<{ stats: Stats; baseUrl: string }> {
return new Observable((subscriber) => {
const webpackCompiler = webpack(config);
let baseUrl: string;
webpackCompiler.hooks.done.tap('build-webpack', (stats) => {
subscriber.next({ stats, baseUrl });
});
const devServerConfig = config.devServer || {};
const originalOnListen = devServerConfig.onListening;
devServerConfig.onListening = function (server: any) {
originalOnListen(server);
const devServerOptions: WebpackDevServerConfiguration = server.options;
baseUrl = url.format({
protocol: devServerOptions.https ? 'https' : 'http',
hostname: server.hostname,
port: server.listeningApp.address().port,
pathname: devServerOptions.publicPath,
});
};
const webpackServer = new WebpackDevServer(
webpackCompiler,
devServerConfig
);
try {
const server = webpackServer.listen(
devServerConfig.port ?? 8080,
devServerConfig.host ?? 'localhost',
function (err) {
if (err) {
subscriber.error(err);
}
}
);
return () => {
server.close();
};
} catch (e) {
throw new Error('Could not start start dev server');
}
});
}
export interface EmittedFile { export interface EmittedFile {
id?: string; id?: string;
name?: string; name?: string;

View File

@ -71,6 +71,7 @@ const IGNORE_MATCHES = {
'karma-jasmine', 'karma-jasmine',
'karma-jasmine-html-reporter', 'karma-jasmine-html-reporter',
'webpack', 'webpack',
'webpack-dev-server',
], ],
}; };