From d237ce36a200bcd0b16a95be8954a9736f86c9f4 Mon Sep 17 00:00:00 2001 From: halillusion Date: Wed, 26 Aug 2026 15:27:44 +0300 Subject: [PATCH 01/10] fix(nuxt): Windows file:// for import and isAbsolute for C:" --- packages/nuxt/rollup.module.config.mjs | 4 ++-- packages/nuxt/src/vite/addServerConfig.ts | 9 ++++++--- .../src/config/wrapServerEntryWithDynamicImport.ts | 9 ++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/rollup.module.config.mjs b/packages/nuxt/rollup.module.config.mjs index f53676433cef..991ea9c58fae 100644 --- a/packages/nuxt/rollup.module.config.mjs +++ b/packages/nuxt/rollup.module.config.mjs @@ -1,5 +1,5 @@ import { readdirSync } from 'node:fs'; -import { join } from 'node:path'; +import { isAbsolute, join } from 'node:path'; import esbuild from 'rollup-plugin-esbuild'; // The Nuxt module ships two kinds of output that live side by side in `build/module`: @@ -11,7 +11,7 @@ import esbuild from 'rollup-plugin-esbuild'; // Anything that isn't a relative path is provided by the consuming app or Node at runtime // (this covers `@sentry/*`, `nuxt/app`, `#imports`, node builtins), so it stays external. -const isExternal = id => !id.startsWith('.') && !id.startsWith('/') && !id.startsWith('\0'); +const isExternal = id => !id.startsWith('.') && !isAbsolute(id) && !id.startsWith('\0'); const transpile = esbuild({ target: 'es2020', diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 8d4d5db0259a..dc99f4ae7db2 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,4 +1,5 @@ import { existsSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; import { createResolver } from '@nuxt/kit'; import { debug } from '@sentry/core'; import * as fs from 'fs'; @@ -217,19 +218,21 @@ function wrapEntryWithDynamicImport({ load(id: string) { if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { const entryId = removeSentryQueryFromPath(id).slice(resolutionIdPrefix.length); + const entryIdUrl = pathToFileURL(entryId).href; + const configUrl = pathToFileURL(resolvedSentryConfigPath).href; // Mostly useful for serverless `handler` functions const reExportedFunctions = id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS) - ? constructFunctionReExport(id, entryId) + ? constructFunctionReExport(id, entryIdUrl) : ''; return ( // Regular `import` of the Sentry config - `import ${JSON.stringify(resolvedSentryConfigPath)};\n` + + `import ${JSON.stringify(configUrl)};\n` + // Dynamic `import()` for the previous, actual entry point. // `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling) - `import(${JSON.stringify(entryId)});\n` + + `import(${JSON.stringify(entryIdUrl)});\n` + `${reExportedFunctions}\n` ); } diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index ef22da065d14..df3b2f188e9f 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -1,3 +1,4 @@ +import { pathToFileURL } from 'node:url'; import { consoleSandbox } from '@sentry/core'; import type { InputPluginOption } from 'rollup'; @@ -81,19 +82,21 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp load(id: string) { if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { const entryId = removeSentryQueryFromPath(id).slice(resolutionIdPrefix.length); + const entryIdUrl = pathToFileURL(entryId).href; + const configUrl = pathToFileURL(resolvedServerConfigPath).href; // Mostly useful for serverless `handler` functions const reExportedFunctions = id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS) - ? constructFunctionReExport(id, entryId) + ? constructFunctionReExport(id, entryIdUrl) : ''; return ( // Regular `import` of the Sentry config - `import ${JSON.stringify(resolvedServerConfigPath)};\n` + + `import ${JSON.stringify(configUrl)};\n` + // Dynamic `import()` for the previous, actual entry point. // `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling) - `import(${JSON.stringify(entryId)});\n` + + `import(${JSON.stringify(entryIdUrl)});\n` + `${reExportedFunctions}\n` ); } From 837e9b546a61592b79664660fff1c12c5b8f17c2 Mon Sep 17 00:00:00 2001 From: halillusion Date: Wed, 26 Aug 2026 16:22:24 +0300 Subject: [PATCH 02/10] fix: introduce Nitro rollup plugins for Sentry server configuration injection and entry file wrapping --- packages/nuxt/src/vite/addServerConfig.ts | 27 ++++++++++++++++--- .../wrapServerEntryWithDynamicImport.ts | 24 ++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index dc99f4ae7db2..99dc7f923d81 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,5 +1,5 @@ import { existsSync } from 'node:fs'; -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { createResolver } from '@nuxt/kit'; import { debug } from '@sentry/core'; import * as fs from 'fs'; @@ -184,8 +184,20 @@ function wrapEntryWithDynamicImport({ return { name: 'sentry-wrap-entry-with-dynamic-import', async resolveId(source, importer, options) { - if (source.includes(`/${SERVER_CONFIG_FILENAME}`)) { - return { id: source, moduleSideEffects: true }; + // Rollup cannot load `file://` URLs directly; normalize to a filesystem path for resolution. + // The emitted import specifier stays as `file://` for Node's ESM loader (required on Windows), + // but Rollup needs a plain path to read the file during bundling. + let normalizedSource = source; + if (source.startsWith('file://')) { + try { + normalizedSource = fileURLToPath(source); + } catch { + return null; + } + } + + if (normalizedSource.includes(`/${SERVER_CONFIG_FILENAME}`)) { + return { id: normalizedSource, moduleSideEffects: true }; } if (options.isEntry && source.includes('.mjs') && !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { @@ -213,6 +225,15 @@ function wrapEntryWithDynamicImport({ ) .concat(QUERY_END_INDICATOR)}`; } + + // Handle file:// specifiers emitted by load() for the wrapped entry / re-exports. + // At runtime Node requires file:// on Windows, but Rollup needs a filesystem path. + if (source.startsWith('file://')) { + const resolved = await this.resolve(normalizedSource, importer, options); + if (resolved) return resolved; + return { id: normalizedSource }; + } + return null; }, load(id: string) { diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index df3b2f188e9f..fbc1a2b3f97f 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -1,4 +1,4 @@ -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { consoleSandbox } from '@sentry/core'; import type { InputPluginOption } from 'rollup'; @@ -47,8 +47,18 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp return { name: 'sentry-wrap-server-entry-with-dynamic-import', async resolveId(source, importer, options) { - if (source.includes(`/${serverConfigFileName}`)) { - return { id: source, moduleSideEffects: true }; + // Rollup cannot load `file://` URLs directly; normalize to a filesystem path for resolution. + let normalizedSource = source; + if (source.startsWith('file://')) { + try { + normalizedSource = fileURLToPath(source); + } catch { + return null; + } + } + + if (normalizedSource.includes(`/${serverConfigFileName}`)) { + return { id: normalizedSource, moduleSideEffects: true }; } if ( @@ -77,6 +87,14 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp ) .concat(QUERY_END_INDICATOR)}`; } + + // Handle file:// specifiers emitted by load() for the wrapped entry / re-exports. + if (source.startsWith('file://')) { + const resolved = await this.resolve(normalizedSource, importer, options); + if (resolved) return resolved; + return { id: normalizedSource }; + } + return null; }, load(id: string) { From f1dbffb57c63dec0c201efb5327ab10d411bacbe Mon Sep 17 00:00:00 2001 From: halillusion Date: Wed, 26 Aug 2026 16:32:15 +0300 Subject: [PATCH 03/10] fix(nuxt): normalize Windows paths for server config detection On Windows fileURLToPath returns backslash paths (C:\...), so includes('/sentry.server.config') never matched. The config was not marked moduleSideEffects:true and could be tree-shaken, disabling Sentry server init. Normalized paths to forward slashes before the check and keep file:// emission for Node ESM while mapping file:// back to filesystem paths for Rollup. Fixes handling for both packages/nuxt and packages/solidstart wrapEntry plugins. --- packages/nuxt/src/vite/addServerConfig.ts | 4 +++- .../solidstart/src/config/wrapServerEntryWithDynamicImport.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 99dc7f923d81..1255e2cd8776 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -196,7 +196,9 @@ function wrapEntryWithDynamicImport({ } } - if (normalizedSource.includes(`/${SERVER_CONFIG_FILENAME}`)) { + // Normalize to forward slashes for cross-platform check (Windows uses backslashes) + const normalizedForCheck = normalizedSource.replace(/\\/g, '/'); + if (normalizedForCheck.includes(`/${SERVER_CONFIG_FILENAME}`)) { return { id: normalizedSource, moduleSideEffects: true }; } diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index fbc1a2b3f97f..94043825968f 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -57,7 +57,8 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp } } - if (normalizedSource.includes(`/${serverConfigFileName}`)) { + const normalizedForCheck = normalizedSource.replace(/\\/g, '/'); + if (normalizedForCheck.includes(`/${serverConfigFileName}`)) { return { id: normalizedSource, moduleSideEffects: true }; } From e50766e69db24de62f488731b6dc580bc6c93733 Mon Sep 17 00:00:00 2001 From: halillusion Date: Wed, 26 Aug 2026 16:50:00 +0300 Subject: [PATCH 04/10] fix: Windows dev overlay by emitting file:// URLs and fixing s1gr1d review (isAbsolute/basename). --- packages/nuxt/src/vite/addServerConfig.ts | 5 ++--- .../src/config/wrapServerEntryWithDynamicImport.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 1255e2cd8776..b8f032de33a1 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,4 +1,5 @@ import { existsSync } from 'node:fs'; +import { basename } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { createResolver } from '@nuxt/kit'; import { debug } from '@sentry/core'; @@ -196,9 +197,7 @@ function wrapEntryWithDynamicImport({ } } - // Normalize to forward slashes for cross-platform check (Windows uses backslashes) - const normalizedForCheck = normalizedSource.replace(/\\/g, '/'); - if (normalizedForCheck.includes(`/${SERVER_CONFIG_FILENAME}`)) { + if (basename(normalizedSource).startsWith(SERVER_CONFIG_FILENAME)) { return { id: normalizedSource, moduleSideEffects: true }; } diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index 94043825968f..60d001b76159 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -1,3 +1,4 @@ +import { basename } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { consoleSandbox } from '@sentry/core'; import type { InputPluginOption } from 'rollup'; @@ -57,8 +58,7 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp } } - const normalizedForCheck = normalizedSource.replace(/\\/g, '/'); - if (normalizedForCheck.includes(`/${serverConfigFileName}`)) { + if (basename(normalizedSource).startsWith(serverConfigFileName)) { return { id: normalizedSource, moduleSideEffects: true }; } From 22cfd7fc513276f489359dc406e9e0c169f1bd3d Mon Sep 17 00:00:00 2001 From: halillusion Date: Wed, 26 Aug 2026 17:01:27 +0300 Subject: [PATCH 05/10] fix(nuxt): prevent double-wrapping of file:// specifiers in server entry plugin --- packages/nuxt/src/vite/addServerConfig.ts | 8 +++++--- .../src/config/wrapServerEntryWithDynamicImport.ts | 11 ++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index b8f032de33a1..4ec339b359bb 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -201,8 +201,8 @@ function wrapEntryWithDynamicImport({ return { id: normalizedSource, moduleSideEffects: true }; } - if (options.isEntry && source.includes('.mjs') && !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { - const resolution = await this.resolve(source, importer, options); + if (options.isEntry && normalizedSource.includes('.mjs') && !normalizedSource.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { + const resolution = await this.resolve(normalizedSource, importer, options); // If it cannot be resolved or is external, just return it so that Rollup can display an error if (!resolution || resolution?.external) return resolution; @@ -229,8 +229,10 @@ function wrapEntryWithDynamicImport({ // Handle file:// specifiers emitted by load() for the wrapped entry / re-exports. // At runtime Node requires file:// on Windows, but Rollup needs a filesystem path. + // Pass isEntry:false to avoid re-entering the isEntry branch and double-wrapping + // (normalizedSource strips the SENTRY_WRAPPED_ENTRY query suffix). if (source.startsWith('file://')) { - const resolved = await this.resolve(normalizedSource, importer, options); + const resolved = await this.resolve(normalizedSource, importer, { ...options, isEntry: false }); if (resolved) return resolved; return { id: normalizedSource }; } diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index 60d001b76159..d7cabe53aeeb 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -64,11 +64,11 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp if ( options.isEntry && - source.includes(serverEntrypointFileName) && - source.includes('.mjs') && - !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) + normalizedSource.includes(serverEntrypointFileName) && + normalizedSource.includes('.mjs') && + !normalizedSource.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) ) { - const resolution = await this.resolve(source, importer, options); + const resolution = await this.resolve(normalizedSource, importer, options); // If it cannot be resolved or is external, just return it so that Rollup can display an error if (!resolution || resolution?.external) return resolution; @@ -90,8 +90,9 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp } // Handle file:// specifiers emitted by load() for the wrapped entry / re-exports. + // Pass isEntry:false to avoid double-wrapping (normalizedSource lacks query suffix). if (source.startsWith('file://')) { - const resolved = await this.resolve(normalizedSource, importer, options); + const resolved = await this.resolve(normalizedSource, importer, { ...options, isEntry: false }); if (resolved) return resolved; return { id: normalizedSource }; } From 5cc77eef09b851578362e54c78d8f1b471ce8584 Mon Sep 17 00:00:00 2001 From: halillusion Date: Mon, 31 Aug 2026 20:03:15 +0300 Subject: [PATCH 06/10] fix(nuxt): emit file:// URLs from server entry wrapper for Windows ESM On Windows, Node's ESM loader rejects bare absolute paths (C:\...) with ERR_UNSUPPORTED_ESM_URL_SCHEME (protocol 'c:'). Emit file:// URLs from wrapEntryWithDynamicImport load hook for the Sentry server config, wrapped entry point, and serverless re-exports. In resolveId, normalize incoming file:// URLs back to filesystem paths via toResolvablePath() and forward them with isEntry: false to prevent double-wrapping. Applied symmetrically to @sentry/nuxt and @sentry/solidstart, with unit tests covering URL normalization and entry resolution. --- packages/nuxt/src/vite/addServerConfig.ts | 34 +++--- packages/nuxt/src/vite/utils.ts | 19 ++++ .../nuxt/test/vite/addServerConfig.test.ts | 99 +++++++++++++++++ .../wrapServerEntryWithDynamicImport.ts | 36 +++++-- .../wrapServerEntryWithDynamicImport.test.ts | 101 ++++++++++++++++++ 5 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 packages/nuxt/test/vite/addServerConfig.test.ts create mode 100644 packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 4ec339b359bb..c1489a79b82b 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,6 +1,6 @@ import { existsSync } from 'node:fs'; import { basename } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { pathToFileURL } from 'node:url'; import { createResolver } from '@nuxt/kit'; import { debug } from '@sentry/core'; import * as fs from 'fs'; @@ -16,6 +16,7 @@ import { SENTRY_REEXPORTED_FUNCTIONS, SENTRY_WRAPPED_ENTRY, SENTRY_WRAPPED_FUNCTIONS, + toResolvablePath, } from './utils'; const SERVER_CONFIG_FILENAME = 'sentry.server.config'; @@ -166,8 +167,10 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, isDebu * A Rollup plugin which wraps the server entry with a dynamic `import()`. This makes it possible to initialize Sentry first * by using a regular `import` and load the server after that. * This also works with serverless `handler` functions, as it re-exports the `handler`. + * + * Only exported for testing. */ -function wrapEntryWithDynamicImport({ +export function wrapEntryWithDynamicImport({ resolvedSentryConfigPath, experimental_entrypointWrappedFunctions, debug, @@ -185,23 +188,23 @@ function wrapEntryWithDynamicImport({ return { name: 'sentry-wrap-entry-with-dynamic-import', async resolveId(source, importer, options) { - // Rollup cannot load `file://` URLs directly; normalize to a filesystem path for resolution. - // The emitted import specifier stays as `file://` for Node's ESM loader (required on Windows), - // but Rollup needs a plain path to read the file during bundling. - let normalizedSource = source; - if (source.startsWith('file://')) { - try { - normalizedSource = fileURLToPath(source); - } catch { - return null; - } + // `load()` emits `file://` specifiers because Node's ESM loader rejects bare Windows paths, + // but Rollup's resolver only understands filesystem paths. + const resolvable = toResolvablePath(source); + if (!resolvable) { + return null; } + const { path: normalizedSource, wasFileUrl } = resolvable; if (basename(normalizedSource).startsWith(SERVER_CONFIG_FILENAME)) { return { id: normalizedSource, moduleSideEffects: true }; } - if (options.isEntry && normalizedSource.includes('.mjs') && !normalizedSource.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { + if ( + options.isEntry && + normalizedSource.includes('.mjs') && + !normalizedSource.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) + ) { const resolution = await this.resolve(normalizedSource, importer, options); // If it cannot be resolved or is external, just return it so that Rollup can display an error @@ -227,11 +230,9 @@ function wrapEntryWithDynamicImport({ .concat(QUERY_END_INDICATOR)}`; } - // Handle file:// specifiers emitted by load() for the wrapped entry / re-exports. - // At runtime Node requires file:// on Windows, but Rollup needs a filesystem path. // Pass isEntry:false to avoid re-entering the isEntry branch and double-wrapping // (normalizedSource strips the SENTRY_WRAPPED_ENTRY query suffix). - if (source.startsWith('file://')) { + if (wasFileUrl) { const resolved = await this.resolve(normalizedSource, importer, { ...options, isEntry: false }); if (resolved) return resolved; return { id: normalizedSource }; @@ -245,6 +246,7 @@ function wrapEntryWithDynamicImport({ const entryIdUrl = pathToFileURL(entryId).href; const configUrl = pathToFileURL(resolvedSentryConfigPath).href; + // Use entryIdUrl so Node's runtime ESM loader receives file:// on Windows; Rollup normalizes it in resolveId. // Mostly useful for serverless `handler` functions const reExportedFunctions = id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS) diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 58288302ae0d..465ea5bb031e 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -2,6 +2,7 @@ import type { Nuxt } from '@nuxt/schema'; import { consoleSandbox } from '@sentry/core'; import * as fs from 'fs'; import * as path from 'path'; +import { fileURLToPath } from 'node:url'; import type { SentryNuxtModuleOptions } from '../common/types'; import { resolvePath } from '@nuxt/kit'; @@ -204,6 +205,24 @@ export function constructFunctionReExport(pathWithQuery: string, entryId: string ); } +/** + * `load()` emits `file://` specifiers because Node's ESM loader rejects bare Windows + * paths (`ERR_UNSUPPORTED_ESM_URL_SCHEME`), but Rollup's resolver only understands + * filesystem paths. Returns `undefined` for a malformed `file://` URL. + * + * Only exported for testing. + */ +export function toResolvablePath(source: string): { path: string; wasFileUrl: boolean } | undefined { + if (!source.startsWith('file://')) { + return { path: source, wasFileUrl: false }; + } + try { + return { path: fileURLToPath(source), wasFileUrl: true }; + } catch { + return undefined; + } +} + /** * Sets up alias to work around OpenTelemetry's incomplete ESM imports. * https://github.com/getsentry/sentry-javascript/issues/15204 diff --git a/packages/nuxt/test/vite/addServerConfig.test.ts b/packages/nuxt/test/vite/addServerConfig.test.ts new file mode 100644 index 000000000000..c5882517ad85 --- /dev/null +++ b/packages/nuxt/test/vite/addServerConfig.test.ts @@ -0,0 +1,99 @@ +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { describe, expect, it, vi } from 'vitest'; +import { wrapEntryWithDynamicImport } from '../../src/vite/addServerConfig'; +import { + QUERY_END_INDICATOR, + SENTRY_REEXPORTED_FUNCTIONS, + SENTRY_WRAPPED_ENTRY, + toResolvablePath, +} from '../../src/vite/utils'; + +const configPath = '/project/sentry.server.config.ts'; +const entryPath = '/project/.nuxt/entry.mjs'; + +describe('toResolvablePath', () => { + it('passes through non-file specifiers', () => { + expect(toResolvablePath('./module')).toEqual({ path: './module', wasFileUrl: false }); + expect(toResolvablePath(configPath)).toEqual({ path: configPath, wasFileUrl: false }); + }); + + it('converts file:// URLs to filesystem paths', () => { + const url = pathToFileURL(entryPath).href; + expect(toResolvablePath(url)).toEqual({ path: fileURLToPath(url), wasFileUrl: true }); + }); + + it('returns undefined for malformed file:// URLs', () => { + expect(toResolvablePath('file://')).toBeUndefined(); + expect(toResolvablePath('file://%E0%A4%A')).toBeUndefined(); + }); +}); + +describe('wrapEntryWithDynamicImport', () => { + const plugin = wrapEntryWithDynamicImport({ + resolvedSentryConfigPath: configPath, + experimental_entrypointWrappedFunctions: ['handler'], + }) as unknown as { + resolveId: (source: string, importer: string | undefined, options: { isEntry?: boolean }) => Promise; + load: (id: string) => string | null; + }; + const { resolveId, load } = plugin; + + it('emits file:// URLs from load() so Node resolves them on Windows', () => { + const code = load.call({}, `\0raw${entryPath}${SENTRY_WRAPPED_ENTRY}${QUERY_END_INDICATOR}`); + + expect(code).toContain(`import ${JSON.stringify(pathToFileURL(configPath).href)}`); + expect(code).toContain(`import(${JSON.stringify(pathToFileURL(entryPath).href)})`); + expect(code).not.toContain(`import ${JSON.stringify(configPath)}`); + }); + + it('uses file:// URLs for re-exported functions', () => { + const id = `\0raw${entryPath}${SENTRY_WRAPPED_ENTRY}${SENTRY_REEXPORTED_FUNCTIONS}handler${QUERY_END_INDICATOR}`; + const code = load.call({}, id); + + expect(code).toContain(`export { handler } from ${JSON.stringify(pathToFileURL(entryPath).href)}`); + }); + + it('resolves a file:// config URL to a filesystem path with moduleSideEffects', async () => { + const source = pathToFileURL(configPath).href; + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, source, undefined, { isEntry: false }); + + expect(result).toEqual({ id: fileURLToPath(source), moduleSideEffects: true }); + }); + + it('resolves a plain config path without converting it', async () => { + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, configPath, undefined, { isEntry: false }); + + expect(result).toEqual({ id: configPath, moduleSideEffects: true }); + }); + + it('resolves file:// entry specifiers without re-entering the entry branch', async () => { + const source = pathToFileURL(entryPath).href; + const fakeResolve = vi.fn(async () => ({ id: 'resolved-id', external: false })); + const result = await resolveId.call({ resolve: fakeResolve, load: vi.fn() }, source, undefined, { isEntry: false }); + + expect(fakeResolve).toHaveBeenCalledWith( + fileURLToPath(source), + undefined, + expect.objectContaining({ isEntry: false }), + ); + expect(result).toEqual({ id: 'resolved-id', external: false }); + }); + + it('returns null for malformed file:// URLs', async () => { + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, 'file://', undefined, { isEntry: false }); + + expect(result).toBeNull(); + }); + + it('wraps the entry with the dynamic-import query suffix', async () => { + const fakeResolve = vi.fn(async () => ({ id: entryPath, external: false })); + const fakeLoad = vi.fn(async () => ({ exportedBindings: { '.': ['handler'] }, moduleSideEffects: false })); + const result = await resolveId.call({ resolve: fakeResolve, load: fakeLoad }, entryPath, undefined, { + isEntry: true, + }); + + expect(result).toContain(SENTRY_WRAPPED_ENTRY); + expect(result).toContain('?sentry-query-wrapped-functions=handler'); + expect(result?.startsWith('\0raw')).toBe(true); + }); +}); diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index d7cabe53aeeb..9dba092b6766 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -10,6 +10,24 @@ export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions='; export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions='; export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END'; +/** + * `load()` emits `file://` specifiers because Node's ESM loader rejects bare Windows + * paths (`ERR_UNSUPPORTED_ESM_URL_SCHEME`), but Rollup's resolver only understands + * filesystem paths. Returns `undefined` for a malformed `file://` URL. + * + * **Only exported for testing** + */ +export function toResolvablePath(source: string): { path: string; wasFileUrl: boolean } | undefined { + if (!source.startsWith('file://')) { + return { path: source, wasFileUrl: false }; + } + try { + return { path: fileURLToPath(source), wasFileUrl: true }; + } catch { + return undefined; + } +} + export type WrapServerEntryPluginOptions = { serverEntrypointFileName: string; serverConfigFileName: string; @@ -48,15 +66,13 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp return { name: 'sentry-wrap-server-entry-with-dynamic-import', async resolveId(source, importer, options) { - // Rollup cannot load `file://` URLs directly; normalize to a filesystem path for resolution. - let normalizedSource = source; - if (source.startsWith('file://')) { - try { - normalizedSource = fileURLToPath(source); - } catch { - return null; - } + // `load()` emits `file://` specifiers because Node's ESM loader rejects bare Windows paths, + // but Rollup's resolver only understands filesystem paths. + const resolvable = toResolvablePath(source); + if (!resolvable) { + return null; } + const { path: normalizedSource, wasFileUrl } = resolvable; if (basename(normalizedSource).startsWith(serverConfigFileName)) { return { id: normalizedSource, moduleSideEffects: true }; @@ -89,9 +105,8 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp .concat(QUERY_END_INDICATOR)}`; } - // Handle file:// specifiers emitted by load() for the wrapped entry / re-exports. // Pass isEntry:false to avoid double-wrapping (normalizedSource lacks query suffix). - if (source.startsWith('file://')) { + if (wasFileUrl) { const resolved = await this.resolve(normalizedSource, importer, { ...options, isEntry: false }); if (resolved) return resolved; return { id: normalizedSource }; @@ -105,6 +120,7 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp const entryIdUrl = pathToFileURL(entryId).href; const configUrl = pathToFileURL(resolvedServerConfigPath).href; + // Use entryIdUrl so Node's runtime ESM loader receives file:// on Windows; Rollup normalizes it in resolveId. // Mostly useful for serverless `handler` functions const reExportedFunctions = id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS) diff --git a/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts b/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts new file mode 100644 index 000000000000..895d0f85b7b0 --- /dev/null +++ b/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts @@ -0,0 +1,101 @@ +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { describe, expect, it, vi } from 'vitest'; +import { + QUERY_END_INDICATOR, + SENTRY_REEXPORTED_FUNCTIONS, + SENTRY_WRAPPED_ENTRY, + toResolvablePath, + wrapServerEntryWithDynamicImport, +} from '../../src/config/wrapServerEntryWithDynamicImport'; + +const configPath = '/project/instrument.server.ts'; +const entryPath = '/project/.build/server/entry.mjs'; + +describe('toResolvablePath', () => { + it('passes through non-file specifiers', () => { + expect(toResolvablePath('./module')).toEqual({ path: './module', wasFileUrl: false }); + expect(toResolvablePath(configPath)).toEqual({ path: configPath, wasFileUrl: false }); + }); + + it('converts file:// URLs to filesystem paths', () => { + const url = pathToFileURL(entryPath).href; + expect(toResolvablePath(url)).toEqual({ path: fileURLToPath(url), wasFileUrl: true }); + }); + + it('returns undefined for malformed file:// URLs', () => { + expect(toResolvablePath('file://')).toBeUndefined(); + expect(toResolvablePath('file://%E0%A4%A')).toBeUndefined(); + }); +}); + +describe('wrapServerEntryWithDynamicImport', () => { + const plugin = wrapServerEntryWithDynamicImport({ + serverConfigFileName: 'instrument.server', + serverEntrypointFileName: 'entry', + resolvedServerConfigPath: configPath, + entrypointWrappedFunctions: ['handler'], + }) as unknown as { + resolveId: (source: string, importer: string | undefined, options: { isEntry?: boolean }) => Promise; + load: (id: string) => string | null; + }; + const { resolveId, load } = plugin; + + it('emits file:// URLs from load() so Node resolves them on Windows', () => { + const code = load.call({}, `\0raw${entryPath}${SENTRY_WRAPPED_ENTRY}${QUERY_END_INDICATOR}`); + + expect(code).toContain(`import ${JSON.stringify(pathToFileURL(configPath).href)}`); + expect(code).toContain(`import(${JSON.stringify(pathToFileURL(entryPath).href)})`); + expect(code).not.toContain(`import ${JSON.stringify(configPath)}`); + }); + + it('uses file:// URLs for re-exported functions', () => { + const id = `\0raw${entryPath}${SENTRY_WRAPPED_ENTRY}${SENTRY_REEXPORTED_FUNCTIONS}handler${QUERY_END_INDICATOR}`; + const code = load.call({}, id); + + expect(code).toContain(`export { handler } from ${JSON.stringify(pathToFileURL(entryPath).href)}`); + }); + + it('resolves a file:// config URL to a filesystem path with moduleSideEffects', async () => { + const source = pathToFileURL(configPath).href; + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, source, undefined, { isEntry: false }); + + expect(result).toEqual({ id: fileURLToPath(source), moduleSideEffects: true }); + }); + + it('resolves a plain config path without converting it', async () => { + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, configPath, undefined, { isEntry: false }); + + expect(result).toEqual({ id: configPath, moduleSideEffects: true }); + }); + + it('resolves file:// entry specifiers without re-entering the entry branch', async () => { + const source = pathToFileURL(entryPath).href; + const fakeResolve = vi.fn(async () => ({ id: 'resolved-id', external: false })); + const result = await resolveId.call({ resolve: fakeResolve, load: vi.fn() }, source, undefined, { isEntry: false }); + + expect(fakeResolve).toHaveBeenCalledWith( + fileURLToPath(source), + undefined, + expect.objectContaining({ isEntry: false }), + ); + expect(result).toEqual({ id: 'resolved-id', external: false }); + }); + + it('returns null for malformed file:// URLs', async () => { + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, 'file://', undefined, { isEntry: false }); + + expect(result).toBeNull(); + }); + + it('wraps the entry with the dynamic-import query suffix', async () => { + const fakeResolve = vi.fn(async () => ({ id: entryPath, external: false })); + const fakeLoad = vi.fn(async () => ({ exportedBindings: { '.': ['handler'] }, moduleSideEffects: false })); + const result = await resolveId.call({ resolve: fakeResolve, load: fakeLoad }, entryPath, undefined, { + isEntry: true, + }); + + expect(result).toContain(SENTRY_WRAPPED_ENTRY); + expect(result).toContain('?sentry-query-wrapped-functions=handler'); + expect(result?.startsWith('\0raw')).toBe(true); + }); +}); From 886b767d6d6c01dbf9cacf92c1e6cdcdca86bd3b Mon Sep 17 00:00:00 2001 From: halillusion Date: Mon, 31 Aug 2026 20:10:28 +0300 Subject: [PATCH 07/10] fix(nuxt): narrow server config detection to exact match and valid extensions --- packages/nuxt/src/vite/addServerConfig.ts | 11 ++++++++++- packages/nuxt/test/vite/addServerConfig.test.ts | 7 +++++++ .../src/config/wrapServerEntryWithDynamicImport.ts | 12 +++++++++++- .../config/wrapServerEntryWithDynamicImport.test.ts | 7 +++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index c1489a79b82b..05db5d8a12df 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -20,6 +20,15 @@ import { } from './utils'; const SERVER_CONFIG_FILENAME = 'sentry.server.config'; +const CONFIG_EXTENSIONS = ['.ts', '.js', '.mjs', '.cjs', '.mts', '.cts']; + +function isServerConfigFile(sourcePath: string, resolvedPath: string): boolean { + if (sourcePath === resolvedPath) { + return true; + } + const name = basename(sourcePath); + return name === SERVER_CONFIG_FILENAME || CONFIG_EXTENSIONS.some(ext => name === `${SERVER_CONFIG_FILENAME}${ext}`); +} /** * 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. @@ -196,7 +205,7 @@ export function wrapEntryWithDynamicImport({ } const { path: normalizedSource, wasFileUrl } = resolvable; - if (basename(normalizedSource).startsWith(SERVER_CONFIG_FILENAME)) { + if (isServerConfigFile(normalizedSource, resolvedSentryConfigPath)) { return { id: normalizedSource, moduleSideEffects: true }; } diff --git a/packages/nuxt/test/vite/addServerConfig.test.ts b/packages/nuxt/test/vite/addServerConfig.test.ts index c5882517ad85..16370f35b48a 100644 --- a/packages/nuxt/test/vite/addServerConfig.test.ts +++ b/packages/nuxt/test/vite/addServerConfig.test.ts @@ -66,6 +66,13 @@ describe('wrapEntryWithDynamicImport', () => { expect(result).toEqual({ id: configPath, moduleSideEffects: true }); }); + it('does not mark backup or test config files as the Sentry server config', async () => { + const backupPath = '/project/sentry.server.config.backup.ts'; + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, backupPath, undefined, { isEntry: false }); + + expect(result).toBeNull(); + }); + it('resolves file:// entry specifiers without re-entering the entry branch', async () => { const source = pathToFileURL(entryPath).href; const fakeResolve = vi.fn(async () => ({ id: 'resolved-id', external: false })); diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index 9dba092b6766..560d39d8067f 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -28,6 +28,16 @@ export function toResolvablePath(source: string): { path: string; wasFileUrl: bo } } +const CONFIG_EXTENSIONS = ['.ts', '.js', '.mjs', '.cjs', '.mts', '.cts']; + +function isServerConfigFile(sourcePath: string, resolvedPath: string, configFileName: string): boolean { + if (sourcePath === resolvedPath) { + return true; + } + const name = basename(sourcePath); + return name === configFileName || CONFIG_EXTENSIONS.some(ext => name === `${configFileName}${ext}`); +} + export type WrapServerEntryPluginOptions = { serverEntrypointFileName: string; serverConfigFileName: string; @@ -74,7 +84,7 @@ export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOp } const { path: normalizedSource, wasFileUrl } = resolvable; - if (basename(normalizedSource).startsWith(serverConfigFileName)) { + if (isServerConfigFile(normalizedSource, resolvedServerConfigPath, serverConfigFileName)) { return { id: normalizedSource, moduleSideEffects: true }; } diff --git a/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts b/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts index 895d0f85b7b0..de3665afaa69 100644 --- a/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts +++ b/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts @@ -68,6 +68,13 @@ describe('wrapServerEntryWithDynamicImport', () => { expect(result).toEqual({ id: configPath, moduleSideEffects: true }); }); + it('does not mark backup or test config files as the server config', async () => { + const backupPath = '/project/instrument.server.backup.ts'; + const result = await resolveId.call({ resolve: vi.fn(), load: vi.fn() }, backupPath, undefined, { isEntry: false }); + + expect(result).toBeNull(); + }); + it('resolves file:// entry specifiers without re-entering the entry branch', async () => { const source = pathToFileURL(entryPath).href; const fakeResolve = vi.fn(async () => ({ id: 'resolved-id', external: false })); From 9ede5889b02e31379324de48ea841a5a2e1da971 Mon Sep 17 00:00:00 2001 From: halillusion Date: Wed, 2 Sep 2026 16:32:37 +0300 Subject: [PATCH 08/10] fix(nuxt): handle POSIX file:// URL validation and drop leading slash in resolve --- CLAUDE.md | 2 +- packages/nuxt/src/vite/addServerConfig.ts | 6 +++--- packages/nuxt/src/vite/utils.ts | 9 ++++++++- packages/nuxt/test/vite/addServerConfig.test.ts | 1 + .../src/config/wrapServerEntryWithDynamicImport.ts | 9 ++++++++- .../test/config/wrapServerEntryWithDynamicImport.test.ts | 1 + 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 47dc3e3d863c..c3170642553f 120000 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -AGENTS.md \ No newline at end of file +AGENTS.md diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index ace0e0a5258f..754bdcd8592f 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -168,7 +168,7 @@ export function addDynamicImportEntryFileWrapper( nitro.options.rollupConfig.plugins.push( wrapEntryWithDynamicImport({ - resolvedSentryConfigPath: createResolver(nitro.options.rootDir).resolve(`/${serverConfigFile}`), + resolvedSentryConfigPath: createResolver(nitro.options.rootDir).resolve(serverConfigFile), experimental_entrypointWrappedFunctions: moduleOptions.experimental_entrypointWrappedFunctions, }), ); @@ -184,7 +184,7 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, isDebu name: 'rollup-plugin-inject-sentry-server-config', buildStart() { - const configPath = createResolver(nitro.options.rootDir).resolve(`/${serverConfigFile}`); + const configPath = createResolver(nitro.options.rootDir).resolve(serverConfigFile); if (!existsSync(configPath)) { if (isDebug) { @@ -204,7 +204,7 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, isDebu resolveId(source) { if (source.startsWith(filePrefix)) { const originalFilePath = source.replace(filePrefix, ''); - const configPath = createResolver(nitro.options.rootDir).resolve(`/${originalFilePath}`); + const configPath = createResolver(nitro.options.rootDir).resolve(originalFilePath); return { id: configPath }; } diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index ca8737d0f9ab..d1f0e95843b4 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -211,8 +211,15 @@ export function toResolvablePath(source: string): { path: string; wasFileUrl: bo if (!source.startsWith('file://')) { return { path: source, wasFileUrl: false }; } + if (source === 'file://' || source === 'file:///') { + return undefined; + } try { - return { path: fileURLToPath(source), wasFileUrl: true }; + const filePath = fileURLToPath(source); + if (!filePath || filePath === '/' || filePath === '\\') { + return undefined; + } + return { path: filePath, wasFileUrl: true }; } catch { return undefined; } diff --git a/packages/nuxt/test/vite/addServerConfig.test.ts b/packages/nuxt/test/vite/addServerConfig.test.ts index 3bfb010b058a..54fdc0e1a33d 100644 --- a/packages/nuxt/test/vite/addServerConfig.test.ts +++ b/packages/nuxt/test/vite/addServerConfig.test.ts @@ -30,6 +30,7 @@ describe('toResolvablePath', () => { it('returns undefined for malformed file:// URLs', () => { expect(toResolvablePath('file://')).toBeUndefined(); + expect(toResolvablePath('file:///')).toBeUndefined(); expect(toResolvablePath('file://%E0%A4%A')).toBeUndefined(); }); }); diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts index 560d39d8067f..6e19df28a3e2 100644 --- a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -21,8 +21,15 @@ export function toResolvablePath(source: string): { path: string; wasFileUrl: bo if (!source.startsWith('file://')) { return { path: source, wasFileUrl: false }; } + if (source === 'file://' || source === 'file:///') { + return undefined; + } try { - return { path: fileURLToPath(source), wasFileUrl: true }; + const filePath = fileURLToPath(source); + if (!filePath || filePath === '/' || filePath === '\\') { + return undefined; + } + return { path: filePath, wasFileUrl: true }; } catch { return undefined; } diff --git a/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts b/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts index de3665afaa69..1b3ea9dfb0d2 100644 --- a/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts +++ b/packages/solidstart/test/config/wrapServerEntryWithDynamicImport.test.ts @@ -24,6 +24,7 @@ describe('toResolvablePath', () => { it('returns undefined for malformed file:// URLs', () => { expect(toResolvablePath('file://')).toBeUndefined(); + expect(toResolvablePath('file:///')).toBeUndefined(); expect(toResolvablePath('file://%E0%A4%A')).toBeUndefined(); }); }); From 68327b9cb896d0bffae79ae62eb92fa50aa22323 Mon Sep 17 00:00:00 2001 From: halillusion Date: Wed, 2 Sep 2026 16:35:54 +0300 Subject: [PATCH 09/10] test(nuxt): make findDefaultSdkInitFile tests cross-platform for Windows --- packages/nuxt/test/vite/utils.test.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index fc8147197f70..3d135a489b6f 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -38,7 +38,7 @@ describe('findDefaultSdkInitFile', () => { }); const result = await findDefaultSdkInitFile('server'); - expect(result).toMatch(`packages/nuxt/sentry.server.config.${ext}`); + expect(result).toMatch(path.join('packages', 'nuxt', `sentry.server.config.${ext}`)); }, ); @@ -50,7 +50,7 @@ describe('findDefaultSdkInitFile', () => { }); const result = await findDefaultSdkInitFile('client'); - expect(result).toMatch(`packages/nuxt/sentry.client.config.${ext}`); + expect(result).toMatch(path.join('packages', 'nuxt', `sentry.client.config.${ext}`)); }, ); @@ -69,7 +69,7 @@ describe('findDefaultSdkInitFile', () => { configDir: '~/config', }); - expect(result).toBe(`${baseDir}/sentry.client.config.${ext}`); + expect(result).toBe(path.resolve(baseDir, `sentry.client.config.${ext}`)); expect(resolvePathMock).toHaveBeenCalledWith('~/config', { type: 'dir' }); }, ); @@ -89,7 +89,7 @@ describe('findDefaultSdkInitFile', () => { configDir: '~/config', }); - expect(result).toBe(`${baseDir}/sentry.server.config.${ext}`); + expect(result).toBe(path.resolve(baseDir, `sentry.server.config.${ext}`)); expect(resolvePathMock).toHaveBeenCalledWith('~/config', { type: 'dir' }); }, ); @@ -136,7 +136,7 @@ describe('findDefaultSdkInitFile', () => { } as unknown as Nuxt; const result = await findDefaultSdkInitFile('client', nuxtMock); - expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); + expect(result).toMatch(path.join('packages', 'nuxt', 'sentry.client.config.ts')); }); it('should return the latest layer config file path if server config exists', async () => { @@ -158,12 +158,15 @@ describe('findDefaultSdkInitFile', () => { } as unknown as Nuxt; const result = await findDefaultSdkInitFile('server', nuxtMock); - expect(result).toMatch('packages/nuxt/sentry.server.config.ts'); + expect(result).toMatch(path.join('packages', 'nuxt', 'sentry.server.config.ts')); }); it('should return the latest layer config file path if client config exists in former layer', async () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { - return !(filePath instanceof URL) && filePath.toString().includes('nuxt/sentry.client.config.ts'); + return ( + !(filePath instanceof URL) && + filePath.toString().includes(path.join('nuxt', 'module', 'sentry.client.config.ts')) + ); }); const nuxtMock = { @@ -180,7 +183,7 @@ describe('findDefaultSdkInitFile', () => { } as unknown as Nuxt; const result = await findDefaultSdkInitFile('client', nuxtMock); - expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); + expect(result).toMatch(path.join('packages', 'nuxt', 'module', 'sentry.client.config.ts')); }); }); From e50d82651b6ddbb7eb6955eeb3df4f676edc6bf1 Mon Sep 17 00:00:00 2001 From: halillusion Date: Thu, 3 Sep 2026 14:11:21 +0300 Subject: [PATCH 10/10] revert: restore CLAUDE.md symlink without trailing newline --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index c3170642553f..47dc3e3d863c 120000 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -AGENTS.md +AGENTS.md \ No newline at end of file