|  | 
|  | 1 | +import * as path from 'path'; | 
|  | 2 | + | 
|  | 3 | +import * as recast from 'recast'; | 
|  | 4 | +import * as acornParser from 'recast/parsers/acorn'; | 
|  | 5 | + | 
|  | 6 | +const POLYFILL_NAMES = new Set([ | 
|  | 7 | +  '_asyncNullishCoalesce', | 
|  | 8 | +  '_asyncOptionalChain', | 
|  | 9 | +  '_asyncOptionalChainDelete', | 
|  | 10 | +  '_createNamedExportFrom', | 
|  | 11 | +  '_createStarExport', | 
|  | 12 | +  '_interopDefault', // rollup's version | 
|  | 13 | +  '_interopNamespace', // rollup's version | 
|  | 14 | +  '_interopNamespaceDefaultOnly', | 
|  | 15 | +  '_interopRequireDefault', // sucrase's version | 
|  | 16 | +  '_interopRequireWildcard', // sucrase's version | 
|  | 17 | +  '_nullishCoalesce', | 
|  | 18 | +  '_optionalChain', | 
|  | 19 | +  '_optionalChainDelete', | 
|  | 20 | +]); | 
|  | 21 | + | 
|  | 22 | +/** | 
|  | 23 | + * Create a plugin which will replace function definitions of any of the above funcions with an `import` or `require` | 
|  | 24 | + * statement pulling them in from a central source. Mimics tsc's `importHelpers` option. | 
|  | 25 | + */ | 
|  | 26 | +export function makeExtractPolyfillsPlugin() { | 
|  | 27 | +  let moduleFormat; | 
|  | 28 | + | 
|  | 29 | +  // For more on the hooks used in this plugin, see https://rollupjs.org/guide/en/#output-generation-hooks | 
|  | 30 | +  return { | 
|  | 31 | +    name: 'extractPolyfills', | 
|  | 32 | + | 
|  | 33 | +    // Figure out which build we're currently in (esm or cjs) | 
|  | 34 | +    outputOptions(options) { | 
|  | 35 | +      moduleFormat = options.format; | 
|  | 36 | +    }, | 
|  | 37 | + | 
|  | 38 | +    // This runs after both the sucrase transpilation (which happens in the `transform` hook) and rollup's own | 
|  | 39 | +    // esm-i-fying or cjs-i-fying work (which happens right before `renderChunk`), in other words, after all polyfills | 
|  | 40 | +    // will have been injected | 
|  | 41 | +    renderChunk(code, chunk) { | 
|  | 42 | +      const sourceFile = chunk.fileName; | 
|  | 43 | + | 
|  | 44 | +      // We don't want to pull the function definitions out of their actual sourcefiles, just the places where they've | 
|  | 45 | +      // been injected | 
|  | 46 | +      if (sourceFile.includes('buildPolyfills')) { | 
|  | 47 | +        return null; | 
|  | 48 | +      } | 
|  | 49 | + | 
|  | 50 | +      const parserOptions = { | 
|  | 51 | +        sourceFileName: sourceFile, | 
|  | 52 | +        // We supply a custom parser which wraps the provided `acorn` parser in order to override the `ecmaVersion` value. | 
|  | 53 | +        // See https://github.com/benjamn/recast/issues/578. | 
|  | 54 | +        parser: { | 
|  | 55 | +          parse(source, options) { | 
|  | 56 | +            return acornParser.parse(source, { | 
|  | 57 | +              ...options, | 
|  | 58 | +              // By this point in the build, everything should already have been down-compiled to whatever JS version | 
|  | 59 | +              // we're targeting. Setting this parser to `latest` just means that whatever that version is (or changes | 
|  | 60 | +              // to in the future), this parser will be able to handle the generated code. | 
|  | 61 | +              ecmaVersion: 'latest', | 
|  | 62 | +            }); | 
|  | 63 | +          }, | 
|  | 64 | +        }, | 
|  | 65 | +      }; | 
|  | 66 | + | 
|  | 67 | +      const ast = recast.parse(code, parserOptions); | 
|  | 68 | + | 
|  | 69 | +      // Find function definitions and function expressions whose identifiers match a known polyfill name | 
|  | 70 | +      const polyfillNodes = findPolyfillNodes(ast); | 
|  | 71 | + | 
|  | 72 | +      if (polyfillNodes.length === 0) { | 
|  | 73 | +        return null; | 
|  | 74 | +      } | 
|  | 75 | + | 
|  | 76 | +      console.log(`${sourceFile} - polyfills: ${polyfillNodes.map(node => node.name)}`); | 
|  | 77 | + | 
|  | 78 | +      // Depending on the output format, generate `import { x, y, z } from '...'` or `var { x, y, z } = require('...')` | 
|  | 79 | +      const importOrRequireNode = createImportOrRequireNode(polyfillNodes, sourceFile, moduleFormat); | 
|  | 80 | + | 
|  | 81 | +      // Insert our new `import` or `require` node at the top of the file, and then delete the function definitions it's | 
|  | 82 | +      // meant to replace (polyfill nodes get marked for deletion in `findPolyfillNodes`) | 
|  | 83 | +      ast.program.body = [importOrRequireNode, ...ast.program.body.filter(node => !node.shouldDelete)]; | 
|  | 84 | + | 
|  | 85 | +      // In spite of the name, this doesn't actually print anything - it just stringifies the code, and keeps track of | 
|  | 86 | +      // where original nodes end up in order to generate a sourcemap. | 
|  | 87 | +      const result = recast.print(ast, { | 
|  | 88 | +        sourceMapName: `${sourceFile}.map`, | 
|  | 89 | +        quote: 'single', | 
|  | 90 | +      }); | 
|  | 91 | + | 
|  | 92 | +      return { code: result.code, map: result.map }; | 
|  | 93 | +    }, | 
|  | 94 | +  }; | 
|  | 95 | +} | 
|  | 96 | + | 
|  | 97 | +/** | 
|  | 98 | + * Extract the function name, regardless of the format in which the function is declared | 
|  | 99 | + */ | 
|  | 100 | +function getNodeName(node) { | 
|  | 101 | +  // Function expressions and functions pulled from objects | 
|  | 102 | +  if (node.type === 'VariableDeclaration') { | 
|  | 103 | +    // In practice sucrase and rollup only ever declare one polyfill at a time, so it's safe to just grab the first | 
|  | 104 | +    // entry here | 
|  | 105 | +    const declarationId = node.declarations[0].id; | 
|  | 106 | + | 
|  | 107 | +    // Note: Sucrase and rollup seem to only use the first type of variable declaration for their polyfills, but good to | 
|  | 108 | +    // cover our bases | 
|  | 109 | + | 
|  | 110 | +    // Declarations of the form | 
|  | 111 | +    //   `const dogs = function() { return "are great"; };` | 
|  | 112 | +    // or | 
|  | 113 | +    //   `const dogs = () => "are great"; | 
|  | 114 | +    if (declarationId.type === 'Identifier') { | 
|  | 115 | +      return declarationId.name; | 
|  | 116 | +    } | 
|  | 117 | +    // Declarations of the form | 
|  | 118 | +    //   `const { dogs } = { dogs: function() { return "are great"; } }` | 
|  | 119 | +    // or | 
|  | 120 | +    //   `const { dogs } = { dogs: () => "are great" }` | 
|  | 121 | +    else if (declarationId.type === 'ObjectPattern') { | 
|  | 122 | +      return declarationId.properties[0].key.name; | 
|  | 123 | +    } | 
|  | 124 | +    // Any other format | 
|  | 125 | +    else { | 
|  | 126 | +      return 'unknown variable'; | 
|  | 127 | +    } | 
|  | 128 | +  } | 
|  | 129 | + | 
|  | 130 | +  // Regular old functions, of the form | 
|  | 131 | +  //   `function dogs() { return "are great"; }` | 
|  | 132 | +  else if (node.type === 'FunctionDeclaration') { | 
|  | 133 | +    return node.id.name; | 
|  | 134 | +  } | 
|  | 135 | + | 
|  | 136 | +  // If we get here, this isn't a node we're interested in, so just return a string we know will never match any of the | 
|  | 137 | +  // polyfill names | 
|  | 138 | +  else { | 
|  | 139 | +    return 'nope'; | 
|  | 140 | +  } | 
|  | 141 | +} | 
|  | 142 | + | 
|  | 143 | +/** | 
|  | 144 | + * Find all nodes whose identifiers match a known polyfill name. | 
|  | 145 | + * | 
|  | 146 | + * Note: In theory, this could yield false positives, if any of the magic names were assigned to something other than a | 
|  | 147 | + * polyfill function, but the chances of that are slim. Also, it only searches the module global scope, but that's | 
|  | 148 | + * always where the polyfills appear, so no reason to traverse the whole tree. | 
|  | 149 | + */ | 
|  | 150 | +function findPolyfillNodes(ast) { | 
|  | 151 | +  const isPolyfillNode = node => { | 
|  | 152 | +    const nodeName = getNodeName(node); | 
|  | 153 | +    if (POLYFILL_NAMES.has(nodeName)) { | 
|  | 154 | +      // Mark this node for later deletion, since we're going to replace it with an import statement | 
|  | 155 | +      node.shouldDelete = true; | 
|  | 156 | +      // Store the name in a consistent spot, regardless of node type | 
|  | 157 | +      node.name = nodeName; | 
|  | 158 | + | 
|  | 159 | +      return true; | 
|  | 160 | +    } | 
|  | 161 | + | 
|  | 162 | +    return false; | 
|  | 163 | +  }; | 
|  | 164 | + | 
|  | 165 | +  return ast.program.body.filter(isPolyfillNode); | 
|  | 166 | +} | 
|  | 167 | + | 
|  | 168 | +/** | 
|  | 169 | + * Create a node representing an `import` or `require` statement of the form | 
|  | 170 | + * | 
|  | 171 | + *     import { < polyfills > } from '...' | 
|  | 172 | + * or | 
|  | 173 | + *     var { < polyfills > } = require('...') | 
|  | 174 | + * | 
|  | 175 | + * @param polyfillNodes The nodes from the current version of the code, defining the polyfill functions | 
|  | 176 | + * @param currentSourceFile The path, relative to `src/`, of the file currently being transpiled | 
|  | 177 | + * @param moduleFormat Either 'cjs' or 'esm' | 
|  | 178 | + * @returns A single node which can be subbed in for the polyfill definition nodes | 
|  | 179 | + */ | 
|  | 180 | +function createImportOrRequireNode(polyfillNodes, currentSourceFile, moduleFormat) { | 
|  | 181 | +  const { | 
|  | 182 | +    callExpression, | 
|  | 183 | +    identifier, | 
|  | 184 | +    importDeclaration, | 
|  | 185 | +    importSpecifier, | 
|  | 186 | +    literal, | 
|  | 187 | +    objectPattern, | 
|  | 188 | +    property, | 
|  | 189 | +    variableDeclaration, | 
|  | 190 | +    variableDeclarator, | 
|  | 191 | +  } = recast.types.builders; | 
|  | 192 | + | 
|  | 193 | +  // Since our polyfills live in `@sentry/utils`, if we're importing or requiring them there the path will have to be | 
|  | 194 | +  // relative | 
|  | 195 | +  const isUtilsPackage = process.cwd().endsWith('packages/utils'); | 
|  | 196 | +  const importSource = literal( | 
|  | 197 | +    isUtilsPackage | 
|  | 198 | +      ? `./${path.relative(path.dirname(currentSourceFile), 'buildPolyfills')}` | 
|  | 199 | +      : `@sentry/utils/${moduleFormat}/buildPolyfills`, | 
|  | 200 | +  ); | 
|  | 201 | + | 
|  | 202 | +  // This is the `x, y, z` of inside of `import { x, y, z }` or `var { x, y, z }` | 
|  | 203 | +  const importees = polyfillNodes.map(({ name: fnName }) => | 
|  | 204 | +    moduleFormat === 'esm' | 
|  | 205 | +      ? importSpecifier(identifier(fnName)) | 
|  | 206 | +      : property.from({ kind: 'init', key: identifier(fnName), value: identifier(fnName), shorthand: true }), | 
|  | 207 | +  ); | 
|  | 208 | + | 
|  | 209 | +  const requireFn = identifier('require'); | 
|  | 210 | + | 
|  | 211 | +  return moduleFormat === 'esm' | 
|  | 212 | +    ? importDeclaration(importees, importSource) | 
|  | 213 | +    : variableDeclaration('var', [ | 
|  | 214 | +        variableDeclarator(objectPattern(importees), callExpression(requireFn, [importSource])), | 
|  | 215 | +      ]); | 
|  | 216 | +} | 
0 commit comments