Skip to content

feat: implement dev server as plugin #3251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ac01ea0
chore: add stub
rishabh3112 May 6, 2021
320d221
chore: validate schema and assign logger
rishabh3112 May 6, 2021
6ba4546
chore: normalize options for single compiler
rishabh3112 May 6, 2021
436e54f
chore: add socket implementaion
rishabh3112 May 6, 2021
96bfc1f
chore: make changes inplace
rishabh3112 May 8, 2021
22ec28a
chore: update tests for plugin syntax
rishabh3112 May 8, 2021
381aebd
chore: test update
rishabh3112 May 8, 2021
f4c16fa
chore: test update
rishabh3112 May 8, 2021
3d56605
chore: minor refactor
rishabh3112 May 8, 2021
2f01e3a
chore: support legacy dev-server usage
rishabh3112 May 8, 2021
15d395b
chore: rebase
rishabh3112 May 8, 2021
a2df5e9
chore: update constructor
rishabh3112 May 8, 2021
32f9779
chore: run cli tests
rishabh3112 May 8, 2021
ce1065e
chore: fix tests
rishabh3112 May 8, 2021
aed845b
chore: rebase
rishabh3112 May 14, 2021
6478644
chore: update options handling
rishabh3112 May 14, 2021
a2bc74f
chore: rebase
rishabh3112 May 15, 2021
ba8209a
chore: rebase
rishabh3112 May 17, 2021
324b733
chore: fix constructor compiler support
rishabh3112 May 17, 2021
055333e
refactor: apply ops on child compilers directly
rishabh3112 May 17, 2021
104d318
chore: remove DevServerPlugin
rishabh3112 May 17, 2021
226bd3c
fix: client entry location
rishabh3112 May 17, 2021
84fa7f5
chore: remove redundant check
rishabh3112 May 17, 2021
69c3ad0
chore: rebase
rishabh3112 Jun 6, 2021
53ce91c
chore: update child compiler setup
rishabh3112 Jun 6, 2021
e84ccdd
chore: delete DevServerPlugin
rishabh3112 Jun 6, 2021
8380a2e
chore: fix
rishabh3112 Jun 7, 2021
4ce3786
chore: update path
rishabh3112 Jun 7, 2021
beba6ff
chore: remove stale test
rishabh3112 Jun 7, 2021
7c70089
chore: fix lint
rishabh3112 Jun 7, 2021
269ffee
chore: resolve conflict
rishabh3112 Jun 7, 2021
cf3da1c
chore: rebase
rishabh3112 Sep 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 221 additions & 8 deletions lib/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,36 @@ if (!process.env.WEBPACK_SERVE) {
}

