ContextDependencyHelpers.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { parseResource } = require("../util/identifier");
  7. /** @import { Expression } from "estree" */
  8. /**
  9. * @import {
  10. * JavascriptParserOptions
  11. * } from "../../declarations/WebpackOptions"
  12. */
  13. /** @import BasicEvaluatedExpression from "../javascript/BasicEvaluatedExpression" */
  14. /** @import JavascriptParser, { Range } from "../javascript/JavascriptParser" */
  15. /**
  16. * @import ContextDependency, {
  17. * ContextDependencyOptions,
  18. * Replaces
  19. * } from "./ContextDependency"
  20. */
  21. /**
  22. * Escapes regular expression metacharacters
  23. * @param {string} str String to quote
  24. * @returns {string} Escaped string
  25. */
  26. const quoteMeta = (str) => str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&");
  27. /**
  28. * Split context from prefix.
  29. * @param {string} prefix prefix
  30. * @returns {{ prefix: string, context: string }} result
  31. */
  32. const splitContextFromPrefix = (prefix) => {
  33. const idx = prefix.lastIndexOf("/");
  34. let context = ".";
  35. if (idx >= 0) {
  36. context = prefix.slice(0, idx);
  37. prefix = `.${prefix.slice(idx)}`;
  38. }
  39. return {
  40. context,
  41. prefix
  42. };
  43. };
  44. /** @typedef {Partial<Omit<ContextDependencyOptions, "resource">>} PartialContextDependencyOptions */
  45. /** @typedef {{ new (options: ContextDependencyOptions, range: Range, valueRange: Range, ...args: EXPECTED_ANY[]): ContextDependency }} ContextDependencyConstructor */
  46. /**
  47. * Defines the get additional dep args type used by this module.
  48. * @template T
  49. * @typedef {T extends new (options: ContextDependencyOptions, range: Range, valueRange: Range, ...remains: infer R) => ContextDependency ? R : []} GetAdditionalDepArgs
  50. */
  51. /**
  52. * Returns the created Dependency.
  53. * @template {ContextDependencyConstructor} T
  54. * @param {T} Dep the Dependency class
  55. * @param {Range} range source range
  56. * @param {BasicEvaluatedExpression} param context param
  57. * @param {Expression} expr expr
  58. * @param {Pick<JavascriptParserOptions, `${"expr" | "wrapped"}Context${"Critical" | "Recursive" | "RegExp"}` | "exprContextRequest">} options options for context creation
  59. * @param {PartialContextDependencyOptions} contextOptions options for the ContextModule
  60. * @param {JavascriptParser} parser the parser
  61. * @param {GetAdditionalDepArgs<T>} depArgs depArgs
  62. * @returns {ContextDependency} the created Dependency
  63. */
  64. module.exports.create = (
  65. Dep,
  66. range,
  67. param,
  68. expr,
  69. options,
  70. contextOptions,
  71. parser,
  72. ...depArgs
  73. ) => {
  74. if (param.isTemplateString()) {
  75. const quasis = /** @type {BasicEvaluatedExpression[]} */ (param.quasis);
  76. const prefixRaw = /** @type {string} */ (quasis[0].string);
  77. const postfixRaw =
  78. /** @type {string} */
  79. (quasis.length > 1 ? quasis[quasis.length - 1].string : "");
  80. const valueRange = /** @type {Range} */ (param.range);
  81. const { context, prefix } = splitContextFromPrefix(prefixRaw);
  82. const {
  83. path: postfix,
  84. query,
  85. fragment
  86. } = parseResource(postfixRaw, parser);
  87. // When there are more than two quasis, the generated RegExp can be more precise
  88. // We join the quasis with the expression regexp
  89. const innerQuasis = quasis.slice(1, -1);
  90. const innerRegExp =
  91. /** @type {RegExp} */ (options.wrappedContextRegExp).source +
  92. innerQuasis
  93. .map(
  94. (q) =>
  95. quoteMeta(/** @type {string} */ (q.string)) +
  96. /** @type {RegExp} */ (options.wrappedContextRegExp).source
  97. )
  98. .join("");
  99. // Example: `./context/pre${e}inner${e}inner2${e}post?query#frag`
  100. // context: "./context"
  101. // prefix: "./pre"
  102. // innerQuasis: [BEE("inner"), BEE("inner2")]
  103. // (BEE = BasicEvaluatedExpression)
  104. // postfix: "post"
  105. // query: "?query"
  106. // fragment: "#frag"
  107. // regExp: /^\.\/pre.*inner.*inner2.*post$/
  108. const regExp = new RegExp(
  109. `^${quoteMeta(prefix)}${innerRegExp}${quoteMeta(postfix)}$`
  110. );
  111. const dep = new Dep(
  112. {
  113. request: context + query + fragment,
  114. recursive: /** @type {boolean} */ (options.wrappedContextRecursive),
  115. regExp,
  116. mode: "sync",
  117. ...contextOptions
  118. },
  119. range,
  120. valueRange,
  121. ...depArgs
  122. );
  123. dep.loc = parser.getLocation(expr);
  124. /** @type {Replaces} */
  125. const replaces = [];
  126. const parts = /** @type {BasicEvaluatedExpression[]} */ (param.parts);
  127. for (const [i, part] of parts.entries()) {
  128. if (i % 2 === 0) {
  129. // Quasis or merged quasi
  130. let range = /** @type {Range} */ (part.range);
  131. let value = /** @type {string} */ (part.string);
  132. if (param.templateStringKind === "cooked") {
  133. value = JSON.stringify(value);
  134. value = value.slice(1, -1);
  135. }
  136. if (i === 0) {
  137. // prefix
  138. value = prefix;
  139. range = [
  140. /** @type {Range} */ (param.range)[0],
  141. /** @type {Range} */ (part.range)[1]
  142. ];
  143. value =
  144. (param.templateStringKind === "cooked" ? "`" : "String.raw`") +
  145. value;
  146. } else if (i === parts.length - 1) {
  147. // postfix
  148. value = postfix;
  149. range = [
  150. /** @type {Range} */ (part.range)[0],
  151. /** @type {Range} */ (param.range)[1]
  152. ];
  153. value = `${value}\``;
  154. } else if (
  155. part.expression &&
  156. part.expression.type === "TemplateElement" &&
  157. part.expression.value.raw === value
  158. ) {
  159. // Shortcut when it's a single quasi and doesn't need to be replaced
  160. continue;
  161. }
  162. replaces.push({
  163. range,
  164. value
  165. });
  166. } else {
  167. // Expression
  168. parser.walkExpression(
  169. /** @type {Expression} */
  170. (part.expression)
  171. );
  172. }
  173. }
  174. dep.replaces = replaces;
  175. dep.critical =
  176. options.wrappedContextCritical &&
  177. "a part of the request of a dependency is an expression";
  178. return dep;
  179. } else if (
  180. param.isWrapped() &&
  181. ((param.prefix && param.prefix.isString()) ||
  182. (param.postfix && param.postfix.isString()))
  183. ) {
  184. const prefixRaw =
  185. /** @type {string} */
  186. (param.prefix && param.prefix.isString() ? param.prefix.string : "");
  187. const postfixRaw =
  188. /** @type {string} */
  189. (param.postfix && param.postfix.isString() ? param.postfix.string : "");
  190. const prefixRange =
  191. param.prefix && param.prefix.isString() ? param.prefix.range : null;
  192. const postfixRange =
  193. param.postfix && param.postfix.isString() ? param.postfix.range : null;
  194. const valueRange = /** @type {Range} */ (param.range);
  195. const { context, prefix } = splitContextFromPrefix(prefixRaw);
  196. const {
  197. path: postfix,
  198. query,
  199. fragment
  200. } = parseResource(postfixRaw, parser);
  201. const regExp = new RegExp(
  202. `^${quoteMeta(prefix)}${
  203. /** @type {RegExp} */ (options.wrappedContextRegExp).source
  204. }${quoteMeta(postfix)}$`
  205. );
  206. const dep = new Dep(
  207. {
  208. request: context + query + fragment,
  209. recursive: /** @type {boolean} */ (options.wrappedContextRecursive),
  210. regExp,
  211. mode: "sync",
  212. ...contextOptions
  213. },
  214. range,
  215. valueRange,
  216. ...depArgs
  217. );
  218. dep.loc = parser.getLocation(expr);
  219. /** @type {Replaces} */
  220. const replaces = [];
  221. if (prefixRange) {
  222. replaces.push({
  223. range: prefixRange,
  224. value: JSON.stringify(prefix)
  225. });
  226. }
  227. if (postfixRange) {
  228. replaces.push({
  229. range: postfixRange,
  230. value: JSON.stringify(postfix)
  231. });
  232. }
  233. dep.replaces = replaces;
  234. dep.critical =
  235. options.wrappedContextCritical &&
  236. "a part of the request of a dependency is an expression";
  237. if (parser && param.wrappedInnerExpressions) {
  238. for (const part of param.wrappedInnerExpressions) {
  239. if (part.expression) {
  240. parser.walkExpression(
  241. /** @type {Expression} */
  242. (part.expression)
  243. );
  244. }
  245. }
  246. }
  247. return dep;
  248. }
  249. const dep = new Dep(
  250. {
  251. request: /** @type {string} */ (options.exprContextRequest),
  252. recursive: /** @type {boolean} */ (options.exprContextRecursive),
  253. regExp: /** @type {RegExp} */ (options.exprContextRegExp),
  254. mode: "sync",
  255. ...contextOptions
  256. },
  257. range,
  258. /** @type {Range} */ (param.range),
  259. ...depArgs
  260. );
  261. dep.loc = parser.getLocation(expr);
  262. dep.critical =
  263. options.exprContextCritical &&
  264. "the request of a dependency is an expression";
  265. parser.walkExpression(/** @type {Expression} */ (param.expression));
  266. return dep;
  267. };