1
1
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2
- import * as fs from 'fs' ;
3
- import * as path from 'path' ;
4
- import type { SourceMap } from 'rollup' ;
5
- import { rollup } from 'rollup' ;
2
+ import type {
3
+ ExportNamedDeclaration ,
4
+ FunctionDeclaration ,
5
+ Program ,
6
+ VariableDeclaration ,
7
+ VariableDeclarator ,
8
+ } from '@babel/types' ;
9
+ import type { ProxifiedModule } from 'magicast' ;
10
+ import { builders , generateCode , parseModule } from 'magicast' ;
6
11
import type { Plugin } from 'vite' ;
7
12
8
- // Just a simple placeholder to make referencing module consistent
9
- const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module' ;
10
-
11
- // Needs to end in .cjs in order for the `commonjs` plugin to pick it up
12
- const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.js' ;
13
-
14
13
export type AutoInstrumentSelection = {
15
14
/**
16
15
* If this flag is `true`, the Sentry plugins will automatically instrument the `load` function of
@@ -41,21 +40,6 @@ type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
41
40
* @returns the plugin
42
41
*/
43
42
export async function makeAutoInstrumentationPlugin ( options : AutoInstrumentPluginOptions ) : Promise < Plugin > {
44
- const universalLoadTemplatePath = path . resolve ( __dirname , 'templates' , 'universalLoadTemplate.js' ) ;
45
- const universalLoadTemplate = ( await fs . promises . readFile ( universalLoadTemplatePath , 'utf-8' ) ) . toString ( ) ;
46
-
47
- const serverLoadTemplatePath = path . resolve ( __dirname , 'templates' , 'serverLoadTemplate.js' ) ;
48
- const serverLoadTemplate = ( await fs . promises . readFile ( serverLoadTemplatePath , 'utf-8' ) ) . toString ( ) ;
49
-
50
- const universalLoadWrappingCode = universalLoadTemplate . replace (
51
- / _ _ S E N T R Y _ W R A P P I N G _ T A R G E T _ F I L E _ _ / g,
52
- WRAPPING_TARGET_MODULE_NAME ,
53
- ) ;
54
- const serverLoadWrappingCode = serverLoadTemplate . replace (
55
- / _ _ S E N T R Y _ W R A P P I N G _ T A R G E T _ F I L E _ _ / g,
56
- WRAPPING_TARGET_MODULE_NAME ,
57
- ) ;
58
-
59
43
const { load : shouldWrapLoad , serverLoad : shouldWrapServerLoad , debug } = options ;
60
44
61
45
return {
@@ -71,7 +55,8 @@ export async function makeAutoInstrumentationPlugin(options: AutoInstrumentPlugi
71
55
if ( shouldApplyUniversalLoadWrapper ) {
72
56
// eslint-disable-next-line no-console
73
57
debug && console . log ( '[Sentry] Applying universal load wrapper to' , id ) ;
74
- return await wrapUserCode ( universalLoadWrappingCode , userCode ) ;
58
+ const wrappedCode = wrapLoad ( userCode , 'wrapLoadWithSentry' ) ;
59
+ return { code : wrappedCode , map : null } ;
75
60
}
76
61
77
62
const shouldApplyServerLoadWrapper =
@@ -82,7 +67,8 @@ export async function makeAutoInstrumentationPlugin(options: AutoInstrumentPlugi
82
67
if ( shouldApplyServerLoadWrapper ) {
83
68
// eslint-disable-next-line no-console
84
69
debug && console . log ( '[Sentry] Applying server load wrapper to' , id ) ;
85
- return await wrapUserCode ( serverLoadWrappingCode , userCode ) ;
70
+ const wrappedCode = wrapLoad ( userCode , 'wrapServerLoadWithSentry' ) ;
71
+ return { code : wrappedCode , map : null } ;
86
72
}
87
73
88
74
return null ;
@@ -91,62 +77,118 @@ export async function makeAutoInstrumentationPlugin(options: AutoInstrumentPlugi
91
77
}
92
78
93
79
/**
94
- * Uses rollup to bundle the wrapper code and the user code together, so that we can use rollup's source map support.
95
- * This works analogously to our NextJS wrapping solution.
96
- * The one exception is that we don't pass in any source map. This is because generating the userCode's
97
- * source map generally works but it breaks SvelteKit's source map generation for some reason.
98
- * Not passing a map actually works and things are still mapped correctly in the end.
99
- * No Sentry code is visible in the final source map.
100
- * @see {@link file:///./../../../nextjs/src/config/loaders/wrappingLoader.ts } for more details.
80
+ * Applies the wrapLoadWithSentry wrapper to the user's load functions
101
81
*/
102
- async function wrapUserCode (
103
- wrapperCode : string ,
104
- userModuleCode : string ,
105
- ) : Promise < { code : string ; map ?: SourceMap | null } > {
106
- const rollupBuild = await rollup ( {
107
- input : SENTRY_WRAPPER_MODULE_NAME ,
108
-
109
- plugins : [
110
- {
111
- name : 'virtualize-sentry-wrapper-modules' ,
112
- resolveId : id => {
113
- if ( id === SENTRY_WRAPPER_MODULE_NAME || id === WRAPPING_TARGET_MODULE_NAME ) {
114
- return id ;
115
- } else {
116
- return null ;
117
- }
118
- } ,
119
- load ( id ) {
120
- if ( id === SENTRY_WRAPPER_MODULE_NAME ) {
121
- return wrapperCode ;
122
- } else if ( id === WRAPPING_TARGET_MODULE_NAME ) {
123
- return {
124
- code : userModuleCode ,
125
- // map: userModuleSourceMap,
126
- } ;
127
- } else {
128
- return null ;
129
- }
130
- } ,
131
- } ,
132
- ] ,
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ function wrapLoad (
84
+ userCode : Readonly < string > ,
85
+ wrapperFunction : 'wrapLoadWithSentry' | 'wrapServerLoadWithSentry' ,
86
+ ) : string {
87
+ const mod = parseModule ( userCode ) ;
88
+
89
+ const modAST = mod . exports . $ast as Program ;
90
+ const namedExports = modAST . body . filter (
91
+ ( node ) : node is ExportNamedDeclaration => node . type === 'ExportNamedDeclaration' ,
92
+ ) ;
133
93
134
- external : sourceId => sourceId !== SENTRY_WRAPPER_MODULE_NAME && sourceId !== WRAPPING_TARGET_MODULE_NAME ,
94
+ let wrappedSucessfully = false ;
95
+ namedExports . forEach ( modExport => {
96
+ const declaration = modExport . declaration ;
97
+ if ( ! declaration ) {
98
+ return ;
99
+ }
100
+ if ( declaration . type === 'FunctionDeclaration' ) {
101
+ if ( ! declaration . id || declaration . id . name !== 'load' ) {
102
+ return ;
103
+ }
104
+ const declarationCode = generateCode ( declaration ) . code ;
105
+ mod . exports . load = builders . raw ( `${ wrapperFunction } (${ declarationCode . replace ( 'load' , '_load' ) } )` ) ;
106
+ // because of an issue with magicast, we need to remove the original export
107
+ modAST . body = modAST . body . filter ( node => node !== modExport ) ;
108
+ wrappedSucessfully = true ;
109
+ } else if ( declaration . type === 'VariableDeclaration' ) {
110
+ declaration . declarations . forEach ( declarator => {
111
+ wrappedSucessfully = wrapDeclarator ( declarator , wrapperFunction ) ;
112
+ } ) ;
113
+ }
114
+ } ) ;
135
115
136
- context : 'this' ,
116
+ if ( wrappedSucessfully ) {
117
+ return generateFinalCode ( mod , wrapperFunction ) ;
118
+ }
137
119
138
- makeAbsoluteExternalsRelative : false ,
120
+ // If we're here, we know that we didn't find a directly exported `load` function yet.
121
+ // We need to look for it in the top level declarations in case it's declared and exported separately.
122
+ // First case: top level variable declaration
123
+ const topLevelVariableDeclarations = modAST . body . filter (
124
+ ( statement ) : statement is VariableDeclaration => statement . type === 'VariableDeclaration' ,
125
+ ) ;
139
126
140
- onwarn : ( _warning , _warn ) => {
141
- // Suppress all warnings - we don't want to bother people with this output
142
- // _warn(_warning); // uncomment to debug
143
- } ,
127
+ topLevelVariableDeclarations . forEach ( declaration => {
128
+ declaration . declarations . forEach ( declarator => {
129
+ wrappedSucessfully = wrapDeclarator ( declarator , wrapperFunction ) ;
130
+ } ) ;
144
131
} ) ;
145
132
146
- const finalBundle = await rollupBuild . generate ( {
147
- format : 'esm' ,
148
- sourcemap : 'hidden' ,
133
+ if ( wrappedSucessfully ) {
134
+ return generateFinalCode ( mod , wrapperFunction ) ;
135
+ }
136
+
137
+ // Second case: top level function declaration
138
+ // This is the most intrusive modification, as we need to replace a top level function declaration with a
139
+ // variable declaration and a function assignment. This changes the spacing formatting of the declarations
140
+ // but the line numbers should stay the same
141
+ const topLevelFunctionDeclarations = modAST . body . filter (
142
+ ( statement ) : statement is FunctionDeclaration => statement . type === 'FunctionDeclaration' ,
143
+ ) ;
144
+
145
+ topLevelFunctionDeclarations . forEach ( declaration => {
146
+ if ( ! declaration . id || declaration . id . name !== 'load' ) {
147
+ return ;
148
+ }
149
+
150
+ const stmtIndex = modAST . body . indexOf ( declaration ) ;
151
+ const declarationCode = generateCode ( declaration ) . code ;
152
+ const wrappedFunctionBody = builders . raw ( `${ wrapperFunction } (${ declarationCode . replace ( 'load' , '_load' ) } )` ) ;
153
+ const stringifiedFunctionBody = generateCode ( wrappedFunctionBody , { } ) . code ;
154
+
155
+ const tmpMod = parseModule ( `const load = ${ stringifiedFunctionBody } ` ) ;
156
+ const newDeclarationNode = ( tmpMod . $ast as Program ) . body [ 0 ] ;
157
+ const nodeWithAdjustedLoc = {
158
+ ...newDeclarationNode ,
159
+ loc : {
160
+ ...declaration . loc ,
161
+ } ,
162
+ } ;
163
+
164
+ // @ts -ignore - this works, magicast can handle this assignement although the types disagree
165
+ modAST . body [ stmtIndex ] = nodeWithAdjustedLoc ;
166
+ wrappedSucessfully = true ;
149
167
} ) ;
150
168
151
- return finalBundle . output [ 0 ] ;
169
+ if ( wrappedSucessfully ) {
170
+ return generateFinalCode ( mod , wrapperFunction ) ;
171
+ }
172
+
173
+ // nothing found, so we just return the original code
174
+ return userCode ;
175
+ }
176
+
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ function generateFinalCode ( mod : ProxifiedModule < any > , wrapperFunction : string ) : string {
179
+ const { code } = generateCode ( mod ) ;
180
+ return `import { ${ wrapperFunction } } from '@sentry/sveltekit'; ${ code } ` ;
181
+ }
182
+
183
+ function wrapDeclarator ( declarator : VariableDeclarator , wrapperFunction : string ) : boolean {
184
+ // @ts -ignore - id should always have a name in this case
185
+ if ( ! declarator . id || declarator . id . name !== 'load' ) {
186
+ return false ;
187
+ }
188
+ const declarationInitCode = declarator . init ;
189
+ // @ts -ignore - we can just place a string here, magicast will convert it to a node
190
+ const stringifiedCode = generateCode ( declarationInitCode ) . code ;
191
+ // @ts -ignore - we can just place a string here, magicast will convert it to a node
192
+ declarator . init = `${ wrapperFunction } (${ stringifiedCode } )` ;
193
+ return true ;
152
194
}
0 commit comments