Skip to content

Commit 6bc37f0

Browse files
authored
feat(nuxt): Make dynamic import() wrapping default (#13958)
BREAKING CHANGE: The `--import` flag must not be added anymore. If it is still set, the server-side will be initialized twice and this leads to unexpected errors. --- First merge this: #13945 Part of this: #13943 This PR makes it the default to include a rollup plugin that wraps the server entry file with a dynamic import (`import()`). This is a replacement for the node `--import` CLI flag. If you still want to manually add the CLI flag you can use this option in the `nuxt.config.ts` file: ```js sentry: { dynamicImportForServerEntry: false, } ``` (option name is up for discussion)
1 parent 8782af8 commit 6bc37f0

File tree

7 files changed

+74
-84
lines changed

7 files changed

+74
-84
lines changed

dev-packages/e2e-tests/test-applications/nuxt-3/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dev": "nuxt dev",
88
"generate": "nuxt generate",
99
"preview": "nuxt preview",
10-
"start": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs",
10+
"start": "node .output/server/index.mjs",
1111
"clean": "npx nuxi cleanup",
1212
"test": "playwright test",
1313
"test:build": "pnpm install && npx playwright install && pnpm build",

dev-packages/e2e-tests/test-applications/nuxt-4/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dev": "nuxt dev",
88
"generate": "nuxt generate",
99
"preview": "nuxt preview",
10-
"start": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs",
10+
"start": "node .output/server/index.mjs",
1111
"clean": "npx nuxi cleanup",
1212
"test": "playwright test",
1313
"test:build": "pnpm install && npx playwright install && pnpm build",

packages/nuxt/src/common/types.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,19 @@ export type SentryNuxtModuleOptions = {
103103
debug?: boolean;
104104

105105
/**
106-
* Enabling basic server tracing can be used for environments where modifying the node option `--import` is not possible.
107-
* However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.).
106+
* Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register
107+
* necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling)
108108
*
109-
* If this option is `true`, the Sentry SDK will import the Sentry server config at the top of the server entry file to load the SDK on the server.
109+
* If this option is `false`, the Sentry SDK won't wrap the server entry file with `import()`. Not wrapping the
110+
* server entry file will disable Sentry on the server-side. When you set this option to `false`, make sure
111+
* to add the Sentry server config with the node `--import` CLI flag to enable Sentry on the server-side.
110112
*
111-
* **DO NOT** enable this option if you've already added the node option `--import` in your node start script. This would initialize Sentry twice on the server-side and leads to unexpected issues.
113+
* **DO NOT** add the node CLI flag `--import` in your node start script, when `dynamicImportForServerEntry` is set to `true` (default).
114+
* This would initialize Sentry twice on the server-side and this leads to unexpected issues.
112115
*
113-
* @default false
116+
* @default true
114117
*/
115-
experimental_basicServerTracing?: boolean;
118+
dynamicImportForServerEntry?: boolean;
116119

117120
/**
118121
* 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.

packages/nuxt/src/module.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as path from 'path';
22
import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit';
33
import { consoleSandbox } from '@sentry/utils';
44
import type { SentryNuxtModuleOptions } from './common/types';
5-
import { addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig';
5+
import { addDynamicImportEntryFileWrapper, addServerConfigToBuild } from './vite/addServerConfig';
66
import { setupSourceMaps } from './vite/sourceMaps';
77
import { findDefaultSdkInitFile } from './vite/utils';
88

@@ -17,7 +17,12 @@ export default defineNuxtModule<ModuleOptions>({
1717
},
1818
},
1919
defaults: {},
20-
setup(moduleOptions, nuxt) {
20+
setup(moduleOptionsParam, nuxt) {
21+
const moduleOptions = {
22+
...moduleOptionsParam,
23+
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
24+
};
25+
2126
const moduleDirResolver = createResolver(import.meta.url);
2227
const buildDirResolver = createResolver(nuxt.options.buildDir);
2328

@@ -48,15 +53,17 @@ export default defineNuxtModule<ModuleOptions>({
4853
const serverConfigFile = findDefaultSdkInitFile('server');
4954

5055
if (serverConfigFile) {
51-
// Inject the server-side Sentry config file with a side effect import
52-
addPluginTemplate({
53-
mode: 'server',
54-
filename: 'sentry-server-config.mjs',
55-
getContents: () =>
56-
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` +
57-
'import { defineNuxtPlugin } from "#imports"\n' +
58-
'export default defineNuxtPlugin(() => {})',
59-
});
56+
if (moduleOptions.dynamicImportForServerEntry === false) {
57+
// Inject the server-side Sentry config file with a side effect import
58+
addPluginTemplate({
59+
mode: 'server',
60+
filename: 'sentry-server-config.mjs',
61+
getContents: () =>
62+
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` +
63+
'import { defineNuxtPlugin } from "#imports"\n' +
64+
'export default defineNuxtPlugin(() => {})',
65+
});
66+
}
6067

6168
addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server'));
6269
}
@@ -67,11 +74,9 @@ export default defineNuxtModule<ModuleOptions>({
6774

6875
nuxt.hooks.hook('nitro:init', nitro => {
6976
if (serverConfigFile && serverConfigFile.includes('.server.config')) {
70-
addServerConfigToBuild(moduleOptions, nuxt, nitro, serverConfigFile);
77+
if (moduleOptions.dynamicImportForServerEntry === false) {
78+
addServerConfigToBuild(moduleOptions, nuxt, nitro, serverConfigFile);
7179

72-
if (moduleOptions.experimental_basicServerTracing) {
73-
addSentryTopImport(moduleOptions, nitro);
74-
} else {
7580
if (moduleOptions.debug) {
7681
const serverDirResolver = createResolver(nitro.options.output.serverDir);
7782
const serverConfigPath = serverDirResolver.resolve('sentry.server.config.mjs');
@@ -86,6 +91,17 @@ export default defineNuxtModule<ModuleOptions>({
8691
);
8792
});
8893
}
94+
} else {
95+
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
96+
97+
if (moduleOptions.debug) {
98+
consoleSandbox(() => {
99+
// eslint-disable-next-line no-console
100+
console.log(
101+
'[Sentry] Wrapping the server entry file with a dynamic `import()`, so Sentry can be preloaded before the server initializes.',
102+
);
103+
});
104+
}
89105
}
90106
}
91107
});

packages/nuxt/src/vite/addServerConfig.ts

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -74,54 +74,6 @@ export function addServerConfigToBuild(
7474
});
7575
}
7676

77-
/**
78-
* Adds the Sentry server config import at the top of the server entry file to load the SDK on the server.
79-
* This is necessary for environments where modifying the node option `--import` is not possible.
80-
* However, only limited tracing instrumentation is supported when doing this.
81-
*/
82-
export function addSentryTopImport(moduleOptions: SentryNuxtModuleOptions, nitro: Nitro): void {
83-
nitro.hooks.hook('close', () => {
84-
// other presets ('node-server' or 'vercel') have an index.mjs
85-
const presetsWithServerFile = ['netlify'];
86-
const entryFileName =
87-
typeof nitro.options.rollupConfig?.output.entryFileNames === 'string'
88-
? nitro.options.rollupConfig?.output.entryFileNames
89-
: presetsWithServerFile.includes(nitro.options.preset)
90-
? 'server.mjs'
91-
: 'index.mjs';
92-
93-
const serverDirResolver = createResolver(nitro.options.output.serverDir);
94-
const entryFilePath = serverDirResolver.resolve(entryFileName);
95-
96-
try {
97-
fs.readFile(entryFilePath, 'utf8', (err, data) => {
98-
const updatedContent = `import './${SERVER_CONFIG_FILENAME}.mjs';\n${data}`;
99-
100-
fs.writeFile(entryFilePath, updatedContent, 'utf8', () => {
101-
if (moduleOptions.debug) {
102-
consoleSandbox(() => {
103-
// eslint-disable-next-line no-console
104-
console.log(
105-
`[Sentry] Successfully added the Sentry import to the server entry file "\`${entryFilePath}\`"`,
106-
);
107-
});
108-
}
109-
});
110-
});
111-
} catch (err) {
112-
if (moduleOptions.debug) {
113-
consoleSandbox(() => {
114-
// eslint-disable-next-line no-console
115-
console.warn(
116-
`[Sentry] An error occurred when trying to add the Sentry import to the server entry file "\`${entryFilePath}\`":`,
117-
err,
118-
);
119-
});
120-
}
121-
}
122-
});
123-
}
124-
12577
/**
12678
* This function modifies the Rollup configuration to include a plugin that wraps the entry file with a dynamic import (`import()`)
12779
* and adds the Sentry server config with the static `import` declaration.
@@ -187,11 +139,8 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
187139
: resolution.id
188140
// 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)
189141
.concat(SENTRY_WRAPPED_ENTRY)
190-
.concat(
191-
exportedFunctions?.length
192-
? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')).concat(QUERY_END_INDICATOR)
193-
: '',
194-
);
142+
.concat(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
143+
.concat(QUERY_END_INDICATOR);
195144
}
196145
return null;
197146
},
@@ -211,7 +160,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
211160
// `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling)
212161
`import(${JSON.stringify(entryId)});\n` +
213162
// 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()`.
214-
"import 'import-in-the-middle/hook.mjs'\n" +
163+
"import 'import-in-the-middle/hook.mjs';\n" +
215164
`${reExportedFunctions}\n`
216165
);
217166
}

packages/nuxt/src/vite/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function extractFunctionReexportQueryParameters(query: string): string[]
5757
return match && match[1]
5858
? match[1]
5959
.split(',')
60-
.filter(param => param !== '' && param !== 'default')
60+
.filter(param => param !== '')
6161
// Sanitize, as code could be injected with another rollup plugin
6262
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
6363
: [];
@@ -72,10 +72,11 @@ export function constructFunctionReExport(pathWithQuery: string, entryId: string
7272
return functionNames.reduce(
7373
(functionsCode, currFunctionName) =>
7474
functionsCode.concat(
75-
`export async function ${currFunctionName}(...args) {\n` +
75+
'async function reExport(...args) {\n' +
7676
` const res = await import(${JSON.stringify(entryId)});\n` +
7777
` return res.${currFunctionName}.call(this, ...args);\n` +
78-
'}\n',
78+
'}\n' +
79+
`export { reExport as ${currFunctionName} };\n`,
7980
),
8081
'',
8182
);

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ describe('findDefaultSdkInitFile', () => {
7171
describe('removeSentryQueryFromPath', () => {
7272
it('strips the Sentry query part from the path', () => {
7373
const url = `/example/path${SENTRY_WRAPPED_ENTRY}${SENTRY_FUNCTIONS_REEXPORT}foo,${QUERY_END_INDICATOR}`;
74+
const url2 = `/example/path${SENTRY_WRAPPED_ENTRY}${QUERY_END_INDICATOR}`;
7475
const result = removeSentryQueryFromPath(url);
76+
const result2 = removeSentryQueryFromPath(url2);
7577
expect(result).toBe('/example/path');
78+
expect(result2).toBe('/example/path');
7679
});
7780

7881
it('returns the same path if the specific query part is not present', () => {
@@ -85,7 +88,7 @@ describe('removeSentryQueryFromPath', () => {
8588
describe('extractFunctionReexportQueryParameters', () => {
8689
it.each([
8790
[`${SENTRY_FUNCTIONS_REEXPORT}foo,bar,${QUERY_END_INDICATOR}`, ['foo', 'bar']],
88-
[`${SENTRY_FUNCTIONS_REEXPORT}foo,bar,default${QUERY_END_INDICATOR}`, ['foo', 'bar']],
91+
[`${SENTRY_FUNCTIONS_REEXPORT}foo,bar,default${QUERY_END_INDICATOR}`, ['foo', 'bar', 'default']],
8992
[
9093
`${SENTRY_FUNCTIONS_REEXPORT}foo,a.b*c?d[e]f(g)h|i\\\\j(){hello},${QUERY_END_INDICATOR}`,
9194
['foo', 'a\\.b\\*c\\?d\\[e\\]f\\(g\\)h\\|i\\\\\\\\j\\(\\)\\{hello\\}'],
@@ -108,18 +111,36 @@ describe('constructFunctionReExport', () => {
108111
const result2 = constructFunctionReExport(query2, entryId);
109112

110113
const expected = `
111-
export async function foo(...args) {
114+
async function reExport(...args) {
112115
const res = await import("./module");
113116
return res.foo.call(this, ...args);
114117
}
115-
export async function bar(...args) {
118+
export { reExport as foo };
119+
async function reExport(...args) {
116120
const res = await import("./module");
117121
return res.bar.call(this, ...args);
118-
}`;
122+
}
123+
export { reExport as bar };
124+
`;
119125
expect(result.trim()).toBe(expected.trim());
120126
expect(result2.trim()).toBe(expected.trim());
121127
});
122128

129+
it('constructs re-export code for a "default" query parameters and entry ID', () => {
130+
const query = `${SENTRY_FUNCTIONS_REEXPORT}default${QUERY_END_INDICATOR}}`;
131+
const entryId = './index';
132+
const result = constructFunctionReExport(query, entryId);
133+
134+
const expected = `
135+
async function reExport(...args) {
136+
const res = await import("./index");
137+
return res.default.call(this, ...args);
138+
}
139+
export { reExport as default };
140+
`;
141+
expect(result.trim()).toBe(expected.trim());
142+
});
143+
123144
it('returns an empty string if the query string is empty', () => {
124145
const query = '';
125146
const entryId = './module';

0 commit comments

Comments
 (0)