HarmonyImportDependencyParserPlugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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 { getImportAttributes } = require("../javascript/JavascriptParser");
  8. const InnerGraph = require("../optimize/InnerGraph");
  9. const ConstDependency = require("./ConstDependency");
  10. const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
  11. const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
  12. const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImportSpecifierDependency");
  13. const HarmonyExports = require("./HarmonyExports");
  14. const { ExportPresenceModes } = require("./HarmonyImportDependency");
  15. const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
  16. const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
  17. /** @typedef {import("estree").Expression} Expression */
  18. /** @typedef {import("estree").Identifier} Identifier */
  19. /** @typedef {import("estree").Literal} Literal */
  20. /** @typedef {import("estree").MemberExpression} MemberExpression */
  21. /** @typedef {import("estree").ObjectExpression} ObjectExpression */
  22. /** @typedef {import("estree").Property} Property */
  23. /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
  24. /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
  25. /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
  26. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  27. /** @typedef {import("../javascript/JavascriptParser").DestructuringAssignmentProperty} DestructuringAssignmentProperty */
  28. /** @typedef {import("../javascript/JavascriptParser").ExportAllDeclaration} ExportAllDeclaration */
  29. /** @typedef {import("../javascript/JavascriptParser").ExportNamedDeclaration} ExportNamedDeclaration */
  30. /** @typedef {import("../javascript/JavascriptParser").ImportAttributes} ImportAttributes */
  31. /** @typedef {import("../javascript/JavascriptParser").ImportDeclaration} ImportDeclaration */
  32. /** @typedef {import("../javascript/JavascriptParser").ImportExpression} ImportExpression */
  33. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  34. /** @typedef {import("../javascript/JavascriptParser").TagData} TagData */
  35. /** @typedef {import("../optimize/InnerGraph").InnerGraph} InnerGraph */
  36. /** @typedef {import("../optimize/InnerGraph").TopLevelSymbol} TopLevelSymbol */
  37. /** @typedef {import("./HarmonyImportDependency")} HarmonyImportDependency */
  38. const harmonySpecifierTag = Symbol("harmony import");
  39. /**
  40. * @typedef {object} HarmonySettings
  41. * @property {string[]} ids
  42. * @property {string} source
  43. * @property {number} sourceOrder
  44. * @property {string} name
  45. * @property {boolean} await
  46. * @property {Record<string, any> | undefined} attributes
  47. */
  48. module.exports = class HarmonyImportDependencyParserPlugin {
  49. /**
  50. * @param {JavascriptParserOptions} options options
  51. */
  52. constructor(options) {
  53. this.exportPresenceMode =
  54. options.importExportsPresence !== undefined
  55. ? ExportPresenceModes.fromUserOption(options.importExportsPresence)
  56. : options.exportsPresence !== undefined
  57. ? ExportPresenceModes.fromUserOption(options.exportsPresence)
  58. : options.strictExportPresence
  59. ? ExportPresenceModes.ERROR
  60. : ExportPresenceModes.AUTO;
  61. this.strictThisContextOnImports = options.strictThisContextOnImports;
  62. }
  63. /**
  64. * @param {JavascriptParser} parser the parser
  65. * @returns {void}
  66. */
  67. apply(parser) {
  68. const { exportPresenceMode } = this;
  69. /**
  70. * @param {string[]} members members
  71. * @param {boolean[]} membersOptionals members Optionals
  72. * @returns {string[]} a non optional part
  73. */
  74. function getNonOptionalPart(members, membersOptionals) {
  75. let i = 0;
  76. while (i < members.length && membersOptionals[i] === false) i++;
  77. return i !== members.length ? members.slice(0, i) : members;
  78. }
  79. /**
  80. * @param {TODO} node member expression
  81. * @param {number} count count
  82. * @returns {Expression} member expression
  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(/** @type {Range} */ (statement.range)[0])
  106. ? ";"
  107. : "",
  108. /** @type {Range} */ (statement.range)
  109. );
  110. clearDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  111. parser.state.module.addPresentationalDependency(clearDep);
  112. parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]);
  113. const attributes = getImportAttributes(statement);
  114. const sideEffectDep = new HarmonyImportSideEffectDependency(
  115. /** @type {string} */ (source),
  116. parser.state.lastHarmonyImportOrder,
  117. attributes
  118. );
  119. sideEffectDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  120. parser.state.module.addDependency(sideEffectDep);
  121. return true;
  122. }
  123. );
  124. parser.hooks.importSpecifier.tap(
  125. "HarmonyImportDependencyParserPlugin",
  126. (statement, source, id, name) => {
  127. const ids = id === null ? [] : [id];
  128. parser.tagVariable(name, harmonySpecifierTag, {
  129. name,
  130. source,
  131. ids,
  132. sourceOrder: parser.state.lastHarmonyImportOrder,
  133. attributes: getImportAttributes(statement)
  134. });
  135. return true;
  136. }
  137. );
  138. parser.hooks.binaryExpression.tap(
  139. "HarmonyImportDependencyParserPlugin",
  140. expression => {
  141. if (expression.operator !== "in") return;
  142. const leftPartEvaluated = parser.evaluateExpression(expression.left);
  143. if (leftPartEvaluated.couldHaveSideEffects()) return;
  144. const leftPart = leftPartEvaluated.asString();
  145. if (!leftPart) return;
  146. const rightPart = parser.evaluateExpression(expression.right);
  147. if (!rightPart.isIdentifier()) return;
  148. const rootInfo = rightPart.rootInfo;
  149. if (
  150. typeof rootInfo === "string" ||
  151. !rootInfo ||
  152. !rootInfo.tagInfo ||
  153. rootInfo.tagInfo.tag !== harmonySpecifierTag
  154. )
  155. return;
  156. const settings = /** @type {TagData} */ (rootInfo.tagInfo.data);
  157. const members =
  158. /** @type {(() => string[])} */
  159. (rightPart.getMembers)();
  160. const dep = new HarmonyEvaluatedImportSpecifierDependency(
  161. settings.source,
  162. settings.sourceOrder,
  163. settings.ids.concat(members).concat([leftPart]),
  164. settings.name,
  165. /** @type {Range} */ (expression.range),
  166. settings.attributes,
  167. "in"
  168. );
  169. dep.directImport = members.length === 0;
  170. dep.asiSafe = !parser.isAsiPosition(
  171. /** @type {Range} */ (expression.range)[0]
  172. );
  173. dep.loc = /** @type {DependencyLocation} */ (expression.loc);
  174. parser.state.module.addDependency(dep);
  175. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  176. return true;
  177. }
  178. );
  179. parser.hooks.expression
  180. .for(harmonySpecifierTag)
  181. .tap("HarmonyImportDependencyParserPlugin", expr => {
  182. const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
  183. const dep = new HarmonyImportSpecifierDependency(
  184. settings.source,
  185. settings.sourceOrder,
  186. settings.ids,
  187. settings.name,
  188. /** @type {Range} */
  189. (expr.range),
  190. exportPresenceMode,
  191. settings.attributes,
  192. []
  193. );
  194. dep.referencedPropertiesInDestructuring =
  195. parser.destructuringAssignmentPropertiesFor(expr);
  196. dep.shorthand = parser.scope.inShorthand;
  197. dep.directImport = true;
  198. dep.asiSafe = !parser.isAsiPosition(
  199. /** @type {Range} */ (expr.range)[0]
  200. );
  201. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  202. dep.call = parser.scope.inTaggedTemplateTag;
  203. parser.state.module.addDependency(dep);
  204. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  205. return true;
  206. });
  207. parser.hooks.expressionMemberChain
  208. .for(harmonySpecifierTag)
  209. .tap(
  210. "HarmonyImportDependencyParserPlugin",
  211. (expression, members, membersOptionals, memberRanges) => {
  212. const settings =
  213. /** @type {HarmonySettings} */
  214. (parser.currentTagData);
  215. const nonOptionalMembers = getNonOptionalPart(
  216. members,
  217. membersOptionals
  218. );
  219. /** @type {Range[]} */
  220. const ranges = memberRanges.slice(
  221. 0,
  222. memberRanges.length - (members.length - nonOptionalMembers.length)
  223. );
  224. const expr =
  225. nonOptionalMembers !== members
  226. ? getNonOptionalMemberChain(
  227. expression,
  228. members.length - nonOptionalMembers.length
  229. )
  230. : expression;
  231. const ids = settings.ids.concat(nonOptionalMembers);
  232. const dep = new HarmonyImportSpecifierDependency(
  233. settings.source,
  234. settings.sourceOrder,
  235. ids,
  236. settings.name,
  237. /** @type {Range} */
  238. (expr.range),
  239. exportPresenceMode,
  240. settings.attributes,
  241. ranges
  242. );
  243. dep.referencedPropertiesInDestructuring =
  244. parser.destructuringAssignmentPropertiesFor(expr);
  245. dep.asiSafe = !parser.isAsiPosition(
  246. /** @type {Range} */
  247. (expr.range)[0]
  248. );
  249. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  250. parser.state.module.addDependency(dep);
  251. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  252. return true;
  253. }
  254. );
  255. parser.hooks.callMemberChain
  256. .for(harmonySpecifierTag)
  257. .tap(
  258. "HarmonyImportDependencyParserPlugin",
  259. (expression, members, membersOptionals, memberRanges) => {
  260. const { arguments: args, callee } = expression;
  261. const settings = /** @type {HarmonySettings} */ (
  262. parser.currentTagData
  263. );
  264. const nonOptionalMembers = getNonOptionalPart(
  265. members,
  266. membersOptionals
  267. );
  268. /** @type {Range[]} */
  269. const ranges = memberRanges.slice(
  270. 0,
  271. memberRanges.length - (members.length - nonOptionalMembers.length)
  272. );
  273. const expr =
  274. nonOptionalMembers !== members
  275. ? getNonOptionalMemberChain(
  276. callee,
  277. members.length - nonOptionalMembers.length
  278. )
  279. : callee;
  280. const ids = settings.ids.concat(nonOptionalMembers);
  281. const dep = new HarmonyImportSpecifierDependency(
  282. settings.source,
  283. settings.sourceOrder,
  284. ids,
  285. settings.name,
  286. /** @type {Range} */ (expr.range),
  287. exportPresenceMode,
  288. settings.attributes,
  289. ranges
  290. );
  291. dep.directImport = members.length === 0;
  292. dep.call = true;
  293. dep.asiSafe = !parser.isAsiPosition(
  294. /** @type {Range} */ (expr.range)[0]
  295. );
  296. // only in case when we strictly follow the spec we need a special case here
  297. dep.namespaceObjectAsContext =
  298. members.length > 0 &&
  299. /** @type {boolean} */ (this.strictThisContextOnImports);
  300. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  301. parser.state.module.addDependency(dep);
  302. if (args) parser.walkExpressions(args);
  303. InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
  304. return true;
  305. }
  306. );
  307. const { hotAcceptCallback, hotAcceptWithoutCallback } =
  308. HotModuleReplacementPlugin.getParserHooks(parser);
  309. hotAcceptCallback.tap(
  310. "HarmonyImportDependencyParserPlugin",
  311. (expr, requests) => {
  312. if (!HarmonyExports.isEnabled(parser.state)) {
  313. // This is not a harmony module, skip it
  314. return;
  315. }
  316. const dependencies = requests.map(request => {
  317. const dep = new HarmonyAcceptImportDependency(request);
  318. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  319. parser.state.module.addDependency(dep);
  320. return dep;
  321. });
  322. if (dependencies.length > 0) {
  323. const dep = new HarmonyAcceptDependency(
  324. /** @type {Range} */
  325. (expr.range),
  326. dependencies,
  327. true
  328. );
  329. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  330. parser.state.module.addDependency(dep);
  331. }
  332. }
  333. );
  334. hotAcceptWithoutCallback.tap(
  335. "HarmonyImportDependencyParserPlugin",
  336. (expr, requests) => {
  337. if (!HarmonyExports.isEnabled(parser.state)) {
  338. // This is not a harmony module, skip it
  339. return;
  340. }
  341. const dependencies = requests.map(request => {
  342. const dep = new HarmonyAcceptImportDependency(request);
  343. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  344. parser.state.module.addDependency(dep);
  345. return dep;
  346. });
  347. if (dependencies.length > 0) {
  348. const dep = new HarmonyAcceptDependency(
  349. /** @type {Range} */
  350. (expr.range),
  351. dependencies,
  352. false
  353. );
  354. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  355. parser.state.module.addDependency(dep);
  356. }
  357. }
  358. );
  359. }
  360. };
  361. module.exports.harmonySpecifierTag = harmonySpecifierTag;