HarmonyImportDependencyParserPlugin.js 17 KB

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