mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-21 22:34:21 +08:00
feat(@angular-devkit/build-angular): update webpack-dev-server
to version 4
BREAKING CHANGE: The dev-server now uses WebSockets to communicate changes to the browser during HMR and live-reloaded. If during your development you are using a proxy you will need to enable proxying of WebSockets.
This commit is contained in:
parent
a0b5897d50
commit
32dbf659ac
@ -124,7 +124,7 @@
|
||||
"@types/semver": "^7.0.0",
|
||||
"@types/text-table": "^0.2.1",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"@types/webpack-dev-server": "^3.1.7",
|
||||
"@types/webpack-dev-server": "^4.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "4.29.3",
|
||||
"@typescript-eslint/parser": "4.29.3",
|
||||
"@yarnpkg/lockfile": "1.1.0",
|
||||
@ -225,7 +225,7 @@
|
||||
"verdaccio-auth-memory": "^10.0.0",
|
||||
"webpack": "5.51.1",
|
||||
"webpack-dev-middleware": "5.0.0",
|
||||
"webpack-dev-server": "3.11.2",
|
||||
"webpack-dev-server": "4.0.0",
|
||||
"webpack-merge": "5.8.0",
|
||||
"webpack-subresource-integrity": "5.0.0",
|
||||
"zone.js": "^0.11.3"
|
||||
|
@ -68,7 +68,7 @@
|
||||
"tslib": "2.3.1",
|
||||
"webpack": "5.51.1",
|
||||
"webpack-dev-middleware": "5.0.0",
|
||||
"webpack-dev-server": "3.11.2",
|
||||
"webpack-dev-server": "4.0.0",
|
||||
"webpack-merge": "5.8.0",
|
||||
"webpack-subresource-integrity": "5.0.0"
|
||||
},
|
||||
|
@ -228,40 +228,6 @@ export function serveWebpackBrowser(
|
||||
throw new Error('Webpack Dev Server configuration was not set.');
|
||||
}
|
||||
|
||||
if (options.liveReload && !options.hmr) {
|
||||
// This is needed because we cannot use the inline option directly in the config
|
||||
// because of the SuppressExtractedTextChunksWebpackPlugin
|
||||
// Consider not using SuppressExtractedTextChunksWebpackPlugin when liveReload is enable.
|
||||
webpackDevServer.addDevServerEntrypoints(config, {
|
||||
...config.devServer,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
// Remove live-reload code from all entrypoints but not main.
|
||||
// Otherwise this will break SuppressExtractedTextChunksWebpackPlugin because
|
||||
// 'addDevServerEntrypoints' adds addional entry-points to all entries.
|
||||
if (
|
||||
config.entry &&
|
||||
typeof config.entry === 'object' &&
|
||||
!Array.isArray(config.entry) &&
|
||||
config.entry.main
|
||||
) {
|
||||
for (const [key, value] of Object.entries(config.entry)) {
|
||||
if (key === 'main' || !Array.isArray(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const webpackClientScriptIndex = value.findIndex((x) =>
|
||||
x.includes('webpack-dev-server/client/index.js'),
|
||||
);
|
||||
if (webpackClientScriptIndex >= 0) {
|
||||
// Remove the webpack-dev-server/client script from array.
|
||||
value.splice(webpackClientScriptIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let locale: string | undefined;
|
||||
if (i18n.shouldInline) {
|
||||
// Dev-server only supports one locale
|
||||
@ -306,7 +272,7 @@ export function serveWebpackBrowser(
|
||||
// The below is needed as otherwise HMR for CSS will break.
|
||||
// styles.js and runtime.js needs to be loaded as a non-module scripts as otherwise `document.currentScript` will be null.
|
||||
// https://github.com/webpack-contrib/mini-css-extract-plugin/blob/90445dd1d81da0c10b9b0e8a17b417d0651816b8/src/hmr/hotModuleReplacement.js#L39
|
||||
isHMREnabled: webpackConfig.devServer?.hot,
|
||||
isHMREnabled: !!webpackConfig.devServer?.hot,
|
||||
});
|
||||
|
||||
webpackConfig.plugins ??= [];
|
||||
@ -336,8 +302,8 @@ export function serveWebpackBrowser(
|
||||
const serverAddress = url.format({
|
||||
protocol: options.ssl ? 'https' : 'http',
|
||||
hostname: options.host === '0.0.0.0' ? 'localhost' : options.host,
|
||||
pathname: webpackConfig.devServer?.publicPath,
|
||||
port: buildEvent.port,
|
||||
pathname: webpackConfig.devServer?.devMiddleware?.publicPath,
|
||||
});
|
||||
|
||||
if (index === 0) {
|
||||
|
@ -10,7 +10,6 @@
|
||||
import { Architect, BuilderRun } from '@angular-devkit/architect';
|
||||
import { tags } from '@angular-devkit/core';
|
||||
import { createProxyServer } from 'http-proxy';
|
||||
import { HTTPResponse } from 'puppeteer/lib/cjs/puppeteer/api-docs-entry';
|
||||
import { Browser } from 'puppeteer/lib/cjs/puppeteer/common/Browser';
|
||||
import { Page } from 'puppeteer/lib/cjs/puppeteer/common/Page';
|
||||
import puppeteer from 'puppeteer/lib/cjs/puppeteer/node';
|
||||
@ -96,15 +95,33 @@ function createProxy(target: string, secure: boolean, ws = true): ProxyInstance
|
||||
};
|
||||
}
|
||||
|
||||
async function goToPageAndWaitForSockJs(page: Page, url: string): Promise<void> {
|
||||
const socksRequest = `${url.endsWith('/') ? url : url + '/'}sockjs-node/info?t=`;
|
||||
async function goToPageAndWaitForWS(page: Page, url: string): Promise<void> {
|
||||
const baseUrl = url.replace(/^http/, 'ws');
|
||||
const socksRequest = baseUrl[baseUrl.length - 1] === '/' ? `${baseUrl}ws` : `${baseUrl}/ws`;
|
||||
// Create a Chrome dev tools session so that we can capturing websocket request.
|
||||
// https://github.com/puppeteer/puppeteer/issues/2974
|
||||
|
||||
// We do this, to ensure that we make the right request with the expected host, port etc...
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Network.enable');
|
||||
await client.send('Page.enable');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForResponse(
|
||||
(r: HTTPResponse) => r.url().startsWith(socksRequest) && r.status() === 200,
|
||||
),
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error(`A Websocket connected to ${socksRequest} was not established.`)),
|
||||
2000,
|
||||
);
|
||||
client.on('Network.webSocketCreated', ({ url }) => {
|
||||
if (url.startsWith(socksRequest)) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
page.goto(url),
|
||||
]);
|
||||
await client.detach();
|
||||
}
|
||||
|
||||
describe('Dev Server Builder live-reload', () => {
|
||||
@ -169,7 +186,7 @@ describe('Dev Server Builder live-reload', () => {
|
||||
const url = buildEvent.baseUrl as string;
|
||||
switch (buildCount) {
|
||||
case 0:
|
||||
await goToPageAndWaitForSockJs(page, url);
|
||||
await goToPageAndWaitForWS(page, url);
|
||||
host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
|
||||
break;
|
||||
case 1:
|
||||
@ -200,7 +217,7 @@ describe('Dev Server Builder live-reload', () => {
|
||||
switch (buildCount) {
|
||||
case 0:
|
||||
proxy = createProxy(url, false);
|
||||
await goToPageAndWaitForSockJs(page, proxy.url);
|
||||
await goToPageAndWaitForWS(page, proxy.url);
|
||||
host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
|
||||
break;
|
||||
case 1:
|
||||
@ -231,43 +248,7 @@ describe('Dev Server Builder live-reload', () => {
|
||||
switch (buildCount) {
|
||||
case 0:
|
||||
proxy = createProxy(url, true);
|
||||
await goToPageAndWaitForSockJs(page, proxy.url);
|
||||
host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
|
||||
break;
|
||||
case 1:
|
||||
const innerText = await page.evaluate(() => document.querySelector('p').innerText);
|
||||
expect(innerText).toBe('app-live-reload');
|
||||
break;
|
||||
}
|
||||
|
||||
buildCount++;
|
||||
}),
|
||||
take(2),
|
||||
)
|
||||
.toPromise();
|
||||
});
|
||||
|
||||
it('works without https -> http proxy without websockets (dotnet emulation)', async () => {
|
||||
const run = await architect.scheduleTarget(target, overrides);
|
||||
runs.push(run);
|
||||
|
||||
let proxy: ProxyInstance | undefined;
|
||||
let buildCount = 0;
|
||||
|
||||
await run.output
|
||||
.pipe(
|
||||
debounceTime(1000),
|
||||
switchMap(async (buildEvent) => {
|
||||
expect(buildEvent.success).toBe(true);
|
||||
const url = buildEvent.baseUrl as string;
|
||||
switch (buildCount) {
|
||||
case 0:
|
||||
proxy = createProxy(url, true, false);
|
||||
await goToPageAndWaitForSockJs(page, proxy.url);
|
||||
await page.waitForResponse(
|
||||
(response: HTTPResponse) =>
|
||||
response.url().includes('xhr_streaming') && response.status() === 200,
|
||||
);
|
||||
await goToPageAndWaitForWS(page, proxy.url);
|
||||
host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
|
||||
break;
|
||||
case 1:
|
||||
|
@ -27,7 +27,9 @@ export async function executeOnceAndFetch<T>(
|
||||
mergeMap(async (executionResult) => {
|
||||
let response = undefined;
|
||||
if (executionResult.result?.success) {
|
||||
const resolvedUrl = new URL(url, `${executionResult.result.baseUrl}/`);
|
||||
let baseUrl = `${executionResult.result.baseUrl}`;
|
||||
baseUrl = baseUrl[baseUrl.length - 1] === '/' ? baseUrl : `${baseUrl}/`;
|
||||
const resolvedUrl = new URL(url, baseUrl);
|
||||
response = await fetch(resolvedUrl, options?.request);
|
||||
}
|
||||
|
||||
|
@ -390,6 +390,9 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
|
||||
syncWebAssembly: true,
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
infrastructureLogging: {
|
||||
level: buildOptions.verbose ? 'verbose' : 'error',
|
||||
},
|
||||
cache: getCacheSettings(wco, buildBrowserFeatures.supportedBrowsers),
|
||||
optimization: {
|
||||
minimizer: extraMinimizers,
|
||||
|
@ -12,56 +12,30 @@ import { posix, resolve } from 'path';
|
||||
import * as url from 'url';
|
||||
import * as webpack from 'webpack';
|
||||
import { Configuration } from 'webpack-dev-server';
|
||||
import { normalizeOptimization } from '../../utils';
|
||||
import { WebpackConfigOptions, WebpackDevServerOptions } from '../../utils/build-options';
|
||||
import { getIndexOutputFile } from '../../utils/webpack-browser-config';
|
||||
import { HmrLoader } from '../plugins/hmr/hmr-loader';
|
||||
import { getWatchOptions } from '../utils/helpers';
|
||||
|
||||
export function getDevServerConfig(
|
||||
wco: WebpackConfigOptions<WebpackDevServerOptions>,
|
||||
): webpack.Configuration {
|
||||
const {
|
||||
buildOptions: {
|
||||
optimization,
|
||||
host,
|
||||
port,
|
||||
index,
|
||||
headers,
|
||||
poll,
|
||||
ssl,
|
||||
hmr,
|
||||
main,
|
||||
disableHostCheck,
|
||||
liveReload,
|
||||
allowedHosts,
|
||||
watch,
|
||||
proxyConfig,
|
||||
},
|
||||
buildOptions: { host, port, index, headers, watch, hmr, main, liveReload, proxyConfig },
|
||||
logger,
|
||||
root,
|
||||
} = wco;
|
||||
|
||||
const servePath = buildServePath(wco.buildOptions, logger);
|
||||
const { styles: stylesOptimization, scripts: scriptsOptimization } = normalizeOptimization(
|
||||
optimization,
|
||||
);
|
||||
|
||||
const extraPlugins = [];
|
||||
|
||||
// Resolve public host and client address.
|
||||
let publicHost = wco.buildOptions.publicHost;
|
||||
if (publicHost) {
|
||||
if (!/^\w+:\/\//.test(publicHost)) {
|
||||
publicHost = `${ssl ? 'https' : 'http'}://${publicHost}`;
|
||||
}
|
||||
|
||||
const parsedHost = url.parse(publicHost);
|
||||
publicHost = parsedHost.host ?? undefined;
|
||||
} else {
|
||||
publicHost = '0.0.0.0:0';
|
||||
const extraRules: webpack.RuleSetRule[] = [];
|
||||
if (hmr) {
|
||||
extraRules.push({
|
||||
loader: HmrLoader,
|
||||
include: [main].map((p) => resolve(wco.root, p)),
|
||||
});
|
||||
}
|
||||
|
||||
const extraPlugins = [];
|
||||
if (!watch) {
|
||||
// There's no option to turn off file watching in webpack-dev-server, but
|
||||
// we can override the file watcher instead.
|
||||
@ -76,13 +50,7 @@ export function getDevServerConfig(
|
||||
});
|
||||
}
|
||||
|
||||
const extraRules: webpack.RuleSetRule[] = [];
|
||||
if (hmr) {
|
||||
extraRules.push({
|
||||
loader: HmrLoader,
|
||||
include: [main].map((p) => resolve(wco.root, p)),
|
||||
});
|
||||
}
|
||||
const webSocketPath = posix.join(servePath, 'ws');
|
||||
|
||||
return {
|
||||
plugins: extraPlugins,
|
||||
@ -97,7 +65,7 @@ export function getDevServerConfig(
|
||||
...headers,
|
||||
},
|
||||
historyApiFallback: !!index && {
|
||||
index: `${servePath}/${getIndexOutputFile(index)}`,
|
||||
index: posix.join(servePath, getIndexOutputFile(index)),
|
||||
disableDotRule: true,
|
||||
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
|
||||
rewrites: [
|
||||
@ -107,30 +75,31 @@ export function getDevServerConfig(
|
||||
},
|
||||
],
|
||||
},
|
||||
sockPath: posix.join(servePath, 'sockjs-node'),
|
||||
stats: false,
|
||||
compress: stylesOptimization.minify || scriptsOptimization,
|
||||
watchOptions: getWatchOptions(poll),
|
||||
https: getSslConfig(root, wco.buildOptions),
|
||||
overlay: {
|
||||
errors: !(stylesOptimization.minify || scriptsOptimization),
|
||||
warnings: false,
|
||||
webSocketServer: {
|
||||
options: {
|
||||
path: webSocketPath,
|
||||
},
|
||||
},
|
||||
compress: false,
|
||||
static: false,
|
||||
https: getSslConfig(root, wco.buildOptions),
|
||||
allowedHosts: getAllowedHostsConfig(wco.buildOptions),
|
||||
devMiddleware: {
|
||||
publicPath: servePath,
|
||||
stats: false,
|
||||
},
|
||||
public: publicHost,
|
||||
allowedHosts,
|
||||
disableHostCheck,
|
||||
// This should always be true, but at the moment this breaks 'SuppressExtractedTextChunksWebpackPlugin'
|
||||
// because it will include addition JS in the styles.js.
|
||||
inline: hmr,
|
||||
publicPath: servePath,
|
||||
liveReload,
|
||||
injectClient: liveReload,
|
||||
hotOnly: hmr && !liveReload,
|
||||
hot: hmr,
|
||||
hot: hmr && !liveReload ? 'only' : hmr,
|
||||
proxy: addProxyConfig(root, proxyConfig),
|
||||
contentBase: false,
|
||||
logLevel: 'error',
|
||||
} as Configuration & { logLevel: Configuration['clientLogLevel'] },
|
||||
client: {
|
||||
logging: 'info',
|
||||
webSocketURL: getPublicHostOptions(wco.buildOptions, webSocketPath),
|
||||
overlay: {
|
||||
errors: true,
|
||||
warnings: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -169,7 +138,7 @@ export function buildServePath(
|
||||
* Private method to enhance a webpack config with SSL configuration.
|
||||
* @private
|
||||
*/
|
||||
function getSslConfig(root: string, options: WebpackDevServerOptions) {
|
||||
function getSslConfig(root: string, options: WebpackDevServerOptions): Configuration['https'] {
|
||||
const { ssl, sslCert, sslKey } = options;
|
||||
if (ssl && sslCert && sslKey) {
|
||||
return {
|
||||
@ -235,3 +204,26 @@ function findDefaultServePath(baseHref?: string, deployUrl?: string): string | n
|
||||
// Join together baseHref and deployUrl
|
||||
return `${normalizedBaseHref}${deployUrl || ''}`;
|
||||
}
|
||||
|
||||
function getAllowedHostsConfig(options: WebpackDevServerOptions): Configuration['allowedHosts'] {
|
||||
if (options.disableHostCheck) {
|
||||
return 'all';
|
||||
} else if (options.allowedHosts?.length) {
|
||||
return options.allowedHosts;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getPublicHostOptions(options: WebpackDevServerOptions, webSocketPath: string): string {
|
||||
let publicHost: string | null | undefined = options.publicHost;
|
||||
if (publicHost) {
|
||||
if (!/^\w+:\/\//.test(publicHost)) {
|
||||
publicHost = `https://${publicHost}`;
|
||||
}
|
||||
|
||||
publicHost = url.parse(publicHost).host;
|
||||
}
|
||||
|
||||
return `auto://${publicHost || '0.0.0.0:0'}${webSocketPath}`;
|
||||
}
|
||||
|
@ -50,5 +50,7 @@ export function getWebpackStatsConfig(verbose = false) {
|
||||
export function getStatsConfig(wco: WebpackConfigOptions) {
|
||||
const verbose = !!wco.buildOptions.verbose;
|
||||
|
||||
return { stats: getWebpackStatsConfig(verbose) };
|
||||
return {
|
||||
stats: getWebpackStatsConfig(verbose),
|
||||
};
|
||||
}
|
||||
|
@ -96,7 +96,9 @@ export function isPolyfillsEntry(name: string): boolean {
|
||||
return name === 'polyfills';
|
||||
}
|
||||
|
||||
export function getWatchOptions(poll: number | undefined): Configuration['watchOptions'] {
|
||||
export function getWatchOptions(
|
||||
poll: number | undefined,
|
||||
): NonNullable<Configuration['watchOptions']> {
|
||||
return {
|
||||
poll,
|
||||
ignored: poll === undefined ? '**/$_lazy_route_resources' : 'node_modules/**',
|
||||
|
Loading…
x
Reference in New Issue
Block a user