HarmonyImportDependencyParserPlugin.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const CommentCompilationWarning = require("../CommentCompilationWarning");
  7. const HotModuleReplacementPlugin = require("../HotModuleReplacementPlugin");
  8. const WebpackError = require("../WebpackError");
  9. const { getImportAttributes } = require("../javascript/JavascriptParser");
  10. const InnerGraph = require("../optimize/InnerGraph");
  11. const ConstDependency = require("./ConstDependency");
  12. const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
  13. const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
  14. const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImportSpecifierDependency");
  15. const HarmonyExports = require("./HarmonyExports");
  16. const { ExportPresenceModes } = require("./HarmonyImportDependency");
  17. const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
  18. const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
  19. /** @typedef {import("estree").Expression} Expression */
  20. /** @typedef {import("estree").Identifier} Identifier */
  21. /** @typedef {import("estree").Literal} Literal */
  22. /** @typedef {import("estree").MemberExpression} MemberExpression */
  23. /** @typedef {import("estree").ObjectExpression} ObjectExpression */
  24. /** @typedef {import("estree").Property} Property */
  25. /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
  26. /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
  27. /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
  28. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  29. /** @typedef {import("../javascript/JavascriptParser").DestructuringAssignmentProperty} DestructuringAssignmentProperty */
  30. /** @typedef {import("../javascript/JavascriptParser").ExportAllDeclaration} ExportAllDeclaration */
  31. /** @typedef {import("../javascript/JavascriptParser").ExportNamedDeclaration} ExportNamedDeclaration */
  32. /** @typedef {import("../javascript/JavascriptParser").ImportAttributes} ImportAttributes */
  33. /** @typedef {import("../javascript/JavascriptParser").ImportDeclaration} ImportDeclaration */
  34. /** @typedef {import("../javascript/JavascriptParser").ImportExpression} ImportExpression */
  35. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  36. /** @typedef {import("../javascript/JavascriptParser").TagData} TagData */
  37. /** @typedef {import("../optimize/InnerGraph").InnerGraph} InnerGraph */
  38. /** @typedef {import("../optimize/InnerGraph").TopLevelSymbol} TopLevelSymbol */
  39. /** @typedef {import("./HarmonyImportDependency")} HarmonyImportDependency */
  40. const harmonySpecifierTag = Symbol("harmony import");
  41. /**
  42. * @typedef {object} HarmonySettings
  43. * @property {string[]} ids
  44. * @property {string} source
  45. * @property {number} sourceOrder
  46. * @property {string} name
  47. * @property {boolean} await
  48. * @property {ImportAttributes=} attributes
  49. * @property {boolean | undefined} defer
  50. */
  51. const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
  52. module.exports = class HarmonyImportDependencyParserPlugin {
  53. /**
  54. * @param {JavascriptParserOptions} options options
  55. * @param {boolean | undefined} deferImport defer import enabled
  56. */
  57. constructor(options, deferImport) {
  58. this.exportPresenceMode =
  59. options.importExportsPresence !== undefined
  60. ? ExportPresenceModes.fromUserOption(options.importExportsPresence)
  61. : options.exportsPresence !== undefined
  62. ? ExportPresenceModes.fromUserOption(options.exportsPresence)
  63. : options.strictExportPresence
  64. ? ExportPresenceModes.ERROR
  65. : ExportPresenceModes.AUTO;
  66. this.strictThisContextOnImports = options.strictThisContextOnImports;
  67. this.deferImport = deferImport;
  68. }
  69. /**
  70. * @param {JavascriptParser} parser the parser
  71. * @returns {void}
  72. */
  73. apply(parser) {
  74. const { exportPresenceMode } = this;
  75. /**
  76. * @param {string[]} members members
  77. * @param {boolean[]} membersOptionals members Optionals
  78. * @returns {string[]} a non optional part
  79. */
  80. function getNonOptionalPart(members, membersOptionals) {
  81. let i = 0;
  82. while (i < members.length && membersOptionals[i] === false) i++;
  83. return i !== members.length ? members.slice(0, i) : members;
  84. }
  85. /**
  86. * @param {MemberExpression} node member expression
  87. * @param {number} count count
  88. * @returns {Expression} member expression
  89. */
  90. function getNonOptionalMemberChain(node, count) {
  91. while (count--) node = /** @type {MemberExpression} */ (node.object);
  92. return node;
  93. }
  94. parser.hooks.isPure.for("Identifier").tap(PLUGIN_NAME, expression => {
  95. const expr = /** @type {Identifier} */ (expression);
  96. if (
  97. parser.isVariableDefined(expr.name) ||
  98. parser.getTagData(expr.name, harmonySpecifierTag)
  99. ) {
  100. return true;
  101. }
  102. });
  103. parser.hooks.import.tap(PLUGIN_NAME, (statement, source) => {
  104. parser.state.lastHarmonyImportOrder =
  105. (parser.state.lastHarmonyImportOrder || 0) + 1;
  106. const clearDep = new ConstDependency(
  107. parser.isAsiPosition(/** @type {Range} */ (statement.range)[0])
  108. ? ";"
  109. : "",
  110. /** @type {Range} */ (statement.range)
  111. );
  112. clearDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  113. parser.state.module.addPresentationalDependency(clearDep);
  114. parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]);
  115. const attributes = getImportAttributes(statement);
  116. const { defer } = getImportMode(parser, statement);
  117. if (
  118. defer &&
  119. (statement.specifiers.length !== 1 ||
  120. statement.specifiers[0].type !== "ImportNamespaceSpecifier")
  121. ) {
  122. const error = new WebpackError(
  123. "Deferred import can only be used with `import * as namespace from '...'` syntax."
  124. );
  125. error.loc = statement.loc || undefined;
  126. parser.state.current.addError(error);
  127. }
  128. const sideEffectDep = new HarmonyImportSideEffectDependency(
  129. /** @type {string} */ (source),
  130. parser.state.lastHarmonyImportOrder,
  131. attributes,
  132. defer
  133. );
  134. sideEffectDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  135. parser.state.module.addDependency(sideEffectDep);
  136. return true;
  137. });
  138. parser.hooks.importSpecifier.tap(
  139. PLUGIN_NAME,
  140. (statement, source, id, name) => {
  141. const ids = id === null ? [] : [id];
  142. const { defer } = getImportMode(parser, statement);
  143. parser.tagVariable(name, harmonySpecifierTag, {
  144. name,
  145. source,
  146. ids,
  147. sourceOrder: parser.state.lastHarmonyImportOrder,
  148. attributes: getImportAttributes(statement),
  149. defer
  150. });
  151. return true;
  152. }
  153. );
  154. parser.hooks.binaryExpression.tap(PLUGIN_NAME, expression => {
  155. if (expression.operator !== "in") return;
  156. const leftPartEvaluated = parser.evaluateExpression(expression.left);
  157. if (leftPartEvaluated.couldHaveSideEffects()) return;
  158. /** @type {string | undefined} */
  159. const leftPart = leftPartEvaluated.asString();
  160. if (!leftPart) return;
  161. const rightPart = parser.evaluateExpression(expression.right);
  162. if (!rightPart.isIdentifier()) return;
  163. const rootInfo = rightPart.rootInfo;
  164. if (
  165. typeof rootInfo === "string" ||
  166. !rootInfo ||
  167. !rootInfo.tagInfo ||
  168. rootInfo.tagInfo.tag !== harmonySpecifierTag
  169. ) {
  170. return;
  171. }
  172. const settings =
  173. /** @type {TagData} */
  174. (rootInfo.tagInfo.data);
  175. const members =
  176. /** @type {(() => string[])} */
  177. (rightPart.getMembers)();
  178. const dep = new HarmonyEvaluatedImportSpecifierDependency(
  179. settings.source,
  180. settings.sourceOrder,
  181. [...settings.ids, ...members, leftPart],
  182. settings.name,
  183. /** @type {Range} */ (expression.range),
  184. settings.attributes,
  185. "in"
  186. );
  187. dep.directImport = members.length === 0;
  188. dep.asiSafe = !parser.isAsiPosition(
  189. /** @type {Range} */ (expression.range)[0]
  190. );
  191. dep.loc = /** @type {DependencyLocation} */ (expression.loc);
  192. parser.state.module.addDependency(dep);
  193. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  194. return true;
  195. });
  196. parser.hooks.expression.for(harmonySpecifierTag).tap(PLUGIN_NAME, expr => {
  197. const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
  198. const dep = new HarmonyImportSpecifierDependency(
  199. settings.source,
  200. settings.sourceOrder,
  201. settings.ids,
  202. settings.name,
  203. /** @type {Range} */
  204. (expr.range),
  205. exportPresenceMode,
  206. settings.attributes,
  207. [],
  208. settings.defer
  209. );
  210. dep.referencedPropertiesInDestructuring =
  211. parser.destructuringAssignmentPropertiesFor(expr);
  212. dep.shorthand = parser.scope.inShorthand;
  213. dep.directImport = true;
  214. dep.asiSafe = !parser.isAsiPosition(/** @type {Range} */ (expr.range)[0]);
  215. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  216. dep.call = parser.scope.inTaggedTemplateTag;
  217. parser.state.module.addDependency(dep);
  218. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  219. return true;
  220. });
  221. parser.hooks.expressionMemberChain
  222. .for(harmonySpecifierTag)
  223. .tap(
  224. PLUGIN_NAME,
  225. (expression, members, membersOptionals, memberRanges) => {
  226. const settings =
  227. /** @type {HarmonySettings} */
  228. (parser.currentTagData);
  229. const nonOptionalMembers = getNonOptionalPart(
  230. members,
  231. membersOptionals
  232. );
  233. /** @type {Range[]} */
  234. const ranges = memberRanges.slice(
  235. 0,
  236. memberRanges.length - (members.length - nonOptionalMembers.length)
  237. );
  238. const expr =
  239. nonOptionalMembers !== members
  240. ? getNonOptionalMemberChain(
  241. expression,
  242. members.length - nonOptionalMembers.length
  243. )
  244. : expression;
  245. const ids = [...settings.ids, ...nonOptionalMembers];
  246. const dep = new HarmonyImportSpecifierDependency(
  247. settings.source,
  248. settings.sourceOrder,
  249. ids,
  250. settings.name,
  251. /** @type {Range} */
  252. (expr.range),
  253. exportPresenceMode,
  254. settings.attributes,
  255. ranges,
  256. settings.defer
  257. );
  258. dep.referencedPropertiesInDestructuring =
  259. parser.destructuringAssignmentPropertiesFor(expr);
  260. dep.asiSafe = !parser.isAsiPosition(
  261. /** @type {Range} */
  262. (expr.range)[0]
  263. );
  264. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  265. parser.state.module.addDependency(dep);
  266. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  267. return true;
  268. }
  269. );
  270. parser.hooks.callMemberChain
  271. .for(harmonySpecifierTag)
  272. .tap(
  273. PLUGIN_NAME,
  274. (expression, members, membersOptionals, memberRanges) => {
  275. const { arguments: args } = expression;
  276. const callee = /** @type {MemberExpression} */ (expression.callee);
  277. const settings = /** @type {HarmonySettings} */ (
  278. parser.currentTagData
  279. );
  280. const nonOptionalMembers = getNonOptionalPart(
  281. members,
  282. membersOptionals
  283. );
  284. /** @type {Range[]} */
  285. const ranges = memberRanges.slice(
  286. 0,
  287. memberRanges.length - (members.length - nonOptionalMembers.length)
  288. );
  289. const expr =
  290. nonOptionalMembers !== members
  291. ? getNonOptionalMemberChain(
  292. callee,
  293. members.length - nonOptionalMembers.length
  294. )
  295. : callee;
  296. const ids = [...settings.ids, ...nonOptionalMembers];
  297. const dep = new HarmonyImportSpecifierDependency(
  298. settings.source,
  299. settings.sourceOrder,
  300. ids,
  301. settings.name,
  302. /** @type {Range} */ (expr.range),
  303. exportPresenceMode,
  304. settings.attributes,
  305. ranges,
  306. settings.defer
  307. );
  308. dep.directImport = members.length === 0;
  309. dep.call = true;
  310. dep.asiSafe = !parser.isAsiPosition(
  311. /** @type {Range} */ (expr.range)[0]
  312. );
  313. // only in case when we strictly follow the spec we need a special case here
  314. dep.namespaceObjectAsContext =
  315. members.length > 0 &&
  316. /** @type {boolean} */ (this.strictThisContextOnImports);
  317. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  318. parser.state.module.addDependency(dep);
  319. if (args) parser.walkExpressions(args);
  320. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  321. return true;
  322. }
  323. );
  324. const { hotAcceptCallback, hotAcceptWithoutCallback } =
  325. HotModuleReplacementPlugin.getParserHooks(parser);
  326. hotAcceptCallback.tap(PLUGIN_NAME, (expr, requests) => {
  327. if (!HarmonyExports.isEnabled(parser.state)) {
  328. // This is not a harmony module, skip it
  329. return;
  330. }
  331. const dependencies = requests.map(request => {
  332. const dep = new HarmonyAcceptImportDependency(request);
  333. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  334. parser.state.module.addDependency(dep);
  335. return dep;
  336. });
  337. if (dependencies.length > 0) {
  338. const dep = new HarmonyAcceptDependency(
  339. /** @type {Range} */
  340. (expr.range),
  341. dependencies,
  342. true
  343. );
  344. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  345. parser.state.module.addDependency(dep);
  346. }
  347. });
  348. hotAcceptWithoutCallback.tap(PLUGIN_NAME, (expr, requests) => {
  349. if (!HarmonyExports.isEnabled(parser.state)) {
  350. // This is not a harmony module, skip it
  351. return;
  352. }
  353. const dependencies = requests.map(request => {
  354. const dep = new HarmonyAcceptImportDependency(request);
  355. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  356. parser.state.module.addDependency(dep);
  357. return dep;
  358. });
  359. if (dependencies.length > 0) {
  360. const dep = new HarmonyAcceptDependency(
  361. /** @type {Range} */
  362. (expr.range),
  363. dependencies,
  364. false
  365. );
  366. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  367. parser.state.module.addDependency(dep);
  368. }
  369. });
  370. }
  371. };
  372. /**
  373. * @param {JavascriptParser} parser parser
  374. * @param {ExportNamedDeclaration | ExportAllDeclaration | ImportDeclaration} node node
  375. * @returns {{defer: boolean}} import attributes
  376. */
  377. function getImportMode(parser, node) {
  378. const result = { defer: "phase" in node && node.phase === "defer" };
  379. if (!node.range) {
  380. return result;
  381. }
  382. const { options, errors } = parser.parseCommentOptions(node.range);
  383. if (errors) {
  384. for (const e of errors) {
  385. const { comment } = e;
  386. if (!comment.loc) continue;
  387. parser.state.module.addWarning(
  388. new CommentCompilationWarning(
  389. `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
  390. comment.loc
  391. )
  392. );
  393. }
  394. }
  395. if (!options) return result;
  396. if (options.webpackDefer) {
  397. if (typeof options.webpackDefer === "boolean") {
  398. result.defer = options.webpackDefer;
  399. } else if (node.loc) {
  400. parser.state.module.addWarning(
  401. new CommentCompilationWarning(
  402. "webpackDefer magic comment expected a boolean value.",
  403. node.loc
  404. )
  405. );
  406. }
  407. }
  408. return result;
  409. }
  410. module.exports.getImportMode = getImportMode;
  411. module.exports.harmonySpecifierTag = harmonySpecifierTag;