Skip to content

Commit d9c6887

Browse files
author
Luca Forstner
authored
ref(nextjs): Use generic loader to inject global values (#6484)
1 parent 49b8fdf commit d9c6887

10 files changed

+115
-154
lines changed

packages/nextjs/rollup.npm.config.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ export default [
2020
...makeNPMConfigVariants(
2121
makeBaseNPMConfig({
2222
entrypoints: [
23-
'src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts',
24-
'src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts',
25-
'src/config/templates/releasePrefixLoaderTemplate.ts',
2623
'src/config/templates/pageProxyLoaderTemplate.ts',
2724
'src/config/templates/apiProxyLoaderTemplate.ts',
2825
],
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export { default as valueInjectionLoader } from './valueInjectionLoader';
12
export { default as prefixLoader } from './prefixLoader';
23
export { default as proxyLoader } from './proxyLoader';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { LoaderThis } from './types';
2+
3+
type LoaderOptions = {
4+
values: Record<string, unknown>;
5+
};
6+
7+
/**
8+
* Set values on the global/window object at the start of a module.
9+
*
10+
* Options:
11+
* - `values`: An object where the keys correspond to the keys of the global values to set and the values
12+
* correspond to the values of the values on the global object. Values must be JSON serializable.
13+
*/
14+
export default function valueInjectionLoader(this: LoaderThis<LoaderOptions>, userCode: string): string {
15+
// We know one or the other will be defined, depending on the version of webpack being used
16+
const { values } = 'getOptions' in this ? this.getOptions() : this.query;
17+
18+
// Define some global proxy that works on server and on the browser.
19+
let injectedCode = 'var _sentryCollisionFreeGlobalObject = typeof window === "undefined" ? global : window;\n';
20+
21+
Object.entries(values).forEach(([key, value]) => {
22+
injectedCode += `_sentryCollisionFreeGlobalObject["${key}"] = ${JSON.stringify(value)};\n`;
23+
});
24+
25+
return `${injectedCode}\n${userCode}`;
26+
}

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

Lines changed: 0 additions & 9 deletions
This file was deleted.

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

Lines changed: 0 additions & 16 deletions
This file was deleted.

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

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/nextjs/src/config/webpack.ts

Lines changed: 66 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -87,26 +87,8 @@ export function constructWebpackConfigFunction(
8787
// `newConfig.module.rules` is required, so we don't have to keep asserting its existence
8888
const newConfig = setUpModuleRules(rawNewConfig);
8989

90-
// Add a loader which will inject code that sets global values for use by `RewriteFrames`
91-
addRewriteFramesLoader(newConfig, isServer ? 'server' : 'client', userNextConfig);
92-
93-
newConfig.module.rules.push({
94-
test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/,
95-
use: [
96-
{
97-
// Inject the release value the same way the webpack plugin does.
98-
loader: path.resolve(__dirname, 'loaders/prefixLoader.js'),
99-
options: {
100-
templatePrefix: 'release',
101-
replacements: [
102-
['__RELEASE__', webpackPluginOptions.release || process.env.SENTRY_RELEASE],
103-
['__ORG__', webpackPluginOptions.org || process.env.SENTRY_ORG],
104-
['__PROJECT__', webpackPluginOptions.project || process.env.SENTRY_PROJECT || ''],
105-
],
106-
},
107-
},
108-
],
109-
});
90+
// Add a loader which will inject code that sets global values
91+
addValueInjectionLoader(newConfig, userNextConfig, webpackPluginOptions);
11092

11193
if (isServer) {
11294
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
@@ -667,49 +649,76 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
667649
}
668650

669651
/**
670-
* Support the `distDir` and `assetPrefix` options by making their values (easy to get here at build-time) available at
671-
* runtime (for use by `RewriteFrames`), by injecting code to attach their values to `global` or `window`.
672-
*
673-
* @param newConfig The webpack config object being constructed
674-
* @param target Either 'server' or 'client'
675-
* @param userNextConfig The user's nextjs config options
652+
* Adds loaders to inject values on the global object based on user configuration.
676653
*/
677-
function addRewriteFramesLoader(
654+
function addValueInjectionLoader(
678655
newConfig: WebpackConfigObjectWithModuleRules,
679-
target: 'server' | 'client',
680656
userNextConfig: NextConfigObject,
657+
webpackPluginOptions: SentryWebpackPlugin.SentryCliPluginOptions,
681658
): void {
682-
// Nextjs will use `basePath` in place of `assetPrefix` if it's defined but `assetPrefix` is not
683659
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
684-
const replacements = {
685-
server: [
686-
[
687-
'__DIST_DIR__',
688-
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
689-
// characters)
690-
userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
691-
],
692-
],
693-
client: [
694-
[
695-
'__ASSET_PREFIX_PATH__',
696-
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
697-
// `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
698-
assetPrefix ? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '') : '',
699-
],
700-
],
660+
const releaseValue = webpackPluginOptions.release || process.env.SENTRY_RELEASE;
661+
const orgValue = webpackPluginOptions.org || process.env.SENTRY_ORG;
662+
const projectValue = webpackPluginOptions.project || process.env.SENTRY_PROJECT;
663+
664+
const isomorphicValues = {
665+
// Inject release into SDK
666+
...(releaseValue
667+
? {
668+
SENTRY_RELEASE: {
669+
id: releaseValue,
670+
},
671+
}
672+
: undefined),
673+
674+
// Enable module federation support (see https://github.com/getsentry/sentry-webpack-plugin/pull/307)
675+
...(projectValue && releaseValue
676+
? {
677+
SENTRY_RELEASES: {
678+
[orgValue ? `${projectValue}@${orgValue}` : projectValue]: { id: releaseValue },
679+
},
680+
}
681+
: undefined),
701682
};
702683

703-
newConfig.module.rules.push({
704-
test: new RegExp(`sentry\\.${target}\\.config\\.(jsx?|tsx?)`),
705-
use: [
706-
{
707-
loader: path.resolve(__dirname, 'loaders/prefixLoader.js'),
708-
options: {
709-
templatePrefix: `${target}RewriteFrames`,
710-
replacements: replacements[target],
684+
const serverValues = {
685+
...isomorphicValues,
686+
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
687+
// characters)
688+
__rewriteFramesDistDir__: userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
689+
};
690+
691+
const clientValues = {
692+
...isomorphicValues,
693+
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
694+
// `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
695+
__rewriteFramesAssetPrefixPath__: assetPrefix
696+
? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
697+
: '',
698+
};
699+
700+
newConfig.module.rules.push(
701+
{
702+
test: /sentry\.server\.config\.(jsx?|tsx?)/,
703+
use: [
704+
{
705+
loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
706+
options: {
707+
values: serverValues,
708+
},
711709
},
712-
},
713-
],
714-
});
710+
],
711+
},
712+
{
713+
test: /sentry\.client\.config\.(jsx?|tsx?)/,
714+
use: [
715+
{
716+
loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
717+
options: {
718+
values: clientValues,
719+
},
720+
},
721+
],
722+
},
723+
);
715724
}

packages/nextjs/src/index.client.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ declare const __SENTRY_TRACING__: boolean;
3535
// https://github.com/vercel/next.js/blob/166e5fb9b92f64c4b5d1f6560a05e2b9778c16fb/packages/next/build/webpack-config.ts#L206
3636
declare const EdgeRuntime: string | undefined;
3737

38-
type GlobalWithAssetPrefixPath = typeof global & { __rewriteFramesAssetPrefixPath__: string };
38+
const globalWithInjectedValues = global as typeof global & {
39+
__rewriteFramesAssetPrefixPath__: string;
40+
__sentryRewritesTunnelPath__?: string;
41+
};
3942

4043
/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
4144
export function init(options: NextjsOptions): void {
@@ -67,7 +70,7 @@ function addClientIntegrations(options: NextjsOptions): void {
6770

6871
// This value is injected at build time, based on the output directory specified in the build config. Though a default
6972
// is set there, we set it here as well, just in case something has gone wrong with the injection.
70-
const assetPrefixPath = (global as GlobalWithAssetPrefixPath).__rewriteFramesAssetPrefixPath__ || '';
73+
const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';
7174

7275
const defaultRewriteFramesIntegration = new RewriteFrames({
7376
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`

packages/nextjs/src/index.server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ export { captureUnderscoreErrorException } from './utils/_error';
1919
// because or SSR of next.js we can only use this.
2020
export { ErrorBoundary, showReportDialog, withErrorBoundary } from '@sentry/react';
2121

22-
type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string };
22+
const globalWithInjectedValues = global as typeof global & {
23+
__rewriteFramesDistDir__: string;
24+
};
25+
2326
const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null };
2427

2528
// This is a variable that Next.js will string replace during build with a string if run in an edge runtime from Next.js
@@ -114,7 +117,7 @@ function addServerIntegrations(options: NextjsOptions): void {
114117

115118
// This value is injected at build time, based on the output directory specified in the build config. Though a default
116119
// is set there, we set it here as well, just in case something has gone wrong with the injection.
117-
const distDirName = (global as GlobalWithDistDir).__rewriteFramesDistDir__ || '.next';
120+
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__ || '.next';
118121
// nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so
119122
// we can read in the project directory from the currently running process
120123
const distDirAbsPath = path.resolve(process.cwd(), distDirName);

packages/nextjs/test/config/loaders.test.ts

Lines changed: 12 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
exportedNextConfig,
88
serverBuildContext,
99
serverWebpackConfig,
10-
userSentryWebpackPluginConfig,
1110
} from './fixtures';
1211
import { materializeFinalWebpackConfig } from './testUtils';
1312

@@ -41,7 +40,7 @@ declare global {
4140

4241
describe('webpack loaders', () => {
4342
describe('server loaders', () => {
44-
it('adds server `RewriteFrames` loader to server config', async () => {
43+
it('adds server `valueInjection` loader to server config', async () => {
4544
const finalWebpackConfig = await materializeFinalWebpackConfig({
4645
exportedNextConfig,
4746
incomingWebpackConfig: serverWebpackConfig,
@@ -52,42 +51,19 @@ describe('webpack loaders', () => {
5251
test: /sentry\.server\.config\.(jsx?|tsx?)/,
5352
use: [
5453
{
55-
loader: expect.stringEndingWith('prefixLoader.js'),
56-
options: expect.objectContaining({ templatePrefix: 'serverRewriteFrames' }),
57-
},
58-
],
59-
});
60-
});
61-
62-
it('adds release prefix loader to server config', async () => {
63-
const finalWebpackConfig = await materializeFinalWebpackConfig({
64-
exportedNextConfig,
65-
incomingWebpackConfig: serverWebpackConfig,
66-
incomingWebpackBuildContext: serverBuildContext,
67-
userSentryWebpackPluginConfig: userSentryWebpackPluginConfig,
68-
});
69-
70-
expect(finalWebpackConfig.module.rules).toContainEqual({
71-
test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/,
72-
use: [
73-
{
74-
loader: expect.stringEndingWith('prefixLoader.js'),
75-
options: {
76-
templatePrefix: 'release',
77-
replacements: [
78-
['__RELEASE__', 'doGsaREgReaT'],
79-
['__ORG__', 'squirrelChasers'],
80-
['__PROJECT__', 'simulator'],
81-
],
82-
},
54+
loader: expect.stringEndingWith('valueInjectionLoader.js'),
55+
// We use `expect.objectContaining({})` rather than `expect.any(Object)` to match any plain object because
56+
// the latter will also match arrays, regexes, dates, sets, etc. - anything whose `typeof` value is
57+
// `'object'`.
58+
options: expect.objectContaining({ values: expect.objectContaining({}) }),
8359
},
8460
],
8561
});
8662
});
8763
});
8864

8965
describe('client loaders', () => {
90-
it('adds `RewriteFrames` loader to client config', async () => {
66+
it('adds `valueInjection` loader to client config', async () => {
9167
const finalWebpackConfig = await materializeFinalWebpackConfig({
9268
exportedNextConfig,
9369
incomingWebpackConfig: clientWebpackConfig,
@@ -98,34 +74,11 @@ describe('webpack loaders', () => {
9874
test: /sentry\.client\.config\.(jsx?|tsx?)/,
9975
use: [
10076
{
101-
loader: expect.stringEndingWith('prefixLoader.js'),
102-
options: expect.objectContaining({ templatePrefix: 'clientRewriteFrames' }),
103-
},
104-
],
105-
});
106-
});
107-
108-
it('adds release prefix loader to client config', async () => {
109-
const finalWebpackConfig = await materializeFinalWebpackConfig({
110-
exportedNextConfig,
111-
incomingWebpackConfig: clientWebpackConfig,
112-
incomingWebpackBuildContext: clientBuildContext,
113-
userSentryWebpackPluginConfig: userSentryWebpackPluginConfig,
114-
});
115-
116-
expect(finalWebpackConfig.module.rules).toContainEqual({
117-
test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/,
118-
use: [
119-
{
120-
loader: expect.stringEndingWith('prefixLoader.js'),
121-
options: {
122-
templatePrefix: 'release',
123-
replacements: [
124-
['__RELEASE__', 'doGsaREgReaT'],
125-
['__ORG__', 'squirrelChasers'],
126-
['__PROJECT__', 'simulator'],
127-
],
128-
},
77+
loader: expect.stringEndingWith('valueInjectionLoader.js'),
78+
// We use `expect.objectContaining({})` rather than `expect.any(Object)` to match any plain object because
79+
// the latter will also match arrays, regexes, dates, sets, etc. - anything whose `typeof` value is
80+
// `'object'`.
81+
options: expect.objectContaining({ values: expect.objectContaining({}) }),
12982
},
13083
],
13184
});

0 commit comments

Comments
 (0)