Skip to content

Commit 2d068e9

Browse files
authored
ref(nextjs): Use virtual file for proxying in proxy loader (#5960)
1 parent 052aa6b commit 2d068e9

File tree

7 files changed

+45
-37
lines changed

7 files changed

+45
-37
lines changed

packages/nextjs/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@rollup/plugin-sucrase": "4.0.4",
21+
"@rollup/plugin-virtual": "3.0.0",
2122
"@sentry/core": "7.16.0",
2223
"@sentry/integrations": "7.16.0",
2324
"@sentry/node": "7.16.0",

packages/nextjs/rollup.npm.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default [
3333
// make it so Rollup calms down about the fact that we're combining default and named exports
3434
exports: 'named',
3535
},
36-
external: ['@sentry/nextjs', '__RESOURCE_PATH__'],
36+
external: ['@sentry/nextjs', /__RESOURCE_PATH__.*/],
3737
},
3838
}),
3939
),

packages/nextjs/src/config/loaders/proxyLoader.ts

+12-17
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default async function proxyLoader(this: LoaderThis<LoaderOptions>, userC
3838
// wrapped file, so that we know that it's already been processed. (Adding this query string is also necessary to
3939
// convince webpack that it's a different file than the one it's in the middle of loading now, so that the originals
4040
// themselves will have a chance to load.)
41-
if (this.resourceQuery.includes('__sentry_wrapped__')) {
41+
if (this.resourceQuery.includes('__sentry_wrapped__') || this.resourceQuery.includes('__sentry_external__')) {
4242
return userCode;
4343
}
4444

@@ -55,34 +55,29 @@ export default async function proxyLoader(this: LoaderThis<LoaderOptions>, userC
5555

5656
// Fill in the path to the file we're wrapping and save the result as a temporary file in the same folder (so that
5757
// relative imports and exports are calculated correctly).
58-
//
59-
// TODO: We're saving the filled-in template to disk, however temporarily, because Rollup expects a path to a code
60-
// file, not code itself. There is a rollup plugin which can fake this (`@rollup/plugin-virtual`) but the virtual file
61-
// seems to be inside of a virtual directory (in other words, one level down from where you'd expect it) and that
62-
// messes up relative imports and exports. Presumably there's a way to make it work, though, and if we can, it would
63-
// be cleaner than having to first write and then delete a temporary file each time we run this loader.
6458
templateCode = templateCode.replace(/__RESOURCE_PATH__/g, this.resourcePath);
65-
const tempFilePath = path.resolve(path.dirname(this.resourcePath), `temp${Math.random()}.js`);
66-
fs.writeFileSync(tempFilePath, templateCode);
6759

6860
// Run the proxy module code through Rollup, in order to split the `export * from '<wrapped file>'` out into
6961
// individual exports (which nextjs seems to require), then delete the tempoary file.
70-
let proxyCode = await rollupize(tempFilePath, this.resourcePath);
71-
fs.unlinkSync(tempFilePath);
62+
63+
let proxyCode = await rollupize(this.resourcePath, templateCode);
7264

7365
if (!proxyCode) {
7466
// We will already have thrown a warning in `rollupize`, so no need to do it again here
7567
return userCode;
7668
}
7769

78-
// Add a query string onto all references to the wrapped file, so that webpack will consider it different from the
79-
// non-query-stringged version (which we're already in the middle of loading as we speak), and load it separately from
80-
// this. When the second load happens this loader will run again, but we'll be able to see the query string and will
81-
// know to immediately return without processing. This avoids an infinite loop.
8270
const resourceFilename = path.basename(this.resourcePath);
71+
72+
// For some reason when using virtual files (via the @rollup/plugin-virtual), rollup will always resolve imports with
73+
// absolute imports to relative imports with `..`.In our case we need`.`, which is why we're replacing for that here.
74+
// Also, we're adding a query string onto all references to the wrapped file, so that webpack will consider it
75+
// different from the non - query - stringged version(which we're already in the middle of loading as we speak), and
76+
// load it separately from this. When the second load happens this loader will run again, but we'll be able to see the
77+
// query string and will know to immediately return without processing. This avoids an infinite loop.
8378
proxyCode = proxyCode.replace(
84-
new RegExp(`/${escapeStringForRegex(resourceFilename)}'`, 'g'),
85-
`/${resourceFilename}?__sentry_wrapped__'`,
79+
new RegExp(`'../${escapeStringForRegex(resourceFilename)}'`, 'g'),
80+
`'./${resourceFilename}?__sentry_wrapped__'`,
8681
);
8782

8883
return proxyCode;

packages/nextjs/src/config/loaders/rollup.ts

+16-17
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
1-
import type { RollupSucraseOptions } from '@rollup/plugin-sucrase';
21
import sucrase from '@rollup/plugin-sucrase';
2+
import virtual from '@rollup/plugin-virtual';
33
import { logger } from '@sentry/utils';
44
import * as path from 'path';
55
import type { InputOptions as RollupInputOptions, OutputOptions as RollupOutputOptions } from 'rollup';
66
import { rollup } from 'rollup';
77

8-
const getRollupInputOptions: (proxyPath: string, resourcePath: string) => RollupInputOptions = (
9-
proxyPath,
10-
resourcePath,
11-
) => ({
12-
input: proxyPath,
8+
const SENTRY_PROXY_MODULE_NAME = 'sentry-proxy-module';
9+
10+
const getRollupInputOptions = (userModulePath: string, proxyTemplateCode: string): RollupInputOptions => ({
11+
input: SENTRY_PROXY_MODULE_NAME,
12+
1313
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.)'
14+
virtual({
15+
[SENTRY_PROXY_MODULE_NAME]: proxyTemplateCode,
16+
}),
1817
sucrase({
1918
transforms: ['jsx', 'typescript'],
20-
} as unknown as RollupSucraseOptions),
19+
}),
2120
],
2221

2322
// 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
2423
// proxy module (living in the temporary file we've created) and the file we're wrapping not to be external, because
2524
// otherwise they won't be processed. (We need Rollup to process the former so that we can use the code, and we need
2625
// it to process the latter so it knows what exports to re-export from the proxy module.) Past that, we don't care, so
2726
// don't bother to process anything else.
28-
external: importPath => importPath !== proxyPath && importPath !== resourcePath,
27+
external: importPath => importPath !== SENTRY_PROXY_MODULE_NAME && importPath !== userModulePath,
2928

3029
// Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the
3130
// user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and
@@ -66,19 +65,19 @@ const rollupOutputOptions: RollupOutputOptions = {
6665
* '<wrapped file>'` call into individual exports (which nextjs seems to need).
6766
*
6867
* @param tempProxyFilePath The path to the temporary file containing the proxy module code
69-
* @param resourcePath The path to the file being wrapped
68+
* @param userModulePath The path to the file being wrapped
7069
* @returns The processed proxy module code, or undefined if an error occurs
7170
*/
72-
export async function rollupize(tempProxyFilePath: string, resourcePath: string): Promise<string | undefined> {
71+
export async function rollupize(userModulePath: string, templateCode: string): Promise<string | undefined> {
7372
let finalBundle;
7473

7574
try {
76-
const intermediateBundle = await rollup(getRollupInputOptions(tempProxyFilePath, resourcePath));
75+
const intermediateBundle = await rollup(getRollupInputOptions(userModulePath, templateCode));
7776
finalBundle = await intermediateBundle.generate(rollupOutputOptions);
7877
} catch (err) {
7978
__DEBUG_BUILD__ &&
8079
logger.warn(
81-
`Could not wrap ${resourcePath}. An error occurred while processing the proxy module template:\n${err}`,
80+
`Could not wrap ${userModulePath}. An error occurred while processing the proxy module template:\n${err}`,
8281
);
8382
return undefined;
8483
}
@@ -92,7 +91,7 @@ export async function rollupize(tempProxyFilePath: string, resourcePath: string)
9291
// square brackets into underscores. Further, Rollup adds file extensions to bare-path-type import and export sources.
9392
// Because it assumes that everything will have already been processed, it always uses `.js` as the added extension.
9493
// 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);
94+
const resourceFilename = path.basename(userModulePath);
9695
const mutatedResourceFilename = resourceFilename
9796
// `[\\[\\]]` is the character class containing `[` and `]`
9897
.replace(new RegExp('[\\[\\]]', 'g'), '_')

packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
*
55
* We use `__RESOURCE_PATH__` as a placeholder for the path to the file being wrapped. Because it's not a real package,
66
* this causes both TS and ESLint to complain, hence the pragma comments below.
7+
*
8+
* The `?__sentry_external__` is used to
9+
* 1) tell rollup to treat the import as external (i.e. not process it)
10+
* 2) tell webpack not to proxy this file again (avoiding an infinite loop)
711
*/
812

913
// @ts-ignore See above
1014
// eslint-disable-next-line import/no-unresolved
11-
import * as origModule from '__RESOURCE_PATH__';
15+
import * as origModule from '__RESOURCE_PATH__?__sentry_external__';
1216
import * as Sentry from '@sentry/nextjs';
1317
import type { PageConfig } from 'next';
1418

packages/nextjs/src/config/templates/pageProxyLoaderTemplate.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
*
55
* We use `__RESOURCE_PATH__` as a placeholder for the path to the file being wrapped. Because it's not a real package,
66
* this causes both TS and ESLint to complain, hence the pragma comments below.
7+
*
8+
* The `?__sentry_external__` is used to
9+
* 1) tell rollup to treat the import as external (i.e. not process it)
10+
* 2) tell webpack not to proxy this file again (avoiding an infinite loop)
711
*/
812

913
// @ts-ignore See above
1014
// eslint-disable-next-line import/no-unresolved
11-
import * as wrapee from '__RESOURCE_PATH__';
15+
import * as wrapee from '__RESOURCE_PATH__?__sentry_external__';
1216
import * as Sentry from '@sentry/nextjs';
1317
import type { GetServerSideProps, GetStaticProps, NextPage as NextPageComponent } from 'next';
1418

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3317,6 +3317,11 @@
33173317
"@rollup/pluginutils" "^4.1.1"
33183318
sucrase "^3.20.0"
33193319

3320+
3321+
version "3.0.0"
3322+
resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.0.tgz#8c3f54b4ab4b267d9cd3dcbaedc58d4fd1deddca"
3323+
integrity sha512-K9KORe1myM62o0lKkNR4MmCxjwuAXsZEtIHpaILfv4kILXTOrXt/R2ha7PzMcCHPYdnkWPiBZK8ed4Zr3Ll5lQ==
3324+
33203325
"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0":
33213326
version "3.1.0"
33223327
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"

0 commit comments

Comments
 (0)