11import { existsSync } from 'node:fs' ;
2+ import { basename } from 'node:path' ;
23import { pathToFileURL } from 'node:url' ;
34import { addTemplate , createResolver } from '@nuxt/kit' ;
45import type { Nuxt } from '@nuxt/schema' ;
@@ -20,6 +21,7 @@ import {
2021 SENTRY_WRAPPED_FUNCTIONS ,
2122 SERVER_CONFIG_FILENAME ,
2223 toImportSpecifier ,
24+ toResolvablePath ,
2325} from './utils' ;
2426
2527/** Path of the generated dev-mode config file, relative to the Nuxt build directory. */
@@ -31,7 +33,7 @@ export const DEV_SERVER_CONFIG_PATH = `dev/${SERVER_CONFIG_FILENAME}.mjs`;
3133 * In dev-mode, Nitro v3 has no server bundle to emit into, so Node loads the server config file as it is written.
3234 */
3335export function addDevServerConfigFile ( nuxt : Nuxt , serverConfigFile : string ) : void {
34- const configPath = createResolver ( nuxt . options . rootDir ) . resolve ( `/ ${ serverConfigFile } ` ) ;
36+ const configPath = createResolver ( nuxt . options . rootDir ) . resolve ( serverConfigFile ) ;
3537 const importSpecifier = toImportSpecifier (
3638 nuxt . options . rootDir ,
3739 path . join ( nuxt . options . buildDir , DEV_SERVER_CONFIG_PATH ) ,
@@ -59,6 +61,15 @@ export function addDevServerConfigFile(nuxt: Nuxt, serverConfigFile: string): vo
5961 ] . join ( '\n' ) ,
6062 } ) ;
6163}
64+ const CONFIG_EXTENSIONS = [ '.ts' , '.js' , '.mjs' , '.cjs' , '.mts' , '.cts' ] ;
65+
66+ function isServerConfigFile ( sourcePath : string , resolvedPath : string ) : boolean {
67+ if ( sourcePath === resolvedPath ) {
68+ return true ;
69+ }
70+ const name = basename ( sourcePath ) ;
71+ return name === SERVER_CONFIG_FILENAME || CONFIG_EXTENSIONS . some ( ext => name === `${ SERVER_CONFIG_FILENAME } ${ ext } ` ) ;
72+ }
6273
6374/**
6475 * Adds the `sentry.server.config.ts` file as `sentry.server.config.mjs` to the `.output` directory to be able to reference this file in the node --import option.
@@ -157,7 +168,7 @@ export function addDynamicImportEntryFileWrapper(
157168
158169 nitro . options . rollupConfig . plugins . push (
159170 wrapEntryWithDynamicImport ( {
160- resolvedSentryConfigPath : createResolver ( nitro . options . rootDir ) . resolve ( `/ ${ serverConfigFile } ` ) ,
171+ resolvedSentryConfigPath : createResolver ( nitro . options . rootDir ) . resolve ( serverConfigFile ) ,
161172 experimental_entrypointWrappedFunctions : moduleOptions . experimental_entrypointWrappedFunctions ,
162173 } ) ,
163174 ) ;
@@ -173,7 +184,7 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, isDebu
173184 name : 'rollup-plugin-inject-sentry-server-config' ,
174185
175186 buildStart ( ) {
176- const configPath = createResolver ( nitro . options . rootDir ) . resolve ( `/ ${ serverConfigFile } ` ) ;
187+ const configPath = createResolver ( nitro . options . rootDir ) . resolve ( serverConfigFile ) ;
177188
178189 if ( ! existsSync ( configPath ) ) {
179190 if ( isDebug ) {
@@ -193,7 +204,7 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, isDebu
193204 resolveId ( source ) {
194205 if ( source . startsWith ( filePrefix ) ) {
195206 const originalFilePath = source . replace ( filePrefix , '' ) ;
196- const configPath = createResolver ( nitro . options . rootDir ) . resolve ( `/ ${ originalFilePath } ` ) ;
207+ const configPath = createResolver ( nitro . options . rootDir ) . resolve ( originalFilePath ) ;
197208
198209 return { id : configPath } ;
199210 }
@@ -206,8 +217,10 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, isDebu
206217 * A Rollup plugin which wraps the server entry with a dynamic `import()`. This makes it possible to initialize Sentry first
207218 * by using a regular `import` and load the server after that.
208219 * This also works with serverless `handler` functions, as it re-exports the `handler`.
220+ *
221+ * Only exported for testing.
209222 */
210- function wrapEntryWithDynamicImport ( {
223+ export function wrapEntryWithDynamicImport ( {
211224 resolvedSentryConfigPath,
212225 experimental_entrypointWrappedFunctions,
213226 debug,
@@ -225,12 +238,24 @@ function wrapEntryWithDynamicImport({
225238 return {
226239 name : 'sentry-wrap-entry-with-dynamic-import' ,
227240 async resolveId ( source , importer , options ) {
228- if ( source . includes ( `/${ SERVER_CONFIG_FILENAME } ` ) ) {
229- return { id : source , moduleSideEffects : true } ;
241+ // `load()` emits `file://` specifiers because Node's ESM loader rejects bare Windows paths,
242+ // but Rollup's resolver only understands filesystem paths.
243+ const resolvable = toResolvablePath ( source ) ;
244+ if ( ! resolvable ) {
245+ return null ;
230246 }
247+ const { path : normalizedSource , wasFileUrl } = resolvable ;
231248
232- if ( options . isEntry && source . includes ( '.mjs' ) && ! source . includes ( `.mjs${ SENTRY_WRAPPED_ENTRY } ` ) ) {
233- const resolution = await this . resolve ( source , importer , options ) ;
249+ if ( isServerConfigFile ( normalizedSource , resolvedSentryConfigPath ) ) {
250+ return { id : normalizedSource , moduleSideEffects : true } ;
251+ }
252+
253+ if (
254+ options . isEntry &&
255+ normalizedSource . includes ( '.mjs' ) &&
256+ ! normalizedSource . includes ( `.mjs${ SENTRY_WRAPPED_ENTRY } ` )
257+ ) {
258+ const resolution = await this . resolve ( normalizedSource , importer , options ) ;
234259
235260 // If it cannot be resolved or is external, just return it so that Rollup can display an error
236261 if ( ! resolution || resolution ?. external ) return resolution ;
@@ -254,24 +279,36 @@ function wrapEntryWithDynamicImport({
254279 )
255280 . concat ( QUERY_END_INDICATOR ) } `;
256281 }
282+
283+ // Pass isEntry:false to avoid re-entering the isEntry branch and double-wrapping
284+ // (normalizedSource strips the SENTRY_WRAPPED_ENTRY query suffix).
285+ if ( wasFileUrl ) {
286+ const resolved = await this . resolve ( normalizedSource , importer , { ...options , isEntry : false } ) ;
287+ if ( resolved ) return resolved ;
288+ return { id : normalizedSource } ;
289+ }
290+
257291 return null ;
258292 } ,
259293 load ( id : string ) {
260294 if ( id . includes ( `.mjs${ SENTRY_WRAPPED_ENTRY } ` ) ) {
261295 const entryId = removeSentryQueryFromPath ( id ) . slice ( resolutionIdPrefix . length ) ;
296+ const entryIdUrl = pathToFileURL ( entryId ) . href ;
297+ const configUrl = pathToFileURL ( resolvedSentryConfigPath ) . href ;
262298
299+ // Use entryIdUrl so Node's runtime ESM loader receives file:// on Windows; Rollup normalizes it in resolveId.
263300 // Mostly useful for serverless `handler` functions
264301 const reExportedFunctions =
265302 id . includes ( SENTRY_WRAPPED_FUNCTIONS ) || id . includes ( SENTRY_REEXPORTED_FUNCTIONS )
266- ? constructFunctionReExport ( id , entryId )
303+ ? constructFunctionReExport ( id , entryIdUrl )
267304 : '' ;
268305
269306 return (
270307 // Regular `import` of the Sentry config
271- `import ${ JSON . stringify ( resolvedSentryConfigPath ) } ;\n` +
308+ `import ${ JSON . stringify ( configUrl ) } ;\n` +
272309 // Dynamic `import()` for the previous, actual entry point.
273310 // `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling)
274- `import(${ JSON . stringify ( entryId ) } );\n` +
311+ `import(${ JSON . stringify ( entryIdUrl ) } );\n` +
275312 `${ reExportedFunctions } \n`
276313 ) ;
277314 }
0 commit comments