ImportMetaPlugin.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Ivan Kopeykin @vankop
  4. */
  5. "use strict";
  6. const { pathToFileURL } = require("url");
  7. const { SyncBailHook } = require("tapable");
  8. const Compilation = require("../Compilation");
  9. const DefinePlugin = require("../DefinePlugin");
  10. const {
  11. JAVASCRIPT_MODULE_TYPE_AUTO,
  12. JAVASCRIPT_MODULE_TYPE_ESM
  13. } = require("../ModuleTypeConstants");
  14. const RuntimeGlobals = require("../RuntimeGlobals");
  15. const Template = require("../Template");
  16. const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
  17. const {
  18. evaluateToIdentifier,
  19. evaluateToNumber,
  20. evaluateToString,
  21. toConstantDependency
  22. } = require("../javascript/JavascriptParserHelpers");
  23. const { propertyAccess } = require("../util/property");
  24. const ConstDependency = require("./ConstDependency");
  25. const ModuleInitFragmentDependency = require("./ModuleInitFragmentDependency");
  26. /** @typedef {import("estree").MemberExpression} MemberExpression */
  27. /** @typedef {import("estree").Identifier} Identifier */
  28. /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
  29. /** @typedef {import("../Compiler")} Compiler */
  30. /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
  31. /** @typedef {import("../NormalModule")} NormalModule */
  32. /** @typedef {import("../javascript/JavascriptParser")} Parser */
  33. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  34. /** @typedef {import("../javascript/JavascriptParser").Members} Members */
  35. /** @typedef {import("../javascript/JavascriptParser").DestructuringAssignmentProperty} DestructuringAssignmentProperty */
  36. /** @typedef {import("./ConstDependency").RawRuntimeRequirements} RawRuntimeRequirements */
  37. const PLUGIN_NAME = "ImportMetaPlugin";
  38. /** @type {WeakMap<Compilation, { stringify: string, env: Record<string, string> }>} */
  39. const compilationMetaEnvMap = new WeakMap();
  40. /**
  41. * Collect import.meta.env definitions from DefinePlugin and build JSON string
  42. * @param {Compilation} compilation the compilation
  43. * @returns {{ stringify: string, env: Record<string, string> }} env object as JSON string
  44. */
  45. const collectImportMetaEnvDefinitions = (compilation) => {
  46. const cached = compilationMetaEnvMap.get(compilation);
  47. if (cached) {
  48. return cached;
  49. }
  50. const definePluginHooks = DefinePlugin.getCompilationHooks(compilation);
  51. const definitions = definePluginHooks.definitions.call({});
  52. /** @type {Record<string, string>} */
  53. const env = {};
  54. /** @type {string[]} */
  55. const pairs = [];
  56. for (const key of Object.keys(definitions)) {
  57. if (key.startsWith("import.meta.env.")) {
  58. const envKey = key.slice("import.meta.env.".length);
  59. const value = definitions[key];
  60. pairs.push(`${JSON.stringify(envKey)}:${value}`);
  61. env[envKey] = /** @type {string} */ (value);
  62. }
  63. }
  64. const result = { stringify: `{${pairs.join(",")}}`, env };
  65. compilationMetaEnvMap.set(compilation, result);
  66. return result;
  67. };
  68. /**
  69. * Defines the import meta plugin hooks type used by this module.
  70. * @typedef {object} ImportMetaPluginHooks
  71. * @property {SyncBailHook<[DestructuringAssignmentProperty], string | void>} propertyInDestructuring
  72. */
  73. /** @type {WeakMap<Compilation, ImportMetaPluginHooks>} */
  74. const compilationHooksMap = new WeakMap();
  75. class ImportMetaPlugin {
  76. /**
  77. * Returns the attached hooks.
  78. * @param {Compilation} compilation the compilation
  79. * @returns {ImportMetaPluginHooks} the attached hooks
  80. */
  81. static getCompilationHooks(compilation) {
  82. if (!(compilation instanceof Compilation)) {
  83. throw new TypeError(
  84. "The 'compilation' argument must be an instance of Compilation"
  85. );
  86. }
  87. let hooks = compilationHooksMap.get(compilation);
  88. if (hooks === undefined) {
  89. hooks = {
  90. propertyInDestructuring: new SyncBailHook(["property"])
  91. };
  92. compilationHooksMap.set(compilation, hooks);
  93. }
  94. return hooks;
  95. }
  96. /**
  97. * Applies the plugin by registering its hooks on the compiler.
  98. * @param {Compiler} compiler compiler
  99. */
  100. apply(compiler) {
  101. compiler.hooks.compilation.tap(
  102. PLUGIN_NAME,
  103. (compilation, { normalModuleFactory }) => {
  104. const hooks = ImportMetaPlugin.getCompilationHooks(compilation);
  105. compilation.dependencyTemplates.set(
  106. ModuleInitFragmentDependency,
  107. new ModuleInitFragmentDependency.Template()
  108. );
  109. /**
  110. * Returns file url.
  111. * @param {NormalModule} module module
  112. * @returns {string} file url
  113. */
  114. const getUrl = (module) => pathToFileURL(module.resource).toString();
  115. /**
  116. * Processes the provided parser.
  117. * @param {Parser} parser parser parser
  118. * @param {JavascriptParserOptions} parserOptions parserOptions
  119. * @returns {void}
  120. */
  121. const parserHandler = (parser, { importMeta }) => {
  122. if (importMeta === false) {
  123. const { importMetaName } = compilation.outputOptions;
  124. if (importMetaName === "import.meta") return;
  125. parser.hooks.expression
  126. .for("import.meta")
  127. .tap(PLUGIN_NAME, (metaProperty) => {
  128. const dep = new ConstDependency(
  129. /** @type {string} */ (importMetaName),
  130. /** @type {Range} */ (metaProperty.range)
  131. );
  132. dep.loc = /** @type {DependencyLocation} */ (metaProperty.loc);
  133. parser.state.module.addPresentationalDependency(dep);
  134. return true;
  135. });
  136. return;
  137. }
  138. // import.meta direct
  139. const webpackVersion = Number.parseInt(
  140. require("../../package.json").version,
  141. 10
  142. );
  143. const importMetaUrl = () =>
  144. JSON.stringify(getUrl(parser.state.module));
  145. const importMetaWebpackVersion = () => JSON.stringify(webpackVersion);
  146. /**
  147. * Import meta unknown property.
  148. * @param {Members} members members
  149. * @returns {string} error message
  150. */
  151. const importMetaUnknownProperty = (members) => {
  152. if (importMeta === "preserve-unknown") {
  153. return `import.meta${propertyAccess(members, 0)}`;
  154. }
  155. return `${Template.toNormalComment(
  156. `unsupported import.meta.${members.join(".")}`
  157. )} undefined${propertyAccess(members, 1)}`;
  158. };
  159. parser.hooks.typeof
  160. .for("import.meta")
  161. .tap(
  162. PLUGIN_NAME,
  163. toConstantDependency(parser, JSON.stringify("object"))
  164. );
  165. parser.hooks.collectDestructuringAssignmentProperties.tap(
  166. PLUGIN_NAME,
  167. (expr) => {
  168. if (expr.type === "MetaProperty") return true;
  169. }
  170. );
  171. parser.hooks.expression
  172. .for("import.meta")
  173. .tap(PLUGIN_NAME, (metaProperty) => {
  174. /** @type {RawRuntimeRequirements} */
  175. const runtimeRequirements = [];
  176. const moduleArgument = parser.state.module.moduleArgument;
  177. const referencedPropertiesInDestructuring =
  178. parser.destructuringAssignmentPropertiesFor(metaProperty);
  179. if (!referencedPropertiesInDestructuring) {
  180. const varName = "__webpack_import_meta__";
  181. const { stringify: envStringify } =
  182. collectImportMetaEnvDefinitions(compilation);
  183. const knownProps =
  184. `{url: ${importMetaUrl()}, ` +
  185. `webpack: ${importMetaWebpackVersion()}, ` +
  186. `main: ${RuntimeGlobals.moduleCache}[${RuntimeGlobals.entryModuleId}] === ${moduleArgument}, ` +
  187. `env: ${envStringify}}`;
  188. const initCode =
  189. importMeta === "preserve-unknown"
  190. ? `var ${varName} = Object.assign(import.meta, ${knownProps});\n`
  191. : `var ${varName} = ${knownProps};\n`;
  192. const initDep = new ModuleInitFragmentDependency(
  193. initCode,
  194. [
  195. RuntimeGlobals.moduleCache,
  196. RuntimeGlobals.entryModuleId,
  197. RuntimeGlobals.module
  198. ],
  199. varName
  200. );
  201. initDep.loc = /** @type {DependencyLocation} */ (
  202. metaProperty.loc
  203. );
  204. parser.state.module.addPresentationalDependency(initDep);
  205. const dep = new ConstDependency(
  206. varName,
  207. /** @type {Range} */ (metaProperty.range),
  208. runtimeRequirements
  209. );
  210. dep.loc = /** @type {DependencyLocation} */ (metaProperty.loc);
  211. parser.state.module.addPresentationalDependency(dep);
  212. return true;
  213. }
  214. let str = "";
  215. for (const prop of referencedPropertiesInDestructuring) {
  216. const value = hooks.propertyInDestructuring.call(prop);
  217. if (value) {
  218. str += value;
  219. continue;
  220. }
  221. switch (prop.id) {
  222. case "url":
  223. str += `url: ${importMetaUrl()},`;
  224. break;
  225. case "webpack":
  226. str += `webpack: ${importMetaWebpackVersion()},`;
  227. break;
  228. case "main":
  229. str += `main: ${RuntimeGlobals.moduleCache}[${RuntimeGlobals.entryModuleId}] === ${moduleArgument},`;
  230. runtimeRequirements.push(
  231. RuntimeGlobals.moduleCache,
  232. RuntimeGlobals.entryModuleId,
  233. RuntimeGlobals.module
  234. );
  235. break;
  236. case "env":
  237. str += `env: ${collectImportMetaEnvDefinitions(compilation).stringify},`;
  238. break;
  239. default:
  240. str += `[${JSON.stringify(
  241. prop.id
  242. )}]: ${importMetaUnknownProperty([prop.id])},`;
  243. break;
  244. }
  245. }
  246. const dep = new ConstDependency(
  247. `({${str}})`,
  248. /** @type {Range} */ (metaProperty.range),
  249. runtimeRequirements
  250. );
  251. dep.loc = /** @type {DependencyLocation} */ (metaProperty.loc);
  252. parser.state.module.addPresentationalDependency(dep);
  253. return true;
  254. });
  255. parser.hooks.evaluateTypeof
  256. .for("import.meta")
  257. .tap(PLUGIN_NAME, evaluateToString("object"));
  258. parser.hooks.evaluateIdentifier.for("import.meta").tap(
  259. PLUGIN_NAME,
  260. evaluateToIdentifier("import.meta", "import.meta", () => [], true)
  261. );
  262. // import.meta.url
  263. parser.hooks.typeof
  264. .for("import.meta.url")
  265. .tap(
  266. PLUGIN_NAME,
  267. toConstantDependency(parser, JSON.stringify("string"))
  268. );
  269. parser.hooks.expression
  270. .for("import.meta.url")
  271. .tap(PLUGIN_NAME, (expr) => {
  272. const dep = new ConstDependency(
  273. importMetaUrl(),
  274. /** @type {Range} */ (expr.range)
  275. );
  276. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  277. parser.state.module.addPresentationalDependency(dep);
  278. return true;
  279. });
  280. parser.hooks.evaluateTypeof
  281. .for("import.meta.url")
  282. .tap(PLUGIN_NAME, evaluateToString("string"));
  283. parser.hooks.evaluateIdentifier
  284. .for("import.meta.url")
  285. .tap(PLUGIN_NAME, (expr) =>
  286. new BasicEvaluatedExpression()
  287. .setString(getUrl(parser.state.module))
  288. .setRange(/** @type {Range} */ (expr.range))
  289. );
  290. // import.meta.webpack
  291. parser.hooks.expression
  292. .for("import.meta.webpack")
  293. .tap(
  294. PLUGIN_NAME,
  295. toConstantDependency(parser, importMetaWebpackVersion())
  296. );
  297. parser.hooks.typeof
  298. .for("import.meta.webpack")
  299. .tap(
  300. PLUGIN_NAME,
  301. toConstantDependency(parser, JSON.stringify("number"))
  302. );
  303. parser.hooks.evaluateTypeof
  304. .for("import.meta.webpack")
  305. .tap(PLUGIN_NAME, evaluateToString("number"));
  306. parser.hooks.evaluateIdentifier
  307. .for("import.meta.webpack")
  308. .tap(PLUGIN_NAME, evaluateToNumber(webpackVersion));
  309. parser.hooks.expression
  310. .for("import.meta.main")
  311. .tap(
  312. PLUGIN_NAME,
  313. toConstantDependency(
  314. parser,
  315. `${RuntimeGlobals.moduleCache}[${RuntimeGlobals.entryModuleId}] === ${RuntimeGlobals.module}`,
  316. [
  317. RuntimeGlobals.moduleCache,
  318. RuntimeGlobals.entryModuleId,
  319. RuntimeGlobals.module
  320. ]
  321. )
  322. );
  323. parser.hooks.typeof
  324. .for("import.meta.main")
  325. .tap(
  326. PLUGIN_NAME,
  327. toConstantDependency(parser, JSON.stringify("boolean"))
  328. );
  329. parser.hooks.evaluateTypeof
  330. .for("import.meta.main")
  331. .tap(PLUGIN_NAME, evaluateToString("boolean"));
  332. // import.meta.env
  333. parser.hooks.typeof
  334. .for("import.meta.env")
  335. .tap(
  336. PLUGIN_NAME,
  337. toConstantDependency(parser, JSON.stringify("object"))
  338. );
  339. parser.hooks.expressionMemberChain
  340. .for("import.meta")
  341. .tap(PLUGIN_NAME, (expr, members) => {
  342. if (members[0] === "env" && members[1]) {
  343. const name = members[1];
  344. const { env } = collectImportMetaEnvDefinitions(compilation);
  345. if (!Object.prototype.hasOwnProperty.call(env, name)) {
  346. const dep = new ConstDependency(
  347. "undefined",
  348. /** @type {Range} */ (expr.range)
  349. );
  350. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  351. parser.state.module.addPresentationalDependency(dep);
  352. return true;
  353. }
  354. }
  355. });
  356. parser.hooks.expression
  357. .for("import.meta.env")
  358. .tap(PLUGIN_NAME, (expr) => {
  359. const { stringify } =
  360. collectImportMetaEnvDefinitions(compilation);
  361. const dep = new ConstDependency(
  362. stringify,
  363. /** @type {Range} */ (expr.range)
  364. );
  365. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  366. parser.state.module.addPresentationalDependency(dep);
  367. return true;
  368. });
  369. parser.hooks.evaluateTypeof
  370. .for("import.meta.env")
  371. .tap(PLUGIN_NAME, evaluateToString("object"));
  372. parser.hooks.evaluateIdentifier
  373. .for("import.meta.env")
  374. .tap(PLUGIN_NAME, (expr) =>
  375. new BasicEvaluatedExpression()
  376. .setTruthy()
  377. .setSideEffects(false)
  378. .setRange(/** @type {Range} */ (expr.range))
  379. );
  380. // Unknown properties
  381. parser.hooks.unhandledExpressionMemberChain
  382. .for("import.meta")
  383. .tap(PLUGIN_NAME, (expr, members) => {
  384. // unknown import.meta properties should be determined at runtime
  385. if (importMeta === "preserve-unknown") {
  386. return true;
  387. }
  388. // keep import.meta.env unknown property
  389. // don't evaluate import.meta.env.UNKNOWN_PROPERTY -> undefined.UNKNOWN_PROPERTY
  390. // `dirname` and `filename` logic in NodeStuffPlugin
  391. if (
  392. members[0] === "env" ||
  393. members[0] === "dirname" ||
  394. members[0] === "filename"
  395. ) {
  396. return true;
  397. }
  398. const dep = new ConstDependency(
  399. importMetaUnknownProperty(members),
  400. /** @type {Range} */ (expr.range)
  401. );
  402. dep.loc = /** @type {DependencyLocation} */ (expr.loc);
  403. parser.state.module.addPresentationalDependency(dep);
  404. return true;
  405. });
  406. parser.hooks.evaluate
  407. .for("MemberExpression")
  408. .tap(PLUGIN_NAME, (expression) => {
  409. const expr = /** @type {MemberExpression} */ (expression);
  410. if (
  411. expr.object.type === "MetaProperty" &&
  412. expr.object.meta.name === "import" &&
  413. expr.object.property.name === "meta" &&
  414. expr.property.type ===
  415. (expr.computed ? "Literal" : "Identifier")
  416. ) {
  417. return new BasicEvaluatedExpression()
  418. .setUndefined()
  419. .setRange(/** @type {Range} */ (expr.range));
  420. }
  421. });
  422. };
  423. normalModuleFactory.hooks.parser
  424. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  425. .tap(PLUGIN_NAME, parserHandler);
  426. normalModuleFactory.hooks.parser
  427. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  428. .tap(PLUGIN_NAME, parserHandler);
  429. }
  430. );
  431. }
  432. }
  433. module.exports = ImportMetaPlugin;