magicComment.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const binarySearchBounds = require("./binarySearchBounds");
  7. const memoize = require("./memoize");
  8. const getVm = memoize(() => require("vm"));
  9. const CompilerHintNotationRegExp = Object.freeze({
  10. Pure: /^\s*(?:#|@)__PURE__\s*$/,
  11. NoSideEffects: /^\s*[#@]__NO_SIDE_EFFECTS__\s*$/
  12. });
  13. // Whole-comment `webpackXxx: <bool|number|null>` pair — parsed without `vm`.
  14. const MAGIC_COMMENT_FAST_PATH =
  15. /^\s*(webpack[A-Z][A-Za-z]+)\s*:\s*(true|false|null|-?\d+(?:\.\d+)?)\s*$/;
  16. const webpackCommentRegExp = new RegExp(/(^|\W)webpack[A-Z][A-Za-z]+:/);
  17. /** @type {Readonly<{ options: null, errors: null }>} */
  18. const EMPTY_COMMENT_OPTIONS = Object.freeze({
  19. options: null,
  20. errors: null
  21. });
  22. /**
  23. * @returns {import("vm").Context} magic comment context
  24. */
  25. const createMagicCommentContext = () =>
  26. getVm().createContext(undefined, {
  27. name: "Webpack Magic Comment Parser",
  28. codeGeneration: { strings: false, wasm: false }
  29. });
  30. /**
  31. * Parse one magic comment's text and merge into `options`. Values are detached
  32. * from the vm context (RegExps recreated, other objects JSON-cloned). Throws
  33. * when the comment body fails to evaluate.
  34. * @param {Record<string, EXPECTED_ANY>} options target to merge into
  35. * @param {string} value comment text (should already match `webpackCommentRegExp`)
  36. * @param {import("vm").Context} context context from `createMagicCommentContext`
  37. * @returns {void}
  38. */
  39. const assignMagicCommentOptions = (options, value, context) => {
  40. const fast = MAGIC_COMMENT_FAST_PATH.exec(value);
  41. if (fast !== null) {
  42. const raw = fast[2];
  43. options[fast[1]] =
  44. raw === "true"
  45. ? true
  46. : raw === "false"
  47. ? false
  48. : raw === "null"
  49. ? null
  50. : Number(raw);
  51. return;
  52. }
  53. for (let [key, val] of Object.entries(
  54. getVm().runInContext(`(function(){return {${value}};})()`, context)
  55. )) {
  56. if (typeof val === "object" && val !== null) {
  57. val =
  58. val.constructor.name === "RegExp"
  59. ? new RegExp(val)
  60. : JSON.parse(JSON.stringify(val));
  61. }
  62. options[key] = val;
  63. }
  64. };
  65. /**
  66. * Parse one magic comment's text into its options object.
  67. * @param {string} value comment text (should already match `webpackCommentRegExp`)
  68. * @param {import("vm").Context} context context from `createMagicCommentContext`
  69. * @returns {Record<string, EXPECTED_ANY>} parsed options
  70. */
  71. const parseMagicComment = (value, context) => {
  72. /** @type {Record<string, EXPECTED_ANY>} */
  73. const options = {};
  74. assignMagicCommentOptions(options, value, context);
  75. return options;
  76. };
  77. /**
  78. * `binarySearchBounds` comparator for `getCommentsInRange`.
  79. * @param {{ range: [number, number] }} comment comment
  80. * @param {number} needle needle (byte offset)
  81. * @returns {number} comparison
  82. */
  83. const compareCommentStart = (comment, needle) => comment.range[0] - needle;
  84. /**
  85. * Comments fully inside `range`, via binary search over source-ordered `comments`.
  86. * @template {object} TComment
  87. * @param {(TComment & { range: [number, number] })[]} comments source-ordered comments
  88. * @param {[number, number]} range range
  89. * @returns {TComment[]} comments in the range
  90. */
  91. const getCommentsInRange = (comments, range) => {
  92. if (comments.length === 0) return [];
  93. const [start, end] = range;
  94. let idx = binarySearchBounds.ge(comments, start, compareCommentStart);
  95. /** @type {TComment[]} */
  96. const commentsInRange = [];
  97. while (comments[idx] && comments[idx].range[1] <= end) {
  98. commentsInRange.push(comments[idx]);
  99. idx++;
  100. }
  101. return commentsInRange;
  102. };
  103. /**
  104. * Merge webpack magic-comment options from `comments` in source order.
  105. * @template {object} TComment
  106. * @param {(TComment & { value: string })[]} comments comments fully inside the range
  107. * @param {import("vm").Context} context context from `createMagicCommentContext`
  108. * @returns {{ options: Record<string, EXPECTED_ANY> | null, errors: (Error & { comment: TComment })[] | null }} result
  109. */
  110. const parseMagicCommentOptions = (comments, context) => {
  111. if (comments.length === 0) {
  112. return /** @type {{ options: Record<string, EXPECTED_ANY> | null, errors: (Error & { comment: TComment })[] | null }} */ (
  113. EMPTY_COMMENT_OPTIONS
  114. );
  115. }
  116. /** @type {Record<string, EXPECTED_ANY>} */
  117. const options = {};
  118. /** @type {(Error & { comment: TComment })[]} */
  119. const errors = [];
  120. for (const comment of comments) {
  121. const { value } = comment;
  122. if (value && webpackCommentRegExp.test(value)) {
  123. try {
  124. assignMagicCommentOptions(options, value, context);
  125. } catch (err) {
  126. const newErr = new Error(String(/** @type {Error} */ (err).message));
  127. newErr.stack = String(/** @type {Error} */ (err).stack);
  128. Object.assign(newErr, { comment });
  129. errors.push(/** @type {Error & { comment: TComment }} */ (newErr));
  130. }
  131. }
  132. }
  133. return { options, errors };
  134. };
  135. /**
  136. * Merge webpack magic-comment options from comments inside `range`.
  137. * @template {object} TComment
  138. * @param {(TComment & { range: [number, number], value: string })[]} comments source-ordered comments
  139. * @param {[number, number]} range range
  140. * @param {import("vm").Context} context context from `createMagicCommentContext`
  141. * @returns {{ options: Record<string, EXPECTED_ANY> | null, errors: (Error & { comment: TComment })[] | null }} result
  142. */
  143. const parseCommentOptionsInRange = (comments, range, context) =>
  144. parseMagicCommentOptions(getCommentsInRange(comments, range), context);
  145. module.exports = {
  146. CompilerHintNotationRegExp,
  147. createMagicCommentContext,
  148. getCommentsInRange,
  149. parseCommentOptionsInRange,
  150. parseMagicComment,
  151. webpackCommentRegExp
  152. };