Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
7 changes: 5 additions & 2 deletions packages/webpack-cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ type WebpackDevServerOptions = DevServerConfig &
config: string[];
configName?: string[];
disableInterpret?: boolean;
extends?: string[];
argv: Argv;
};

Expand All @@ -186,8 +187,10 @@ type Callback<T extends unknown[]> = (...args: T) => void;
/**
* Webpack
*/

type WebpackConfiguration = Configuration;
type WebpackConfiguration = Configuration & {
// TODO add extends to webpack types
extends?: string | string[];
};
type ConfigOptions = PotentialPromise<WebpackConfiguration | CallableOption>;
type CallableOption = (env: Env | undefined, argv: Argv) => WebpackConfiguration;
type WebpackCompiler = Compiler | MultiCompiler;
Expand Down
92 changes: 92 additions & 0 deletions packages/webpack-cli/src/webpack-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,18 @@ class WebpackCLI implements IWebpackCLI {
description: "Stop webpack-cli process with non-zero exit code on warnings from webpack",
helpLevel: "minimum",
},
{
name: "extends",
alias: "e",
configs: [
{
type: "string",
},
],
multiple: true,
description: "Extend webpack configuration",
helpLevel: "minimum",
},
];

const minimumHelpFlags = [
Expand Down Expand Up @@ -1929,6 +1941,86 @@ class WebpackCLI implements IWebpackCLI {
}
}

const flattenConfigs = async (
configPaths: string | string[],
): Promise<WebpackConfiguration> => {
configPaths = Array.isArray(configPaths) ? configPaths : [configPaths];

// fetch configs by path
const configs = await Promise.all(
configPaths.map((configPath: string) =>
loadConfigByPath(path.resolve(configPath), options.argv),
),
);

// extract options from configs
const extendedConfigOptions = configs.map((extendedConfig) =>
Array.isArray(extendedConfig.options) ? extendedConfig.options : [extendedConfig.options],
);

// recursively flatten the configs by looking for extends property
for (let i = 0; i < extendedConfigOptions.length; i++) {
const extendedConfigs = extendedConfigOptions[i];
for (let j = 0; j < extendedConfigs.length; j++) {
const extendedConfig = extendedConfigs[j] as WebpackConfiguration;
if (extendedConfig.extends) {
const flattenedExtendedConfig = await flattenConfigs(extendedConfig.extends);
extendedConfigOptions[i][j] = webpackMerge(flattenedExtendedConfig, extendedConfig);
}
}
}

const flattenedConfigs = extendedConfigOptions.reduce(
(acc: Array<ConfigOptions>, options) => {
acc.push(options[0]);
return acc;
},
[],
);

const merge = await this.tryRequireThenImport<typeof webpackMerge>("webpack-merge");
const mergedConfig = flattenedConfigs.reduce((accumulator: object, options) => {
return merge(accumulator, options);
}, {});

delete (mergedConfig as WebpackConfiguration).extends;
return mergedConfig;
};

