HarmonyImportDependencyParserPlugin.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const HotModuleReplacementPlugin = require("../HotModuleReplacementPlugin");
  7. const InnerGraph = require("../optimize/InnerGraph");
  8. const ConstDependency = require("./ConstDependency");
  9. const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
  10. const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
  11. const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImportSpecifierDependency");
  12. const HarmonyExports = require("./HarmonyExports");
  13. const { ExportPresenceModes } = require("./HarmonyImportDependency");
  14. const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
  15. const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
  16. /** @typedef {import("estree").ExportAllDeclaration} ExportAllDeclaration */
  17. /** @typedef {import("estree").ExportNamedDeclaration} ExportNamedDeclaration */
  18. /** @typedef {import("estree").Identifier} Identifier */
  19. /** @typedef {import("estree").ImportDeclaration} ImportDeclaration */
  20. /** @typedef {import("estree").ImportExpression} ImportExpression */
  21. /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
  22. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  23. /** @typedef {import("../optimize/InnerGraph").InnerGraph} InnerGraph */
  24. /** @typedef {import("../optimize/InnerGraph").TopLevelSymbol} TopLevelSymbol */
  25. /** @typedef {import("./HarmonyImportDependency")} HarmonyImportDependency */
  26. const harmonySpecifierTag = Symbol("harmony import");
  27. /**
  28. * @typedef {Object} HarmonySettings
  29. * @property {string[]} ids
  30. * @property {string} source
  31. * @property {number} sourceOrder
  32. * @property {string} name
  33. * @property {boolean} await
  34. * @property {Record<string, any> | undefined} assertions
  35. */
  36. /**
  37. * @param {ImportDeclaration | ExportNamedDeclaration | ExportAllDeclaration | ImportExpression} node node with assertions
  38. * @returns {Record<string, any> | undefined} assertions
  39. */
  40. function getAssertions(node) {
  41. // TODO remove cast when @types/estree has been updated to import assertions
  42. const assertions = /** @type {{ assertions?: ImportAttributeNode[] }} */ (
  43. node
  44. ).assertions;
  45. if (assertions === undefined) {
  46. return undefined;
  47. }
  48. const result = {};
  49. for (const assertion of assertions) {
  50. const key =
  51. assertion.key.type === "Identifier"
  52. ? assertion.key.name
  53. : assertion.key.value;
  54. result[key] = assertion.value.value;
  55. }
  56. return result;
  57. }
  58. module.exports = class HarmonyImportDependencyParserPlugin {
  59. /**
  60. * @param {JavascriptParserOptions} options options
  61. */
  62. constructor(options) {
  63. this.exportPresenceMode =
  64. options.importExportsPresence !== undefined
  65. ? ExportPresenceModes.fromUserOption(options.importExportsPresence)
  66. : options.exportsPresence !== undefined
  67. ? ExportPresenceModes.fromUserOption(options.exportsPresence)
  68. : options.strictExportPresence
  69. ? ExportPresenceModes.ERROR
  70. : ExportPresenceModes.AUTO;
  71. this.strictThisContextOnImports = options.strictThisContextOnImports;
  72. }
  73. /**
  74. * @param {JavascriptParser} parser the parser
  75. * @returns {void}
  76. */
  77. apply(parser) {
  78. const { exportPresenceMode } = this;
  79. function getNonOptionalPart(members, membersOptionals) {
  80. let i = 0;
  81. while (i < members.length && membersOptionals[i] === false) i++;
  82. return i !== members.length ? members.slice(0, i) : members;
  83. }
  84. function getNonOptionalMemberChain(node, count) {
  85. while (count--) node = node.object;
  86. return node;
  87. }
  88. parser.hooks.isPure
  89. .for("Identifier")
  90. .tap("HarmonyImportDependencyParserPlugin", expression => {
  91. const expr = /** @type {Identifier} */ (expression);
  92. if (
  93. parser.isVariableDefined(expr.name) ||
  94. parser.getTagData(expr.name, harmonySpecifierTag)
  95. ) {
  96. return true;
  97. }
  98. });
  99. parser.hooks.import.tap(
  100. "HarmonyImportDependencyParserPlugin",
  101. (statement, source) => {
  102. parser.state.lastHarmonyImportOrder =
  103. (parser.state.lastHarmonyImportOrder || 0) + 1;
  104. const clearDep = new ConstDependency(
  105. parser.isAsiPosition(statement.range[0]) ? ";" : "",
  106. statement.range
  107. );
  108. clearDep.loc = statement.loc;
  109. parser.state.module.addPresentationalDependency(clearDep);
  110. parser.unsetAsiPosition(statement.range[1]);
  111. const assertions = getAssertions(statement);
  112. const sideEffectDep = new HarmonyImportSideEffectDependency(
  113. source,
  114. parser.state.lastHarmonyImportOrder,
  115. assertions
  116. );
  117. sideEffectDep.loc = statement.loc;
  118. parser.state.module.addDependency(sideEffectDep);
  119. return true;
  120. }
  121. );
  122. parser.hooks.importSpecifier.tap(
  123. "HarmonyImportDependencyParserPlugin",
  124. (statement, source, id, name) => {
  125. const ids = id === null ? [] : [id];
  126. parser.tagVariable(name, harmonySpecifierTag, {
  127. name,
  128. source,
  129. ids,
  130. sourceOrder: parser.state.lastHarmonyImportOrder,
  131. assertions: getAssertions(statement)
  132. });
  133. return true;
  134. }
  135. );
  136. parser.hooks.binaryExpression.tap(
  137. "HarmonyImportDependencyParserPlugin",
  138. expression => {
  139. if (expression.operator !== "in") return;
  140. const leftPartEvaluated = parser.evaluateExpression(expression.left);
  141. if (leftPartEvaluated.couldHaveSideEffects()) return;
  142. const leftPart = leftPartEvaluated.asString();
  143. if (!leftPart) return;
  144. const rightPart = parser.evaluateExpression(expression.right);
  145. if (!rightPart.isIdentifier()) return;
  146. const rootInfo = rightPart.rootInfo;
  147. if (
  148. !rootInfo ||
  149. !rootInfo.tagInfo ||
  150. rootInfo.tagInfo.tag !== harmonySpecifierTag
  151. )
  152. return;
  153. const settings = rootInfo.tagInfo.data;
  154. const members = rightPart.getMembers();
  155. const dep = new HarmonyEvaluatedImportSpecifierDependency(
  156. settings.source,
  157. settings.sourceOrder,
  158. settings.ids.concat(members).concat([leftPart]),
  159. settings.name,
  160. expression.range,
  161. settings.assertions,
  162. "in"
  163. );
  164. dep.directImport = members.length === 0;
  165. dep.asiSafe = !parser.isAsiPosition(expression.range[0]);
  166. dep.loc = expression.loc;
  167. parser.state.module.addDependency(dep);
  168. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  169. return true;
  170. }
  171. );
  172. parser.hooks.expression
  173. .for(harmonySpecifierTag)
  174. .tap("HarmonyImportDependencyParserPlugin", expr => {
  175. const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
  176. const dep = new HarmonyImportSpecifierDependency(
  177. settings.source,
  178. settings.sourceOrder,
  179. settings.ids,
  180. settings.name,
  181. expr.range,
  182. exportPresenceMode,
  183. settings.assertions
  184. );
  185. dep.referencedPropertiesInDestructuring =
  186. parser.destructuringAssignmentPropertiesFor(expr);
  187. dep.shorthand = parser.scope.inShorthand;
  188. dep.directImport = true;
  189. dep.asiSafe = !parser.isAsiPosition(expr.range[0]);
  190. dep.loc = expr.loc;
  191. parser.state.module.addDependency(dep);
  192. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  193. return true;
  194. });
  195. parser.hooks.expressionMemberChain
  196. .for(harmonySpecifierTag)
  197. .tap(
  198. "HarmonyImportDependencyParserPlugin",
  199. (expression, members, membersOptionals) => {
  200. const settings = /** @type {HarmonySettings} */ (
  201. parser.currentTagData
  202. );
  203. const nonOptionalMembers = getNonOptionalPart(
  204. members,
  205. membersOptionals
  206. );
  207. const expr =
  208. nonOptionalMembers !== members
  209. ? getNonOptionalMemberChain(
  210. expression,
  211. members.length - nonOptionalMembers.length
  212. )
  213. : expression;
  214. const ids = settings.ids.concat(nonOptionalMembers);
  215. const dep = new HarmonyImportSpecifierDependency(
  216. settings.source,
  217. settings.sourceOrder,
  218. ids,
  219. settings.name,
  220. expr.range,
  221. exportPresenceMode,
  222. settings.assertions
  223. );
  224. dep.referencedPropertiesInDestructuring =
  225. parser.destructuringAssignmentPropertiesFor(expr);
  226. dep.asiSafe = !parser.isAsiPosition(expr.range[0]);
  227. dep.loc = expr.loc;
  228. parser.state.module.addDependency(dep);
  229. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  230. return true;
  231. }
  232. );
  233. parser.hooks.callMemberChain
  234. .for(harmonySpecifierTag)
  235. .tap(
  236. "HarmonyImportDependencyParserPlugin",
  237. (expression, members, membersOptionals) => {
  238. const { arguments: args, callee } = expression;
  239. const settings = /** @type {HarmonySettings} */ (
  240. parser.currentTagData
  241. );
  242. const nonOptionalMembers = getNonOptionalPart(
  243. members,
  244. membersOptionals
  245. );
  246. const expr =
  247. nonOptionalMembers !== members
  248. ? getNonOptionalMemberChain(
  249. callee,
  250. members.length - nonOptionalMembers.length
  251. )
  252. : callee;
  253. const ids = settings.ids.concat(nonOptionalMembers);
  254. const dep = new HarmonyImportSpecifierDependency(
  255. settings.source,
  256. settings.sourceOrder,
  257. ids,
  258. settings.name,
  259. expr.range,
  260. exportPresenceMode,
  261. settings.assertions
  262. );
  263. dep.directImport = members.length === 0;
  264. dep.call = true;
  265. dep.asiSafe = !parser.isAsiPosition(expr.range[0]);
  266. // only in case when we strictly follow the spec we need a special case here
  267. dep.namespaceObjectAsContext =
  268. members.length > 0 && this.strictThisContextOnImports;
  269. dep.loc = expr.loc;
  270. parser.state.module.addDependency(dep);
  271. if (args) parser.walkExpressions(args);
  272. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  273. return true;
  274. }
  275. );
  276. const { hotAcceptCallback, hotAcceptWithoutCallback } =
  277. HotModuleReplacementPlugin.getParserHooks(parser);
  278. hotAcceptCallback.tap(
  279. "HarmonyImportDependencyParserPlugin",
  280. (expr, requests) => {
  281. if (!HarmonyExports.isEnabled(parser.state)) {
  282. // This is not a harmony module, skip it
  283. return;
  284. }
  285. const dependencies = requests.map(request => {
  286. const dep = new HarmonyAcceptImportDependency(request);
  287. dep.loc = expr.loc;
  288. parser.state.module.addDependency(dep);
  289. return dep;
  290. });
  291. if (dependencies.length > 0) {
  292. const dep = new HarmonyAcceptDependency(
  293. expr.range,
  294. dependencies,
  295. true
  296. );
  297. dep.loc = expr.loc;
  298. parser.state.module.addDependency(dep);
  299. }
  300. }
  301. );
  302. hotAcceptWithoutCallback.tap(
  303. "HarmonyImportDependencyParserPlugin",
  304. (expr, requests) => {
  305. if (!HarmonyExports.isEnabled(parser.state)) {
  306. // This is not a harmony module, skip it
  307. return;
  308. }
  309. const dependencies = requests.map(request => {
  310. const dep = new HarmonyAcceptImportDependency(request);
  311. dep.loc = expr.loc;
  312. parser.state.module.addDependency(dep);
  313. return dep;
  314. });
  315. if (dependencies.length > 0) {
  316. const dep = new HarmonyAcceptDependency(
  317. expr.range,
  318. dependencies,
  319. false
  320. );
  321. dep.loc = expr.loc;
  322. parser.state.module.addDependency(dep);
  323. }
  324. }
  325. );
  326. }
  327. };
  328. module.exports.harmonySpecifierTag = harmonySpecifierTag;
  329. module.exports.getAssertions = getAssertions;