webpack-dev-server.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env node
  2. /* Based on webpack/bin/webpack.js */
  3. /* eslint-disable no-console */
  4. "use strict";
  5. /**
  6. * @param {string} command process to run
  7. * @param {string[]} args command line arguments
  8. * @returns {Promise<void>} promise
  9. */
  10. const runCommand = (command, args) => {
  11. const cp = require("child_process");
  12. return new Promise((resolve, reject) => {
  13. const executedCommand = cp.spawn(command, args, {
  14. stdio: "inherit",
  15. shell: true,
  16. });
  17. executedCommand.on("error", (error) => {
  18. reject(error);
  19. });
  20. executedCommand.on("exit", (code) => {
  21. if (code === 0) {
  22. resolve();
  23. } else {
  24. reject();
  25. }
  26. });
  27. });
  28. };
  29. /**
  30. * @param {string} packageName name of the package
  31. * @returns {boolean} is the package installed?
  32. */
  33. const isInstalled = (packageName) => {
  34. if (process.versions.pnp) {
  35. return true;
  36. }
  37. const path = require("path");
  38. const fs = require("graceful-fs");
  39. let dir = __dirname;
  40. do {
  41. try {
  42. if (
  43. fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()
  44. ) {
  45. return true;
  46. }
  47. } catch (_error) {
  48. // Nothing
  49. }
  50. // eslint-disable-next-line no-cond-assign
  51. } while (dir !== (dir = path.dirname(dir)));
  52. // https://github.com/nodejs/node/blob/v18.9.1/lib/internal/modules/cjs/loader.js#L1274
  53. // @ts-ignore
  54. for (const internalPath of require("module").globalPaths) {
  55. try {
  56. if (fs.statSync(path.join(internalPath, packageName)).isDirectory()) {
  57. return true;
  58. }
  59. } catch (_error) {
  60. // Nothing
  61. }
  62. }
  63. return false;
  64. };
  65. /**
  66. * @param {CliOption} cli options
  67. * @returns {void}
  68. */
  69. const runCli = (cli) => {
  70. if (cli.preprocess) {
  71. cli.preprocess();
  72. }
  73. const path = require("path");
  74. const pkgPath = require.resolve(`${cli.package}/package.json`);
  75. // eslint-disable-next-line import/no-dynamic-require
  76. const pkg = require(pkgPath);
  77. if (pkg.type === "module" || /\.mjs/i.test(pkg.bin[cli.binName])) {
  78. import(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName])).catch(
  79. (error) => {
  80. console.error(error);
  81. process.exitCode = 1;
  82. }
  83. );
  84. } else {
  85. // eslint-disable-next-line import/no-dynamic-require
  86. require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
  87. }
  88. };
  89. /**
  90. * @typedef {Object} CliOption
  91. * @property {string} name display name
  92. * @property {string} package npm package name
  93. * @property {string} binName name of the executable file
  94. * @property {boolean} installed currently installed?
  95. * @property {string} url homepage
  96. * @property {function} preprocess preprocessor
  97. */
  98. /** @type {CliOption} */
  99. const cli = {
  100. name: "webpack-cli",
  101. package: "webpack-cli",
  102. binName: "webpack-cli",
  103. installed: isInstalled("webpack-cli"),
  104. url: "https://github.com/webpack/webpack-cli",
  105. preprocess() {
  106. process.argv.splice(2, 0, "serve");
  107. },
  108. };
  109. if (!cli.installed) {
  110. const path = require("path");
  111. const fs = require("graceful-fs");
  112. const readLine = require("readline");
  113. const notify = `CLI for webpack must be installed.\n ${cli.name} (${cli.url})\n`;
  114. console.error(notify);
  115. /**
  116. * @type {string}
  117. */
  118. let packageManager;
  119. if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
  120. packageManager = "yarn";
  121. } else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
  122. packageManager = "pnpm";
  123. } else {
  124. packageManager = "npm";
  125. }
  126. const installOptions = [packageManager === "yarn" ? "add" : "install", "-D"];
  127. console.error(
  128. `We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
  129. " "
  130. )} ${cli.package}".`
  131. );
  132. const question = `Do you want to install 'webpack-cli' (yes/no): `;
  133. const questionInterface = readLine.createInterface({
  134. input: process.stdin,
  135. output: process.stderr,
  136. });
  137. // In certain scenarios (e.g. when STDIN is not in terminal mode), the callback function will not be
  138. // executed. Setting the exit code here to ensure the script exits correctly in those cases. The callback
  139. // function is responsible for clearing the exit code if the user wishes to install webpack-cli.
  140. process.exitCode = 1;
  141. questionInterface.question(question, (answer) => {
  142. questionInterface.close();
  143. const normalizedAnswer = answer.toLowerCase().startsWith("y");
  144. if (!normalizedAnswer) {
  145. console.error(
  146. "You need to install 'webpack-cli' to use webpack via CLI.\n" +
  147. "You can also install the CLI manually."
  148. );
  149. return;
  150. }
  151. process.exitCode = 0;
  152. console.log(
  153. `Installing '${
  154. cli.package
  155. }' (running '${packageManager} ${installOptions.join(" ")} ${
  156. cli.package
  157. }')...`
  158. );
  159. runCommand(packageManager, installOptions.concat(cli.package))
  160. .then(() => {
  161. runCli(cli);
  162. })
  163. .catch((error) => {
  164. console.error(error);
  165. process.exitCode = 1;
  166. });
  167. });
  168. } else {
  169. runCli(cli);
  170. }