From 1c7ab6dfd56facc8fd898e3ade888ae52eda9158 Mon Sep 17 00:00:00 2001 From: Fy <1114550440@qq.com> Date: Tue, 14 Feb 2023 14:44:15 +0800 Subject: [PATCH] feat: rspack-cli and devServer support multiCompiler (#1816) Co-authored-by: fengyu.shelby --- .changeset/clever-years-notice.md | 7 + packages/rspack-cli/src/commands/build.ts | 12 +- packages/rspack-cli/src/commands/serve.ts | 65 +++++- packages/rspack-cli/src/rspack-cli.ts | 217 ++++++++++++------ packages/rspack-cli/src/types.ts | 1 + packages/rspack-cli/src/utils/options.ts | 5 + .../normalizeOptions.test.ts.snap | 77 ------- .../tests/normalizeOptions.test.ts | 26 +-- packages/rspack/src/config/index.ts | 6 +- .../src/error/ConcurrentCompilationError.ts | 2 +- packages/rspack/src/multiCompiler.ts | 7 +- packages/rspack/src/rspack.ts | 20 +- packages/rspack/tests/Defaults.unittest.ts | 2 +- packages/rspack/tests/MultiCompiler.test.ts | 7 +- 14 files changed, 266 insertions(+), 188 deletions(-) create mode 100644 .changeset/clever-years-notice.md diff --git a/.changeset/clever-years-notice.md b/.changeset/clever-years-notice.md new file mode 100644 index 000000000000..7e5a8e21550b --- /dev/null +++ b/.changeset/clever-years-notice.md @@ -0,0 +1,7 @@ +--- +"@rspack/core": patch +"@rspack/cli": patch +"@rspack/dev-server": patch +--- + +feat: rspack-cli and devServer support multiCompiler diff --git a/packages/rspack-cli/src/commands/build.ts b/packages/rspack-cli/src/commands/build.ts index 0e2341d17b07..0389d073f447 100644 --- a/packages/rspack-cli/src/commands/build.ts +++ b/packages/rspack-cli/src/commands/build.ts @@ -3,6 +3,8 @@ import type { RspackCLI } from "../rspack-cli"; import { RspackCommand } from "../types"; import { commonOptions } from "../utils/options"; import { Stats } from "@rspack/core/src/stats"; +import { Compiler } from "@rspack/core"; +import MultiStats from "@rspack/core/src/multiStats"; export class BuildCommand implements RspackCommand { async apply(cli: RspackCLI): Promise { @@ -28,7 +30,7 @@ export class BuildCommand implements RspackCommand { createJsonStringifyStream = jsonExt.stringifyStream; } - const callback = (error, stats: Stats) => { + const callback = (error, stats: Stats | MultiStats) => { if (error) { logger.error(error); process.exit(2); @@ -39,7 +41,13 @@ export class BuildCommand implements RspackCommand { if (!compiler || !stats) { return; } - const statsOptions = compiler.options + const statsOptions = cli.isMultipleCompiler(compiler) + ? { + children: compiler.compilers.map(compiler => + compiler.options ? compiler.options.stats : undefined + ) + } + : compiler.options ? compiler.options.stats : undefined; if (options.json && createJsonStringifyStream) { diff --git a/packages/rspack-cli/src/commands/serve.ts b/packages/rspack-cli/src/commands/serve.ts index c3e4375db399..4fd72435caa7 100644 --- a/packages/rspack-cli/src/commands/serve.ts +++ b/packages/rspack-cli/src/commands/serve.ts @@ -1,7 +1,8 @@ import type { RspackCLI } from "../rspack-cli"; import { RspackDevServer } from "@rspack/dev-server"; import { RspackCommand } from "../types"; -import { commonOptions, normalizeEnv } from "../utils/options"; +import { commonOptions } from "../utils/options"; +import { Compiler, DevServer } from "@rspack/core"; export class ServeCommand implements RspackCommand { async apply(cli: RspackCLI): Promise { cli.program.command( @@ -16,11 +17,65 @@ export class ServeCommand implements RspackCommand { } }; const compiler = await cli.createCompiler(rspackOptions, "development"); - const server = new RspackDevServer( - compiler.options.devServer, - compiler + const compilers = cli.isMultipleCompiler(compiler) + ? compiler.compilers + : [compiler]; + const possibleCompilers = compilers.filter( + (compiler: Compiler) => compiler.options.devServer ); - await server.start(); + + const usedPorts: number[] = []; + const servers: RspackDevServer[] = []; + + /** + * Webpack uses an Array of compilerForDevServer, + * however according to it's doc https://webpack.js.org/configuration/dev-server/#devserverhot + * It should use only the first one + * + * Choose the one for configure devServer + */ + const compilerForDevServer = + possibleCompilers.length > 0 ? possibleCompilers[0] : compilers[0]; + + /** + * Rspack relies on devServer.hot to enable HMR + */ + for (const compiler of compilers) { + const devServer = (compiler.options.devServer ??= {}); + devServer.hot ??= true; + } + + const result = (compilerForDevServer.options.devServer ??= {}); + /** + * Enable this to tell Rspack that we need to enable React Refresh by default + */ + result.hot ??= true; + + const devServerOptions = result as DevServer; + if (devServerOptions.port) { + const portNumber = Number(devServerOptions.port); + + if (usedPorts.find(port => portNumber === port)) { + throw new Error( + "Unique ports must be specified for each devServer option in your rspack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config." + ); + } + + usedPorts.push(portNumber); + } + + try { + const server = new RspackDevServer(devServerOptions, compiler); + + await server.start(); + + servers.push(server); + } catch (error) { + const logger = cli.getLogger(); + logger.error(error); + + process.exit(2); + } } ); } diff --git a/packages/rspack-cli/src/rspack-cli.ts b/packages/rspack-cli/src/rspack-cli.ts index 74ba5a2cedb1..9cad8cf52403 100644 --- a/packages/rspack-cli/src/rspack-cli.ts +++ b/packages/rspack-cli/src/rspack-cli.ts @@ -6,8 +6,19 @@ import fs from "fs"; import { RspackCLIColors, RspackCLILogger, RspackCLIOptions } from "./types"; import { BuildCommand } from "./commands/build"; import { ServeCommand } from "./commands/serve"; -import { rspack, RspackOptions, createCompiler } from "@rspack/core"; +import { + RspackOptions, + MultiCompilerOptions, + createMultiCompiler, + createCompiler, + MultiCompiler, + Compiler, + rspack, + MultiRspackOptions +} from "@rspack/core"; import { normalizeEnv } from "./utils/options"; +import { Mode } from "@rspack/core/src/config/mode"; + const defaultConfig = "rspack.config.js"; const defaultEntry = "src/index.js"; type Callback = (err: Error, res?: T) => void; @@ -19,7 +30,10 @@ export class RspackCLI { this.colors = this.createColors(); this.program = yargs(); } - async createCompiler(options: RspackCLIOptions, rspackEnv: RspackEnv) { + async createCompiler( + options: RspackCLIOptions, + rspackEnv: RspackEnv + ): Promise { if (typeof options.nodeEnv === "string") { process.env.NODE_ENV = options.nodeEnv; } else { @@ -27,7 +41,8 @@ export class RspackCLI { } let config = await this.loadConfig(options); config = await this.buildConfig(config, options, rspackEnv); - const compiler = createCompiler(config); + // @ts-ignore + const compiler = rspack(config); return compiler; } createColors(useColor?: boolean): RspackCLIColors { @@ -71,85 +86,108 @@ export class RspackCLI { } } async buildConfig( - item: any, + item: RspackOptions | MultiRspackOptions, options: RspackCLIOptions, rspackEnv: RspackEnv - ) { - const isEnvProduction = rspackEnv === "production"; - const isEnvDevelopment = rspackEnv === "development"; - - if (options.analyze) { - const { BundleAnalyzerPlugin } = await import("webpack-bundle-analyzer"); - (item.plugins ??= []).push({ - name: "rspack-bundle-analyzer", - apply(compiler) { - new BundleAnalyzerPlugin({ - generateStatsFile: true - }).apply(compiler as any); - } - }); - } - // auto set default mode if user config don't set it - if (!item.mode) { - item.mode = rspackEnv ?? "none"; - } - // user parameters always has highest priority than default mode and config mode - if (options.mode) { - item.mode = options.mode; - } - - // false is also a valid value for sourcemap, so don't override it - if (typeof item.devtool === "undefined") { - item.devtool = isEnvProduction ? "source-map" : "cheap-module-source-map"; - } - item.builtins = { - ...item.builtins, - minify: item.builtins?.minify ?? item.mode === "production" - }; + ): Promise { + const internalBuildConfig = async (item: RspackOptions) => { + const isEnvProduction = rspackEnv === "production"; + const isEnvDevelopment = rspackEnv === "development"; - // no emit assets when run dev server, it will use node_binding api get file content - if (typeof item.builtins.noEmitAssets === "undefined") { - item.builtins.noEmitAssets = false; // @FIXME memory fs currently cause problems for outputFileSystem, so we disable it temporarily - } + if (options.analyze) { + const { BundleAnalyzerPlugin } = await import( + "webpack-bundle-analyzer" + ); + (item.plugins ??= []).push({ + name: "rspack-bundle-analyzer", + apply(compiler) { + new BundleAnalyzerPlugin({ + generateStatsFile: true + }).apply(compiler as any); + } + }); + } + // auto set default mode if user config don't set it + if (!item.mode) { + item.mode = rspackEnv ?? "none"; + } + // user parameters always has highest priority than default mode and config mode + if (options.mode) { + item.mode = options.mode as Mode; + } - // Tells webpack to set process.env.NODE_ENV to a given string value. - // optimization.nodeEnv uses DefinePlugin unless set to false. - // optimization.nodeEnv defaults to mode if set, else falls back to 'production'. - // See doc: https://webpack.js.org/configuration/optimization/#optimizationnodeenv - // See source: https://github.com/webpack/webpack/blob/8241da7f1e75c5581ba535d127fa66aeb9eb2ac8/lib/WebpackOptionsApply.js#L563 - - // When mode is set to 'none', optimization.nodeEnv defaults to false. - if (item.mode !== "none") { - item.builtins.define = { - // User defined `process.env.NODE_ENV` always has highest priority than default define - "process.env.NODE_ENV": JSON.stringify(item.mode), - ...item.builtins.define + // false is also a valid value for sourcemap, so don't override it + if (typeof item.devtool === "undefined") { + item.devtool = isEnvProduction + ? "source-map" + : "cheap-module-source-map"; + } + item.builtins = { + ...item.builtins, + minify: item.builtins?.minify ?? item.mode === "production" }; - } - item.output = { - ...item.output, - publicPath: item.output?.publicPath ?? "/" + // no emit assets when run dev server, it will use node_binding api get file content + if (typeof item.builtins.noEmitAssets === "undefined") { + item.builtins.noEmitAssets = false; // @FIXME memory fs currently cause problems for outputFileSystem, so we disable it temporarily + } + + // Tells webpack to set process.env.NODE_ENV to a given string value. + // optimization.nodeEnv uses DefinePlugin unless set to false. + // optimization.nodeEnv defaults to mode if set, else falls back to 'production'. + // See doc: https://webpack.js.org/configuration/optimization/#optimizationnodeenv + // See source: https://github.com/webpack/webpack/blob/8241da7f1e75c5581ba535d127fa66aeb9eb2ac8/lib/WebpackOptionsApply.js#L563 + + // When mode is set to 'none', optimization.nodeEnv defaults to false. + if (item.mode !== "none") { + item.builtins.define = { + // User defined `process.env.NODE_ENV` always has highest priority than default define + "process.env.NODE_ENV": JSON.stringify(item.mode), + ...item.builtins.define + }; + } + + item.output = { + ...item.output, + publicPath: item.output?.publicPath ?? "/" + }; + if (typeof item.stats === "undefined") { + item.stats = { preset: "normal" }; + } else if (typeof item.stats === "boolean") { + item.stats = item.stats ? { preset: "normal" } : { preset: "none" }; + } else if (typeof item.stats === "string") { + item.stats = { + preset: item.stats as + | "normal" + | "none" + | "verbose" + | "errors-only" + | "errors-warnings" + }; + } + if (this.colors.isColorSupported && !item.stats.colors) { + item.stats.colors = true; + } + return item; }; - if (typeof item.stats === "undefined") { - item.stats = { preset: "normal" }; - } else if (typeof item.stats === "boolean") { - item.stats = item.stats ? { preset: "normal" } : { preset: "none" }; - } else if (typeof item.stats === "string") { - item.stats = { preset: item.stats }; - } - if (this.colors.isColorSupported && !item.stats.colors) { - item.stats.colors = true; + + if (Array.isArray(item)) { + return Promise.all(item.map(internalBuildConfig)); + } else { + return internalBuildConfig(item as RspackOptions); } - return item; } - async loadConfig(options: RspackCLIOptions): Promise { + async loadConfig( + options: RspackCLIOptions + ): Promise { let loadedConfig: + | undefined | RspackOptions + | MultiRspackOptions | (( env: Record, argv: Record - ) => RspackOptions); + ) => RspackOptions | MultiRspackOptions); // if we pass config paras if (options.config) { const resolvedConfigPath = path.resolve(process.cwd(), options.config); @@ -177,9 +215,52 @@ export class RspackCLI { }; } } + + if (options.configName) { + const notFoundConfigNames: string[] = []; + + // @ts-ignore + loadedConfig = options.configName.map((configName: string) => { + let found: RspackOptions | MultiRspackOptions | undefined; + + if (Array.isArray(loadedConfig)) { + found = loadedConfig.find(options => options.name === configName); + } else { + found = + (loadedConfig as RspackOptions).name === configName + ? (loadedConfig as RspackOptions) + : undefined; + } + + if (!found) { + notFoundConfigNames.push(configName); + } + + return found; + }); + + if (notFoundConfigNames.length > 0) { + this.getLogger().error( + notFoundConfigNames + .map( + configName => + `Configuration with the name "${configName}" was not found.` + ) + .join(" ") + ); + process.exit(2); + } + } + if (typeof loadedConfig === "function") { loadedConfig = loadedConfig(options.argv?.env, options.argv); } return loadedConfig; } + + isMultipleCompiler( + compiler: Compiler | MultiCompiler + ): compiler is MultiCompiler { + return Boolean((compiler as MultiCompiler).compilers); + } } diff --git a/packages/rspack-cli/src/types.ts b/packages/rspack-cli/src/types.ts index 5c8e57e9e0ef..27ff9d3a97ac 100644 --- a/packages/rspack-cli/src/types.ts +++ b/packages/rspack-cli/src/types.ts @@ -29,6 +29,7 @@ export interface RspackCLIOptions { argv?: Record; env?: Record; nodeEnv: string; + configName?: string[]; } export interface RspackCommand { diff --git a/packages/rspack-cli/src/utils/options.ts b/packages/rspack-cli/src/utils/options.ts index 4efb1594dd9a..7ed309ab341b 100644 --- a/packages/rspack-cli/src/utils/options.ts +++ b/packages/rspack-cli/src/utils/options.ts @@ -32,6 +32,11 @@ export const commonOptions = (yargs: yargs.Argv<{}>) => { type: "boolean", default: false, describe: "devtool" + }, + configName: { + type: "array", + string: true, + describe: "Name of the configuration to use." } }); }; diff --git a/packages/rspack-dev-server/tests/__snapshots__/normalizeOptions.test.ts.snap b/packages/rspack-dev-server/tests/__snapshots__/normalizeOptions.test.ts.snap index 5e89b62e4258..835d3cda75b9 100644 --- a/packages/rspack-dev-server/tests/__snapshots__/normalizeOptions.test.ts.snap +++ b/packages/rspack-dev-server/tests/__snapshots__/normalizeOptions.test.ts.snap @@ -12,83 +12,6 @@ exports[`normalize options snapshot additional entires should added 1`] = ` } `; -exports[`normalize options snapshot compier.options.devServer should be equal to server.options when devServer is undefined 1`] = ` -{ - "builtins": { - "browserslist": undefined, - "decorator": { - "emitMetadata": true, - "legacy": true, - }, - "define": {}, - "emotion": undefined, - "html": [], - "minify": { - "enable": true, - "passes": 1, - }, - "react": { - "development": true, - "refresh": true, - }, - }, - "devServer": { - "allowedHosts": "auto", - "bonjour": false, - "client": { - "logging": "info", - "overlay": true, - "reconnect": 10, - "webSocketURL": {}, - }, - "compress": true, - "devMiddleware": {}, - "historyApiFallback": false, - "host": undefined, - "hot": true, - "liveReload": true, - "magicHtml": true, - "open": [], - "port": 8080, - "server": { - "options": {}, - "type": "http", - }, - "setupExitSignals": true, - "static": [ - { - "directory": "/public", - "publicPath": [ - "/", - ], - "serveIndex": { - "icons": true, - }, - "staticOptions": {}, - "watch": { - "alwaysStat": true, - "atomic": false, - "followSymlinks": false, - "ignoreInitial": true, - "ignorePermissionErrors": true, - "ignored": undefined, - "interval": undefined, - "persistent": true, - "usePolling": false, - }, - }, - ], - "watchFiles": [], - "webSocketServer": { - "options": { - "path": "/ws", - }, - "type": "ws", - }, - }, -} -`; - exports[`normalize options snapshot no options 1`] = ` { "allowedHosts": "auto", diff --git a/packages/rspack-dev-server/tests/normalizeOptions.test.ts b/packages/rspack-dev-server/tests/normalizeOptions.test.ts index c02edf266e13..ebdd56a76592 100644 --- a/packages/rspack-dev-server/tests/normalizeOptions.test.ts +++ b/packages/rspack-dev-server/tests/normalizeOptions.test.ts @@ -42,21 +42,7 @@ describe("normalize options snapshot", () => { hot: true } }); - const server = new RspackDevServer(compiler.options.devServer, compiler); - await server.start(); - expect({ - builtins: compiler.options.builtins, - devServer: compiler.options.devServer - }).toMatchSnapshot(); - await server.stop(); - // should pointed to the same memory. - expect(compiler.options.devServer === server.options).toBeTruthy(); - }); - it("compier.options.devServer should be equal to server.options when devServer is undefined", async () => { - const compiler = createCompiler({ - stats: "none" - }); - const server = new RspackDevServer(compiler.options.devServer, compiler); + const server = new RspackDevServer(compiler.options.devServer!, compiler); await server.start(); expect({ builtins: compiler.options.builtins, @@ -80,7 +66,10 @@ async function match(config: RspackOptions) { } } }); - const server = new RspackDevServer(compiler.options.devServer, compiler); + const server = new RspackDevServer( + compiler.options.devServer ?? {}, + compiler + ); await server.start(); expect(server.options).toMatchSnapshot(); await server.stop(); @@ -97,7 +86,10 @@ async function matchAdditionEntries(config: RspackOptions) { } } }); - const server = new RspackDevServer(compiler.options.devServer, compiler); + const server = new RspackDevServer( + compiler.options.devServer ?? {}, + compiler + ); await server.start(); const entires = Object.entries(compiler.options.entry); // some hack for snapshot diff --git a/packages/rspack/src/config/index.ts b/packages/rspack/src/config/index.ts index eeb935f25727..def77cee70a0 100644 --- a/packages/rspack/src/config/index.ts +++ b/packages/rspack/src/config/index.ts @@ -85,9 +85,7 @@ export interface RspackOptionsNormalized { entry: ResolvedEntry; context: ResolvedContext; plugins: PluginInstance[]; - // TODO: this is optional in webpack, but we need 'devServer.hot' to enable - // HMR which is implemented in rust side. - devServer: DevServer; + devServer?: DevServer; module: ResolvedModule; target: ResolvedTarget; mode: ResolvedMode; @@ -176,7 +174,7 @@ export function getNormalizedRspackOptions( node, watch: config.watch, watchOptions: cloneObject(config.watchOptions), - devServer: config.devServer ?? {} + devServer: config.devServer }; } diff --git a/packages/rspack/src/error/ConcurrentCompilationError.ts b/packages/rspack/src/error/ConcurrentCompilationError.ts index fdea82c4b1d0..3b8249550468 100644 --- a/packages/rspack/src/error/ConcurrentCompilationError.ts +++ b/packages/rspack/src/error/ConcurrentCompilationError.ts @@ -16,6 +16,6 @@ export default class ConcurrentCompilationError extends Error { super(); this.name = "ConcurrentCompilationError"; this.message = - "You ran run rspack twice. Each instance only supports a single concurrent compilation at a time."; + "You ran rspack twice. Each instance only supports a single concurrent compilation at a time."; } } diff --git a/packages/rspack/src/multiCompiler.ts b/packages/rspack/src/multiCompiler.ts index c1d2d86b95ed..4992dd249c82 100644 --- a/packages/rspack/src/multiCompiler.ts +++ b/packages/rspack/src/multiCompiler.ts @@ -50,12 +50,15 @@ export interface Node { | "done"; } -export type MultiCompilerOptions = { +export interface MultiCompilerOptions extends ReadonlyArray { /** * how many Compilers are allows to run at the same time in parallel */ parallelism?: number; -} & RspackOptions[]; +} + +export type MultiRspackOptions = ReadonlyArray & + MultiCompilerOptions; export class MultiCompiler { // @ts-expect-error diff --git a/packages/rspack/src/rspack.ts b/packages/rspack/src/rspack.ts index 8e07058127dc..cbf6760d7cd4 100644 --- a/packages/rspack/src/rspack.ts +++ b/packages/rspack/src/rspack.ts @@ -9,11 +9,15 @@ import util from "util"; import { RspackOptionsApply } from "./rspackOptionsApply"; import NodeEnvironmentPlugin from "./node/NodeEnvironmentPlugin"; -import { MultiCompiler, MultiCompilerOptions } from "./multiCompiler"; +import { + MultiCompiler, + MultiCompilerOptions, + MultiRspackOptions +} from "./multiCompiler"; +import { Callback } from "tapable"; import MultiStats from "./multiStats"; -type Callback = (err: Error, t: T) => void; -function createMultiCompiler(options: MultiCompilerOptions): MultiCompiler { +function createMultiCompiler(options: MultiRspackOptions): MultiCompiler { const compilers = options.map(option => { const compiler = createCompiler(option); @@ -81,10 +85,13 @@ function createCompiler(userOptions: RspackOptions): Compiler { function rspack( options: MultiCompilerOptions, - callback?: Callback + callback?: Callback ): MultiCompiler; -function rspack(options: RspackOptions, callback?: Callback): Compiler; -function rspack(options: any, callback?: Callback) { +function rspack( + options: RspackOptions, + callback?: Callback +): Compiler; +function rspack(options: any, callback?: Callback) { let compiler: Compiler | MultiCompiler; if (Array.isArray(options)) { compiler = createMultiCompiler(options); @@ -93,7 +100,6 @@ function rspack(options: any, callback?: Callback) { } if (callback) { - // @ts-expect-error compiler.run(callback); return compiler; } else { diff --git a/packages/rspack/tests/Defaults.unittest.ts b/packages/rspack/tests/Defaults.unittest.ts index 959fdcaf3353..9801d9512ab5 100644 --- a/packages/rspack/tests/Defaults.unittest.ts +++ b/packages/rspack/tests/Defaults.unittest.ts @@ -100,7 +100,7 @@ describe("snapshots", () => { }, "context": "", "dependencies": undefined, - "devServer": {}, + "devServer": undefined, "devtool": "", "entry": { "main": { diff --git a/packages/rspack/tests/MultiCompiler.test.ts b/packages/rspack/tests/MultiCompiler.test.ts index 2c08c6610886..956c40e081fe 100644 --- a/packages/rspack/tests/MultiCompiler.test.ts +++ b/packages/rspack/tests/MultiCompiler.test.ts @@ -1,8 +1,7 @@ import path from "path"; -import { createFsFromVolume, Volume, IFs } from "memfs"; -import { Callback } from "tapable"; +import { createFsFromVolume, Volume } from "memfs"; import { FileSystemInfoEntry, Watcher } from "../src/util/fs"; -import { MultiCompilerOptions, rspack, RspackOptions } from "../src"; +import { MultiRspackOptions, rspack, RspackOptions } from "../src"; const createMultiCompiler = ( options?: RspackOptions[] | { parallelism?: number } @@ -388,7 +387,7 @@ describe("MultiCompiler", function () { }); }); it("should respect parallelism when using invalidate", done => { - const configs: MultiCompilerOptions = [ + const configs: MultiRspackOptions = [ { name: "a", mode: "development",