ContextDependencyHelpers.js 7.8 KB

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