Skip to content

Commit

Permalink
feat(nuxt): Add asyncFunctionReExports to define re-exported server…
Browse files Browse the repository at this point in the history
… functions
  • Loading branch information
s1gr1d committed Oct 28, 2024
1 parent 2cf3ef7 commit 031e219
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 18 deletions.
11 changes: 11 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,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.
Expand Down
5 changes: 4 additions & 1 deletion packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
asyncFunctionReExports: moduleOptionsParam.asyncFunctionReExports
? moduleOptionsParam.asyncFunctionReExports
: ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand Down Expand Up @@ -101,7 +104,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 31 additions & 8 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SentryNuxtModuleOptions, 'asyncFunctionReExports'> &
Required<Pick<SentryNuxtModuleOptions, 'asyncFunctionReExports'>>,
): void {
if (!nitro.options.rollupConfig) {
nitro.options.rollupConfig = { output: {} };
}
Expand All @@ -95,7 +100,10 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:

nitro.options.rollupConfig.plugins.push(
// @ts-expect-error - This is the correct type, but it shows an error because of two different definitions
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
asyncFunctionReExports: moduleOptions.asyncFunctionReExports,
}),
);
}

Expand All @@ -104,7 +112,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) {
Expand All @@ -130,17 +142,28 @@ 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}`)
? 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '')
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All @@ -150,7 +173,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)
: '';

Expand All @@ -162,7 +185,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`
);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
),
'',
);
Expand Down
12 changes: 6 additions & 6 deletions packages/nuxt/test/vite/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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());
});
Expand Down

0 comments on commit 031e219

Please sign in to comment.