NodeStuffPlugin.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const {
  7. JAVASCRIPT_MODULE_TYPE_AUTO,
  8. JAVASCRIPT_MODULE_TYPE_DYNAMIC,
  9. JAVASCRIPT_MODULE_TYPE_ESM
  10. } = require("./ModuleTypeConstants");
  11. const NodeStuffInWebError = require("./NodeStuffInWebError");
  12. const RuntimeGlobals = require("./RuntimeGlobals");
  13. const CachedConstDependency = require("./dependencies/CachedConstDependency");
  14. const ConstDependency = require("./dependencies/ConstDependency");
  15. const ExternalModuleDependency = require("./dependencies/ExternalModuleDependency");
  16. const ExternalModuleInitFragmentDependency = require("./dependencies/ExternalModuleInitFragmentDependency");
  17. const ImportMetaPlugin = require("./dependencies/ImportMetaPlugin");
  18. const { evaluateToString } = require("./javascript/JavascriptParserHelpers");
  19. const { relative } = require("./util/fs");
  20. const { parseResource } = require("./util/identifier");
  21. /** @typedef {import("../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
  22. /** @typedef {import("../declarations/WebpackOptions").NodeOptions} NodeOptions */
  23. /** @typedef {import("./Compiler")} Compiler */
  24. /** @typedef {import("./Dependency").DependencyLocation} DependencyLocation */
  25. /** @typedef {import("./NormalModule")} NormalModule */
  26. /** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */
  27. /** @typedef {import("./javascript/JavascriptParser").Expression} Expression */
  28. /** @typedef {import("./javascript/JavascriptParser").Range} Range */
  29. /** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
  30. const PLUGIN_NAME = "NodeStuffPlugin";
  31. const URL_MODULE_CONSTANT_FUNCTION_NAME = "__webpack_fileURLToPath__";
  32. class NodeStuffPlugin {
  33. /**
  34. * Creates an instance of NodeStuffPlugin.
  35. * @param {NodeOptions} options options
  36. */
  37. constructor(options) {
  38. this.options = options;
  39. }
  40. /**
  41. * Applies the plugin by registering its hooks on the compiler.
  42. * @param {Compiler} compiler the compiler instance
  43. * @returns {void}
  44. */
  45. apply(compiler) {
  46. const { options } = this;
  47. compiler.hooks.compilation.tap(
  48. PLUGIN_NAME,
  49. (compilation, { normalModuleFactory }) => {
  50. compilation.dependencyTemplates.set(
  51. ExternalModuleDependency,
  52. new ExternalModuleDependency.Template()
  53. );
  54. compilation.dependencyTemplates.set(
  55. ExternalModuleInitFragmentDependency,
  56. new ExternalModuleInitFragmentDependency.Template()
  57. );
  58. /**
  59. * Processes the provided parser.
  60. * @param {JavascriptParser} parser the parser
  61. * @param {NodeOptions} nodeOptions options
  62. * @returns {void}
  63. */
  64. const globalHandler = (parser, nodeOptions) => {
  65. /**
  66. * Returns const dependency.
  67. * @param {Expression} expr expression
  68. * @returns {ConstDependency} const dependency
  69. */
  70. const getGlobalDep = (expr) => {
  71. if (compilation.outputOptions.environment.globalThis) {
  72. return new ConstDependency(
  73. "globalThis",
  74. /** @type {Range} */ (expr.range)
  75. );
  76. }
  77. return new ConstDependency(
  78. RuntimeGlobals.global,
  79. /** @type {Range} */ (expr.range),
  80. [RuntimeGlobals.global]
  81. );
  82. };
  83. const withWarning = nodeOptions.global === "warn";
  84. parser.hooks.expression.for("global").tap(PLUGIN_NAME, (expr) => {
  85. const dep = getGlobalDep(expr);
  86. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  87. parser.state.module.addPresentationalDependency(dep);
  88. if (withWarning) {
  89. parser.state.module.addWarning(
  90. new NodeStuffInWebError(
  91. dep.loc,
  92. "global",
  93. "The global namespace object is a Node.js feature and isn't available in browsers."
  94. )
  95. );
  96. }
  97. });
  98. parser.hooks.rename.for("global").tap(PLUGIN_NAME, (expr) => {
  99. const dep = getGlobalDep(expr);
  100. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  101. parser.state.module.addPresentationalDependency(dep);
  102. return false;
  103. });
  104. };
  105. const hooks = ImportMetaPlugin.getCompilationHooks(compilation);
  106. /**
  107. * Sets module constant.
  108. * @param {JavascriptParser} parser the parser
  109. * @param {"__filename" | "__dirname" | "import.meta.filename" | "import.meta.dirname"} expressionName expression name
  110. * @param {(module: NormalModule) => string} fn function
  111. * @param {"filename" | "dirname"} property a property
  112. * @returns {void}
  113. */
  114. const setModuleConstant = (parser, expressionName, fn, property) => {
  115. parser.hooks.expression
  116. .for(expressionName)
  117. .tap(PLUGIN_NAME, (expr) => {
  118. const dep = new ConstDependency(
  119. fn(parser.state.module),
  120. /** @type {Range} */
  121. (expr.range)
  122. );
  123. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  124. parser.state.module.addPresentationalDependency(dep);
  125. return true;
  126. });
  127. if (
  128. expressionName === "import.meta.filename" ||
  129. expressionName === "import.meta.dirname"
  130. ) {
  131. hooks.propertyInDestructuring.tap(PLUGIN_NAME, (usingProperty) => {
  132. if (usingProperty.id === property) {
  133. return `${property}: ${fn(parser.state.module)},`;
  134. }
  135. });
  136. }
  137. };
  138. /**
  139. * Sets cached module constant.
  140. * @param {JavascriptParser} parser the parser
  141. * @param {"__filename" | "__dirname" | "import.meta.filename" | "import.meta.dirname"} expressionName expression name
  142. * @param {(module: NormalModule) => string} fn function
  143. * @param {"filename" | "dirname"} property a property
  144. * @param {string=} warning warning
  145. * @returns {void}
  146. */
  147. const setCachedModuleConstant = (
  148. parser,
  149. expressionName,
  150. fn,
  151. property,
  152. warning
  153. ) => {
  154. parser.hooks.expression
  155. .for(expressionName)
  156. .tap(PLUGIN_NAME, (expr) => {
  157. const dep = new CachedConstDependency(
  158. JSON.stringify(fn(parser.state.module)),
  159. /** @type {Range} */
  160. (expr.range),
  161. `__webpack_${property}__`
  162. );
  163. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  164. parser.state.module.addPresentationalDependency(dep);
  165. if (warning) {
  166. parser.state.module.addWarning(
  167. new NodeStuffInWebError(dep.loc, expressionName, warning)
  168. );
  169. }
  170. return true;
  171. });
  172. if (
  173. expressionName === "import.meta.filename" ||
  174. expressionName === "import.meta.dirname"
  175. ) {
  176. hooks.propertyInDestructuring.tap(PLUGIN_NAME, (usingProperty) => {
  177. if (property === usingProperty.id) {
  178. if (warning) {
  179. parser.state.module.addWarning(
  180. new NodeStuffInWebError(
  181. usingProperty.loc,
  182. expressionName,
  183. warning
  184. )
  185. );
  186. }
  187. return `${property}: ${JSON.stringify(fn(parser.state.module))},`;
  188. }
  189. });
  190. }
  191. };
  192. /**
  193. * Updates constant using the provided parser.
  194. * @param {JavascriptParser} parser the parser
  195. * @param {"__filename" | "__dirname" | "import.meta.filename" | "import.meta.dirname"} expressionName expression name
  196. * @param {string} value value
  197. * @param {"filename" | "dirname"} property a property
  198. * @param {string=} warning warning
  199. * @returns {void}
  200. */
  201. const setConstant = (
  202. parser,
  203. expressionName,
  204. value,
  205. property,
  206. warning
  207. ) =>
  208. setCachedModuleConstant(
  209. parser,
  210. expressionName,
  211. () => value,
  212. property,
  213. warning
  214. );
  215. /**
  216. * Sets url module constant.
  217. * @param {JavascriptParser} parser the parser
  218. * @param {"__filename" | "__dirname" | "import.meta.filename" | "import.meta.dirname"} expressionName expression name
  219. * @param {"dirname" | "filename"} property property
  220. * @param {() => string} value function to get value
  221. * @returns {void}
  222. */
  223. const setUrlModuleConstant = (
  224. parser,
  225. expressionName,
  226. property,
  227. value
  228. ) => {
  229. parser.hooks.expression
  230. .for(expressionName)
  231. .tap(PLUGIN_NAME, (expr) => {
  232. // We use `CachedConstDependency` because of `eval` devtool, there is no `import.meta` inside `eval()`
  233. const { importMetaName, environment, module } =
  234. compilation.outputOptions;
  235. // Generate `import.meta.dirname` and `import.meta.filename` when:
  236. // - they are supported by the environment
  237. // - it is a universal target, because we can't use `import mod from "node:url"; ` at the top file
  238. if (
  239. environment.importMetaDirnameAndFilename ||
  240. (compiler.platform.web === null &&
  241. compiler.platform.node === null &&
  242. module)
  243. ) {
  244. const dep = new CachedConstDependency(
  245. `${importMetaName}.${property}`,
  246. /** @type {Range} */
  247. (expr.range),
  248. `__webpack_${property}__`,
  249. CachedConstDependency.PLACE_CHUNK
  250. );
  251. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  252. parser.state.module.addPresentationalDependency(dep);
  253. return;
  254. }
  255. const dep = new ExternalModuleDependency(
  256. "url",
  257. [
  258. {
  259. name: "fileURLToPath",
  260. value: URL_MODULE_CONSTANT_FUNCTION_NAME
  261. }
  262. ],
  263. undefined,
  264. `${URL_MODULE_CONSTANT_FUNCTION_NAME}(${value()})`,
  265. /** @type {Range} */ (expr.range),
  266. `__webpack_${property}__`,
  267. ExternalModuleDependency.PLACE_CHUNK
  268. );
  269. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  270. parser.state.module.addPresentationalDependency(dep);
  271. return true;
  272. });
  273. if (
  274. expressionName === "import.meta.filename" ||
  275. expressionName === "import.meta.dirname"
  276. ) {
  277. hooks.propertyInDestructuring.tap(PLUGIN_NAME, (usingProperty) => {
  278. if (property === usingProperty.id) {
  279. const { importMetaName, environment, module } =
  280. compilation.outputOptions;
  281. if (
  282. environment.importMetaDirnameAndFilename ||
  283. (compiler.platform.web === null &&
  284. compiler.platform.node === null &&
  285. module)
  286. ) {
  287. const dep = new CachedConstDependency(
  288. `${importMetaName}.${property}`,
  289. null,
  290. `__webpack_${property}__`,
  291. CachedConstDependency.PLACE_CHUNK
  292. );
  293. dep.loc = /** @type {DependencyLocation} */ (
  294. usingProperty.loc
  295. );
  296. parser.state.module.addPresentationalDependency(dep);
  297. return `${property}: __webpack_${property}__,`;
  298. }
  299. const dep = new ExternalModuleDependency(
  300. "url",
  301. [
  302. {
  303. name: "fileURLToPath",
  304. value: URL_MODULE_CONSTANT_FUNCTION_NAME
  305. }
  306. ],
  307. undefined,
  308. `${URL_MODULE_CONSTANT_FUNCTION_NAME}(${value()})`,
  309. null,
  310. `__webpack_${property}__`,
  311. ExternalModuleDependency.PLACE_CHUNK
  312. );
  313. dep.loc = /** @type {DependencyLocation} */ (usingProperty.loc);
  314. parser.state.module.addPresentationalDependency(dep);
  315. return `${property}: __webpack_${property}__,`;
  316. }
  317. });
  318. }
  319. };
  320. /**
  321. * Dirname and filename handler.
  322. * @param {JavascriptParser} parser the parser
  323. * @param {NodeOptions} nodeOptions options
  324. * @param {{ dirname: "__dirname" | "import.meta.dirname", filename: "__filename" | "import.meta.filename" }} identifiers options
  325. * @returns {void}
  326. */
  327. const dirnameAndFilenameHandler = (
  328. parser,
  329. nodeOptions,
  330. { dirname, filename }
  331. ) => {
  332. // Keep `import.meta.filename` in code
  333. if (
  334. nodeOptions.__filename === false &&
  335. filename === "import.meta.filename"
  336. ) {
  337. setModuleConstant(parser, filename, () => filename, "filename");
  338. }
  339. if (nodeOptions.__filename) {
  340. switch (nodeOptions.__filename) {
  341. case "mock":
  342. setConstant(parser, filename, "/index.js", "filename");
  343. break;
  344. case "warn-mock":
  345. setConstant(
  346. parser,
  347. filename,
  348. "/index.js",
  349. "filename",
  350. "__filename is a Node.js feature and isn't available in browsers."
  351. );
  352. break;
  353. case "node-module": {
  354. const importMetaName = compilation.outputOptions.importMetaName;
  355. setUrlModuleConstant(
  356. parser,
  357. filename,
  358. "filename",
  359. () => `${importMetaName}.url`
  360. );
  361. break;
  362. }
  363. case "eval-only":
  364. // Keep `import.meta.filename` in the source code for the ES module output, or create a fallback using `import.meta.url` if possible
  365. if (compilation.outputOptions.module) {
  366. const { importMetaName } = compilation.outputOptions;
  367. setUrlModuleConstant(
  368. parser,
  369. filename,
  370. "filename",
  371. () => `${importMetaName}.url`
  372. );
  373. }
  374. // Replace `import.meta.filename` with `__filename` for the non-ES module output
  375. else if (filename === "import.meta.filename") {
  376. setModuleConstant(
  377. parser,
  378. filename,
  379. () => "__filename",
  380. "filename"
  381. );
  382. }
  383. break;
  384. case true:
  385. setCachedModuleConstant(
  386. parser,
  387. filename,
  388. (module) =>
  389. relative(
  390. /** @type {InputFileSystem} */ (compiler.inputFileSystem),
  391. compiler.context,
  392. module.resource
  393. ),
  394. "filename"
  395. );
  396. break;
  397. }
  398. parser.hooks.evaluateIdentifier
  399. .for("__filename")
  400. .tap(PLUGIN_NAME, (expr) => {
  401. if (!parser.state.module) return;
  402. const resource = parseResource(parser.state.module.resource);
  403. return evaluateToString(resource.path)(expr);
  404. });
  405. }
  406. // Keep `import.meta.dirname` in code
  407. if (
  408. nodeOptions.__dirname === false &&
  409. dirname === "import.meta.dirname"
  410. ) {
  411. setModuleConstant(parser, dirname, () => dirname, "dirname");
  412. }
  413. if (nodeOptions.__dirname) {
  414. switch (nodeOptions.__dirname) {
  415. case "mock":
  416. setConstant(parser, dirname, "/", "dirname");
  417. break;
  418. case "warn-mock":
  419. setConstant(
  420. parser,
  421. dirname,
  422. "/",
  423. "dirname",
  424. "__dirname is a Node.js feature and isn't available in browsers."
  425. );
  426. break;
  427. case "node-module": {
  428. const importMetaName = compilation.outputOptions.importMetaName;
  429. setUrlModuleConstant(
  430. parser,
  431. dirname,
  432. "dirname",
  433. () => `${importMetaName}.url.replace(/\\/(?:[^\\/]*)$/, "")`
  434. );
  435. break;
  436. }
  437. case "eval-only":
  438. // Keep `import.meta.dirname` in the source code for the ES module output and replace `__dirname` on `import.meta.dirname`
  439. if (compilation.outputOptions.module) {
  440. const { importMetaName } = compilation.outputOptions;
  441. setUrlModuleConstant(
  442. parser,
  443. dirname,
  444. "dirname",
  445. () => `${importMetaName}.url.replace(/\\/(?:[^\\/]*)$/, "")`
  446. );
  447. }
  448. // Replace `import.meta.dirname` with `__dirname` for the non-ES module output
  449. else if (dirname === "import.meta.dirname") {
  450. setModuleConstant(
  451. parser,
  452. dirname,
  453. () => "__dirname",
  454. "dirname"
  455. );
  456. }
  457. break;
  458. case true:
  459. setCachedModuleConstant(
  460. parser,
  461. dirname,
  462. (module) =>
  463. relative(
  464. /** @type {InputFileSystem} */ (compiler.inputFileSystem),
  465. compiler.context,
  466. /** @type {string} */ (module.context)
  467. ),
  468. "dirname"
  469. );
  470. break;
  471. }
  472. parser.hooks.evaluateIdentifier
  473. .for(dirname)
  474. .tap(PLUGIN_NAME, (expr) => {
  475. if (!parser.state.module) return;
  476. return evaluateToString(
  477. /** @type {string} */
  478. (parser.state.module.context)
  479. )(expr);
  480. });
  481. }
  482. };
  483. /**
  484. * Handles the hook callback for this code path.
  485. * @param {JavascriptParser} parser the parser
  486. * @param {JavascriptParserOptions} parserOptions the javascript parser options
  487. * @param {boolean} a true when we need to handle `__filename` and `__dirname`, otherwise false
  488. * @param {boolean} b true when we need to handle `import.meta.filename` and `import.meta.dirname`, otherwise false
  489. */
  490. const handler = (parser, parserOptions, a, b) => {
  491. if (b && parserOptions.node === false) {
  492. // Keep `import.meta.dirname` and `import.meta.filename` in code
  493. setModuleConstant(
  494. parser,
  495. "import.meta.dirname",
  496. () => "import.meta.dirname",
  497. "dirname"
  498. );
  499. setModuleConstant(
  500. parser,
  501. "import.meta.filename",
  502. () => "import.meta.filename",
  503. "filename"
  504. );
  505. return;
  506. }
  507. let localOptions = options;
  508. if (parserOptions.node) {
  509. localOptions = { ...localOptions, ...parserOptions.node };
  510. }
  511. if (localOptions.global !== false) {
  512. globalHandler(parser, localOptions);
  513. }
  514. if (a) {
  515. dirnameAndFilenameHandler(parser, localOptions, {
  516. dirname: "__dirname",
  517. filename: "__filename"
  518. });
  519. }
  520. if (b && parserOptions.importMeta !== false) {
  521. dirnameAndFilenameHandler(parser, localOptions, {
  522. dirname: "import.meta.dirname",
  523. filename: "import.meta.filename"
  524. });
  525. }
  526. };
  527. normalModuleFactory.hooks.parser
  528. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  529. .tap(PLUGIN_NAME, (parser, parserOptions) => {
  530. handler(parser, parserOptions, true, true);
  531. });
  532. normalModuleFactory.hooks.parser
  533. .for(JAVASCRIPT_MODULE_TYPE_DYNAMIC)
  534. .tap(PLUGIN_NAME, (parser, parserOptions) => {
  535. handler(parser, parserOptions, true, false);
  536. });
  537. normalModuleFactory.hooks.parser
  538. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  539. .tap(PLUGIN_NAME, (parser, parserOptions) => {
  540. handler(parser, parserOptions, false, true);
  541. });
  542. }
  543. );
  544. }
  545. }
  546. module.exports = NodeStuffPlugin;