Skip to content

Commit

Permalink
feat: support of injectHot and injectClient on specific chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromeh committed Feb 22, 2021
1 parent b38c257 commit 8845e26
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 37 deletions.
18 changes: 16 additions & 2 deletions lib/options.json
Expand Up @@ -228,6 +228,13 @@
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
},
{
"instanceof": "Function"
}
Expand All @@ -238,6 +245,13 @@
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
},
{
"instanceof": "Function"
}
Expand Down Expand Up @@ -395,8 +409,8 @@
"hot": "should be {Boolean|String} (https://webpack.js.org/configuration/dev-server/#devserverhot)",
"http2": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhttp2)",
"https": "should be {Object|Boolean} (https://webpack.js.org/configuration/dev-server/#devserverhttps)",
"injectClient": "should be {Boolean|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjectclient)",
"injectHot": "should be {Boolean|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjecthot)",
"injectClient": "should be {Boolean|String[]|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjectclient)",
"injectHot": "should be {Boolean|String[]|Function} (https://webpack.js.org/configuration/dev-server/#devserverinjecthot)",
"liveReload": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverlivereload)",
"onAfterSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverafter)",
"onBeforeSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverbefore)",
Expand Down
120 changes: 92 additions & 28 deletions lib/utils/DevServerPlugin.js
Expand Up @@ -20,6 +20,13 @@ class DevServerPlugin {
* @typedef {(string[] | string | Object<string | string[],string>)} Entry
*/

/**
* Additional entry to add to specific chunk
* @typedef {Object} AdditionalChunkEntry
* @property {Entry} entry
* @property {string[]} [chunks]
*/

/**
* Apply the plugin
* @param {Object} compiler the compiler instance
Expand Down Expand Up @@ -66,7 +73,7 @@ class DevServerPlugin {
/**
* prependEntry Method for webpack 4
* @param {Entry} originalEntry
* @param {Entry} additionalEntries
* @param {AdditionalChunkEntry[]} additionalEntries
* @returns {Entry}
*/
const prependEntry = (originalEntry, additionalEntries) => {
Expand All @@ -83,8 +90,13 @@ class DevServerPlugin {

Object.keys(originalEntry).forEach((key) => {
// entry[key] should be a string here
const chunkAdditionalEntries = additionalEntries.filter(
(additionalEntry) =>
!additionalEntry.chunks || additionalEntry.chunks.includes(key)
);

const entryDescription = originalEntry[key];
clone[key] = prependEntry(entryDescription, additionalEntries);
clone[key] = prependEntry(entryDescription, chunkAdditionalEntries);
});

return clone;
Expand All @@ -93,13 +105,15 @@ class DevServerPlugin {
// 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);
const newEntries = additionalEntries.map(
(additionalEntry) => additionalEntry.entry
);
[].concat(originalEntry).forEach((newEntry) => {
if (!entriesClone.includes(newEntry)) {
entriesClone.push(newEntry);
if (!newEntries.includes(newEntry)) {
newEntries.push(newEntry);
}
});
return entriesClone;
return newEntries;
};

/**
Expand All @@ -112,14 +126,15 @@ class DevServerPlugin {

/**
*
* @param {Boolean | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean
* @param {Boolean | string[] | checkInjectOptionsParam} option - inject(Hot|Client) it is Boolean | fn => Boolean
* @param {Object} _config
* @param {Boolean} defaultValue
* @return {Boolean}
* @return {Boolean | string[]}
*/
// eslint-disable-next-line no-shadow
const checkInject = (option, _config, defaultValue) => {
if (typeof option === 'boolean') return option;
if (Array.isArray(option)) return option;
if (typeof option === 'function') return option(_config);
return defaultValue;
};
Expand All @@ -138,37 +153,86 @@ class DevServerPlugin {
undefined,
null,
].includes(compilerOptions.target);
/** @type {Entry} */
const additionalEntries = checkInject(
/** @type {AdditionalChunkEntry[]} */
const additionalEntries = [];

const checkInjectClientResult = checkInject(
options.injectClient,
compilerOptions,
isWebTarget
)
? [clientEntry]
: [];
);
if (checkInjectClientResult) {
additionalEntries.push({
entry: clientEntry,
chunks: Array.isArray(checkInjectClientResult)
? checkInjectClientResult
: null,
});
}

if (hotEntry && checkInject(options.injectHot, compilerOptions, true)) {
additionalEntries.push(hotEntry);
if (hotEntry) {
const checkInjectHotResult = checkInject(
options.injectHot,
compilerOptions,
true
);
if (checkInjectHotResult) {
additionalEntries.push({
entry: hotEntry,
chunks: Array.isArray(checkInjectHotResult)
? checkInjectHotResult
: null,
});
}
}

// use a hook to add entries if available
if (EntryPlugin) {
compiler.hooks.make.tapPromise('DevServerPlugin', (compilation) =>
Promise.all(
additionalEntries.map(
(entry) =>
new Promise((resolve, reject) => {
compilation.addEntry(
compiler.context,
EntryPlugin.createDependency(entry, {}),
{}, // global entry
(err) => {
if (err) return reject(err);
resolve();
}
additionalEntries.map((additionalChunkEntry) => {
// add entry to existing chunks
if (
additionalChunkEntry.chunks &&
Array.isArray(additionalChunkEntry.chunks)
) {
let promise = Promise.resolve();
additionalChunkEntry.chunks.forEach((chunkName) => {
promise = promise.then(
() =>
new Promise((resolve, reject) => {
compilation.addEntry(
compiler.context,
EntryPlugin.createDependency(
additionalChunkEntry.entry,
{}
),
chunkName,
(err) => {
if (err) return reject(err);
resolve();
}
);
})
);
})
)
});

return promise;
}

// add new entry
return new Promise((resolve, reject) => {
compilation.addEntry(
compiler.context,
EntryPlugin.createDependency(additionalChunkEntry.entry, {}),
{}, // global entry
(err) => {
if (err) return reject(err);
resolve();
}
);
});
})
)
);
} else {
Expand Down
4 changes: 3 additions & 1 deletion test/__snapshots__/Validation.test.js.snap
Expand Up @@ -12,9 +12,11 @@ exports[`Validation validation should fail validation for invalid \`hot\` config
exports[`Validation validation should fail validation for invalid \`injectHot\` configuration 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.injectHot should be one of these:
boolean | function
boolean | [string, ...] (should not have fewer than 1 item) | function
Details:
* configuration.injectHot should be a boolean.
* configuration.injectHot should be an array:
[string, ...] (should not have fewer than 1 item)
* configuration.injectHot should be an instance of function."
`;

Expand Down
4 changes: 2 additions & 2 deletions test/options.test.js
Expand Up @@ -359,11 +359,11 @@ describe('options', () => {
],
},
injectClient: {
success: [true, () => {}],
success: [true, ['a'], () => {}],
failure: [''],
},
injectHot: {
success: [true, () => {}],
success: [true, ['a'], () => {}],
failure: [''],
},
onListening: {
Expand Down
67 changes: 63 additions & 4 deletions test/server/utils/DevServerPlugin.test.js
Expand Up @@ -17,9 +17,22 @@ describe('DevServerPlugin util', () => {
const entries = [];

const compilation = {
addEntry(_context, dep, _options, cb) {
addEntry(_context, dep, _optionsOrName, cb) {
if (!dep.loc.name) {
entries.push(dep.request);
const name =
typeof _optionsOrName === 'object'
? _optionsOrName.name
: _optionsOrName;

if (name && entryOption[name]) {
const entry = entryOption[name];
entries[name] = {
...entry,
import: [dep.request, ...entry.import],
};
} else {
entries.push(dep.request);
}
}
cb();
},
Expand All @@ -39,8 +52,7 @@ describe('DevServerPlugin util', () => {
entries.push(...entryOption.main.import);
}
// merge named exports into entries
Object.assign(entries, entryOption);
return entries;
return Object.assign([], entryOption, entries);
}
return entryOption;
}
Expand Down Expand Up @@ -621,6 +633,53 @@ describe('DevServerPlugin util', () => {
);
});

it('should allows selecting chunks to inline the client into', async () => {
const webpackOptions = [
Object.assign({}, config, {
entry: {
chunk1: ['./foo.js'],
chunk2: './foo.js',
chunk3: './foo.js',
},
}),
];
const compiler = webpack(webpackOptions);

const devServerOptions = {
injectClient: ['chunk1', 'chunk3'],
transportMode: {
server: 'sockjs',
client: 'sockjs',
},
};

await Promise.all(
// eslint-disable-next-line no-shadow
compiler.compilers.map((compiler) => {
const plugin = new DevServerPlugin(devServerOptions);
plugin.apply(compiler);
return getEntries(compiler).then((entries) => {
expect(Object.keys(entries).length).toEqual(3);
expect(entries.chunk1.import.length).toEqual(2);
expect(entries.chunk2.import.length).toEqual(1);
expect(entries.chunk3.import.length).toEqual(2);

expect(
normalize(entries.chunk1.import[0]).indexOf(
'client/default/index.js?'
) !== -1
).toBeTruthy();
expect(normalize(entries.chunk2.import[0])).toEqual('./foo.js');
expect(
normalize(entries.chunk3.import[0]).indexOf(
'client/default/index.js?'
) !== -1
).toBeTruthy();
});
})
);
});

it('should prepends the hot runtime to all targets by default (when hot)', async () => {
const webpackOptions = [
Object.assign({ target: 'web' }, config),
Expand Down

0 comments on commit 8845e26

Please sign in to comment.