NodeStuffPlugin.js 17 KB

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