From f2f75d7b90b0cad0507dde743ea8cfc250ab078a Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 28 Oct 2024 17:18:54 +0100 Subject: [PATCH 1/5] feat(nuxt): Add `asyncFunctionReExports` to define re-exported server functions --- packages/nuxt/src/common/types.ts | 11 +++++++ packages/nuxt/src/module.ts | 5 ++- packages/nuxt/src/vite/addServerConfig.ts | 39 ++++++++++++++++++----- packages/nuxt/src/vite/utils.ts | 6 ++-- packages/nuxt/test/vite/utils.test.ts | 12 +++---- 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 6ba29752a308..787d9625d853 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -117,6 +117,17 @@ export type SentryNuxtModuleOptions = { */ dynamicImportForServerEntry?: boolean; + /** + * The `asyncFunctionReExports` option is only relevant when `dynamicImportForServerEntry: true` (default value). + * + * As the server entry file is wrapped with a dynamic `import()`, previous async function exports need to be re-exported. + * The SDK detects and re-exports those exports (mostly serverless functions). This is why they are re-exported as async functions. + * In case you have a custom setup and your server exports other async functions, you can override the default array with this option. + * + * @default ['default', 'handler', 'server'] + */ + asyncFunctionReExports?: string[]; + /** * Options to be passed directly to the Sentry Rollup Plugin (`@sentry/rollup-plugin`) and Sentry Vite Plugin (`@sentry/vite-plugin`) that ship with the Sentry Nuxt SDK. * You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin. diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 56fa71ad95a3..c7e64061c26a 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -21,6 +21,9 @@ export default defineNuxtModule({ const moduleOptions = { ...moduleOptionsParam, dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true + asyncFunctionReExports: moduleOptionsParam.asyncFunctionReExports + ? moduleOptionsParam.asyncFunctionReExports + : ['default', 'handler', 'server'], }; const moduleDirResolver = createResolver(import.meta.url); @@ -101,7 +104,7 @@ export default defineNuxtModule({ }); } } else { - addDynamicImportEntryFileWrapper(nitro, serverConfigFile); + addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions); if (moduleOptions.debug) { consoleSandbox(() => { diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index fe86895a76f2..4e1a8fa16de1 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -81,7 +81,12 @@ export function addServerConfigToBuild( * With this, the Sentry server config can be loaded before all other modules of the application (which is needed for import-in-the-middle). * See: https://nodejs.org/api/module.html#enabling */ -export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile: string): void { +export function addDynamicImportEntryFileWrapper( + nitro: Nitro, + serverConfigFile: string, + moduleOptions: Omit & + Required>, +): void { if (!nitro.options.rollupConfig) { nitro.options.rollupConfig = { output: {} }; } @@ -94,7 +99,10 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile: } nitro.options.rollupConfig.plugins.push( - wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)), + wrapEntryWithDynamicImport({ + resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`), + asyncFunctionReExports: moduleOptions.asyncFunctionReExports, + }), ); } @@ -103,7 +111,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile: * by using a regular `import` and load the server after that. * This also works with serverless `handler` functions, as it re-exports the `handler`. */ -function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPluginOption { +function wrapEntryWithDynamicImport({ + resolvedSentryConfigPath, + asyncFunctionReExports, + debug, +}: { resolvedSentryConfigPath: string; asyncFunctionReExports: string[]; debug?: boolean }): InputPluginOption { return { name: 'sentry-wrap-entry-with-dynamic-import', async resolveId(source, importer, options) { @@ -129,9 +141,20 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug moduleInfo.moduleSideEffects = true; - // `exportedBindings` can look like this: `{ '.': [ 'handler' ], './firebase-gen-1.mjs': [ 'server' ] }` + // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. - const exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {})); + const functionsToExport = flatten(Object.values(moduleInfo.exportedBindings || {})).filter(functionName => + asyncFunctionReExports.includes(functionName), + ); + + if (debug && functionsToExport.length === 0) { + consoleSandbox(() => + // eslint-disable-next-line no-console + console.warn( + "[Sentry] No functions found for re-export. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.asyncFunctionReExports` in your `nuxt.config.ts`.", + ), + ); + } // The enclosing `if` already checks for the suffix in `source`, but a check in `resolution.id` is needed as well to prevent multiple attachment of the suffix return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) @@ -139,7 +162,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug : resolution.id // Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler) .concat(SENTRY_WRAPPED_ENTRY) - .concat(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '') + .concat(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '') .concat(QUERY_END_INDICATOR); } return null; @@ -149,7 +172,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug const entryId = removeSentryQueryFromPath(id); // Mostly useful for serverless `handler` functions - const reExportedFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT) + const reExportedAsyncFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT) ? constructFunctionReExport(id, entryId) : ''; @@ -161,7 +184,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug `import(${JSON.stringify(entryId)});\n` + // By importing "import-in-the-middle/hook.mjs", we can make sure this file wil be included, as not all node builders are including files imported with `module.register()`. "import 'import-in-the-middle/hook.mjs';\n" + - `${reExportedFunctions}\n` + `${reExportedAsyncFunctions}\n` ); } diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 1737a47e8062..18b3c6846dfb 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -70,13 +70,13 @@ export function constructFunctionReExport(pathWithQuery: string, entryId: string const functionNames = extractFunctionReexportQueryParameters(pathWithQuery); return functionNames.reduce( - (functionsCode, currFunctionName) => + (functionsCode, currFunctionName, idx) => functionsCode.concat( - 'async function reExport(...args) {\n' + + `async function reExport${idx}(...args) {\n` + ` const res = await import(${JSON.stringify(entryId)});\n` + ` return res.${currFunctionName}.call(this, ...args);\n` + '}\n' + - `export { reExport as ${currFunctionName} };\n`, + `export { reExport${idx} as ${currFunctionName} };\n`, ), '', ); diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index a38dbdc44793..200eab9e414c 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -111,16 +111,16 @@ describe('constructFunctionReExport', () => { const result2 = constructFunctionReExport(query2, entryId); const expected = ` -async function reExport(...args) { +async function reExport0(...args) { const res = await import("./module"); return res.foo.call(this, ...args); } -export { reExport as foo }; -async function reExport(...args) { +export { reExport0 as foo }; +async function reExport1(...args) { const res = await import("./module"); return res.bar.call(this, ...args); } -export { reExport as bar }; +export { reExport1 as bar }; `; expect(result.trim()).toBe(expected.trim()); expect(result2.trim()).toBe(expected.trim()); @@ -132,11 +132,11 @@ export { reExport as bar }; const result = constructFunctionReExport(query, entryId); const expected = ` -async function reExport(...args) { +async function reExport0(...args) { const res = await import("./index"); return res.default.call(this, ...args); } -export { reExport as default }; +export { reExport0 as default }; `; expect(result.trim()).toBe(expected.trim()); }); From 6a29eba27c0dbee893bab954a4469d5896159e5f Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 31 Oct 2024 09:38:00 +0100 Subject: [PATCH 2/5] review comments: add test --- packages/nuxt/src/common/types.ts | 9 ++-- packages/nuxt/src/module.ts | 4 +- packages/nuxt/src/vite/addServerConfig.ts | 20 ++------ packages/nuxt/src/vite/utils.ts | 34 +++++++++++-- packages/nuxt/test/vite/utils.test.ts | 60 ++++++++++++++++++++--- 5 files changed, 94 insertions(+), 33 deletions(-) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 787d9625d853..06c4ea241ef5 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -118,11 +118,12 @@ export type SentryNuxtModuleOptions = { dynamicImportForServerEntry?: boolean; /** - * The `asyncFunctionReExports` option is only relevant when `dynamicImportForServerEntry: true` (default value). + * By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your application entrypoint + * with a dynamic `import()` to ensure all dependencies can be properly instrumented. * - * As the server entry file is wrapped with a dynamic `import()`, previous async function exports need to be re-exported. - * The SDK detects and re-exports those exports (mostly serverless functions). This is why they are re-exported as async functions. - * In case you have a custom setup and your server exports other async functions, you can override the default array with this option. + * By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint. + * If your application has a different main export that is used to run the application, you can overwrite this by providing an array of export names to wrap. + * Any wrapped export is expected to be an async function. * * @default ['default', 'handler', 'server'] */ diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index c7e64061c26a..1d3ee2c3c544 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -21,9 +21,7 @@ export default defineNuxtModule({ const moduleOptions = { ...moduleOptionsParam, dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true - asyncFunctionReExports: moduleOptionsParam.asyncFunctionReExports - ? moduleOptionsParam.asyncFunctionReExports - : ['default', 'handler', 'server'], + asyncFunctionReExports: moduleOptionsParam.asyncFunctionReExports || ['default', 'handler', 'server'], }; const moduleDirResolver = createResolver(import.meta.url); diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 4e1a8fa16de1..667c33e9f8e2 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { createResolver } from '@nuxt/kit'; import type { Nuxt } from '@nuxt/schema'; -import { consoleSandbox, flatten } from '@sentry/utils'; +import { consoleSandbox } from '@sentry/utils'; import type { Nitro } from 'nitropack'; import type { InputPluginOption } from 'rollup'; import type { SentryNuxtModuleOptions } from '../common/types'; @@ -10,6 +10,7 @@ import { SENTRY_FUNCTIONS_REEXPORT, SENTRY_WRAPPED_ENTRY, constructFunctionReExport, + constructFunctionsReExportQuery, removeSentryQueryFromPath, } from './utils'; @@ -141,28 +142,13 @@ function wrapEntryWithDynamicImport({ moduleInfo.moduleSideEffects = true; - // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` - // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. - const functionsToExport = flatten(Object.values(moduleInfo.exportedBindings || {})).filter(functionName => - asyncFunctionReExports.includes(functionName), - ); - - if (debug && functionsToExport.length === 0) { - consoleSandbox(() => - // eslint-disable-next-line no-console - console.warn( - "[Sentry] No functions found for re-export. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.asyncFunctionReExports` in your `nuxt.config.ts`.", - ), - ); - } - // The enclosing `if` already checks for the suffix in `source`, but a check in `resolution.id` is needed as well to prevent multiple attachment of the suffix return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) ? resolution.id : resolution.id // Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler) .concat(SENTRY_WRAPPED_ENTRY) - .concat(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '') + .concat(constructFunctionsReExportQuery(moduleInfo.exportedBindings, asyncFunctionReExports, debug)) .concat(QUERY_END_INDICATOR); } return null; diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 18b3c6846dfb..17560e8abf8b 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import { consoleSandbox, flatten } from '@sentry/utils'; /** * Find the default SDK init file for the given type (client or server). @@ -63,6 +64,33 @@ export function extractFunctionReexportQueryParameters(query: string): string[] : []; } +/** + * Constructs a comma-separated string with all functions that need to be re-exported later from the server entry. + * It uses Rollup's `exportedBindings` to determine the functions to re-export + */ +export function constructFunctionsReExportQuery( + exportedBindings: Record | null, + asyncFunctionReExports: string[], + debug?: boolean, +): string { + // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` + // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. + const functionsToExport = flatten(Object.values(exportedBindings || {})).filter(functionName => + asyncFunctionReExports.includes(functionName), + ); + + if (debug && functionsToExport.length === 0) { + consoleSandbox(() => + // eslint-disable-next-line no-console + console.warn( + "[Sentry] No functions found for re-export. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.asyncFunctionReExports` in your `nuxt.config.ts`.", + ), + ); + } + + return functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : ''; +} + /** * Constructs a code snippet with function reexports (can be used in Rollup plugins) */ @@ -70,13 +98,13 @@ export function constructFunctionReExport(pathWithQuery: string, entryId: string const functionNames = extractFunctionReexportQueryParameters(pathWithQuery); return functionNames.reduce( - (functionsCode, currFunctionName, idx) => + (functionsCode, currFunctionName) => functionsCode.concat( - `async function reExport${idx}(...args) {\n` + + `async function reExport${currFunctionName.toUpperCase()}(...args) {\n` + ` const res = await import(${JSON.stringify(entryId)});\n` + ` return res.${currFunctionName}.call(this, ...args);\n` + '}\n' + - `export { reExport${idx} as ${currFunctionName} };\n`, + `export { reExport${currFunctionName.toUpperCase()} as ${currFunctionName} };\n`, ), '', ); diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 200eab9e414c..20f8d6de2e62 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -5,6 +5,7 @@ import { SENTRY_FUNCTIONS_REEXPORT, SENTRY_WRAPPED_ENTRY, constructFunctionReExport, + constructFunctionsReExportQuery, extractFunctionReexportQueryParameters, findDefaultSdkInitFile, removeSentryQueryFromPath, @@ -102,6 +103,38 @@ describe('extractFunctionReexportQueryParameters', () => { }); }); +describe('constructFunctionsReExportQuery', () => { + it.each([ + [{ '.': ['handler'] }, ['handler'], '?sentry-query-functions-reexport=handler'], + [{ '.': ['handler'], './module': ['server'] }, [], ''], + [{ '.': ['handler'], './module': ['server'] }, ['server'], '?sentry-query-functions-reexport=server'], + [{ '.': ['handler', 'otherFunction'] }, ['handler'], '?sentry-query-functions-reexport=handler'], + [{ '.': ['handler', 'otherFn'] }, ['handler', 'otherFn'], '?sentry-query-functions-reexport=handler,otherFn'], + [{ '.': ['bar'], './module': ['foo'] }, ['bar', 'foo'], '?sentry-query-functions-reexport=bar,foo'], + ])( + 'constructs re-export query for exportedBindings: %j and asyncFunctionReExports: %j', + (exportedBindings, asyncFunctionReExports, expected) => { + const result = constructFunctionsReExportQuery(exportedBindings, asyncFunctionReExports); + expect(result).toBe(expected); + }, + ); + + it('logs a warning if no functions are found for re-export and debug is true', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const exportedBindings = { '.': ['handler'] }; + const asyncFunctionReExports = ['nonExistentFunction']; + const debug = true; + + const result = constructFunctionsReExportQuery(exportedBindings, asyncFunctionReExports, debug); + expect(result).toBe(''); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "[Sentry] No functions found for re-export. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.asyncFunctionReExports` in your `nuxt.config.ts`.", + ); + + consoleWarnSpy.mockRestore(); + }); +}); + describe('constructFunctionReExport', () => { it('constructs re-export code for given query parameters and entry ID', () => { const query = `${SENTRY_FUNCTIONS_REEXPORT}foo,bar,${QUERY_END_INDICATOR}}`; @@ -111,16 +144,16 @@ describe('constructFunctionReExport', () => { const result2 = constructFunctionReExport(query2, entryId); const expected = ` -async function reExport0(...args) { +async function reExportFOO(...args) { const res = await import("./module"); return res.foo.call(this, ...args); } -export { reExport0 as foo }; -async function reExport1(...args) { +export { reExportFOO as foo }; +async function reExportBAR(...args) { const res = await import("./module"); return res.bar.call(this, ...args); } -export { reExport1 as bar }; +export { reExportBAR as bar }; `; expect(result.trim()).toBe(expected.trim()); expect(result2.trim()).toBe(expected.trim()); @@ -132,11 +165,26 @@ export { reExport1 as bar }; const result = constructFunctionReExport(query, entryId); const expected = ` -async function reExport0(...args) { +async function reExportDEFAULT(...args) { + const res = await import("./index"); + return res.default.call(this, ...args); +} +export { reExportDEFAULT as default }; +`; + expect(result.trim()).toBe(expected.trim()); + }); + + it('constructs re-export code for a "default" query parameters and entry ID', () => { + const query = `${SENTRY_FUNCTIONS_REEXPORT}default${QUERY_END_INDICATOR}}`; + const entryId = './index'; + const result = constructFunctionReExport(query, entryId); + + const expected = ` +async function reExportDEFAULT(...args) { const res = await import("./index"); return res.default.call(this, ...args); } -export { reExport0 as default }; +export { reExportDEFAULT as default }; `; expect(result.trim()).toBe(expected.trim()); }); From 9ed947178392f564ba803afb2a54e63e74314691 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 31 Oct 2024 13:26:19 +0100 Subject: [PATCH 3/5] reexport and wrap functions --- packages/nuxt/src/common/types.ts | 5 +- packages/nuxt/src/module.ts | 2 +- packages/nuxt/src/vite/addServerConfig.ts | 28 +++-- packages/nuxt/src/vite/utils.ts | 104 ++++++++++++------ packages/nuxt/test/vite/utils.test.ts | 127 ++++++++++++++++------ 5 files changed, 183 insertions(+), 83 deletions(-) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 06c4ea241ef5..39158f28bb14 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -119,7 +119,8 @@ export type SentryNuxtModuleOptions = { /** * By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your application entrypoint - * with a dynamic `import()` to ensure all dependencies can be properly instrumented. + * with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported. + * Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is. * * By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint. * If your application has a different main export that is used to run the application, you can overwrite this by providing an array of export names to wrap. @@ -127,7 +128,7 @@ export type SentryNuxtModuleOptions = { * * @default ['default', 'handler', 'server'] */ - asyncFunctionReExports?: string[]; + entrypointWrappedFunctions?: string[]; /** * Options to be passed directly to the Sentry Rollup Plugin (`@sentry/rollup-plugin`) and Sentry Vite Plugin (`@sentry/vite-plugin`) that ship with the Sentry Nuxt SDK. diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 1d3ee2c3c544..426ab2f41edf 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -21,7 +21,7 @@ export default defineNuxtModule({ const moduleOptions = { ...moduleOptionsParam, dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true - asyncFunctionReExports: moduleOptionsParam.asyncFunctionReExports || ['default', 'handler', 'server'], + entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'], }; const moduleDirResolver = createResolver(import.meta.url); diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 667c33e9f8e2..cf4b2a95473e 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -7,10 +7,11 @@ import type { InputPluginOption } from 'rollup'; import type { SentryNuxtModuleOptions } from '../common/types'; import { QUERY_END_INDICATOR, - SENTRY_FUNCTIONS_REEXPORT, + SENTRY_REEXPORTED_FUNCTIONS, SENTRY_WRAPPED_ENTRY, + SENTRY_WRAPPED_FUNCTIONS, constructFunctionReExport, - constructFunctionsReExportQuery, + constructWrappedFunctionExportQuery, removeSentryQueryFromPath, } from './utils'; @@ -85,8 +86,8 @@ export function addServerConfigToBuild( export function addDynamicImportEntryFileWrapper( nitro: Nitro, serverConfigFile: string, - moduleOptions: Omit & - Required>, + moduleOptions: Omit & + Required>, ): void { if (!nitro.options.rollupConfig) { nitro.options.rollupConfig = { output: {} }; @@ -102,7 +103,7 @@ export function addDynamicImportEntryFileWrapper( nitro.options.rollupConfig.plugins.push( wrapEntryWithDynamicImport({ resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`), - asyncFunctionReExports: moduleOptions.asyncFunctionReExports, + entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions, }), ); } @@ -114,9 +115,9 @@ export function addDynamicImportEntryFileWrapper( */ function wrapEntryWithDynamicImport({ resolvedSentryConfigPath, - asyncFunctionReExports, + entrypointWrappedFunctions, debug, -}: { resolvedSentryConfigPath: string; asyncFunctionReExports: string[]; debug?: boolean }): InputPluginOption { +}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption { return { name: 'sentry-wrap-entry-with-dynamic-import', async resolveId(source, importer, options) { @@ -148,7 +149,9 @@ function wrapEntryWithDynamicImport({ : resolution.id // Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler) .concat(SENTRY_WRAPPED_ENTRY) - .concat(constructFunctionsReExportQuery(moduleInfo.exportedBindings, asyncFunctionReExports, debug)) + .concat( + constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug), + ) .concat(QUERY_END_INDICATOR); } return null; @@ -158,9 +161,10 @@ function wrapEntryWithDynamicImport({ const entryId = removeSentryQueryFromPath(id); // Mostly useful for serverless `handler` functions - const reExportedAsyncFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT) - ? constructFunctionReExport(id, entryId) - : ''; + const reExportedFunctions = + id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS) + ? constructFunctionReExport(id, entryId) + : ''; return ( // Regular `import` of the Sentry config @@ -170,7 +174,7 @@ function wrapEntryWithDynamicImport({ `import(${JSON.stringify(entryId)});\n` + // By importing "import-in-the-middle/hook.mjs", we can make sure this file wil be included, as not all node builders are including files imported with `module.register()`. "import 'import-in-the-middle/hook.mjs';\n" + - `${reExportedAsyncFunctions}\n` + `${reExportedFunctions}\n` ); } diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 17560e8abf8b..8822ad0e6e8f 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -27,7 +27,8 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde } export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry'; -export const SENTRY_FUNCTIONS_REEXPORT = '?sentry-query-functions-reexport='; +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'; /** @@ -49,63 +50,102 @@ export function removeSentryQueryFromPath(url: string): string { * * Only exported for testing. */ -export function extractFunctionReexportQueryParameters(query: string): string[] { +export function extractFunctionReexportQueryParameters(query: string): { wrap: string[]; reexport: string[] } { // Regex matches the comma-separated params between the functions query // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor - const regex = new RegExp(`\\${SENTRY_FUNCTIONS_REEXPORT}(.*?)\\${QUERY_END_INDICATOR}`); - const match = query.match(regex); - - return match && match[1] - ? match[1] - .split(',') - .filter(param => param !== '') - // Sanitize, as code could be injected with another rollup plugin - .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - : []; + const wrapRegex = new RegExp( + `\\${SENTRY_WRAPPED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR}|\\${SENTRY_REEXPORTED_FUNCTIONS})`, + ); + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const reexportRegex = new RegExp(`\\${SENTRY_REEXPORTED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR})`); + + const wrapMatch = query.match(wrapRegex); + const reexportMatch = query.match(reexportRegex); + + const wrap = + wrapMatch && wrapMatch[1] + ? wrapMatch[1] + .split(',') + .filter(param => param !== '') + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + const reexport = + reexportMatch && reexportMatch[1] + ? reexportMatch[1] + .split(',') + .filter(param => param !== '' && param !== 'default') + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + return { wrap, reexport }; } /** * Constructs a comma-separated string with all functions that need to be re-exported later from the server entry. * It uses Rollup's `exportedBindings` to determine the functions to re-export */ -export function constructFunctionsReExportQuery( +export function constructWrappedFunctionExportQuery( exportedBindings: Record | null, - asyncFunctionReExports: string[], + entrypointWrappedFunctions: string[], debug?: boolean, ): string { // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. - const functionsToExport = flatten(Object.values(exportedBindings || {})).filter(functionName => - asyncFunctionReExports.includes(functionName), + const functionsToExport = flatten(Object.values(exportedBindings || {})).reduce( + (functions, currFunctionName) => { + if (entrypointWrappedFunctions.includes(currFunctionName)) { + functions.wrap.push(currFunctionName); + } else { + functions.reexport.push(currFunctionName); + } + return functions; + }, + { wrap: [], reexport: [] } as { wrap: string[]; reexport: string[] }, ); - if (debug && functionsToExport.length === 0) { + if (debug && functionsToExport.wrap.length === 0) { consoleSandbox(() => // eslint-disable-next-line no-console console.warn( - "[Sentry] No functions found for re-export. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.asyncFunctionReExports` in your `nuxt.config.ts`.", + "[Sentry] No functions found to wrap. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in your `nuxt.config.ts`.", ), ); } - return functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : ''; + const wrapQuery = functionsToExport.wrap.length + ? `${SENTRY_WRAPPED_FUNCTIONS}${functionsToExport.wrap.join(',')}` + : ''; + const reexportQuery = functionsToExport.reexport.length + ? `${SENTRY_REEXPORTED_FUNCTIONS}${functionsToExport.reexport.join(',')}` + : ''; + + return [wrapQuery, reexportQuery].filter(Boolean).join(''); } /** - * Constructs a code snippet with function reexports (can be used in Rollup plugins) + * Constructs a code snippet with function reexports (can be used in Rollup plugins as a return value for `load()`) */ export function constructFunctionReExport(pathWithQuery: string, entryId: string): string { - const functionNames = extractFunctionReexportQueryParameters(pathWithQuery); - - return functionNames.reduce( - (functionsCode, currFunctionName) => - functionsCode.concat( - `async function reExport${currFunctionName.toUpperCase()}(...args) {\n` + - ` const res = await import(${JSON.stringify(entryId)});\n` + - ` return res.${currFunctionName}.call(this, ...args);\n` + - '}\n' + - `export { reExport${currFunctionName.toUpperCase()} as ${currFunctionName} };\n`, + const { wrap: wrapFunctions, reexport: reexportFunctions } = extractFunctionReexportQueryParameters(pathWithQuery); + + return wrapFunctions + .reduce( + (functionsCode, currFunctionName) => + functionsCode.concat( + `async function ${currFunctionName}_sentryWrapped(...args) {\n` + + ` const res = await import(${JSON.stringify(entryId)});\n` + + ` return res.${currFunctionName}.call(this, ...args);\n` + + '}\n' + + `export { ${currFunctionName}_sentryWrapped as ${currFunctionName} };\n`, + ), + '', + ) + .concat( + reexportFunctions.reduce( + (functionsCode, currFunctionName) => + functionsCode.concat(`export { ${currFunctionName} } from ${JSON.stringify(entryId)};`), + '', ), - '', - ); + ); } diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 20f8d6de2e62..5696d849ff28 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -2,10 +2,11 @@ import * as fs from 'fs'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { QUERY_END_INDICATOR, - SENTRY_FUNCTIONS_REEXPORT, + SENTRY_REEXPORTED_FUNCTIONS, SENTRY_WRAPPED_ENTRY, + SENTRY_WRAPPED_FUNCTIONS, constructFunctionReExport, - constructFunctionsReExportQuery, + constructWrappedFunctionExportQuery, extractFunctionReexportQueryParameters, findDefaultSdkInitFile, removeSentryQueryFromPath, @@ -71,7 +72,7 @@ describe('findDefaultSdkInitFile', () => { describe('removeSentryQueryFromPath', () => { it('strips the Sentry query part from the path', () => { - const url = `/example/path${SENTRY_WRAPPED_ENTRY}${SENTRY_FUNCTIONS_REEXPORT}foo,${QUERY_END_INDICATOR}`; + const url = `/example/path${SENTRY_WRAPPED_ENTRY}${SENTRY_WRAPPED_FUNCTIONS}foo,${QUERY_END_INDICATOR}`; const url2 = `/example/path${SENTRY_WRAPPED_ENTRY}${QUERY_END_INDICATOR}`; const result = removeSentryQueryFromPath(url); const result2 = removeSentryQueryFromPath(url2); @@ -88,33 +89,56 @@ describe('removeSentryQueryFromPath', () => { describe('extractFunctionReexportQueryParameters', () => { it.each([ - [`${SENTRY_FUNCTIONS_REEXPORT}foo,bar,${QUERY_END_INDICATOR}`, ['foo', 'bar']], - [`${SENTRY_FUNCTIONS_REEXPORT}foo,bar,default${QUERY_END_INDICATOR}`, ['foo', 'bar', 'default']], + [`${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }], [ - `${SENTRY_FUNCTIONS_REEXPORT}foo,a.b*c?d[e]f(g)h|i\\\\j(){hello},${QUERY_END_INDICATOR}`, - ['foo', 'a\\.b\\*c\\?d\\[e\\]f\\(g\\)h\\|i\\\\\\\\j\\(\\)\\{hello\\}'], + `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,default${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'bar', 'default'], reexport: [] }, ], - [`/example/path/${SENTRY_FUNCTIONS_REEXPORT}foo,bar${QUERY_END_INDICATOR}`, ['foo', 'bar']], - [`${SENTRY_FUNCTIONS_REEXPORT}${QUERY_END_INDICATOR}`, []], - ['?other-query=param', []], + [ + `${SENTRY_WRAPPED_FUNCTIONS}foo,a.b*c?d[e]f(g)h|i\\\\j(){hello},${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'a\\.b\\*c\\?d\\[e\\]f\\(g\\)h\\|i\\\\\\\\j\\(\\)\\{hello\\}'], reexport: [] }, + ], + [`/example/path/${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }], + [ + `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'bar'], reexport: [] }, + ], + [`${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`, { wrap: [], reexport: [] }], + [ + `/path${SENTRY_WRAPPED_FUNCTIONS}foo,bar${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'bar'], reexport: ['bar'] }, + ], + ['?other-query=param', { wrap: [], reexport: [] }], ])('extracts parameters from the query string: %s', (query, expected) => { const result = extractFunctionReexportQueryParameters(query); expect(result).toEqual(expected); }); }); -describe('constructFunctionsReExportQuery', () => { +describe('constructWrappedFunctionExportQuery', () => { it.each([ - [{ '.': ['handler'] }, ['handler'], '?sentry-query-functions-reexport=handler'], - [{ '.': ['handler'], './module': ['server'] }, [], ''], - [{ '.': ['handler'], './module': ['server'] }, ['server'], '?sentry-query-functions-reexport=server'], - [{ '.': ['handler', 'otherFunction'] }, ['handler'], '?sentry-query-functions-reexport=handler'], - [{ '.': ['handler', 'otherFn'] }, ['handler', 'otherFn'], '?sentry-query-functions-reexport=handler,otherFn'], - [{ '.': ['bar'], './module': ['foo'] }, ['bar', 'foo'], '?sentry-query-functions-reexport=bar,foo'], + [{ '.': ['handler'] }, ['handler'], `${SENTRY_WRAPPED_FUNCTIONS}handler`], + [{ '.': ['handler'], './module': ['server'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}handler,server`], + [ + { '.': ['handler'], './module': ['server'] }, + ['server'], + `${SENTRY_WRAPPED_FUNCTIONS}server${SENTRY_REEXPORTED_FUNCTIONS}handler`, + ], + [ + { '.': ['handler', 'otherFunction'] }, + ['handler'], + `${SENTRY_WRAPPED_FUNCTIONS}handler${SENTRY_REEXPORTED_FUNCTIONS}otherFunction`, + ], + [{ '.': ['handler', 'otherFn'] }, ['handler', 'otherFn'], `${SENTRY_WRAPPED_FUNCTIONS}handler,otherFn`], + [{ '.': ['bar'], './module': ['foo'] }, ['bar', 'foo'], `${SENTRY_WRAPPED_FUNCTIONS}bar,foo`], + [{ '.': ['foo', 'bar'] }, ['foo'], `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}bar`], + [{ '.': ['foo', 'bar'] }, ['bar'], `${SENTRY_WRAPPED_FUNCTIONS}bar${SENTRY_REEXPORTED_FUNCTIONS}foo`], + [{ '.': ['foo', 'bar'] }, ['foo', 'bar'], `${SENTRY_WRAPPED_FUNCTIONS}foo,bar`], + [{ '.': ['foo', 'bar'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}foo,bar`], ])( - 'constructs re-export query for exportedBindings: %j and asyncFunctionReExports: %j', - (exportedBindings, asyncFunctionReExports, expected) => { - const result = constructFunctionsReExportQuery(exportedBindings, asyncFunctionReExports); + 'constructs re-export query for exportedBindings: %j and entrypointWrappedFunctions: %j', + (exportedBindings, entrypointWrappedFunctions, expected) => { + const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions); expect(result).toBe(expected); }, ); @@ -122,13 +146,13 @@ describe('constructFunctionsReExportQuery', () => { it('logs a warning if no functions are found for re-export and debug is true', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const exportedBindings = { '.': ['handler'] }; - const asyncFunctionReExports = ['nonExistentFunction']; + const entrypointWrappedFunctions = ['nonExistentFunction']; const debug = true; - const result = constructFunctionsReExportQuery(exportedBindings, asyncFunctionReExports, debug); - expect(result).toBe(''); + const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions, debug); + expect(result).toBe('?sentry-query-reexported-functions=handler'); expect(consoleWarnSpy).toHaveBeenCalledWith( - "[Sentry] No functions found for re-export. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.asyncFunctionReExports` in your `nuxt.config.ts`.", + "[Sentry] No functions found to wrap. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in your `nuxt.config.ts`.", ); consoleWarnSpy.mockRestore(); @@ -137,54 +161,85 @@ describe('constructFunctionsReExportQuery', () => { describe('constructFunctionReExport', () => { it('constructs re-export code for given query parameters and entry ID', () => { - const query = `${SENTRY_FUNCTIONS_REEXPORT}foo,bar,${QUERY_END_INDICATOR}}`; - const query2 = `${SENTRY_FUNCTIONS_REEXPORT}foo,bar${QUERY_END_INDICATOR}}`; + const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}}`; + const query2 = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}}`; const entryId = './module'; const result = constructFunctionReExport(query, entryId); const result2 = constructFunctionReExport(query2, entryId); const expected = ` -async function reExportFOO(...args) { +async function foo_sentryWrapped(...args) { const res = await import("./module"); return res.foo.call(this, ...args); } -export { reExportFOO as foo }; -async function reExportBAR(...args) { +export { foo_sentryWrapped as foo }; +async function bar_sentryWrapped(...args) { const res = await import("./module"); return res.bar.call(this, ...args); } -export { reExportBAR as bar }; +export { bar_sentryWrapped as bar }; `; expect(result.trim()).toBe(expected.trim()); expect(result2.trim()).toBe(expected.trim()); }); it('constructs re-export code for a "default" query parameters and entry ID', () => { - const query = `${SENTRY_FUNCTIONS_REEXPORT}default${QUERY_END_INDICATOR}}`; + const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`; const entryId = './index'; const result = constructFunctionReExport(query, entryId); const expected = ` -async function reExportDEFAULT(...args) { +async function default_sentryWrapped(...args) { const res = await import("./index"); return res.default.call(this, ...args); } -export { reExportDEFAULT as default }; +export { default_sentryWrapped as default }; `; expect(result.trim()).toBe(expected.trim()); }); it('constructs re-export code for a "default" query parameters and entry ID', () => { - const query = `${SENTRY_FUNCTIONS_REEXPORT}default${QUERY_END_INDICATOR}}`; + const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`; const entryId = './index'; const result = constructFunctionReExport(query, entryId); const expected = ` -async function reExportDEFAULT(...args) { +async function default_sentryWrapped(...args) { const res = await import("./index"); return res.default.call(this, ...args); } -export { reExportDEFAULT as default }; +export { default_sentryWrapped as default }; +`; + expect(result.trim()).toBe(expected.trim()); + }); + + it('constructs re-export code for a mix of wrapped and re-exported functions', () => { + const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`; + const entryId = './module'; + const result = constructFunctionReExport(query, entryId); + + const expected = ` +async function foo_sentryWrapped(...args) { + const res = await import("./module"); + return res.foo.call(this, ...args); +} +export { foo_sentryWrapped as foo }; +export { bar } from "./module"; +`; + expect(result.trim()).toBe(expected.trim()); + }); + + it('does not re-export a default export for regular re-exported functions', () => { + const query = `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}default${QUERY_END_INDICATOR}`; + const entryId = './module'; + const result = constructFunctionReExport(query, entryId); + + const expected = ` +async function foo_sentryWrapped(...args) { + const res = await import("./module"); + return res.foo.call(this, ...args); +} +export { foo_sentryWrapped as foo }; `; expect(result.trim()).toBe(expected.trim()); }); From b8a73ca5bd9509b8d62366f2f996430f2156b568 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 31 Oct 2024 13:26:19 +0100 Subject: [PATCH 4/5] reexport and wrap functions --- packages/nuxt/src/vite/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 8822ad0e6e8f..031031cdd4fe 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -44,9 +44,8 @@ export function removeSentryQueryFromPath(url: string): string { } /** - * Extracts and sanitizes function re-export query parameters from a query string. - * If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting - * serverless `handler` functions. + * Extracts and sanitizes function re-export and function wrap query parameters from a query string. + * If it is a default export, it is not considered for re-exporting. * * Only exported for testing. */ @@ -83,7 +82,8 @@ export function extractFunctionReexportQueryParameters(query: string): { wrap: s /** * Constructs a comma-separated string with all functions that need to be re-exported later from the server entry. - * It uses Rollup's `exportedBindings` to determine the functions to re-export + * It uses Rollup's `exportedBindings` to determine the functions to re-export. Functions which should be wrapped + * (e.g. serverless handlers) are wrapped by Sentry. */ export function constructWrappedFunctionExportQuery( exportedBindings: Record | null, From 62e95b26e25d23ec7e08870842b675e3472c6138 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 31 Oct 2024 16:29:20 +0100 Subject: [PATCH 5/5] review suggestions --- packages/nuxt/src/common/types.ts | 4 ++-- packages/nuxt/src/vite/utils.ts | 6 ++++-- packages/nuxt/test/vite/utils.test.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 39158f28bb14..46f390120cfe 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -118,12 +118,12 @@ export type SentryNuxtModuleOptions = { dynamicImportForServerEntry?: boolean; /** - * By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your application entrypoint + * By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your Nitro server entrypoint * with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported. * Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is. * * By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint. - * If your application has a different main export that is used to run the application, you can overwrite this by providing an array of export names to wrap. + * If your server has a different main export that is used to run the server, you can overwrite this by providing an array of export names to wrap. * Any wrapped export is expected to be an async function. * * @default ['default', 'handler', 'server'] diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 031031cdd4fe..8fffc8fe06c9 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -66,6 +66,7 @@ export function extractFunctionReexportQueryParameters(query: string): { wrap: s ? wrapMatch[1] .split(',') .filter(param => param !== '') + // Sanitize, as code could be injected with another rollup plugin .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) : []; @@ -74,6 +75,7 @@ export function extractFunctionReexportQueryParameters(query: string): { wrap: s ? reexportMatch[1] .split(',') .filter(param => param !== '' && param !== 'default') + // Sanitize, as code could be injected with another rollup plugin .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) : []; @@ -108,7 +110,7 @@ export function constructWrappedFunctionExportQuery( consoleSandbox(() => // eslint-disable-next-line no-console console.warn( - "[Sentry] No functions found to wrap. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in your `nuxt.config.ts`.", + "[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.", ), ); } @@ -120,7 +122,7 @@ export function constructWrappedFunctionExportQuery( ? `${SENTRY_REEXPORTED_FUNCTIONS}${functionsToExport.reexport.join(',')}` : ''; - return [wrapQuery, reexportQuery].filter(Boolean).join(''); + return [wrapQuery, reexportQuery].join(''); } /** diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 5696d849ff28..a35f9cf8ca34 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -152,7 +152,7 @@ describe('constructWrappedFunctionExportQuery', () => { const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions, debug); expect(result).toBe('?sentry-query-reexported-functions=handler'); expect(consoleWarnSpy).toHaveBeenCalledWith( - "[Sentry] No functions found to wrap. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in your `nuxt.config.ts`.", + "[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.", ); consoleWarnSpy.mockRestore();