// extends param in CLI gets priority over extends in config file
if (options.extends && options.extends.length > 0) {
// load the config from the extends option
const extendedConfig = await flattenConfigs(options.extends);

const merge = await this.tryRequireThenImport<typeof webpackMerge>("webpack-merge");

config.options = Array.isArray(config.options) ? config.options : [config.options];

// merge extended config with all configs
config.options = config.options.map((options) => {
return merge(extendedConfig, options);
});
}
// if no extends option is passed, check if the config file has extends
else if (
(!Array.isArray(config.options) && config.options.extends) ||
(Array.isArray(config.options) && config.options.some((options) => options.extends))
) {
config.options = Array.isArray(config.options) ? config.options : [config.options];

const merge = await this.tryRequireThenImport<typeof webpackMerge>("webpack-merge");

for (let index = 0; index < config.options.length; index++) {
const configOptions = config.options[index];
if (configOptions.extends) {
const extendedConfig = await flattenConfigs(configOptions.extends);

config.options[index] = merge(extendedConfig, configOptions);
delete config.options[index].extends;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move it to own function and just use && { extend: string[] } to avoid ts-except-error

}

if (options.merge) {
const merge = await this.tryRequireThenImport<typeof webpackMerge>("webpack-merge");

Expand Down
9 changes: 9 additions & 0 deletions test/build/extends/extends-cli-option/base.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = () => {
console.log("base.webpack.config.js");

return {
name: "base_config",
mode: "development",
entry: "./src/index.js",
};
};
1 change: 1 addition & 0 deletions test/build/extends/extends-cli-option/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("i am index.js")
9 changes: 9 additions & 0 deletions test/build/extends/extends-cli-option/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const WebpackCLITestPlugin = require("../../../utils/webpack-cli-test-plugin");

module.exports = () => {
console.log("derived.webpack.config.js");

return {
plugins: [new WebpackCLITestPlugin()],
};
};
68 changes: 68 additions & 0 deletions test/build/extends/extends.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use strict";

const { run } = require("../../utils/test-utils");

describe("extends property", () => {
it("extends a provided webpack config correctly", async () => {
const { exitCode, stderr, stdout } = await run(__dirname + "/simple-case");

expect(exitCode).toBe(0);
expect(stderr).toBeFalsy();
expect(stdout).toContain("base.webpack.config.js");
expect(stdout).toContain("derived.webpack.config.js");
expect(stdout).toContain("name: 'base_config'");
expect(stdout).toContain("mode: 'development'");
});

it("extends a provided array of webpack configs correctly", async () => {
const { exitCode, stderr, stdout } = await run(__dirname + "/multiple-extends");

expect(exitCode).toBe(0);
expect(stderr).toBeFalsy();
expect(stdout).toContain("base1.webpack.config.js");
expect(stdout).toContain("base2.webpack.config.js");
expect(stdout).toContain("derived.webpack.config.js");
expect(stdout).toContain("name: 'base_config2'");
expect(stdout).toContain("mode: 'production'");
});

it("extends a multilevel config correctly", async () => {
const { exitCode, stderr, stdout } = await run(__dirname + "/multi-level-extends");

expect(exitCode).toBe(0);
expect(stderr).toBeFalsy();
expect(stdout).toContain("base1.webpack.config.js");
expect(stdout).toContain("base2.webpack.config.js");
expect(stdout).toContain("derived.webpack.config.js");
expect(stdout).toContain("name: 'base_config1'");
expect(stdout).toContain("mode: 'production'");
});

it("extends a provided webpack config passed in the cli correctly", async () => {
const { exitCode, stderr, stdout } = await run(__dirname + "/extends-cli-option", [
"--extends",
"./base.webpack.config.js",
]);

expect(exitCode).toBe(0);
expect(stderr).toBeFalsy();
expect(stdout).toContain("base.webpack.config.js");
expect(stdout).toContain("derived.webpack.config.js");
expect(stdout).toContain("name: 'base_config'");
expect(stdout).toContain("mode: 'development'");
});

it("extends a provided webpack config for multiple configs correctly", async () => {
const { exitCode, stderr, stdout } = await run(__dirname + "/multiple-configs");

expect(exitCode).toBe(0);
expect(stderr).toBeFalsy();
expect(stdout).toContain("base.webpack.config.js");
expect(stdout).toContain("derived.webpack.config.js");
expect(stdout).toContain("name: 'derived_config1'");
expect(stdout).toContain("name: 'derived_config2'");
expect(stdout).not.toContain("name: 'base_config'");
expect(stdout).toContain("mode: 'development'");
expect(stdout).toContain("topLevelAwait: true");
});
});
13 changes: 13 additions & 0 deletions test/build/extends/multi-level-extends/base1.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = () => {
console.log("base1.webpack.config.js");

return {
name: "base_config1",
extends: ["./base2.webpack.config.js"],
mode: "production",
entry: "./src/index1.js",
output: {
filename: "bundle1.js",
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = () => {
console.log("base2.webpack.config.js");

return {
name: "base_config2",
mode: "development",
entry: "./src/index2.js",
};
};
1 change: 1 addition & 0 deletions test/build/extends/multi-level-extends/src/index1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("i am index1");
10 changes: 10 additions & 0 deletions test/build/extends/multi-level-extends/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const WebpackCLITestPlugin = require("../../../utils/webpack-cli-test-plugin");

module.exports = () => {
console.log("derived.webpack.config.js");

return {
extends: ["./base1.webpack.config.js"],
plugins: [new WebpackCLITestPlugin()],
};
};
15 changes: 15 additions & 0 deletions test/build/extends/multiple-configs/base.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const WebpackCLITestPlugin = require("../../../utils/webpack-cli-test-plugin");

module.exports = () => {
console.log("base.webpack.config.js");

return {
name: "base_config",
mode: "development",
plugins: [new WebpackCLITestPlugin()],

experiments: {
topLevelAwait: true,
},
};
};
1 change: 1 addition & 0 deletions test/build/extends/multiple-configs/src/index1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("i am index1.js");
1 change: 1 addition & 0 deletions test/build/extends/multiple-configs/src/index2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("i am index2.js");
16 changes: 16 additions & 0 deletions test/build/extends/multiple-configs/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = () => {
console.log("derived.webpack.config.js");

return [
{
name: "derived_config1",
extends: "./base.webpack.config.js",
entry: "./src/index1.js",
},
{
name: "derived_config2",
extends: "./base.webpack.config.js",
entry: "./src/index2.js",
},
];
};
9 changes: 9 additions & 0 deletions test/build/extends/multiple-extends/base1.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = () => {
console.log("base1.webpack.config.js");

return {
name: "base_config1",
mode: "development",
entry: "./src/index.js",
};
};
9 changes: 9 additions & 0 deletions test/build/extends/multiple-extends/base2.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = () => {
console.log("base2.webpack.config.js");

return {
name: "base_config2",
mode: "production",
entry: "./src/index2.js",
};
};
1 change: 1 addition & 0 deletions test/build/extends/multiple-extends/src/index2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("i am index2");
10 changes: 10 additions & 0 deletions test/build/extends/multiple-extends/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const WebpackCLITestPlugin = require("../../../utils/webpack-cli-test-plugin");

module.exports = () => {
console.log("derived.webpack.config.js");

return {
extends: ["./base1.webpack.config.js", "./base2.webpack.config.js"],
plugins: [new WebpackCLITestPlugin()],
};
};
9 changes: 9 additions & 0 deletions test/build/extends/simple-case/base.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = () => {
console.log("base.webpack.config.js");

return {
name: "base_config",
mode: "development",
entry: "./src/index.js",
};
};
1 change: 1 addition & 0 deletions test/build/extends/simple-case/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("index.js")
10 changes: 10 additions & 0 deletions test/build/extends/simple-case/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const WebpackCLITestPlugin = require("../../../utils/webpack-cli-test-plugin");

module.exports = () => {
console.log("derived.webpack.config.js");

return {
extends: "./base.webpack.config.js",
plugins: [new WebpackCLITestPlugin()],
};
};
Loading