Skip to content

Commit f2f75d7

Browse files
committed
feat(nuxt): Add asyncFunctionReExports to define re-exported server functions
1 parent d2a9826 commit f2f75d7

File tree

5 files changed

+55
-18
lines changed

5 files changed

+55
-18
lines changed

packages/nuxt/src/common/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ export type SentryNuxtModuleOptions = {
117117
*/
118118
dynamicImportForServerEntry?: boolean;
119119

120+
/**
121+
* The `asyncFunctionReExports` option is only relevant when `dynamicImportForServerEntry: true` (default value).
122+
*
123+
* As the server entry file is wrapped with a dynamic `import()`, previous async function exports need to be re-exported.
124+
* The SDK detects and re-exports those exports (mostly serverless functions). This is why they are re-exported as async functions.
125+
* In case you have a custom setup and your server exports other async functions, you can override the default array with this option.
126+
*
127+
* @default ['default', 'handler', 'server']
128+
*/
129+
asyncFunctionReExports?: string[];
130+
120131
/**
121132
* 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.
122133
* 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

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export default defineNuxtModule<ModuleOptions>({
2121
const moduleOptions = {
2222
...moduleOptionsParam,
2323
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
24+
asyncFunctionReExports: moduleOptionsParam.asyncFunctionReExports
25+
? moduleOptionsParam.asyncFunctionReExports
26+
: ['default', 'handler', 'server'],
2427
};
2528

2629
const moduleDirResolver = createResolver(import.meta.url);
@@ -101,7 +104,7 @@ export default defineNuxtModule<ModuleOptions>({
101104
});
102105
}
103106
} else {
104-
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
107+
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);
105108

106109
if (moduleOptions.debug) {
107110
consoleSandbox(() => {

packages/nuxt/src/vite/addServerConfig.ts

+31-8
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
}
@@ -94,7 +99,10 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
9499
}
95100

96101
nitro.options.rollupConfig.plugins.push(
97-
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
102+
wrapEntryWithDynamicImport({
103+
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
104+
asyncFunctionReExports: moduleOptions.asyncFunctionReExports,
105+
}),
98106
);
99107
}
100108

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

130142
moduleInfo.moduleSideEffects = true;
131143

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

136159
// 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
137160
return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)
138161
? resolution.id
139162
: resolution.id
140163
// 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)
141164
.concat(SENTRY_WRAPPED_ENTRY)
142-
.concat(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
165+
.concat(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '')
143166
.concat(QUERY_END_INDICATOR);
144167
}
145168
return null;
@@ -149,7 +172,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
149172
const entryId = removeSentryQueryFromPath(id);
150173

151174
// Mostly useful for serverless `handler` functions
152-
const reExportedFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT)
175+
const reExportedAsyncFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT)
153176
? constructFunctionReExport(id, entryId)
154177
: '';
155178

@@ -161,7 +184,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
161184
`import(${JSON.stringify(entryId)});\n` +
162185
// 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()`.
163186
"import 'import-in-the-middle/hook.mjs';\n" +
164-
`${reExportedFunctions}\n`
187+
`${reExportedAsyncFunctions}\n`
165188
);
166189
}
167190

packages/nuxt/src/vite/utils.ts

+3-3
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

+6-6
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)