build-helper-metadata.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. // NOTE: This file must be compatible with old Node.js versions, since it runs
  2. // during testing.
  3. /**
  4. * @typedef {Object} HelperMetadata
  5. * @property {string[]} globals
  6. * @property {{ [name: string]: string[] }} locals
  7. * @property {{ [name: string]: string[] }} dependencies
  8. * @property {string[]} exportBindingAssignments
  9. * @property {string} exportName
  10. */
  11. /**
  12. * Given a file AST for a given helper, get a bunch of metadata about it so that Babel can quickly render
  13. * the helper is whatever context it is needed in.
  14. *
  15. * @param {typeof import("@babel/core")} babel
  16. *
  17. * @returns {HelperMetadata}
  18. */
  19. export function getHelperMetadata(babel, code, helperName) {
  20. const globals = new Set();
  21. // Maps imported identifier name -> helper name
  22. const dependenciesBindings = new Map();
  23. let exportName;
  24. const exportBindingAssignments = [];
  25. // helper name -> reference paths
  26. const dependencies = new Map();
  27. // local variable name -> reference paths
  28. const locals = new Map();
  29. const spansToRemove = [];
  30. const validateDefaultExport = decl => {
  31. if (exportName) {
  32. throw new Error(
  33. `Helpers can have only one default export (in ${helperName})`
  34. );
  35. }
  36. if (!decl.isFunctionDeclaration() || !decl.node.id) {
  37. throw new Error(
  38. `Helpers can only export named function declarations (in ${helperName})`
  39. );
  40. }
  41. };
  42. /** @type {import("@babel/traverse").Visitor} */
  43. const dependencyVisitor = {
  44. Program(path) {
  45. for (const child of path.get("body")) {
  46. if (child.isImportDeclaration()) {
  47. if (
  48. child.get("specifiers").length !== 1 ||
  49. !child.get("specifiers.0").isImportDefaultSpecifier()
  50. ) {
  51. throw new Error(
  52. `Helpers can only import a default value (in ${helperName})`
  53. );
  54. }
  55. dependenciesBindings.set(
  56. child.node.specifiers[0].local.name,
  57. child.node.source.value
  58. );
  59. dependencies.set(child.node.source.value, []);
  60. spansToRemove.push([child.node.start, child.node.end]);
  61. child.remove();
  62. }
  63. }
  64. for (const child of path.get("body")) {
  65. if (child.isExportDefaultDeclaration()) {
  66. const decl = child.get("declaration");
  67. validateDefaultExport(decl);
  68. exportName = decl.node.id.name;
  69. spansToRemove.push([child.node.start, decl.node.start]);
  70. child.replaceWith(decl.node);
  71. } else if (
  72. child.isExportNamedDeclaration() &&
  73. child.node.specifiers.length === 1 &&
  74. child.get("specifiers.0.exported").isIdentifier({ name: "default" })
  75. ) {
  76. const { name } = child.node.specifiers[0].local;
  77. validateDefaultExport(child.scope.getBinding(name).path);
  78. exportName = name;
  79. spansToRemove.push([child.node.start, child.node.end]);
  80. child.remove();
  81. } else if (
  82. process.env.IS_BABEL_OLD_E2E &&
  83. child.isExportNamedDeclaration() &&
  84. child.node.specifiers.length === 0
  85. ) {
  86. spansToRemove.push([child.node.start, child.node.end]);
  87. child.remove();
  88. } else if (
  89. child.isExportAllDeclaration() ||
  90. child.isExportNamedDeclaration()
  91. ) {
  92. throw new Error(`Helpers can only export default (in ${helperName})`);
  93. }
  94. }
  95. path.scope.crawl();
  96. const bindings = path.scope.getAllBindings();
  97. Object.keys(bindings).forEach(name => {
  98. if (dependencies.has(name)) return;
  99. const binding = bindings[name];
  100. const references = [
  101. ...binding.path.getBindingIdentifierPaths(true)[name].map(makePath),
  102. ...binding.referencePaths.map(makePath),
  103. ];
  104. for (const violation of binding.constantViolations) {
  105. violation.getBindingIdentifierPaths(true)[name].forEach(path => {
  106. references.push(makePath(path));
  107. });
  108. }
  109. locals.set(name, references);
  110. });
  111. },
  112. ReferencedIdentifier(child) {
  113. const name = child.node.name;
  114. const binding = child.scope.getBinding(name);
  115. if (!binding) {
  116. if (dependenciesBindings.has(name)) {
  117. dependencies
  118. .get(dependenciesBindings.get(name))
  119. .push(makePath(child));
  120. } else if (name !== "arguments" || child.scope.path.isProgram()) {
  121. globals.add(name);
  122. }
  123. }
  124. },
  125. AssignmentExpression(child) {
  126. const left = child.get("left");
  127. if (!(exportName in left.getBindingIdentifiers())) return;
  128. if (!left.isIdentifier()) {
  129. throw new Error(
  130. `Only simple assignments to exports are allowed in helpers (in ${helperName})`
  131. );
  132. }
  133. const binding = child.scope.getBinding(exportName);
  134. if (binding && binding.scope.path.isProgram()) {
  135. exportBindingAssignments.push(makePath(child));
  136. }
  137. },
  138. };
  139. babel.transformSync(code, {
  140. configFile: false,
  141. babelrc: false,
  142. plugins: [() => ({ visitor: dependencyVisitor })],
  143. });
  144. if (!exportName) throw new Error("Helpers must have a named default export.");
  145. // Process these in reverse so that mutating the references does not invalidate any later paths in
  146. // the list.
  147. exportBindingAssignments.reverse();
  148. spansToRemove.sort(([start1], [start2]) => start2 - start1);
  149. for (const [start, end] of spansToRemove) {
  150. code = code.slice(0, start) + code.slice(end);
  151. }
  152. return [
  153. code,
  154. {
  155. globals: Array.from(globals),
  156. locals: Object.fromEntries(locals),
  157. dependencies: Object.fromEntries(dependencies),
  158. exportBindingAssignments,
  159. exportName,
  160. },
  161. ];
  162. }
  163. function makePath(path) {
  164. const parts = [];
  165. for (; path.parentPath; path = path.parentPath) {
  166. parts.push(path.key);
  167. if (path.inList) parts.push(path.listKey);
  168. }
  169. return parts.reverse().join(".");
  170. }
  171. export function stringifyMetadata(metadata) {
  172. return `\
  173. {
  174. globals: ${JSON.stringify(metadata.globals)},
  175. locals: ${JSON.stringify(metadata.locals)},
  176. exportBindingAssignments: ${JSON.stringify(metadata.exportBindingAssignments)},
  177. exportName: ${JSON.stringify(metadata.exportName)},
  178. dependencies: ${JSON.stringify(metadata.dependencies)},
  179. }
  180. `;
  181. }