webpack.js 4.5 KB

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