HarmonyImportDependencyParserPlugin.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const CommentCompilationWarning = require("../CommentCompilationWarning");
  7. const HotModuleReplacementPlugin = require("../HotModuleReplacementPlugin");
  8. const WebpackError = require("../WebpackError");
  9. const {
  10. VariableInfo,
  11. getImportAttributes
  12. } = require("../javascript/JavascriptParser");
  13. const InnerGraph = require("../optimize/InnerGraph");
  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 { ExportPresenceModes } = require("./HarmonyImportDependency");
  20. const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
  21. const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
  22. /** @typedef {import("estree").Expression} Expression */
  23. /** @typedef {import("estree").Identifier} Identifier */
  24. /** @typedef {import("estree").Literal} Literal */
  25. /** @typedef {import("estree").MemberExpression} MemberExpression */
  26. /** @typedef {import("estree").ObjectExpression} ObjectExpression */
  27. /** @typedef {import("estree").Property} Property */
  28. /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
  29. /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
  30. /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
  31. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  32. /** @typedef {import("../javascript/JavascriptParser").DestructuringAssignmentProperty} DestructuringAssignmentProperty */
  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").ImportExpression} ImportExpression */
  38. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  39. /** @typedef {import("../javascript/JavascriptParser").TagData} TagData */
  40. /** @typedef {import("../optimize/InnerGraph").InnerGraph} InnerGraph */
  41. /** @typedef {import("../optimize/InnerGraph").TopLevelSymbol} TopLevelSymbol */
  42. /** @typedef {import("./HarmonyImportDependency")} HarmonyImportDependency */
  43. const harmonySpecifierTag = Symbol("harmony import");
  44. /**
  45. * @typedef {object} HarmonySettings
  46. * @property {string[]} ids
  47. * @property {string} source
  48. * @property {number} sourceOrder
  49. * @property {string} name
  50. * @property {boolean} await
  51. * @property {ImportAttributes=} attributes
  52. * @property {boolean | undefined} defer
  53. */
  54. const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
  55. module.exports = class HarmonyImportDependencyParserPlugin {
  56. /**
  57. * @param {JavascriptParserOptions} options options
  58. */
  59. constructor(options) {
  60. this.exportPresenceMode =
  61. options.importExportsPresence !== undefined
  62. ? ExportPresenceModes.fromUserOption(options.importExportsPresence)
  63. : options.exportsPresence !== undefined
  64. ? ExportPresenceModes.fromUserOption(options.exportsPresence)
  65. : options.strictExportPresence
  66. ? ExportPresenceModes.ERROR
  67. : ExportPresenceModes.AUTO;
  68. this.strictThisContextOnImports = options.strictThisContextOnImports;
  69. this.deferImport = options.deferImport;
  70. }
  71. /**
  72. * @param {JavascriptParser} parser the parser
  73. * @returns {void}
  74. */
  75. apply(parser) {
  76. const { exportPresenceMode } = this;
  77. /**
  78. * @param {string[]} members members
  79. * @param {boolean[]} membersOptionals members Optionals
  80. * @returns {string[]} a non optional part
  81. */
  82. function getNonOptionalPart(members, membersOptionals) {
  83. let i = 0;
  84. while (i < members.length && membersOptionals[i] === false) i++;
  85. return i !== members.length ? members.slice(0, i) : members;
  86. }
  87. /**
  88. * @param {MemberExpression} node member expression
  89. * @param {number} count count
  90. * @returns {Expression} member expression
  91. */
  92. function getNonOptionalMemberChain(node, count) {
  93. while (count--) node = /** @type {MemberExpression} */ (node.object);
  94. return node;
  95. }
  96. parser.hooks.isPure.for("Identifier").tap(PLUGIN_NAME, (expression) => {
  97. const expr = /** @type {Identifier} */ (expression);
  98. if (
  99. parser.isVariableDefined(expr.name) ||
  100. parser.getTagData(expr.name, harmonySpecifierTag)
  101. ) {
  102. return true;
  103. }
  104. });
  105. parser.hooks.import.tap(PLUGIN_NAME, (statement, source) => {
  106. parser.state.lastHarmonyImportOrder =
  107. (parser.state.lastHarmonyImportOrder || 0) + 1;
  108. const clearDep = new ConstDependency(
  109. parser.isAsiPosition(/** @type {Range} */ (statement.range)[0])
  110. ? ";"
  111. : "",
  112. /** @type {Range} */ (statement.range)
  113. );
  114. clearDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  115. parser.state.module.addPresentationalDependency(clearDep);
  116. parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]);
  117. const attributes = getImportAttributes(statement);
  118. let defer = false;
  119. if (this.deferImport) {
  120. ({ defer } = getImportMode(parser, statement));
  121. if (
  122. defer &&
  123. (statement.specifiers.length !== 1 ||
  124. statement.specifiers[0].type !== "ImportNamespaceSpecifier")
  125. ) {
  126. const error = new WebpackError(
  127. "Deferred import can only be used with `import * as namespace from '...'` syntax."
  128. );
  129. error.loc = statement.loc || undefined;
  130. parser.state.current.addError(error);
  131. }
  132. }
  133. const sideEffectDep = new HarmonyImportSideEffectDependency(
  134. /** @type {string} */ (source),
  135. parser.state.lastHarmonyImportOrder,
  136. attributes,
  137. defer
  138. );
  139. sideEffectDep.loc = /** @type {DependencyLocation} */ (statement.loc);
  140. parser.state.module.addDependency(sideEffectDep);
  141. return true;
  142. });
  143. parser.hooks.importSpecifier.tap(
  144. PLUGIN_NAME,
  145. (statement, source, id, name) => {
  146. const ids = id === null ? [] : [id];
  147. const defer = this.deferImport
  148. ? getImportMode(parser, statement).defer
  149. : false;
  150. parser.tagVariable(name, harmonySpecifierTag, {
  151. name,
  152. source,
  153. ids,
  154. sourceOrder: parser.state.lastHarmonyImportOrder,
  155. attributes: getImportAttributes(statement),
  156. defer
  157. });
  158. return true;
  159. }
  160. );
  161. parser.hooks.binaryExpression.tap(PLUGIN_NAME, (expression) => {
  162. if (expression.operator !== "in") return;
  163. const leftPartEvaluated = parser.evaluateExpression(expression.left);
  164. if (leftPartEvaluated.couldHaveSideEffects()) return;
  165. /** @type {string | undefined} */
  166. const leftPart = leftPartEvaluated.asString();
  167. if (!leftPart) return;
  168. const rightPart = parser.evaluateExpression(expression.right);
  169. if (!rightPart.isIdentifier()) return;
  170. const rootInfo = rightPart.rootInfo;
  171. if (
  172. typeof rootInfo === "string" ||
  173. !rootInfo ||
  174. !rootInfo.tagInfo ||
  175. rootInfo.tagInfo.tag !== harmonySpecifierTag
  176. ) {
  177. return;
  178. }
  179. const settings =
  180. /** @type {TagData} */
  181. (rootInfo.tagInfo.data);
  182. const members =
  183. /** @type {(() => string[])} */
  184. (rightPart.getMembers)();
  185. const dep = new HarmonyEvaluatedImportSpecifierDependency(
  186. settings.source,
  187. settings.sourceOrder,
  188. [...settings.ids, ...members, leftPart],
  189. settings.name,
  190. /** @type {Range} */ (expression.range),
  191. settings.attributes,
  192. "in"
  193. );
  194. dep.directImport = members.length === 0;
  195. dep.asiSafe = !parser.isAsiPosition(
  196. /** @type {Range} */ (expression.range)[0]
  197. );
  198. dep.loc = /** @type {DependencyLocation} */ (expression.loc);
  199. parser.state.module.addDependency(dep);
  200. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  201. return true;
  202. });
  203. parser.hooks.collectDestructuringAssignmentProperties.tap(
  204. PLUGIN_NAME,
  205. (expr) => {
  206. const nameInfo = parser.getNameForExpression(expr);
  207. if (
  208. nameInfo &&
  209. nameInfo.rootInfo instanceof VariableInfo &&
  210. nameInfo.rootInfo.name &&
  211. parser.getTagData(nameInfo.rootInfo.name, harmonySpecifierTag)
  212. ) {
  213. return true;
  214. }
  215. }
  216. );
  217. parser.hooks.expression
  218. .for(harmonySpecifierTag)
  219. .tap(PLUGIN_NAME, (expr) => {
  220. const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
  221. const dep = new HarmonyImportSpecifierDependency(
  222. settings.source,
  223. settings.sourceOrder,
  224. settings.ids,
  225. settings.name,
  226. /** @type {Range} */
  227. (expr.range),
  228. exportPresenceMode,
  229. settings.attributes,
  230. [],
  231. settings.defer
  232. );
  233. dep.referencedPropertiesInDestructuring =
  234. parser.destructuringAssignmentPropertiesFor(expr);
  235. dep.shorthand = parser.scope.inShorthand;
  236. dep.directImport = true;
  237. dep.asiSafe = !parser.isAsiPosition(
  238. /** @type {Range} */ (expr.range)[0]
  239. );
  240. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  241. dep.call = parser.scope.inTaggedTemplateTag;
  242. parser.state.module.addDependency(dep);
  243. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  244. return true;
  245. });
  246. parser.hooks.expressionMemberChain
  247. .for(harmonySpecifierTag)
  248. .tap(
  249. PLUGIN_NAME,
  250. (expression, members, membersOptionals, memberRanges) => {
  251. const settings =
  252. /** @type {HarmonySettings} */
  253. (parser.currentTagData);
  254. const nonOptionalMembers = getNonOptionalPart(
  255. members,
  256. membersOptionals
  257. );
  258. /** @type {Range[]} */
  259. const ranges = memberRanges.slice(
  260. 0,
  261. memberRanges.length - (members.length - nonOptionalMembers.length)
  262. );
  263. const expr =
  264. nonOptionalMembers !== members
  265. ? getNonOptionalMemberChain(
  266. expression,
  267. members.length - nonOptionalMembers.length
  268. )
  269. : expression;
  270. const ids = [...settings.ids, ...nonOptionalMembers];
  271. const dep = new HarmonyImportSpecifierDependency(
  272. settings.source,
  273. settings.sourceOrder,
  274. ids,
  275. settings.name,
  276. /** @type {Range} */
  277. (expr.range),
  278. exportPresenceMode,
  279. settings.attributes,
  280. ranges,
  281. settings.defer
  282. );
  283. dep.referencedPropertiesInDestructuring =
  284. parser.destructuringAssignmentPropertiesFor(expr);
  285. dep.asiSafe = !parser.isAsiPosition(
  286. /** @type {Range} */
  287. (expr.range)[0]
  288. );
  289. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  290. parser.state.module.addDependency(dep);
  291. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  292. return true;
  293. }
  294. );
  295. parser.hooks.callMemberChain
  296. .for(harmonySpecifierTag)
  297. .tap(
  298. PLUGIN_NAME,
  299. (expression, members, membersOptionals, memberRanges) => {
  300. const { arguments: args } = expression;
  301. const callee = /** @type {MemberExpression} */ (expression.callee);
  302. const settings = /** @type {HarmonySettings} */ (
  303. parser.currentTagData
  304. );
  305. const nonOptionalMembers = getNonOptionalPart(
  306. members,
  307. membersOptionals
  308. );
  309. /** @type {Range[]} */
  310. const ranges = memberRanges.slice(
  311. 0,
  312. memberRanges.length - (members.length - nonOptionalMembers.length)
  313. );
  314. const expr =
  315. nonOptionalMembers !== members
  316. ? getNonOptionalMemberChain(
  317. callee,
  318. members.length - nonOptionalMembers.length
  319. )
  320. : callee;
  321. const ids = [...settings.ids, ...nonOptionalMembers];
  322. const dep = new HarmonyImportSpecifierDependency(
  323. settings.source,
  324. settings.sourceOrder,
  325. ids,
  326. settings.name,
  327. /** @type {Range} */ (expr.range),
  328. exportPresenceMode,
  329. settings.attributes,
  330. ranges,
  331. settings.defer
  332. );
  333. dep.directImport = members.length === 0;
  334. dep.call = true;
  335. dep.asiSafe = !parser.isAsiPosition(
  336. /** @type {Range} */ (expr.range)[0]
  337. );
  338. // only in case when we strictly follow the spec we need a special case here
  339. dep.namespaceObjectAsContext =
  340. members.length > 0 &&
  341. /** @type {boolean} */ (this.strictThisContextOnImports);
  342. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  343. parser.state.module.addDependency(dep);
  344. if (args) parser.walkExpressions(args);
  345. InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
  346. return true;
  347. }
  348. );
  349. const { hotAcceptCallback, hotAcceptWithoutCallback } =
  350. HotModuleReplacementPlugin.getParserHooks(parser);
  351. hotAcceptCallback.tap(PLUGIN_NAME, (expr, requests) => {
  352. if (!HarmonyExports.isEnabled(parser.state)) {
  353. // This is not a harmony module, skip it
  354. return;
  355. }
  356. const dependencies = requests.map((request) => {
  357. const dep = new HarmonyAcceptImportDependency(request);
  358. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  359. parser.state.module.addDependency(dep);
  360. return dep;
  361. });
  362. if (dependencies.length > 0) {
  363. const dep = new HarmonyAcceptDependency(
  364. /** @type {Range} */
  365. (expr.range),
  366. dependencies,
  367. true
  368. );
  369. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  370. parser.state.module.addDependency(dep);
  371. }
  372. });
  373. hotAcceptWithoutCallback.tap(PLUGIN_NAME, (expr, requests) => {
  374. if (!HarmonyExports.isEnabled(parser.state)) {
  375. // This is not a harmony module, skip it
  376. return;
  377. }
  378. const dependencies = requests.map((request) => {
  379. const dep = new HarmonyAcceptImportDependency(request);
  380. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  381. parser.state.module.addDependency(dep);
  382. return dep;
  383. });
  384. if (dependencies.length > 0) {
  385. const dep = new HarmonyAcceptDependency(
  386. /** @type {Range} */
  387. (expr.range),
  388. dependencies,
  389. false
  390. );
  391. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  392. parser.state.module.addDependency(dep);
  393. }
  394. });
  395. }
  396. };
  397. /**
  398. * @param {JavascriptParser} parser parser
  399. * @param {ExportNamedDeclaration | ExportAllDeclaration | ImportDeclaration} node node
  400. * @returns {{ defer: boolean }} import attributes
  401. */
  402. function getImportMode(parser, node) {
  403. const result = { defer: "phase" in node && node.phase === "defer" };
  404. if (!node.range) {
  405. return result;
  406. }
  407. const { options, errors } = parser.parseCommentOptions(node.range);
  408. if (errors) {
  409. for (const e of errors) {
  410. const { comment } = e;
  411. if (!comment.loc) continue;
  412. parser.state.module.addWarning(
  413. new CommentCompilationWarning(
  414. `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
  415. comment.loc
  416. )
  417. );
  418. }
  419. }
  420. if (!options) return result;
  421. if (options.webpackDefer) {
  422. if (typeof options.webpackDefer === "boolean") {
  423. result.defer = options.webpackDefer;
  424. } else if (node.loc) {
  425. parser.state.module.addWarning(
  426. new CommentCompilationWarning(
  427. "webpackDefer magic comment expected a boolean value.",
  428. node.loc
  429. )
  430. );
  431. }
  432. }
  433. return result;
  434. }
  435. module.exports.getImportMode = getImportMode;
  436. module.exports.harmonySpecifierTag = harmonySpecifierTag;