HarmonyImportDependencyParserPlugin.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  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 WebpackError = require("../WebpackError");
  8. const {
  9. VariableInfo,
  10. getImportAttributes
  11. } = require("../javascript/JavascriptParser");
  12. const InnerGraph = require("../optimize/InnerGraph");
  13. const AppendOnlyStackedSet = require("../util/AppendOnlyStackedSet");
  14. const ConstDependency = require("./ConstDependency");
  15. const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
  16. const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
  17. const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImportSpecifierDependency");
  18. const HarmonyExports = require("./HarmonyExports");
  19. const {
  20. ExportPresenceModes,
  21. getNonOptionalPart
  22. } = require("./HarmonyImportDependency");
  23. const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
  24. const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
  25. const { ImportPhaseUtils, createGetImportPhase } = require("./ImportPhase");
  26. /** @typedef {import("estree").Expression} Expression */
  27. /** @typedef {import("estree").PrivateIdentifier} PrivateIdentifier */
  28. /** @typedef {import("estree").Identifier} Identifier */
  29. /** @typedef {import("estree").MemberExpression} MemberExpression */
  30. /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
  31. /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
  32. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  33. /** @typedef {import("../javascript/JavascriptParser").ExportAllDeclaration} ExportAllDeclaration */
  34. /** @typedef {import("../javascript/JavascriptParser").ExportNamedDeclaration} ExportNamedDeclaration */
  35. /** @typedef {import("../javascript/JavascriptParser").ImportAttributes} ImportAttributes */
  36. /** @typedef {import("../javascript/JavascriptParser").ImportDeclaration} ImportDeclaration */
  37. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  38. /** @typedef {import("../javascript/JavascriptParser").Members} Members */
  39. /** @typedef {import("../javascript/JavascriptParser").MembersOptionals} MembersOptionals */
  40. /** @typedef {import("./HarmonyImportDependency").Ids} Ids */
  41. /** @typedef {import("./HarmonyImportDependency").ExportPresenceMode} ExportPresenceMode */
  42. /** @typedef {import("./ImportPhase").ImportPhaseType} ImportPhaseType */
  43. /**
  44. * Defines the harmony specifier guards type used by this module.
  45. * @typedef {object} HarmonySpecifierGuards
  46. * @property {AppendOnlyStackedSet<string> | undefined} guards
  47. */
  48. /** @typedef {Map<string, Set<string>>} Guards Map of import root to guarded member keys */
  49. const harmonySpecifierTag = Symbol("harmony import");
  50. const harmonySpecifierGuardTag = Symbol("harmony import guard");
  51. /**
  52. * Defines the harmony settings type used by this module.
  53. * @typedef {object} HarmonySettings
  54. * @property {Ids} ids
  55. * @property {string} source
  56. * @property {number} sourceOrder
  57. * @property {string} name
  58. * @property {boolean} await
  59. * @property {ImportAttributes=} attributes
  60. * @property {ImportPhaseType} phase
  61. */
  62. const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
  63. /**
  64. * Gets in operator harmony import info.
  65. * @param {JavascriptParser} parser the parser
  66. * @param {PrivateIdentifier | Expression} left left expression
  67. * @param {Expression} right right expression
  68. * @returns {{ leftPart: string, members: Members, settings: HarmonySettings } | undefined} info
  69. */
  70. const getInOperatorHarmonyImportInfo = (parser, left, right) => {
  71. const leftPartEvaluated = parser.evaluateExpression(left);
  72. if (leftPartEvaluated.couldHaveSideEffects()) return;
  73. /** @type {string | undefined} */
  74. const leftPart = leftPartEvaluated.asString();
  75. if (!leftPart) return;
  76. const rightPart = parser.evaluateExpression(right);
  77. if (!rightPart.isIdentifier()) return;
  78. const rootInfo = rightPart.rootInfo;
  79. const root =
  80. typeof rootInfo === "string"
  81. ? rootInfo
  82. : rootInfo instanceof VariableInfo
  83. ? rootInfo.name
  84. : undefined;
  85. if (!root) return;
  86. const settings = /** @type {HarmonySettings | undefined} */ (
  87. parser.getTagData(root, harmonySpecifierTag)
  88. );
  89. if (!settings) {
  90. return;
  91. }
  92. return {
  93. leftPart,
  94. members: /** @type {(() => Members)} */ (rightPart.getMembers)(),
  95. settings
  96. };
  97. };
  98. module.exports = class HarmonyImportDependencyParserPlugin {
  99. /**
  100. * Creates an instance of HarmonyImportDependencyParserPlugin.
  101. * @param {JavascriptParserOptions} options options
  102. */
  103. constructor(options) {
  104. this.options = options;
  105. /** @type {ExportPresenceMode} */
  106. this.exportPresenceMode = ExportPresenceModes.resolveFromOptions(
  107. options.importExportsPresence,
  108. options
  109. );
  110. this.strictThisContextOnImports = options.strictThisContextOnImports;
  111. }
  112. /**
  113. * Gets export presence mode.
  114. * @param {JavascriptParser} parser the parser
  115. * @param {HarmonySettings} settings settings
  116. * @param {Ids} ids ids
  117. * @returns {ExportPresenceMode} exportPresenceMode
  118. */
  119. getExportPresenceMode(parser, settings, ids) {
  120. // Guards only apply to namespace imports
  121. if (settings.ids.length) return this.exportPresenceMode;
  122. const harmonySettings = /** @type {HarmonySettings=} */ (
  123. parser.currentTagData
  124. );
  125. if (!harmonySettings) return this.exportPresenceMode;
  126. const data = /** @type {HarmonySpecifierGuards=} */ (
  127. parser.getTagData(harmonySettings.name, harmonySpecifierGuardTag)
  128. );
  129. if (data && data.guards && data.guards.has(ids[0])) {
  130. return ExportPresenceModes.NONE;
  131. }
  132. return this.exportPresenceMode;
  133. }
  134. /**
  135. * Applies the plugin by registering its hooks on the compiler.
  136. * @param {JavascriptParser} parser the parser
  137. * @returns {void}
  138. */
  139. apply(parser) {
  140. const getImportPhase = createGetImportPhase(
  141. this.options.deferImport,
  142. this.options.sourceImport
  143. );
  144. /**
  145. * Gets non optional member chain.
  146. * @param {MemberExpression} node member expression
  147. * @param {number} count count
  148. * @returns {Expression} member expression
  149. */
  150. function getNonOptionalMemberChain(node, count) {
  151. while (count--) node = /** @type {MemberExpression} */ (node.object);
  152. return node;
  153. }
  154. parser.hooks.isPure.for("Identifier").tap(PLUGIN_NAME, (expression) => {
  155. const expr = /** @type {Identifier} */ (expression);
  156. if (
  157. parser.isVariableDefined(expr.name) ||
  158. parser.getTagData(expr.name, harmonySpecifierTag)
  159. ) {
  160. return true;
  161. }
  162. });
  163. parser.hooks.import.tap(PLUGIN_NAME, (statement, source) => {
  164. parser.state.lastHarmonyImportOrder =
  165. (parser.state.lastHarmonyImportOrder || 0) + 1;
  166. const clearDep = new ConstDependency(
  167. parser.isAsiPosition(/** @type {Range} */ (statement.range)[0])
  168. ? ";"
  169. : "",
  170. /** @type {Range} */ (statement.range)
  171. );
  172. clearDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  173. parser.state.module.addPresentationalDependency(clearDep);
  174. parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]);
  175. const attributes = getImportAttributes(statement);
  176. const phase = getImportPhase(parser, statement);
  177. if (
  178. ImportPhaseUtils.isDefer(phase) &&
  179. (statement.specifiers.length !== 1 ||
  180. statement.specifiers[0].type !== "ImportNamespaceSpecifier")
  181. ) {
  182. const error = new WebpackError(
  183. "Deferred import can only be used with `import * as namespace from '...'` syntax."
  184. );
  185. error.loc = statement.loc || undefined;
  186. parser.state.current.addError(error);
  187. }
  188. const sideEffectDep = new HarmonyImportSideEffectDependency(
  189. /** @type {string} */ (source),
  190. parser.state.lastHarmonyImportOrder,
  191. phase,
  192. attributes
  193. );
  194. sideEffectDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  195. parser.state.module.addDependency(sideEffectDep);
  196. return true;
  197. });
  198. parser.hooks.importSpecifier.tap(
  199. PLUGIN_NAME,
  200. (statement, source, id, name) => {
  201. const ids = id === null ? [] : [id];
  202. const phase = getImportPhase(parser, statement);
  203. parser.tagVariable(
  204. name,
  205. harmonySpecifierTag,
  206. /** @type {HarmonySettings} */ ({
  207. name,
  208. source,
  209. ids,
  210. sourceOrder: parser.state.lastHarmonyImportOrder,
  211. attributes: getImportAttributes(statement),
  212. phase
  213. })
  214. );
  215. return true;
  216. }
  217. );
  218. parser.hooks.binaryExpression.tap(PLUGIN_NAME, (expression) => {
  219. if (expression.operator !== "in") return;
  220. const info = getInOperatorHarmonyImportInfo(
  221. parser,
  222. expression.left,
  223. expression.right
  224. );
  225. if (!info) return;
  226. const { leftPart, members, settings } = info;
  227. const dep = new HarmonyEvaluatedImportSpecifierDependency(
  228. settings.source,
  229. settings.sourceOrder,
  230. [...settings.ids, ...members, leftPart],
  231. settings.name,
  232. /** @type {Range} */ (expression.range),
  233. settings.attributes,
  234. "in"
  235. );
  236. dep.directImport = members.length === 0;
  237. dep.asiSafe = !parser.isAsiPosition(
  238. /** @type {Range} */ (expression.range)[0]
  239. );
  240. dep.loc = /** @type {DependencyLocation} */ (expression.loc);
  241. parser.state.module.addDependency(dep);
  242. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  243. return true;
  244. });
  245. parser.hooks.collectDestructuringAssignmentProperties.tap(
  246. PLUGIN_NAME,
  247. (expr) => {
  248. const nameInfo = parser.getNameForExpression(expr);
  249. if (
  250. nameInfo &&
  251. nameInfo.rootInfo instanceof VariableInfo &&
  252. nameInfo.rootInfo.name &&
  253. parser.getTagData(nameInfo.rootInfo.name, harmonySpecifierTag)
  254. ) {
  255. return true;
  256. }
  257. }
  258. );
  259. parser.hooks.expression
  260. .for(harmonySpecifierTag)
  261. .tap(PLUGIN_NAME, (expr) => {
  262. const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
  263. const dep = new HarmonyImportSpecifierDependency(
  264. settings.source,
  265. settings.sourceOrder,
  266. settings.ids,
  267. settings.name,
  268. /** @type {Range} */
  269. (expr.range),
  270. this.exportPresenceMode,
  271. settings.phase,
  272. settings.attributes,
  273. []
  274. );
  275. dep.referencedPropertiesInDestructuring =
  276. parser.destructuringAssignmentPropertiesFor(expr);
  277. dep.shorthand = parser.scope.inShorthand;
  278. dep.directImport = true;
  279. dep.asiSafe = !parser.isAsiPosition(
  280. /** @type {Range} */ (expr.range)[0]
  281. );
  282. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  283. dep.call = parser.scope.inTaggedTemplateTag;
  284. parser.state.module.addDependency(dep);
  285. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  286. return true;
  287. });
  288. parser.hooks.expressionMemberChain
  289. .for(harmonySpecifierTag)
  290. .tap(
  291. PLUGIN_NAME,
  292. (expression, members, membersOptionals, memberRanges) => {
  293. const settings =
  294. /** @type {HarmonySettings} */
  295. (parser.currentTagData);
  296. const nonOptionalMembers = getNonOptionalPart(
  297. members,
  298. membersOptionals
  299. );
  300. /** @type {Range[]} */
  301. const ranges = memberRanges.slice(
  302. 0,
  303. memberRanges.length - (members.length - nonOptionalMembers.length)
  304. );
  305. const expr =
  306. nonOptionalMembers !== members
  307. ? getNonOptionalMemberChain(
  308. expression,
  309. members.length - nonOptionalMembers.length
  310. )
  311. : expression;
  312. const ids = [...settings.ids, ...nonOptionalMembers];
  313. const dep = new HarmonyImportSpecifierDependency(
  314. settings.source,
  315. settings.sourceOrder,
  316. ids,
  317. settings.name,
  318. /** @type {Range} */
  319. (expr.range),
  320. this.getExportPresenceMode(parser, settings, ids),
  321. settings.phase,
  322. settings.attributes,
  323. ranges
  324. );
  325. dep.referencedPropertiesInDestructuring =
  326. parser.destructuringAssignmentPropertiesFor(expr);
  327. dep.asiSafe = !parser.isAsiPosition(
  328. /** @type {Range} */
  329. (expr.range)[0]
  330. );
  331. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  332. parser.state.module.addDependency(dep);
  333. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  334. return true;
  335. }
  336. );
  337. parser.hooks.callMemberChain
  338. .for(harmonySpecifierTag)
  339. .tap(
  340. PLUGIN_NAME,
  341. (expression, members, membersOptionals, memberRanges) => {
  342. const { arguments: args } = expression;
  343. const callee = /** @type {MemberExpression} */ (expression.callee);
  344. const settings = /** @type {HarmonySettings} */ (
  345. parser.currentTagData
  346. );
  347. const nonOptionalMembers = getNonOptionalPart(
  348. members,
  349. membersOptionals
  350. );
  351. /** @type {Range[]} */
  352. const ranges = memberRanges.slice(
  353. 0,
  354. memberRanges.length - (members.length - nonOptionalMembers.length)
  355. );
  356. const expr =
  357. nonOptionalMembers !== members
  358. ? getNonOptionalMemberChain(
  359. callee,
  360. members.length - nonOptionalMembers.length
  361. )
  362. : callee;
  363. const ids = [...settings.ids, ...nonOptionalMembers];
  364. const dep = new HarmonyImportSpecifierDependency(
  365. settings.source,
  366. settings.sourceOrder,
  367. ids,
  368. settings.name,
  369. /** @type {Range} */ (expr.range),
  370. this.getExportPresenceMode(parser, settings, ids),
  371. settings.phase,
  372. settings.attributes,
  373. ranges
  374. );
  375. dep.directImport = members.length === 0;
  376. dep.call = true;
  377. dep.asiSafe = !parser.isAsiPosition(
  378. /** @type {Range} */ (expr.range)[0]
  379. );
  380. // only in case when we strictly follow the spec we need a special case here
  381. dep.namespaceObjectAsContext =
  382. members.length > 0 &&
  383. /** @type {boolean} */ (this.strictThisContextOnImports);
  384. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  385. parser.state.module.addDependency(dep);
  386. if (args) parser.walkExpressions(args);
  387. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  388. return true;
  389. }
  390. );
  391. const { hotAcceptCallback, hotAcceptWithoutCallback } =
  392. HotModuleReplacementPlugin.getParserHooks(parser);
  393. hotAcceptCallback.tap(PLUGIN_NAME, (expr, requests) => {
  394. if (!HarmonyExports.isEnabled(parser.state)) {
  395. // This is not a harmony module, skip it
  396. return;
  397. }
  398. const dependencies = requests.map((request) => {
  399. const dep = new HarmonyAcceptImportDependency(request);
  400. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  401. parser.state.module.addDependency(dep);
  402. return dep;
  403. });
  404. if (dependencies.length > 0) {
  405. const dep = new HarmonyAcceptDependency(
  406. /** @type {Range} */
  407. (expr.range),
  408. dependencies,
  409. true
  410. );
  411. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  412. parser.state.module.addDependency(dep);
  413. }
  414. });
  415. hotAcceptWithoutCallback.tap(PLUGIN_NAME, (expr, requests) => {
  416. if (!HarmonyExports.isEnabled(parser.state)) {
  417. // This is not a harmony module, skip it
  418. return;
  419. }
  420. const dependencies = requests.map((request) => {
  421. const dep = new HarmonyAcceptImportDependency(request);
  422. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  423. parser.state.module.addDependency(dep);
  424. return dep;
  425. });
  426. if (dependencies.length > 0) {
  427. const dep = new HarmonyAcceptDependency(
  428. /** @type {Range} */
  429. (expr.range),
  430. dependencies,
  431. false
  432. );
  433. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  434. parser.state.module.addDependency(dep);
  435. }
  436. });
  437. /**
  438. * Processes the provided guard.
  439. * @param {Guards} guards guards
  440. * @param {() => void} walk walk callback
  441. * @returns {void}
  442. */
  443. const withGuards = (guards, walk) => {
  444. const applyGuards = () => {
  445. /** @type {(() => void)[]} */
  446. const restoreFns = [];
  447. for (const [rootName, members] of guards) {
  448. const previous = parser.getVariableInfo(rootName);
  449. const exist = /** @type {HarmonySpecifierGuards=} */ (
  450. parser.getTagData(rootName, harmonySpecifierGuardTag)
  451. );
  452. const mergedGuards =
  453. exist && exist.guards
  454. ? exist.guards.createChild()
  455. : new AppendOnlyStackedSet();
  456. for (const memberKey of members) mergedGuards.add(memberKey);
  457. parser.tagVariable(rootName, harmonySpecifierGuardTag, {
  458. guards: mergedGuards
  459. });
  460. restoreFns.push(() => {
  461. parser.setVariable(rootName, previous);
  462. });
  463. }
  464. return () => {
  465. for (const restore of restoreFns) {
  466. restore();
  467. }
  468. };
  469. };
  470. const restore = applyGuards();
  471. try {
  472. walk();
  473. } finally {
  474. restore();
  475. }
  476. };
  477. if (this.exportPresenceMode !== ExportPresenceModes.NONE) {
  478. parser.hooks.collectGuards.tap(PLUGIN_NAME, (expression) => {
  479. if (parser.scope.isAsmJs) return;
  480. /** @type {Guards} */
  481. const guards = new Map();
  482. /**
  483. * Processes the provided expression.
  484. * @param {Expression} expression expression
  485. * @param {boolean} needTruthy need to be truthy
  486. */
  487. const collect = (expression, needTruthy) => {
  488. if (
  489. expression.type === "UnaryExpression" &&
  490. expression.operator === "!"
  491. ) {
  492. collect(expression.argument, !needTruthy);
  493. return;
  494. } else if (expression.type === "LogicalExpression" && needTruthy) {
  495. if (expression.operator === "&&") {
  496. collect(expression.left, true);
  497. collect(expression.right, true);
  498. } else if (expression.operator === "||") {
  499. const leftEvaluation = parser.evaluateExpression(expression.left);
  500. const leftBool = leftEvaluation.asBool();
  501. if (leftBool === false) {
  502. collect(expression.right, true);
  503. }
  504. } else if (expression.operator === "??") {
  505. const leftEvaluation = parser.evaluateExpression(expression.left);
  506. const leftNullish = leftEvaluation.asNullish();
  507. if (leftNullish === true) {
  508. collect(expression.right, true);
  509. }
  510. }
  511. return;
  512. }
  513. if (!needTruthy) return;
  514. // Direct `"x" in ns` guards
  515. if (
  516. expression.type === "BinaryExpression" &&
  517. expression.operator === "in"
  518. ) {
  519. if (expression.right.type !== "Identifier") {
  520. return;
  521. }
  522. const info = getInOperatorHarmonyImportInfo(
  523. parser,
  524. expression.left,
  525. expression.right
  526. );
  527. if (!info) return;
  528. const { settings, leftPart, members } = info;
  529. // Only direct namespace guards
  530. if (members.length > 0) return;
  531. const guarded = guards.get(settings.name);
  532. if (guarded) {
  533. guarded.add(leftPart);
  534. return;
  535. }
  536. guards.set(settings.name, new Set([leftPart]));
  537. }
  538. };
  539. collect(expression, true);
  540. if (guards.size === 0) return;
  541. return (walk) => {
  542. withGuards(guards, walk);
  543. };
  544. });
  545. }
  546. }
  547. };
  548. module.exports.harmonySpecifierGuardTag = harmonySpecifierGuardTag;
  549. module.exports.harmonySpecifierTag = harmonySpecifierTag;