HarmonyImportDependencyParserPlugin.js 18 KB

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