|
| 1 | +import type { RollupSucraseOptions } from '@rollup/plugin-sucrase'; |
| 2 | +import sucrase from '@rollup/plugin-sucrase'; |
| 3 | +import { logger } from '@sentry/utils'; |
| 4 | +import * as path from 'path'; |
| 5 | +import type { InputOptions as RollupInputOptions, OutputOptions as RollupOutputOptions } from 'rollup'; |
| 6 | +import { rollup } from 'rollup'; |
| 7 | + |
| 8 | +const getRollupInputOptions: (proxyPath: string, resourcePath: string) => RollupInputOptions = ( |
| 9 | + proxyPath, |
| 10 | + resourcePath, |
| 11 | +) => ({ |
| 12 | + input: proxyPath, |
| 13 | + plugins: [ |
| 14 | + // For some reason, even though everything in `RollupSucraseOptions` besides `transforms` is supposed to be |
| 15 | + // optional, TS complains that there are a bunch of missing properties (hence the typecast). Similar to |
| 16 | + // https://github.com/microsoft/TypeScript/issues/20722, though that's been fixed. (In this case it's an interface |
| 17 | + // exporting a `Pick` picking optional properties which is turning them required somehow.)' |
| 18 | + sucrase({ |
| 19 | + transforms: ['jsx', 'typescript'], |
| 20 | + } as unknown as RollupSucraseOptions), |
| 21 | + ], |
| 22 | + |
| 23 | + // We want to process as few files as possible, so as not to slow down the build any more than we have to. We need the |
| 24 | + // proxy module (living in the temporary file we've created) and the file we're wrapping not to be external, because |
| 25 | + // otherwise they won't be processed. (We need Rollup to process the former so that we can use the code, and we need |
| 26 | + // it to process the latter so it knows what exports to re-export from the proxy module.) Past that, we don't care, so |
| 27 | + // don't bother to process anything else. |
| 28 | + external: importPath => importPath !== proxyPath && importPath !== resourcePath, |
| 29 | + |
| 30 | + // Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the |
| 31 | + // user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and |
| 32 | + // https://stackoverflow.com/a/60347490.) |
| 33 | + context: 'this', |
| 34 | + |
| 35 | + // Rollup's path-resolution logic when handling re-exports can go wrong when wrapping pages which aren't at the root |
| 36 | + // level of the `pages` directory. This may be a bug, as it doesn't match the behavior described in the docs, but what |
| 37 | + // seems to happen is this: |
| 38 | + // |
| 39 | + // - We try to wrap `pages/xyz/userPage.js`, which contains `export { helperFunc } from '../../utils/helper'` |
| 40 | + // - Rollup converts '../../utils/helper' into an absolute path |
| 41 | + // - We mark the helper module as external |
| 42 | + // - Rollup then converts it back to a relative path, but relative to `pages/` rather than `pages/xyz/`. (This is |
| 43 | + // the part which doesn't match the docs. They say that Rollup will use the common ancestor of all modules in the |
| 44 | + // bundle as the basis for the relative path calculation, but both our temporary file and the page being wrapped |
| 45 | + // live in `pages/xyz/`, and they're the only two files in the bundle, so `pages/xyz/`` should be used as the |
| 46 | + // root. Unclear why it's not.) |
| 47 | + // - As a result of the miscalculation, our proxy module will include `export { helperFunc } from '../utils/helper'` |
| 48 | + // rather than the expected `export { helperFunc } from '../../utils/helper'`, thereby causing a build error in |
| 49 | + // nextjs.. |
| 50 | + // |
| 51 | + // It's not 100% clear why, but telling it not to do the conversion back from absolute to relative (by setting |
| 52 | + // `makeAbsoluteExternalsRelative` to `false`) seems to also prevent it from going from relative to absolute in the |
| 53 | + // first place, with the result that the path remains untouched (which is what we want.) |
| 54 | + makeAbsoluteExternalsRelative: false, |
| 55 | +}); |
| 56 | + |
| 57 | +const rollupOutputOptions: RollupOutputOptions = { |
| 58 | + format: 'esm', |
| 59 | + |
| 60 | + // Don't create a bundle - we just want the transformed entrypoint file |
| 61 | + preserveModules: true, |
| 62 | +}; |
| 63 | + |
| 64 | +/** |
| 65 | + * Use Rollup to process the proxy module file (located at `tempProxyFilePath`) in order to split its `export * from |
| 66 | + * '<wrapped file>'` call into individual exports (which nextjs seems to need). |
| 67 | + * |
| 68 | + * @param tempProxyFilePath The path to the temporary file containing the proxy module code |
| 69 | + * @param resourcePath The path to the file being wrapped |
| 70 | + * @returns The processed proxy module code, or undefined if an error occurs |
| 71 | + */ |
| 72 | +export async function rollupize(tempProxyFilePath: string, resourcePath: string): Promise<string | undefined> { |
| 73 | + let finalBundle; |
| 74 | + |
| 75 | + try { |
| 76 | + const intermediateBundle = await rollup(getRollupInputOptions(tempProxyFilePath, resourcePath)); |
| 77 | + finalBundle = await intermediateBundle.generate(rollupOutputOptions); |
| 78 | + } catch (err) { |
| 79 | + __DEBUG_BUILD__ && |
| 80 | + logger.warn( |
| 81 | + `Could not wrap ${resourcePath}. An error occurred while processing the proxy module template:\n${err}`, |
| 82 | + ); |
| 83 | + return undefined; |
| 84 | + } |
| 85 | + |
| 86 | + // The module at index 0 is always the entrypoint, which in this case is the proxy module. |
| 87 | + let { code } = finalBundle.output[0]; |
| 88 | + |
| 89 | + // Rollup does a few things to the code we *don't* want. Undo those changes before returning the code. |
| 90 | + // |
| 91 | + // Nextjs uses square brackets surrounding a path segment to denote a parameter in the route, but Rollup turns those |
| 92 | + // square brackets into underscores. Further, Rollup adds file extensions to bare-path-type import and export sources. |
| 93 | + // Because it assumes that everything will have already been processed, it always uses `.js` as the added extension. |
| 94 | + // We need to restore the original name and extension so that Webpack will be able to find the wrapped file. |
| 95 | + const resourceFilename = path.basename(resourcePath); |
| 96 | + const mutatedResourceFilename = resourceFilename |
| 97 | + // `[\\[\\]]` is the character class containing `[` and `]` |
| 98 | + .replace(new RegExp('[\\[\\]]', 'g'), '_') |
| 99 | + .replace(/(jsx?|tsx?)$/, 'js'); |
| 100 | + code = code.replace(new RegExp(mutatedResourceFilename, 'g'), resourceFilename); |
| 101 | + |
| 102 | + return code; |
| 103 | +} |
0 commit comments