NodeStuffPlugin.js 18 KB

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