generate-helpers.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. /* eslint-disable import/no-extraneous-dependencies */
  2. import fs from "fs";
  3. import { join } from "path";
  4. import { URL, fileURLToPath } from "url";
  5. import { minify } from "terser";
  6. import { babel, presetTypescript } from "$repo-utils/babel-top-level";
  7. import { IS_BABEL_8 } from "$repo-utils";
  8. import { gzipSync } from "zlib";
  9. import {
  10. getHelperMetadata,
  11. stringifyMetadata,
  12. } from "./build-helper-metadata.js";
  13. const HELPERS_FOLDER = new URL("../src/helpers", import.meta.url);
  14. const IGNORED_FILES = new Set(["package.json", "tsconfig.json"]);
  15. export default async function generateHelpers() {
  16. let output = `/*
  17. * This file is auto-generated! Do not modify it directly.
  18. * To re-generate run 'yarn gulp generate-runtime-helpers'
  19. */
  20. import template from "@babel/template";
  21. import type * as t from "@babel/types";
  22. interface Helper {
  23. minVersion: string;
  24. ast: () => t.Program;
  25. metadata: HelperMetadata;
  26. }
  27. export interface HelperMetadata {
  28. globals: string[];
  29. locals: { [name: string]: string[] };
  30. dependencies: { [name: string]: string[] };
  31. exportBindingAssignments: string[];
  32. exportName: string;
  33. }
  34. function helper(minVersion: string, source: string, metadata: HelperMetadata): Helper {
  35. return Object.freeze({
  36. minVersion,
  37. ast: () => template.program.ast(source, { preserveComments: true }),
  38. metadata,
  39. })
  40. }
  41. export { helpers as default };
  42. const helpers: Record<string, Helper> = {
  43. __proto__: null,
  44. `;
  45. let babel7extraOutput = "";
  46. for (const file of (await fs.promises.readdir(HELPERS_FOLDER)).sort()) {
  47. if (IGNORED_FILES.has(file)) continue;
  48. if (file.startsWith(".")) continue; // ignore e.g. vim swap files
  49. const [helperName] = file.split(".");
  50. const isTs = file.endsWith(".ts");
  51. const filePath = join(fileURLToPath(HELPERS_FOLDER), file);
  52. if (!file.endsWith(".js") && !isTs) {
  53. console.error("ignoring", filePath);
  54. continue;
  55. }
  56. let code = await fs.promises.readFile(filePath, "utf8");
  57. const minVersionMatch = code.match(
  58. /^\s*\/\*\s*@minVersion\s+(?<minVersion>\S+)\s*\*\/\s*$/m
  59. );
  60. if (!minVersionMatch) {
  61. throw new Error(`@minVersion number missing in ${filePath}`);
  62. }
  63. const { minVersion } = minVersionMatch.groups;
  64. const onlyBabel7 = code.includes("@onlyBabel7");
  65. const mangleFns = code.includes("@mangleFns");
  66. const noMangleFns = [];
  67. code = babel.transformSync(code, {
  68. configFile: false,
  69. babelrc: false,
  70. filename: filePath,
  71. presets: [
  72. [
  73. presetTypescript,
  74. {
  75. onlyRemoveTypeImports: true,
  76. optimizeConstEnums: true,
  77. },
  78. ],
  79. ],
  80. plugins: [
  81. /**
  82. * @type {import("@babel/core").PluginObj}
  83. */
  84. ({ types: t }) => ({
  85. // These pre/post hooks are needed because the TS transform is,
  86. // when building in the old Babel e2e test, removing the
  87. // `export { OverloadYield as default }` in the OverloadYield helper.
  88. // TODO: Remove in Babel 8.
  89. pre(file) {
  90. if (!process.env.IS_BABEL_OLD_E2E) return;
  91. file.metadata.exportName = null;
  92. file.path.traverse({
  93. ExportSpecifier(path) {
  94. if (path.node.exported.name === "default") {
  95. file.metadata.exportName = path.node.local.name;
  96. }
  97. },
  98. });
  99. },
  100. post(file) {
  101. if (!process.env.IS_BABEL_OLD_E2E) return;
  102. if (!file.metadata.exportName) return;
  103. file.path.traverse({
  104. ExportNamedDeclaration(path) {
  105. if (
  106. !path.node.declaration &&
  107. path.node.specifiers.length === 0
  108. ) {
  109. path.node.specifiers.push(
  110. t.exportSpecifier(
  111. t.identifier(file.metadata.exportName),
  112. t.identifier("default")
  113. )
  114. );
  115. }
  116. },
  117. });
  118. },
  119. visitor: {
  120. ImportDeclaration(path) {
  121. const source = path.node.source;
  122. source.value = source.value
  123. .replace(/\.ts$/, "")
  124. .replace(/^\.\//, "");
  125. },
  126. FunctionDeclaration(path) {
  127. if (
  128. mangleFns &&
  129. path.node.leadingComments?.find(c =>
  130. c.value.includes("@no-mangle")
  131. )
  132. ) {
  133. const name = path.node.id.name;
  134. if (name) noMangleFns.push(name);
  135. }
  136. },
  137. },
  138. }),
  139. ],
  140. }).code;
  141. code = (
  142. await minify(code, {
  143. ecma: 5,
  144. mangle: {
  145. keep_fnames: mangleFns ? new RegExp(noMangleFns.join("|")) : true,
  146. },
  147. // The _typeof helper has a custom directive that we must keep
  148. compress: {
  149. directives: false,
  150. passes: 10,
  151. unsafe: true,
  152. unsafe_proto: true,
  153. },
  154. })
  155. ).code;
  156. let metadata;
  157. // eslint-disable-next-line prefer-const
  158. [code, metadata] = getHelperMetadata(babel, code, helperName);
  159. const helperStr = `\
  160. // size: ${code.length}, gzip size: ${gzipSync(code).length}
  161. ${JSON.stringify(helperName)}: helper(
  162. ${JSON.stringify(minVersion)},
  163. ${JSON.stringify(code)},
  164. ${stringifyMetadata(metadata)}
  165. ),
  166. `;
  167. if (onlyBabel7) {
  168. if (!IS_BABEL_8()) babel7extraOutput += helperStr;
  169. } else {
  170. output += helperStr;
  171. }
  172. }
  173. output += "};";
  174. if (babel7extraOutput) {
  175. output += `
  176. if (!process.env.BABEL_8_BREAKING) {
  177. Object.assign(helpers, {
  178. ${babel7extraOutput}
  179. });
  180. }
  181. `;
  182. }
  183. return output;
  184. }