index.js 14 KB

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