Skip to content

Commit 75db7b5

Browse files
committed
feat(nuxt): Add asyncFunctionReExports to define re-exported server functions
1 parent 2cf3ef7 commit 75db7b5

File tree

5 files changed

+53
-18
lines changed

5 files changed

+53
-18
lines changed

packages/nuxt/src/common/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ export type SentryNuxtModuleOptions = {
128128
*/
129129
dynamicImportForServerEntry?: boolean;
130130

131+
/**
132+
* The `asyncFunctionReExports` option is only relevant when `dynamicImportForServerEntry: true` (default value).
133+
*
134+
* As the server entry file is wrapped with a dynamic `import()`, previous async function exports need to be re-exported.
135+
* The SDK detects and re-exports those exports (mostly serverless functions). This is why they are re-exported as async functions.
136+
* In case you have a custom setup and your server exports other async functions, you can override the default array with this option.
137+
*
138+
* @default ['default', 'handler', 'server']
139+
*/
140+
asyncFunctionReExports?: string[];
141+
131142
/**
132143
* 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.
133144
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.

packages/nuxt/src/module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
2121
const moduleOptions = {
2222
...moduleOptionsParam,
2323
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
24+
asyncFunctionReExports: ['default', 'handler', 'server'],
2425
};
2526

2627
const moduleDirResolver = createResolver(import.meta.url);
@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
101102
});
102103
}
103104
} else {
104-
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
105+
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);
105106

106107
if (moduleOptions.debug) {
107108
consoleSandbox(() => {

packages/nuxt/src/vite/addServerConfig.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ export function addServerConfigToBuild(
8181
* With this, the Sentry server config can be loaded before all other modules of the application (which is needed for import-in-the-middle).
8282
* See: https://nodejs.org/api/module.html#enabling
8383
*/
84-
export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile: string): void {
84+
export function addDynamicImportEntryFileWrapper(
85+
nitro: Nitro,
86+
serverConfigFile: string,
87+
moduleOptions: Omit<SentryNuxtModuleOptions, 'asyncFunctionReExports'> &
88+
Required<Pick<SentryNuxtModuleOptions, 'asyncFunctionReExports'>>,
89+
): void {
8590
if (!nitro.options.rollupConfig) {
8691
nitro.options.rollupConfig = { output: {} };
8792
}
@@ -95,7 +100,10 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
95100

96101
nitro.options.rollupConfig.plugins.push(
97102
// @ts-expect-error - This is the correct type, but it shows an error because of two different definitions
98-
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
103+
wrapEntryWithDynamicImport({
104+
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
105+
asyncFunctionReExports: moduleOptions.asyncFunctionReExports,
106+
}),
99107
);
100108
}
101109

@@ -104,7 +112,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
104112
* by using a regular `import` and load the server after that.
105113
* This also works with serverless `handler` functions, as it re-exports the `handler`.
106114
*/
107-
function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPluginOption {
115+
function wrapEntryWithDynamicImport({
116+
resolvedSentryConfigPath,
117+
asyncFunctionReExports,
118+
debug,
119+
}: { resolvedSentryConfigPath: string; asyncFunctionReExports: string[]; debug?: boolean }): InputPluginOption {
108120
return {
109121
name: 'sentry-wrap-entry-with-dynamic-import',
110122
async resolveId(source, importer, options) {
@@ -130,17 +142,28 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
130142

131143
moduleInfo.moduleSideEffects = true;
132144

133-
// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './firebase-gen-1.mjs': [ 'server' ] }`
145+
// `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }`
134146
// The key `.` refers to exports within the current file, while other keys show from where exports were imported first.
135-
const exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));
147+
const functionsToExport = flatten(Object.values(moduleInfo.exportedBindings || {})).filter(functionName =>
148+
asyncFunctionReExports.includes(functionName),
149+
);
150+
151+
if (debug && functionsToExport.length === 0) {
152+
consoleSandbox(() =>
153+
// eslint-disable-next-line no-console
154+
console.warn(
155+
"[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`.",
156+
),
157+
);
158+
}
136159

137160
// 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
138161
return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)
139162
? resolution.id
140163
: resolution.id
141164
// 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)
142165
.concat(SENTRY_WRAPPED_ENTRY)
143-
.concat(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
166+
.concat(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '')
144167
.concat(QUERY_END_INDICATOR);
145168
}
146169
return null;
@@ -150,7 +173,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
150173
const entryId = removeSentryQueryFromPath(id);
151174

152175
// Mostly useful for serverless `handler` functions
153-
const reExportedFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT)
176+
const reExportedAsyncFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT)
154177
? constructFunctionReExport(id, entryId)
155178
: '';
156179

@@ -162,7 +185,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
162185
`import(${JSON.stringify(entryId)});\n` +
163186
// 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()`.
164187
"import 'import-in-the-middle/hook.mjs';\n" +
165-
`${reExportedFunctions}\n`
188+
`${reExportedAsyncFunctions}\n`
166189
);
167190
}
168191

packages/nuxt/src/vite/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ export function constructFunctionReExport(pathWithQuery: string, entryId: string
7070
const functionNames = extractFunctionReexportQueryParameters(pathWithQuery);
7171

7272
return functionNames.reduce(
73-
(functionsCode, currFunctionName) =>
73+
(functionsCode, currFunctionName, idx) =>
7474
functionsCode.concat(
75-
'async function reExport(...args) {\n' +
75+
`async function reExport${idx}(...args) {\n` +
7676
` const res = await import(${JSON.stringify(entryId)});\n` +
7777
` return res.${currFunctionName}.call(this, ...args);\n` +
7878
'}\n' +
79-
`export { reExport as ${currFunctionName} };\n`,
79+
`export { reExport${idx} as ${currFunctionName} };\n`,
8080
),
8181
'',
8282
);

packages/nuxt/test/vite/utils.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,16 @@ describe('constructFunctionReExport', () => {
111111
const result2 = constructFunctionReExport(query2, entryId);
112112

113113
const expected = `
114-
async function reExport(...args) {
114+
async function reExport0(...args) {
115115
const res = await import("./module");
116116
return res.foo.call(this, ...args);
117117
}
118-
export { reExport as foo };
119-
async function reExport(...args) {
118+
export { reExport0 as foo };
119+
async function reExport1(...args) {
120120
const res = await import("./module");
121121
return res.bar.call(this, ...args);
122122
}
123-
export { reExport as bar };
123+
export { reExport1 as bar };
124124
`;
125125
expect(result.trim()).toBe(expected.trim());
126126
expect(result2.trim()).toBe(expected.trim());
@@ -132,11 +132,11 @@ export { reExport as bar };
132132
const result = constructFunctionReExport(query, entryId);
133133

134134
const expected = `
135-
async function reExport(...args) {
135+
async function reExport0(...args) {
136136
const res = await import("./index");
137137
return res.default.call(this, ...args);
138138
}
139-
export { reExport as default };
139+
export { reExport0 as default };
140140
`;
141141
expect(result.trim()).toBe(expected.trim());
142142
});

0 commit comments

Comments
 (0)