@@ -2,34 +2,46 @@ import { getSentryRelease } from '@sentry/node';
2
2
import { logger } from '@sentry/utils' ;
3
3
import defaultWebpackPlugin , { SentryCliPluginOptions } from '@sentry/webpack-plugin' ;
4
4
import * as SentryWebpackPlugin from '@sentry/webpack-plugin' ;
5
+ import * as fs from 'fs' ;
6
+ import * as path from 'path' ;
7
+
8
+ const SENTRY_CLIENT_CONFIG_FILE = './sentry.client.config.js' ;
9
+ const SENTRY_SERVER_CONFIG_FILE = './sentry.server.config.js' ;
10
+ // this is where the transpiled/bundled version of `USER_SERVER_CONFIG_FILE` will end up
11
+ export const SERVER_SDK_INIT_PATH = 'sentry/initServerSDK.js' ;
5
12
6
13
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7
14
type PlainObject < T = any > = { [ key : string ] : T } ;
8
15
9
- // Man are these types hard to name well. "Entry" = an item in some collection of items, but in our case, one of the
10
- // things we're worried about here is property (entry) in an object called... entry. So henceforth, the specific
11
- // property we're modifying is going to be known as an EntryProperty.
12
-
13
16
// The function which is ultimately going to be exported from `next.config.js` under the name `webpack`
14
17
type WebpackExport = ( config : WebpackConfig , options : WebpackOptions ) => WebpackConfig ;
15
18
16
19
// The two arguments passed to the exported `webpack` function, as well as the thing it returns
17
- type WebpackConfig = { devtool : string ; plugins : PlainObject [ ] ; entry : EntryProperty } ;
20
+ type WebpackConfig = {
21
+ devtool : string ;
22
+ plugins : PlainObject [ ] ;
23
+ entry : EntryProperty ;
24
+ output : { path : string } ;
25
+ target : string ;
26
+ context : string ;
27
+ } ;
18
28
type WebpackOptions = { dev : boolean ; isServer : boolean ; buildId : string } ;
19
29
20
30
// For our purposes, the value for `entry` is either an object, or a function which returns such an object
21
31
type EntryProperty = ( ( ) => Promise < EntryPropertyObject > ) | EntryPropertyObject ;
22
-
23
32
// Each value in that object is either a string representing a single entry point, an array of such strings, or an
24
33
// object containing either of those, along with other configuration options. In that third case, the entry point(s) are
25
34
// listed under the key `import`.
26
- type EntryPropertyObject = PlainObject < string | Array < string > | EntryPointObject > ;
35
+ type EntryPropertyObject = PlainObject < string > | PlainObject < Array < string > > | PlainObject < EntryPointObject > ;
27
36
type EntryPointObject = { import : string | Array < string > } ;
28
37
29
- const sentryClientConfig = './sentry.client.config.js' ;
30
- const sentryServerConfig = './sentry.server.config.js' ;
31
-
32
- /** Add a file (`injectee`) to a given element (`injectionPoint`) of the `entry` property */
38
+ /**
39
+ * Add a file to a specific element of the given `entry` webpack config property.
40
+ *
41
+ * @param entryProperty The existing `entry` config object
42
+ * @param injectionPoint The key where the file should be injected
43
+ * @param injectee The path to the injected file
44
+ */
33
45
const _injectFile = ( entryProperty : EntryPropertyObject , injectionPoint : string , injectee : string ) : void => {
34
46
// can be a string, array of strings, or object whose `import` property is one of those two
35
47
let injectedInto = entryProperty [ injectionPoint ] ;
@@ -41,21 +53,19 @@ const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string,
41
53
return ;
42
54
}
43
55
44
- // In case we inject our client config, we need to add it after the frontend code
45
- // otherwise the runtime config isn't loaded. See: https://github.com/getsentry/sentry-javascript/issues/3485
46
- const isClient = injectee === sentryClientConfig ;
47
-
56
+ // We inject the user's client config file after the existing code so that the config file has access to
57
+ // `publicRuntimeConfig`. See https://github.com/getsentry/sentry-javascript/issues/3485
48
58
if ( typeof injectedInto === 'string' ) {
49
- injectedInto = isClient ? [ injectedInto , injectee ] : [ injectee , injectedInto ] ;
59
+ injectedInto = [ injectedInto , injectee ] ;
50
60
} else if ( Array . isArray ( injectedInto ) ) {
51
- injectedInto = isClient ? [ ...injectedInto , injectee ] : [ injectee , ... injectedInto ] ;
61
+ injectedInto = [ ...injectedInto , injectee ] ;
52
62
} else {
53
- let importVal : string | string [ ] | EntryPointObject ;
63
+ let importVal : string | string [ ] ;
64
+
54
65
if ( typeof injectedInto . import === 'string' ) {
55
- importVal = isClient ? [ injectedInto . import , injectee ] : [ injectee , injectedInto . import ] ;
66
+ importVal = [ injectedInto . import , injectee ] ;
56
67
} else {
57
- // If it's not a string, the inner value is an array
58
- importVal = isClient ? [ ...injectedInto . import , injectee ] : [ injectee , ...injectedInto . import ] ;
68
+ importVal = [ ...injectedInto . import , injectee ] ;
59
69
}
60
70
61
71
injectedInto = {
@@ -68,8 +78,6 @@ const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string,
68
78
} ;
69
79
70
80
const injectSentry = async ( origEntryProperty : EntryProperty , isServer : boolean ) : Promise < EntryProperty > => {
71
- // Out of the box, nextjs uses the `() => Promise<EntryPropertyObject>)` flavor of EntryProperty, where the returned
72
- // object has string arrays for values.
73
81
// The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs
74
82
// sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether
75
83
// someone else has come along before us and changed that, we need to check a few things along the way. The one thing
@@ -80,19 +88,19 @@ const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean)
80
88
newEntryProperty = await origEntryProperty ( ) ;
81
89
}
82
90
newEntryProperty = newEntryProperty as EntryPropertyObject ;
83
- // On the server, we need to inject the SDK into both into the base page (`_document`) and into individual API routes
84
- // (which have no common base).
91
+ // Add a new element to the `entry` array, we force webpack to create a bundle out of the user's
92
+ // `sentry.server.config.js` file and output it to `SERVER_INIT_LOCATION`. (See
93
+ // https://webpack.js.org/guides/code-splitting/#entry-points.) We do this so that the user's config file is run
94
+ // through babel (and any other processors through which next runs the rest of the user-provided code - pages, API
95
+ // routes, etc.). Specifically, we need any ESM-style `import` code to get transpiled into ES5, so that we can call
96
+ // `require()` on the resulting file when we're instrumenting the sesrver. (We can't use a dynamic import there
97
+ // because that then forces the user into a particular TS config.)
85
98
if ( isServer ) {
86
- Object . keys ( newEntryProperty ) . forEach ( key => {
87
- if ( key === 'pages/_document' || key . includes ( 'pages/api' ) ) {
88
- // for some reason, because we're now in a function, we have to cast again
89
- _injectFile ( newEntryProperty as EntryPropertyObject , key , sentryServerConfig ) ;
90
- }
91
- } ) ;
99
+ newEntryProperty [ SERVER_SDK_INIT_PATH ] = SENTRY_SERVER_CONFIG_FILE ;
92
100
}
93
101
// On the client, it's sufficient to inject it into the `main` JS code, which is included in every browser page.
94
102
else {
95
- _injectFile ( newEntryProperty , 'main' , sentryClientConfig ) ;
103
+ _injectFile ( newEntryProperty , 'main' , SENTRY_CLIENT_CONFIG_FILE ) ;
96
104
}
97
105
// TODO: hack made necessary because the async-ness of this function turns our object back into a promise, meaning the
98
106
// internal `next` code which should do this doesn't
@@ -109,11 +117,18 @@ type NextConfigExports = {
109
117
webpack ?: WebpackExport ;
110
118
} ;
111
119
120
+ /**
121
+ * Add Sentry options to the config to be exported from the user's `next.config.js` file.
122
+ *
123
+ * @param providedExports The existing config to be exported ,prior to adding Sentry
124
+ * @param providedSentryWebpackPluginOptions Configuration for SentryWebpackPlugin
125
+ * @returns The modified config to be exported
126
+ */
112
127
export function withSentryConfig (
113
128
providedExports : NextConfigExports = { } ,
114
- providedWebpackPluginOptions : Partial < SentryCliPluginOptions > = { } ,
129
+ providedSentryWebpackPluginOptions : Partial < SentryCliPluginOptions > = { } ,
115
130
) : NextConfigExports {
116
- const defaultWebpackPluginOptions = {
131
+ const defaultSentryWebpackPluginOptions = {
117
132
url : process . env . SENTRY_URL ,
118
133
org : process . env . SENTRY_ORG ,
119
134
project : process . env . SENTRY_PROJECT ,
@@ -122,22 +137,30 @@ export function withSentryConfig(
122
137
stripPrefix : [ 'webpack://_N_E/' ] ,
123
138
urlPrefix : `~/_next` ,
124
139
include : '.next/' ,
125
- ignore : [ 'node_modules ' , 'webpack.config .js' ] ,
140
+ ignore : [ '.next/cache ' , 'server/ssr-module-cache.js' , 'static/*/_ssgManifest.js' , 'static/*/_buildManifest .js'] ,
126
141
} ;
127
142
128
143
// warn if any of the default options for the webpack plugin are getting overridden
129
- const webpackPluginOptionOverrides = Object . keys ( defaultWebpackPluginOptions )
144
+ const sentryWebpackPluginOptionOverrides = Object . keys ( defaultSentryWebpackPluginOptions )
130
145
. concat ( 'dryrun' )
131
- . filter ( key => key in Object . keys ( providedWebpackPluginOptions ) ) ;
132
- if ( webpackPluginOptionOverrides . length > 0 ) {
146
+ . filter ( key => key in Object . keys ( providedSentryWebpackPluginOptions ) ) ;
147
+ if ( sentryWebpackPluginOptionOverrides . length > 0 ) {
133
148
logger . warn (
134
149
'[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' +
135
- `\t${ webpackPluginOptionOverrides . toString ( ) } ,\n` +
150
+ `\t${ sentryWebpackPluginOptionOverrides . toString ( ) } ,\n` +
136
151
"which has the possibility of breaking source map upload and application. This is only a good idea if you know what you're doing." ,
137
152
) ;
138
153
}
139
154
140
155
const newWebpackExport = ( config : WebpackConfig , options : WebpackOptions ) : WebpackConfig => {
156
+ // if we're building server code, store the webpack output path as an env variable, so we know where to look for the
157
+ // webpack-processed version of `sentry.server.config.js` when we need it
158
+ if ( config . target === 'node' ) {
159
+ const serverSDKInitOutputPath = path . join ( config . output . path , SERVER_SDK_INIT_PATH ) ;
160
+ const projectDir = config . context ;
161
+ setRuntimeEnvVars ( projectDir , { SENTRY_SERVER_INIT_PATH : serverSDKInitOutputPath } ) ;
162
+ }
163
+
141
164
let newConfig = config ;
142
165
143
166
if ( typeof providedExports . webpack === 'function' ) {
@@ -161,8 +184,8 @@ export function withSentryConfig(
161
184
new ( ( SentryWebpackPlugin as unknown ) as typeof defaultWebpackPlugin ) ( {
162
185
dryRun : options . dev ,
163
186
release : getSentryRelease ( options . buildId ) ,
164
- ...defaultWebpackPluginOptions ,
165
- ...providedWebpackPluginOptions ,
187
+ ...defaultSentryWebpackPluginOptions ,
188
+ ...providedSentryWebpackPluginOptions ,
166
189
} ) ,
167
190
) ;
168
191
@@ -175,3 +198,39 @@ export function withSentryConfig(
175
198
webpack : newWebpackExport ,
176
199
} ;
177
200
}
201
+
202
+ /**
203
+ * Set variables to be added to the env at runtime, by storing them in `.env.local` (which `next` automatically reads
204
+ * into memory at server startup).
205
+ *
206
+ * @param projectDir The path to the project root
207
+ * @param vars Object containing vars to set
208
+ */
209
+ function setRuntimeEnvVars ( projectDir : string , vars : PlainObject < string > ) : void {
210
+ // ensure the file exists
211
+ const envFilePath = path . join ( projectDir , '.env.local' ) ;
212
+ if ( ! fs . existsSync ( envFilePath ) ) {
213
+ fs . writeFileSync ( envFilePath , '' ) ;
214
+ }
215
+
216
+ let fileContents = fs
217
+ . readFileSync ( envFilePath )
218
+ . toString ( )
219
+ . trim ( ) ;
220
+
221
+ Object . entries ( vars ) . forEach ( entry => {
222
+ const [ varName , value ] = entry ;
223
+ const envVarString = `${ varName } =${ value } ` ;
224
+
225
+ // new entry
226
+ if ( ! fileContents . includes ( varName ) ) {
227
+ fileContents = `${ fileContents } \n${ envVarString } ` ;
228
+ }
229
+ // existing entry; make sure value is up to date
230
+ else {
231
+ fileContents = fileContents . replace ( new RegExp ( `${ varName } =\\S+` ) , envVarString ) ;
232
+ }
233
+ } ) ;
234
+
235
+ fs . writeFileSync ( envFilePath , `${ fileContents . trim ( ) } \n` ) ;
236
+ }
0 commit comments