URLParserPlugin.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Haijie Xie @hai-x
  4. */
  5. "use strict";
  6. const { pathToFileURL } = require("url");
  7. const RuntimeGlobals = require("../RuntimeGlobals");
  8. const ConstDependency = require("../dependencies/ConstDependency");
  9. const ContextDependencyHelpers = require("../dependencies/ContextDependencyHelpers");
  10. const {
  11. isImportMetaFieldEnabled
  12. } = require("../dependencies/ImportMetaPlugin");
  13. const URLContextDependency = require("../dependencies/URLContextDependency");
  14. const URLDependency = require("../dependencies/URLDependency");
  15. const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
  16. const { approve } = require("../javascript/JavascriptParserHelpers");
  17. const { getInnerGraphUtils } = require("../optimize/InnerGraph");
  18. const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin");
  19. const memoize = require("../util/memoize");
  20. const getUnsupportedFeatureWarning = memoize(() =>
  21. require("../errors/UnsupportedFeatureWarning")
  22. );
  23. const getCommentCompilationWarning = memoize(() =>
  24. require("../errors/CommentCompilationWarning")
  25. );
  26. /**
  27. * @import {
  28. * MemberExpression,
  29. * NewExpression as NewExpressionNode
  30. * } from "estree"
  31. */
  32. /**
  33. * @import {
  34. * JavascriptParserOptions
  35. * } from "../../declarations/WebpackOptions"
  36. */
  37. /** @import NormalModule from "../NormalModule" */
  38. /** @import JavascriptParser, { Range } from "../javascript/JavascriptParser" */
  39. const PLUGIN_NAME = "URLParserPlugin";
  40. /**
  41. * Returns file url.
  42. * @param {NormalModule} module module
  43. * @returns {URL} file url
  44. */
  45. const getUrl = (module) => pathToFileURL(module.resource);
  46. /**
  47. * Checks whether this object is meta url.
  48. * @param {JavascriptParser} parser parser parser
  49. * @param {MemberExpression} arg arg
  50. * @returns {boolean} true when it is `meta.url`, otherwise false
  51. */
  52. const isMetaUrl = (parser, arg) => {
  53. const chain = parser.extractMemberExpressionChain(arg);
  54. if (
  55. chain.members.length !== 1 ||
  56. chain.object.type !== "MetaProperty" ||
  57. chain.object.meta.name !== "import" ||
  58. chain.object.property.name !== "meta" ||
  59. chain.members[0] !== "url"
  60. ) {
  61. return false;
  62. }
  63. return true;
  64. };
  65. /**
  66. * Whether the request points to a directory and thus can't be an asset.
  67. * @param {string} request request
  68. * @returns {boolean} true when the request is a directory reference
  69. */
  70. const isDirectoryRequest = (request) =>
  71. request === "." || request === ".." || request.endsWith("/");
  72. /**
  73. * Replaces `import.meta.url` with the runtime base URI, leaving the
  74. * `new URL(...)` construction untouched so it's evaluated at runtime.
  75. * @param {JavascriptParser} parser parser
  76. * @param {NewExpressionNode} expr expression
  77. * @returns {void}
  78. */
  79. const keepNewURL = (parser, expr) => {
  80. const [, arg2] = expr.arguments;
  81. const dep = new ConstDependency(
  82. RuntimeGlobals.baseURI,
  83. /** @type {Range} */ (arg2.range),
  84. [RuntimeGlobals.baseURI]
  85. );
  86. dep.loc = parser.getLocation(expr);
  87. parser.state.module.addPresentationalDependency(dep);
  88. };
  89. /** @type {WeakMap<NewExpressionNode, BasicEvaluatedExpression | undefined>} */
  90. const getEvaluatedExprCache = new WeakMap();
  91. /**
  92. * Gets evaluated expr.
  93. * @param {NewExpressionNode} expr expression
  94. * @param {JavascriptParser} parser parser parser
  95. * @param {boolean} importMetaUrlEnabled true when import.meta.url should be handled
  96. * @returns {BasicEvaluatedExpression | undefined} basic evaluated expression
  97. */
  98. const getEvaluatedExpr = (expr, parser, importMetaUrlEnabled) => {
  99. let result = getEvaluatedExprCache.get(expr);
  100. if (result !== undefined) return result;
  101. /**
  102. * Returns basic evaluated expression.
  103. * @returns {BasicEvaluatedExpression | undefined} basic evaluated expression
  104. */
  105. const evaluate = () => {
  106. if (expr.arguments.length !== 2) return;
  107. const [arg1, arg2] = expr.arguments;
  108. if (arg2.type !== "MemberExpression" || arg1.type === "SpreadElement") {
  109. return;
  110. }
  111. if (!importMetaUrlEnabled || !isMetaUrl(parser, arg2)) {
  112. return;
  113. }
  114. return parser.evaluateExpression(arg1);
  115. };
  116. result = evaluate();
  117. getEvaluatedExprCache.set(expr, result);
  118. return result;
  119. };
  120. /**
  121. * @typedef {object} ResolvedResourceHints
  122. * @property {boolean=} prefetch project-wide prefetch default
  123. * @property {boolean=} preload project-wide preload default
  124. * @property {("low" | "high" | "auto" | false)=} fetchPriority project-wide fetchPriority default
  125. */
  126. class URLParserPlugin {
  127. /**
  128. * Creates an instance of URLParserPlugin.
  129. * @param {JavascriptParserOptions} options options
  130. */
  131. constructor(options) {
  132. /** @type {JavascriptParserOptions} */
  133. this.options = options;
  134. }
  135. /**
  136. * Applies the plugin by registering its hooks on the compiler.
  137. * @param {JavascriptParser} parser the parser
  138. * @returns {void}
  139. */
  140. apply(parser) {
  141. const relative = this.options.url === "relative";
  142. const importMetaUrlEnabled = isImportMetaFieldEnabled(
  143. this.options.importMeta,
  144. "url"
  145. );
  146. parser.hooks.canRename.for("URL").tap(PLUGIN_NAME, approve);
  147. parser.hooks.evaluateNewExpression.for("URL").tap(PLUGIN_NAME, (expr) => {
  148. const evaluatedExpr = getEvaluatedExpr(
  149. expr,
  150. parser,
  151. importMetaUrlEnabled
  152. );
  153. const request = evaluatedExpr && evaluatedExpr.asString();
  154. if (!request) return;
  155. const url = new URL(request, getUrl(parser.state.module));
  156. return new BasicEvaluatedExpression()
  157. .setString(url.toString())
  158. .setRange(/** @type {Range} */ (expr.range));
  159. });
  160. parser.hooks.new.for("URL").tap(PLUGIN_NAME, (_expr) => {
  161. const expr = /** @type {NewExpressionNode} */ (_expr);
  162. const { options: importOptions, errors: commentErrors } =
  163. parser.parseCommentOptions(/** @type {Range} */ (expr.range));
  164. if (commentErrors) {
  165. for (const e of commentErrors) {
  166. const { comment } = e;
  167. parser.state.module.addWarning(
  168. new (getCommentCompilationWarning())(
  169. `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
  170. parser.getLocation(comment)
  171. )
  172. );
  173. }
  174. }
  175. if (importOptions && importOptions.webpackIgnore !== undefined) {
  176. if (typeof importOptions.webpackIgnore !== "boolean") {
  177. parser.state.module.addWarning(
  178. new (getUnsupportedFeatureWarning())(
  179. `\`webpackIgnore\` expected a boolean, but received: ${importOptions.webpackIgnore}.`,
  180. parser.getLocation(expr)
  181. )
  182. );
  183. return;
  184. } else if (importOptions.webpackIgnore) {
  185. if (expr.arguments.length !== 2) return;
  186. const [, arg2] = expr.arguments;
  187. if (
  188. arg2.type !== "MemberExpression" ||
  189. !importMetaUrlEnabled ||
  190. !isMetaUrl(parser, arg2)
  191. ) {
  192. return;
  193. }
  194. keepNewURL(parser, expr);
  195. return true;
  196. }
  197. }
  198. const evaluatedExpr = getEvaluatedExpr(
  199. expr,
  200. parser,
  201. importMetaUrlEnabled
  202. );
  203. if (!evaluatedExpr) return;
  204. /** @type {string | undefined} */
  205. let request;
  206. // static URL
  207. if ((request = evaluatedExpr.asString())) {
  208. // A directory is not an asset; keep `new URL(...)` as-is, matching Node/browsers.
  209. if (isDirectoryRequest(request)) {
  210. keepNewURL(parser, expr);
  211. return true;
  212. }
  213. const [arg1, arg2] = expr.arguments;
  214. const dep = new URLDependency(
  215. request,
  216. [
  217. /** @type {Range} */ (arg1.range)[0],
  218. /** @type {Range} */ (arg2.range)[1]
  219. ],
  220. /** @type {Range} */ (expr.range),
  221. relative
  222. );
  223. dep.loc = parser.getLocation(expr);
  224. // `parser.javascript.urlHints` provides project-wide defaults
  225. // (resolved per-request, with `test` / `include` / `exclude`
  226. // matching); a per-URL magic comment for the same hint wins
  227. // when both are set.
  228. ResourceHintPlugin.applyResourceHints(
  229. dep,
  230. this.options.urlHints,
  231. request,
  232. importOptions,
  233. parser.state.module,
  234. parser.getLocation(expr)
  235. );
  236. parser.state.current.addDependency(dep);
  237. // For ESM module output the template embeds the asset's resolved
  238. // filename, so the asset must be code-generated first. Only added in
  239. // that mode — a code-gen dependency on an asset that a later watch
  240. // rebuild leaves unaffected can otherwise deadlock the scheduler.
  241. if (parser.state.compilation.options.output.module) {
  242. parser.state.module.addCodeGenerationDependency(dep);
  243. }
  244. getInnerGraphUtils(parser.state.compilation).onUsage(
  245. parser.state,
  246. (e) => (dep.usedByExports = e)
  247. );
  248. return true;
  249. }
  250. if (this.options.dynamicUrl === false) return;
  251. // context URL
  252. /** @type {undefined | RegExp} */
  253. let include;
  254. /** @type {undefined | RegExp} */
  255. let exclude;
  256. if (importOptions) {
  257. if (importOptions.webpackInclude !== undefined) {
  258. if (
  259. !importOptions.webpackInclude ||
  260. !(importOptions.webpackInclude instanceof RegExp)
  261. ) {
  262. parser.state.module.addWarning(
  263. new (getUnsupportedFeatureWarning())(
  264. `\`webpackInclude\` expected a regular expression, but received: ${importOptions.webpackInclude}.`,
  265. parser.getLocation(expr)
  266. )
  267. );
  268. } else {
  269. include = importOptions.webpackInclude;
  270. }
  271. }
  272. if (importOptions.webpackExclude !== undefined) {
  273. if (
  274. !importOptions.webpackExclude ||
  275. !(importOptions.webpackExclude instanceof RegExp)
  276. ) {
  277. parser.state.module.addWarning(
  278. new (getUnsupportedFeatureWarning())(
  279. `\`webpackExclude\` expected a regular expression, but received: ${importOptions.webpackExclude}.`,
  280. parser.getLocation(expr)
  281. )
  282. );
  283. } else {
  284. exclude = importOptions.webpackExclude;
  285. }
  286. }
  287. }
  288. const dep = ContextDependencyHelpers.create(
  289. URLContextDependency,
  290. /** @type {Range} */ (expr.range),
  291. evaluatedExpr,
  292. expr,
  293. this.options,
  294. {
  295. include,
  296. exclude,
  297. mode: "sync",
  298. typePrefix: "new URL with import.meta.url",
  299. category: "url"
  300. },
  301. parser
  302. );
  303. if (!dep) return;
  304. dep.loc = parser.getLocation(expr);
  305. dep.optional = Boolean(parser.scope.inTry);
  306. parser.state.current.addDependency(dep);
  307. return true;
  308. });
  309. parser.hooks.isPure.for("NewExpression").tap(PLUGIN_NAME, (_expr) => {
  310. const expr = /** @type {NewExpressionNode} */ (_expr);
  311. const { callee } = expr;
  312. if (callee.type !== "Identifier") return;
  313. const calleeInfo = parser.getFreeInfoFromVariable(callee.name);
  314. if (!calleeInfo || calleeInfo.name !== "URL") return;
  315. const evaluatedExpr = getEvaluatedExpr(
  316. expr,
  317. parser,
  318. importMetaUrlEnabled
  319. );
  320. const request = evaluatedExpr && evaluatedExpr.asString();
  321. if (request) return true;
  322. });
  323. }
  324. }
  325. module.exports = URLParserPlugin;