ContextDependencyHelpers.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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").Node} EsTreeNode */
  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: [number, number], ...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 {EsTreeNode} 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 {...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(part.expression);
  156. }
  157. }
  158. dep.replaces = replaces;
  159. dep.critical =
  160. options.wrappedContextCritical &&
  161. "a part of the request of a dependency is an expression";
  162. return dep;
  163. } else if (
  164. param.isWrapped() &&
  165. ((param.prefix && param.prefix.isString()) ||
  166. (param.postfix && param.postfix.isString()))
  167. ) {
  168. const prefixRaw =
  169. /** @type {string} */
  170. (param.prefix && param.prefix.isString() ? param.prefix.string : "");
  171. const postfixRaw =
  172. /** @type {string} */
  173. (param.postfix && param.postfix.isString() ? param.postfix.string : "");
  174. const prefixRange =
  175. param.prefix && param.prefix.isString() ? param.prefix.range : null;
  176. const postfixRange =
  177. param.postfix && param.postfix.isString() ? param.postfix.range : null;
  178. const valueRange = /** @type {Range} */ (param.range);
  179. const { context, prefix } = splitContextFromPrefix(prefixRaw);
  180. const {
  181. path: postfix,
  182. query,
  183. fragment
  184. } = parseResource(postfixRaw, parser);
  185. const regExp = new RegExp(
  186. `^${quoteMeta(prefix)}${
  187. /** @type {RegExp} */ (options.wrappedContextRegExp).source
  188. }${quoteMeta(postfix)}$`
  189. );
  190. const dep = new Dep(
  191. {
  192. request: context + query + fragment,
  193. recursive: /** @type {boolean} */ (options.wrappedContextRecursive),
  194. regExp,
  195. mode: "sync",
  196. ...contextOptions
  197. },
  198. range,
  199. valueRange,
  200. ...depArgs
  201. );
  202. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  203. const replaces = [];
  204. if (prefixRange) {
  205. replaces.push({
  206. range: prefixRange,
  207. value: JSON.stringify(prefix)
  208. });
  209. }
  210. if (postfixRange) {
  211. replaces.push({
  212. range: postfixRange,
  213. value: JSON.stringify(postfix)
  214. });
  215. }
  216. dep.replaces = replaces;
  217. dep.critical =
  218. options.wrappedContextCritical &&
  219. "a part of the request of a dependency is an expression";
  220. if (parser && param.wrappedInnerExpressions) {
  221. for (const part of param.wrappedInnerExpressions) {
  222. if (part.expression) parser.walkExpression(part.expression);
  223. }
  224. }
  225. return dep;
  226. }
  227. const dep = new Dep(
  228. {
  229. request: /** @type {string} */ (options.exprContextRequest),
  230. recursive: /** @type {boolean} */ (options.exprContextRecursive),
  231. regExp: /** @type {RegExp} */ (options.exprContextRegExp),
  232. mode: "sync",
  233. ...contextOptions
  234. },
  235. range,
  236. /** @type {Range} */ (param.range),
  237. ...depArgs
  238. );
  239. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  240. dep.critical =
  241. options.exprContextCritical &&
  242. "the request of a dependency is an expression";
  243. parser.walkExpression(param.expression);
  244. return dep;
  245. };