index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const WEBPACK_PACKAGE = process.env.WEBPACK_PACKAGE || "webpack";
  4. const WEBPACK_DEV_SERVER_PACKAGE = process.env.WEBPACK_DEV_SERVER_PACKAGE || "webpack-dev-server";
  5. class ServeCommand {
  6. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
  7. async apply(cli) {
  8. const loadDevServerOptions = () => {
  9. // TODO simplify this after drop webpack v4 and webpack-dev-server v3
  10. // eslint-disable-next-line @typescript-eslint/no-var-requires
  11. const devServer = require(WEBPACK_DEV_SERVER_PACKAGE);
  12. const isNewDevServerCLIAPI = typeof devServer.schema !== "undefined";
  13. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  14. let options = {};
  15. if (isNewDevServerCLIAPI) {
  16. if (cli.webpack.cli && typeof cli.webpack.cli.getArguments === "function") {
  17. options = cli.webpack.cli.getArguments(devServer.schema);
  18. }
  19. else {
  20. options = devServer.cli.getArguments();
  21. }
  22. }
  23. else {
  24. options = require(`${WEBPACK_DEV_SERVER_PACKAGE}/bin/cli-flags`);
  25. }
  26. // Old options format
  27. // { devServer: [{...}, {}...] }
  28. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  29. // @ts-ignore
  30. if (options.devServer) {
  31. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  32. // @ts-ignore
  33. return options.devServer;
  34. }
  35. // New options format
  36. // { flag1: {}, flag2: {} }
  37. return Object.keys(options).map((key) => {
  38. options[key].name = key;
  39. return options[key];
  40. });
  41. };
  42. await cli.makeCommand({
  43. name: "serve [entries...]",
  44. alias: ["server", "s"],
  45. description: "Run the webpack dev server.",
  46. usage: "[entries...] [options]",
  47. pkg: "@webpack-cli/serve",
  48. dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE],
  49. }, async () => {
  50. let devServerFlags = [];
  51. cli.webpack = await cli.loadWebpack();
  52. try {
  53. devServerFlags = loadDevServerOptions();
  54. }
  55. catch (error) {
  56. cli.logger.error(`You need to install 'webpack-dev-server' for running 'webpack serve'.\n${error}`);
  57. process.exit(2);
  58. }
  59. const builtInOptions = cli.getBuiltInOptions().filter(
  60. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  61. (option) => option.name !== "watch");
  62. return [...builtInOptions, ...devServerFlags];
  63. },
  64. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  65. async (entries, options) => {
  66. const builtInOptions = cli.getBuiltInOptions();
  67. let devServerFlags = [];
  68. try {
  69. devServerFlags = loadDevServerOptions();
  70. }
  71. catch (error) {
  72. // Nothing, to prevent future updates
  73. }
  74. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  75. const webpackCLIOptions = {};
  76. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  77. const devServerCLIOptions = {};
  78. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  79. const processors = [];
  80. for (const optionName in options) {
  81. const kebabedOption = cli.toKebabCase(optionName);
  82. // `webpack-dev-server` has own logic for the `--hot` option
  83. const isBuiltInOption = kebabedOption !== "hot" &&
  84. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  85. builtInOptions.find((builtInOption) => builtInOption.name === kebabedOption);
  86. if (isBuiltInOption) {
  87. webpackCLIOptions[optionName] = options[optionName];
  88. }
  89. else {
  90. const needToProcess = devServerFlags.find(
  91. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  92. (devServerOption) => devServerOption.name === kebabedOption && devServerOption.processor);
  93. if (needToProcess) {
  94. processors.push(needToProcess.processor);
  95. }
  96. devServerCLIOptions[optionName] = options[optionName];
  97. }
  98. }
  99. for (const processor of processors) {
  100. processor(devServerCLIOptions);
  101. }
  102. if (entries.length > 0) {
  103. webpackCLIOptions.entry = [...entries, ...(webpackCLIOptions.entry || [])];
  104. }
  105. webpackCLIOptions.argv = Object.assign(Object.assign({}, options), { env: Object.assign({ WEBPACK_SERVE: true }, options.env) });
  106. const compiler = await cli.createCompiler(webpackCLIOptions);
  107. if (!compiler) {
  108. return;
  109. }
  110. const servers = [];
  111. if (cli.needWatchStdin(compiler) || devServerCLIOptions.stdin) {
  112. // TODO remove in the next major release
  113. // Compatibility with old `stdin` option for `webpack-dev-server`
  114. // Should be removed for the next major release on both sides
  115. if (devServerCLIOptions.stdin) {
  116. delete devServerCLIOptions.stdin;
  117. }
  118. process.stdin.on("end", () => {
  119. Promise.all(servers.map((server) => {
  120. if (typeof server.stop === "function") {
  121. return server.stop();
  122. }
  123. // TODO remove in the next major release
  124. return new Promise((resolve) => {
  125. server.close(() => {
  126. resolve();
  127. });
  128. });
  129. })).then(() => {
  130. process.exit(0);
  131. });
  132. });
  133. process.stdin.resume();
  134. }
  135. // eslint-disable-next-line @typescript-eslint/no-var-requires
  136. const DevServer = require(WEBPACK_DEV_SERVER_PACKAGE);
  137. const isNewDevServerCLIAPI = typeof DevServer.schema !== "undefined";
  138. let devServerVersion;
  139. try {
  140. // eslint-disable-next-line @typescript-eslint/no-var-requires
  141. devServerVersion = require(`${WEBPACK_DEV_SERVER_PACKAGE}/package.json`).version;
  142. }
  143. catch (err) {
  144. cli.logger.error(`You need to install 'webpack-dev-server' for running 'webpack serve'.\n${err}`);
  145. process.exit(2);
  146. }
  147. const compilers = typeof compiler.compilers !== "undefined" ? compiler.compilers : [compiler];
  148. const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer);
  149. const compilersForDevServer = possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]];
  150. const isDevServer4 = devServerVersion.startsWith("4");
  151. const usedPorts = [];
  152. for (const compilerForDevServer of compilersForDevServer) {
  153. let devServerOptions;
  154. if (isNewDevServerCLIAPI) {
  155. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  156. const args = devServerFlags.reduce((accumulator, flag) => {
  157. accumulator[flag.name] = flag;
  158. return accumulator;
  159. }, {});
  160. const values = Object.keys(devServerCLIOptions).reduce(
  161. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  162. (accumulator, name) => {
  163. const kebabName = cli.toKebabCase(name);
  164. if (args[kebabName]) {
  165. accumulator[kebabName] = options[name];
  166. }
  167. return accumulator;
  168. }, {});
  169. const result = Object.assign({}, (compilerForDevServer.options.devServer || {}));
  170. const problems = (cli.webpack.cli && typeof cli.webpack.cli.processArguments === "function"
  171. ? cli.webpack.cli
  172. : DevServer.cli).processArguments(args, result, values);
  173. if (problems) {
  174. const groupBy = (xs, key) => {
  175. return xs.reduce((rv, x) => {
  176. (rv[x[key]] = rv[x[key]] || []).push(x);
  177. return rv;
  178. }, {});
  179. };
  180. const problemsByPath = groupBy(problems, "path");
  181. for (const path in problemsByPath) {
  182. const problems = problemsByPath[path];
  183. problems.forEach((problem) => {
  184. cli.logger.error(`${cli.capitalizeFirstLetter(problem.type.replace(/-/g, " "))}${problem.value ? ` '${problem.value}'` : ""} for the '--${problem.argument}' option${problem.index ? ` by index '${problem.index}'` : ""}`);
  185. if (problem.expected) {
  186. cli.logger.error(`Expected: '${problem.expected}'`);
  187. }
  188. });
  189. }
  190. process.exit(2);
  191. }
  192. devServerOptions = result;
  193. }
  194. else {
  195. // TODO remove in the next major release
  196. const mergeOptions = (devServerOptions, devServerCliOptions) => {
  197. // CLI options should take precedence over devServer options,
  198. // and CLI options should have no default values included
  199. const options = Object.assign(Object.assign({}, devServerOptions), devServerCliOptions);
  200. if (devServerOptions.client && devServerCliOptions.client) {
  201. // the user could set some client options in their devServer config,
  202. // then also specify client options on the CLI
  203. options.client = Object.assign(Object.assign({}, devServerOptions.client), devServerCliOptions.client);
  204. }
  205. return options;
  206. };
  207. devServerOptions = mergeOptions(compilerForDevServer.options.devServer || {}, devServerCLIOptions);
  208. }
  209. // TODO remove in the next major release
  210. if (!isDevServer4) {
  211. const getPublicPathOption = () => {
  212. const normalizePublicPath = (publicPath) => typeof publicPath === "undefined" || publicPath === "auto" ? "/" : publicPath;
  213. if (options.outputPublicPath) {
  214. return normalizePublicPath(compilerForDevServer.options.output.publicPath);
  215. }
  216. if (devServerOptions.publicPath) {
  217. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  218. // @ts-ignore
  219. return normalizePublicPath(devServerOptions.publicPath);
  220. }
  221. return normalizePublicPath(compilerForDevServer.options.output.publicPath);
  222. };
  223. const getStatsOption = () => {
  224. if (options.stats) {
  225. return options.stats;
  226. }
  227. if (devServerOptions.stats) {
  228. return devServerOptions.stats;
  229. }
  230. return compilerForDevServer.options.stats;
  231. };
  232. devServerOptions.host = devServerOptions.host || "localhost";
  233. devServerOptions.port =
  234. typeof devServerOptions.port !== "undefined" ? devServerOptions.port : 8080;
  235. devServerOptions.stats = getStatsOption();
  236. devServerOptions.publicPath = getPublicPathOption();
  237. }
  238. if (devServerOptions.port) {
  239. const portNumber = Number(devServerOptions.port);
  240. if (usedPorts.find((port) => portNumber === port)) {
  241. throw new Error("Unique ports must be specified for each devServer option in your webpack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config.");
  242. }
  243. usedPorts.push(portNumber);
  244. }
  245. try {
  246. let server;
  247. // TODO: remove after dropping webpack-dev-server@v3
  248. if (isDevServer4) {
  249. server = new DevServer(devServerOptions, compiler);
  250. }
  251. else {
  252. server = new DevServer(compiler, devServerOptions);
  253. }
  254. if (typeof server.start === "function") {
  255. await server.start();
  256. }
  257. else {
  258. // TODO remove in the next major release
  259. server.listen(devServerOptions.port, devServerOptions.host, (error) => {
  260. if (error) {
  261. throw error;
  262. }
  263. });
  264. }
  265. servers.push(server);
  266. }
  267. catch (error) {
  268. if (cli.isValidationError(error)) {
  269. cli.logger.error(error.message);
  270. }
  271. else {
  272. cli.logger.error(error);
  273. }
  274. process.exit(2);
  275. }
  276. }
  277. });
  278. }
  279. }
  280. exports.default = ServeCommand;