ContextDependencyHelpers.js 8.1 KB

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