ContextDependencyHelpers.js 8.2 KB

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