class Server {
// TODO: change this after legacy dev server usage is deprecated and serve package is updated
constructor(options = {}, compiler) {
this.name = 'webpack-dev-server';

// TODO: remove this after plugin support is published
if (options.hooks) {
[options, compiler] = [compiler, options];
}

validate(schema, options, 'webpack Dev Server');

this.compiler = compiler;
this.options = options;
this.logger = this.compiler.getInfrastructureLogger('webpack-dev-server');
this.sockets = [];
this.staticWatchers = [];
// Keep track of websocket proxies for external websocket upgrade.
this.websocketProxies = [];
// this value of ws can be overwritten for tests
this.wsHeartbeatInterval = 30000;

if (compiler) {
this.apply(compiler);
}
}

apply(compiler) {
this.compiler = compiler;
this.logger = this.compiler.getInfrastructureLogger(this.name);

normalizeOptions(this.compiler, this.options);
this.applyDevServerPlugin();
this.configureChildCompilers();

this.SocketServerImplementation = getSocketServerImplementation(
this.options
Expand Down Expand Up @@ -74,14 +84,218 @@ class Server {
}, this);
}

applyDevServerPlugin() {
const DevServerPlugin = require('./utils/DevServerPlugin');
configureChildCompilers() {
const {
EntryPlugin,
ProvidePlugin,
HotModuleReplacementPlugin,
} = require('webpack');
const createDomain = require('./utils/createDomain');
const getSocketClientPath = require('./utils/getSocketClientPath');

const compilers = this.compiler.compilers || [this.compiler];
const options = this.options;

// eslint-disable-next-line no-shadow
compilers.forEach((compiler) => {
new DevServerPlugin(this.options).apply(compiler);
/** @type {string} */
const domain = createDomain(options);

// SockJS is not supported server mode, so `host` and `port` can't specified, let's ignore them
// TODO show warning about this
const isSockJSType = options.webSocketServer.type === 'sockjs';

/** @type {string} */
let hostString = '';

if (options.client && options.client.host) {
hostString = `&host=${options.client.host}`;
} else if (options.webSocketServer.options.host && !isSockJSType) {
hostString = `&host=${options.webSocketServer.options.host}`;
}

/** @type {string} */
let portString = '';

if (options.client && options.client.port) {
portString = `&port=${options.client.port}`;
} else if (options.webSocketServer.options.port && !isSockJSType) {
portString = `&port=${options.webSocketServer.options.port}`;
} else {
portString = `&port=${options.port}`;
}

/** @type {string} */
let pathString = '';

// Only add the path if it is not default
if (options.client && options.client.path && options.client.path) {
pathString = `&path=${options.client.path}`;
} else if (options.webSocketServer.options.path) {
pathString = `&path=${options.webSocketServer.options.path}`;
}

/** @type {string} */
const logging =
options.client && options.client.logging
? `&logging=${options.client.logging}`
: '';

/** @type {string} */
const clientEntry = `${require.resolve(
'../client/index.js'
)}?${domain}${hostString}${pathString}${portString}${logging}`;

/** @type {(string[] | string)} */
let hotEntry;

if (options.hot === 'only') {
hotEntry = require.resolve('webpack/hot/only-dev-server');
} else if (options.hot) {
hotEntry = require.resolve('webpack/hot/dev-server');
}
/**
* prependEntry Method for webpack 4
* @param {Entry} originalEntry
* @param {Entry} additionalEntries
* @returns {Entry}
*/
const prependEntry = (originalEntry, additionalEntries) => {
if (typeof originalEntry === 'function') {
return () =>
Promise.resolve(originalEntry()).then((entry) =>
prependEntry(entry, additionalEntries)
);
}

if (
typeof originalEntry === 'object' &&
!Array.isArray(originalEntry)
) {
/** @type {Object<string,string>} */
const clone = {};

Object.keys(originalEntry).forEach((key) => {
// entry[key] should be a string here
const entryDescription = originalEntry[key];
clone[key] = prependEntry(entryDescription, additionalEntries);
});

return clone;
}

// in this case, entry is a string or an array.
// make sure that we do not add duplicates.
/** @type {Entry} */
const entriesClone = additionalEntries.slice(0);
[].concat(originalEntry).forEach((newEntry) => {
if (!entriesClone.includes(newEntry)) {
entriesClone.push(newEntry);
}
});
return entriesClone;
};

/**
*
* Description of the option for checkInject method
* @typedef {Function} checkInjectOptionsParam
* @param {Object} _config - compilerConfig
* @return {Boolean}
*/

/**
*
* @param {Boolean | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean
* @param {Object} _config
* @param {Boolean} defaultValue
* @return {Boolean}
*/
// eslint-disable-next-line no-shadow
const checkInject = (option, _config, defaultValue) => {
if (typeof option === 'boolean') {
return option;
}

if (typeof option === 'function') {
return option(_config);
}

return defaultValue;
};

const compilerOptions = compiler.options;

compilerOptions.plugins = compilerOptions.plugins || [];

/** @type {boolean} */
const isWebTarget = compilerOptions.externalsPresets
? compilerOptions.externalsPresets.web
: [
'web',
'webworker',
'electron-renderer',
'node-webkit',
// eslint-disable-next-line no-undefined
undefined,
null,
].includes(compilerOptions.target);

/** @type {Entry} */
const additionalEntries = checkInject(
options.client ? options.client.needClientEntry : null,
compilerOptions,
isWebTarget
)
? [clientEntry]
: [];

if (
hotEntry &&
checkInject(
options.client ? options.client.hotEntry : null,
compilerOptions,
true
)
) {
additionalEntries.push(hotEntry);
}

// use a hook to add entries if available
if (EntryPlugin) {
for (const additionalEntry of additionalEntries) {
new EntryPlugin(compiler.context, additionalEntry, {
// eslint-disable-next-line no-undefined
name: undefined,
}).apply(compiler);
}
} else {
compilerOptions.entry = prependEntry(
compilerOptions.entry || './src',
additionalEntries
);
compiler.hooks.entryOption.call(
compilerOptions.context,
compilerOptions.entry
);
}

const providePlugin = new ProvidePlugin({
__webpack_dev_server_client__: getSocketClientPath(options),
});

providePlugin.apply(compiler);

if (
hotEntry &&
!compilerOptions.plugins.find(
(p) => p.constructor === HotModuleReplacementPlugin
)
) {
// apply the HMR plugin, if it didn't exist before.
const plugin = new HotModuleReplacementPlugin();

plugin.apply(compiler);
}
});
}

Expand Down Expand Up @@ -824,7 +1038,6 @@ class Server {
this.staticWatchers.map((watcher) => watcher.close())
);
this.staticWatchers = [];

this.server.kill(() => {
// watchers must be closed before closing middleware
prom.then(() => {
Expand Down
Loading