Skip to content

Commit d04d0b9

Browse files
authored
feat: shareable webpack configs using extends (#3738)
1 parent c09c918 commit d04d0b9

38 files changed

+542
-14
lines changed

packages/configtest/src/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ class ConfigTestCommand {
2121

2222
if (Array.isArray(config.options)) {
2323
config.options.forEach((options) => {
24-
if (config.path.get(options)) {
25-
configPaths.add(config.path.get(options) as string);
24+
const loadedConfigPaths = config.path.get(options);
25+
26+
if (loadedConfigPaths) {
27+
loadedConfigPaths.forEach((path) => configPaths.add(path));
2628
}
2729
});
2830
} else {
2931
if (config.path.get(config.options)) {
30-
configPaths.add(config.path.get(config.options) as string);
32+
const loadedConfigPaths = config.path.get(config.options);
33+
34+
if (loadedConfigPaths) {
35+
loadedConfigPaths.forEach((path) => configPaths.add(path));
36+
}
3137
}
3238
}
3339

packages/webpack-cli/src/plugins/cli-plugin.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ export class CLIPlugin {
5353
logCompilation(`Compiler${name ? ` ${name}` : ""} starting... `);
5454

5555
if (configPath) {
56-
this.logger.log(`Compiler${name ? ` ${name}` : ""} is using config: '${configPath}'`);
56+
this.logger.log(
57+
`Compiler${name ? ` ${name}` : ""} is using config: ${configPath
58+
.map((path) => `'${path}'`)
59+
.join(", ")}`,
60+
);
5761
}
5862
});
5963

packages/webpack-cli/src/types.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ interface WebpackCLICommandOption extends CommanderOption {
9494

9595
interface WebpackCLIConfig {
9696
options: WebpackConfiguration | WebpackConfiguration[];
97-
path: WeakMap<object, string>;
97+
path: WeakMap<object, string[]>;
9898
}
9999

100100
interface WebpackCLICommand extends Command {
@@ -178,6 +178,7 @@ type WebpackDevServerOptions = DevServerConfig &
178178
config: string[];
179179
configName?: string[];
180180
disableInterpret?: boolean;
181+
extends?: string[];
181182
argv: Argv;
182183
};
183184

@@ -186,8 +187,10 @@ type Callback<T extends unknown[]> = (...args: T) => void;
186187
/**
187188
* Webpack
188189
*/
189-
190-
type WebpackConfiguration = Configuration;
190+
type WebpackConfiguration = Configuration & {
191+
// TODO add extends to webpack types
192+
extends?: string | string[];
193+
};
191194
type ConfigOptions = PotentialPromise<WebpackConfiguration | CallableOption>;
192195
type CallableOption = (env: Env | undefined, argv: Argv) => WebpackConfiguration;
193196
type WebpackCompiler = Compiler | MultiCompiler;
@@ -236,7 +239,7 @@ interface BasicPackageJsonContent {
236239
*/
237240

238241
interface CLIPluginOptions {
239-
configPath?: string;
242+
configPath?: string[];
240243
helpfulOutput: boolean;
241244
hot?: boolean | "only";
242245
progress?: boolean | "profile";

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,18 @@ class WebpackCLI implements IWebpackCLI {
975975
description: "Stop webpack-cli process with non-zero exit code on warnings from webpack",
976976
helpLevel: "minimum",
977977
},
978+
{
979+
name: "extends",
980+
alias: "e",
981+
configs: [
982+
{
983+
type: "string",
984+
},
985+
],
986+
multiple: true,
987+
description: "Extend webpack configuration",
988+
helpLevel: "minimum",
989+
},
978990
];
979991

980992
const minimumHelpFlags = [
@@ -1814,6 +1826,7 @@ class WebpackCLI implements IWebpackCLI {
18141826
return { options, path: configPath };
18151827
};
18161828

1829+
// TODO better name and better type
18171830
const config: WebpackCLIConfig = {
18181831
options: {} as WebpackConfiguration,
18191832
path: new WeakMap(),
@@ -1850,10 +1863,10 @@ class WebpackCLI implements IWebpackCLI {
18501863

18511864
if (isArray) {
18521865
(loadedConfig.options as ConfigOptions[]).forEach((options) => {
1853-
config.path.set(options, loadedConfig.path);
1866+
config.path.set(options, [loadedConfig.path]);
18541867
});
18551868
} else {
1856-
config.path.set(loadedConfig.options, loadedConfig.path);
1869+
config.path.set(loadedConfig.options, [loadedConfig.path]);
18571870
}
18581871
});
18591872

@@ -1892,10 +1905,10 @@ class WebpackCLI implements IWebpackCLI {
18921905

18931906
if (Array.isArray(config.options)) {
18941907
config.options.forEach((item) => {
1895-
config.path.set(item, loadedConfig.path);
1908+
config.path.set(item, [loadedConfig.path]);
18961909
});
18971910
} else {
1898-
config.path.set(loadedConfig.options, loadedConfig.path);
1911+
config.path.set(loadedConfig.options, [loadedConfig.path]);
18991912
}
19001913
}
19011914
}
@@ -1929,6 +1942,92 @@ class WebpackCLI implements IWebpackCLI {
19291942
}
19301943
}
19311944

1945+
const resolveExtends = async (
1946+
config: WebpackConfiguration,
1947+
configPaths: WebpackCLIConfig["path"],
1948+
extendsPaths: string[],
1949+
): Promise<WebpackConfiguration> => {
1950+
delete config.extends;
1951+
1952+
const loadedConfigs = await Promise.all(
1953+
extendsPaths.map((extendsPath) =>
1954+
loadConfigByPath(path.resolve(extendsPath), options.argv),
1955+
),
1956+
);
1957+
1958+
const merge = await this.tryRequireThenImport<typeof webpackMerge>("webpack-merge");
1959+
const loadedOptions = loadedConfigs.flatMap((config) => config.options);
1960+
1961+
if (loadedOptions.length > 0) {
1962+
const prevPaths = configPaths.get(config);
1963+
const loadedPaths = loadedConfigs.flatMap((config) => config.path);
1964+
1965+
if (prevPaths) {
1966+
const intersection = loadedPaths.filter((element) => prevPaths.includes(element));
1967+
1968+
if (intersection.length > 0) {
1969+
this.logger.error(`Recursive configuration detected, exiting.`);
1970+
process.exit(2);
1971+
}
1972+
}
1973+
1974+
config = merge(
1975+
...(loadedOptions as [WebpackConfiguration, ...WebpackConfiguration[]]),
1976+
config,
1977+
);
1978+
1979+
if (prevPaths) {
1980+
configPaths.set(config, [...prevPaths, ...loadedPaths]);
1981+
}
1982+
}
1983+
1984+
if (config.extends) {
1985+
const extendsPaths = typeof config.extends === "string" ? [config.extends] : config.extends;
1986+
1987+
config = await resolveExtends(config, configPaths, extendsPaths);
1988+
}
1989+
1990+
return config;
1991+
};
1992+
1993+
// The `extends` param in CLI gets priority over extends in config file
1994+
if (options.extends && options.extends.length > 0) {
1995+
const extendsPaths = options.extends;
1996+
1997+
if (Array.isArray(config.options)) {
1998+
config.options = await Promise.all(
1999+
config.options.map((options) => resolveExtends(options, config.path, extendsPaths)),
2000+
);
2001+
} else {
2002+
// load the config from the extends option
2003+
config.options = await resolveExtends(config.options, config.path, extendsPaths);
2004+
}
2005+
}
2006+
// if no extends option is passed, check if the config file has extends
2007+
else if (Array.isArray(config.options) && config.options.some((options) => options.extends)) {
2008+
config.options = await Promise.all(
2009+
config.options.map((options) => {
2010+
if (options.extends) {
2011+
return resolveExtends(
2012+
options,
2013+
config.path,
2014+
typeof options.extends === "string" ? [options.extends] : options.extends,
2015+
);
2016+
} else {
2017+
return options;
2018+
}
2019+
}),
2020+
);
2021+
} else if (!Array.isArray(config.options) && config.options.extends) {
2022+
config.options = await resolveExtends(
2023+
config.options,
2024+
config.path,
2025+
typeof config.options.extends === "string"
2026+
? [config.options.extends]
2027+
: config.options.extends,
2028+
);
2029+
}
2030+
19322031
if (options.merge) {
19332032
const merge = await this.tryRequireThenImport<typeof webpackMerge>("webpack-merge");
19342033

@@ -1946,11 +2045,13 @@ class WebpackCLI implements IWebpackCLI {
19462045
const configPath = config.path.get(options);
19472046
const mergedOptions = merge(accumulator, options);
19482047

1949-
mergedConfigPaths.push(configPath as string);
2048+
if (configPath) {
2049+
mergedConfigPaths.push(...configPath);
2050+
}
19502051

19512052
return mergedOptions;
19522053
}, {});
1953-
config.path.set(config.options, mergedConfigPaths as unknown as string);
2054+
config.path.set(config.options, mergedConfigPaths);
19542055
}
19552056

19562057
return config;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = () => {
2+
console.log("base.webpack.config.js");
3+
4+
return {
5+
name: "base_config",
6+
mode: "development",
7+
entry: "./src/index.js",
8+
};
9+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = () => {
2+
console.log("deep.base.webpack.config.js");
3+
4+
return {
5+
name: "base_config",
6+
mode: "development",
7+
entry: "./src/index.js",
8+
bail: true,
9+
};
10+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("i am index.js")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const WebpackCLITestPlugin = require("../../../utils/webpack-cli-test-plugin");
2+
3+
module.exports = () => {
4+
console.log("derived.webpack.config.js");
5+
6+
return {
7+
plugins: [new WebpackCLITestPlugin()],
8+
};
9+
};

0 commit comments

Comments
 (0)