Skip to content

Commit bb4595c

Browse files
authored
feat(nextjs): Allow for TypeScript user config files (#3847)
TL;DR: If the user provides `sentry.server.config.ts` and `sentry.client.config.ts` (rather than `.js`), we will now find and use them.
1 parent b89b086 commit bb4595c

File tree

3 files changed

+113
-20
lines changed

3 files changed

+113
-20
lines changed

packages/nextjs/src/config/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export type WebpackConfigObject = {
4343
};
4444

4545
// Information about the current build environment
46-
export type BuildContext = { dev: boolean; isServer: boolean; buildId: string };
46+
export type BuildContext = { dev: boolean; isServer: boolean; buildId: string; dir: string };
4747

4848
/**
4949
* Webpack `entry` config

packages/nextjs/src/config/webpack.ts

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { getSentryRelease } from '@sentry/node';
22
import { dropUndefinedKeys, logger } from '@sentry/utils';
33
import * as SentryWebpackPlugin from '@sentry/webpack-plugin';
4+
import * as fs from 'fs';
5+
import * as path from 'path';
46

57
import {
68
BuildContext,
@@ -19,9 +21,6 @@ export { SentryWebpackPlugin };
1921
// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
2022
// TODO: drop merged keys from override check? `includeDefaults` option?
2123

22-
export const CLIENT_SDK_CONFIG_FILE = './sentry.client.config.js';
23-
export const SERVER_SDK_CONFIG_FILE = './sentry.server.config.js';
24-
2524
const defaultSentryWebpackPluginOptions = dropUndefinedKeys({
2625
url: process.env.SENTRY_URL,
2726
org: process.env.SENTRY_ORG,
@@ -132,17 +131,40 @@ async function addSentryToEntryProperty(
132131
const newEntryProperty =
133132
typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty };
134133

135-
const userConfigFile = buildContext.isServer ? SERVER_SDK_CONFIG_FILE : CLIENT_SDK_CONFIG_FILE;
134+
const userConfigFile = buildContext.isServer
135+
? getUserConfigFile(buildContext.dir, 'server')
136+
: getUserConfigFile(buildContext.dir, 'client');
136137

137138
for (const entryPointName in newEntryProperty) {
138139
if (entryPointName === 'pages/_app' || entryPointName.includes('pages/api')) {
139-
addFileToExistingEntryPoint(newEntryProperty, entryPointName, userConfigFile);
140+
// we need to turn the filename into a path so webpack can find it
141+
addFileToExistingEntryPoint(newEntryProperty, entryPointName, `./${userConfigFile}`);
140142
}
141143
}
142144

143145
return newEntryProperty;
144146
}
145147

148+
/**
149+
* Search the project directory for a valid user config file for the given platform, allowing for it to be either a
150+
* TypeScript or JavaScript file.
151+
*
152+
* @param projectDir The root directory of the project, where the file should be located
153+
* @param platform Either "server" or "client", so that we know which file to look for
154+
* @returns The name of the relevant file. If no file is found, this method throws an error.
155+
*/
156+
export function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string {
157+
const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`];
158+
159+
for (const filename of possibilities) {
160+
if (fs.existsSync(path.resolve(projectDir, filename))) {
161+
return filename;
162+
}
163+
}
164+
165+
throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`);
166+
}
167+
146168
/**
147169
* Add a file to a specific element of the given `entry` webpack config property.
148170
*

packages/nextjs/test/config.test.ts

+85-14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import * as rimraf from 'rimraf';
5+
16
import { withSentryConfig } from '../src/config';
27
import {
38
BuildContext,
@@ -7,12 +12,24 @@ import {
712
SentryWebpackPluginOptions,
813
WebpackConfigObject,
914
} from '../src/config/types';
10-
import {
11-
CLIENT_SDK_CONFIG_FILE,
12-
constructWebpackConfigFunction,
13-
SentryWebpackPlugin,
14-
SERVER_SDK_CONFIG_FILE,
15-
} from '../src/config/webpack';
15+
import { constructWebpackConfigFunction, getUserConfigFile, SentryWebpackPlugin } from '../src/config/webpack';
16+
17+
const SERVER_SDK_CONFIG_FILE = 'sentry.server.config.js';
18+
const CLIENT_SDK_CONFIG_FILE = 'sentry.client.config.js';
19+
20+
// We use `fs.existsSync()` in `getUserConfigFile()`. When we're not testing `getUserConfigFile()` specifically, all we
21+
// need is for it to give us any valid answer, so make it always find what it's looking for. Since this is a core node
22+
// built-in, though, which jest itself uses, otherwise let it do the normal thing. Storing the real version of the
23+
// function also lets us restore the original when we do want to test `getUserConfigFile()`.
24+
const realExistsSync = jest.requireActual('fs').existsSync;
25+
const mockExistsSync = (path: fs.PathLike) => {
26+
if ((path as string).endsWith(SERVER_SDK_CONFIG_FILE) || (path as string).endsWith(CLIENT_SDK_CONFIG_FILE)) {
27+
return true;
28+
}
29+
30+
return realExistsSync(path);
31+
};
32+
const exitsSync = jest.spyOn(fs, 'existsSync').mockImplementation(mockExistsSync);
1633

1734
/** Mocks of the arguments passed to `withSentryConfig` */
1835
const userNextConfig = {
@@ -63,8 +80,13 @@ const clientWebpackConfig = {
6380
target: 'web',
6481
context: '/Users/Maisey/projects/squirrelChasingSimulator',
6582
};
66-
const serverBuildContext = { isServer: true, dev: false, buildId: 'doGsaREgReaT' };
67-
const clientBuildContext = { isServer: false, dev: false, buildId: 'doGsaREgReaT' };
83+
const baseBuildContext = {
84+
dev: false,
85+
buildId: 'doGsaREgReaT',
86+
dir: '/Users/Maisey/projects/squirrelChasingSimulator',
87+
};
88+
const serverBuildContext = { isServer: true, ...baseBuildContext };
89+
const clientBuildContext = { isServer: false, ...baseBuildContext };
6890

6991
/**
7092
* Derive the final values of all next config options, by first applying `withSentryConfig` and then, if it returns a
@@ -223,6 +245,9 @@ describe('webpack config', () => {
223245
});
224246

225247
describe('webpack `entry` property config', () => {
248+
const serverConfigFilePath = `./${SERVER_SDK_CONFIG_FILE}`;
249+
const clientConfigFilePath = `./${CLIENT_SDK_CONFIG_FILE}`;
250+
226251
it('handles various entrypoint shapes', async () => {
227252
const finalWebpackConfig = await materializeFinalWebpackConfig({
228253
userNextConfig,
@@ -234,23 +259,23 @@ describe('webpack config', () => {
234259
expect.objectContaining({
235260
// original entry point value is a string
236261
// (was 'private-next-pages/api/dogs/[name].js')
237-
'pages/api/dogs/[name]': [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/dogs/[name].js'],
262+
'pages/api/dogs/[name]': [serverConfigFilePath, 'private-next-pages/api/dogs/[name].js'],
238263

239264
// original entry point value is a string array
240265
// (was ['./node_modules/smellOVision/index.js', 'private-next-pages/_app.js'])
241-
'pages/_app': [SERVER_SDK_CONFIG_FILE, './node_modules/smellOVision/index.js', 'private-next-pages/_app.js'],
266+
'pages/_app': [serverConfigFilePath, './node_modules/smellOVision/index.js', 'private-next-pages/_app.js'],
242267

243268
// original entry point value is an object containing a string `import` value
244269
// (`import` was 'private-next-pages/api/simulator/dogStats/[name].js')
245270
'pages/api/simulator/dogStats/[name]': {
246-
import: [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/simulator/dogStats/[name].js'],
271+
import: [serverConfigFilePath, 'private-next-pages/api/simulator/dogStats/[name].js'],
247272
},
248273

249274
// original entry point value is an object containing a string array `import` value
250275
// (`import` was ['./node_modules/dogPoints/converter.js', 'private-next-pages/api/simulator/leaderboard.js'])
251276
'pages/api/simulator/leaderboard': {
252277
import: [
253-
SERVER_SDK_CONFIG_FILE,
278+
serverConfigFilePath,
254279
'./node_modules/dogPoints/converter.js',
255280
'private-next-pages/api/simulator/leaderboard.js',
256281
],
@@ -259,7 +284,7 @@ describe('webpack config', () => {
259284
// original entry point value is an object containg properties besides `import`
260285
// (`dependOn` remains untouched)
261286
'pages/api/tricks/[trickName]': {
262-
import: [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/tricks/[trickName].js'],
287+
import: [serverConfigFilePath, 'private-next-pages/api/tricks/[trickName].js'],
263288
dependOn: 'treats',
264289
},
265290
}),
@@ -278,7 +303,7 @@ describe('webpack config', () => {
278303
// no injected file
279304
main: './src/index.ts',
280305
// was 'next-client-pages-loader?page=%2F_app'
281-
'pages/_app': [CLIENT_SDK_CONFIG_FILE, 'next-client-pages-loader?page=%2F_app'],
306+
'pages/_app': [clientConfigFilePath, 'next-client-pages-loader?page=%2F_app'],
282307
}),
283308
);
284309
});
@@ -340,4 +365,50 @@ describe('Sentry webpack plugin config', () => {
340365

341366
expect(finalWebpackConfig?.devtool).not.toEqual('source-map');
342367
});
368+
369+
describe('getUserConfigFile', () => {
370+
let tempDir: string;
371+
372+
beforeAll(() => {
373+
exitsSync.mockImplementation(realExistsSync);
374+
});
375+
376+
beforeEach(() => {
377+
const tempDirPathPrefix = path.join(os.tmpdir(), 'sentry-nextjs-test-');
378+
tempDir = fs.mkdtempSync(tempDirPathPrefix);
379+
});
380+
381+
afterEach(() => {
382+
rimraf.sync(tempDir);
383+
});
384+
385+
afterAll(() => {
386+
exitsSync.mockImplementation(mockExistsSync);
387+
});
388+
389+
it('successfully finds js files', () => {
390+
fs.writeFileSync(path.resolve(tempDir, 'sentry.server.config.js'), 'Dogs are great!');
391+
fs.writeFileSync(path.resolve(tempDir, 'sentry.client.config.js'), 'Squirrel!');
392+
393+
expect(getUserConfigFile(tempDir, 'server')).toEqual('sentry.server.config.js');
394+
expect(getUserConfigFile(tempDir, 'client')).toEqual('sentry.client.config.js');
395+
});
396+
397+
it('successfully finds ts files', () => {
398+
fs.writeFileSync(path.resolve(tempDir, 'sentry.server.config.ts'), 'Sit. Stay. Lie Down.');
399+
fs.writeFileSync(path.resolve(tempDir, 'sentry.client.config.ts'), 'Good dog!');
400+
401+
expect(getUserConfigFile(tempDir, 'server')).toEqual('sentry.server.config.ts');
402+
expect(getUserConfigFile(tempDir, 'client')).toEqual('sentry.client.config.ts');
403+
});
404+
405+
it('errors when files are missing', () => {
406+
expect(() => getUserConfigFile(tempDir, 'server')).toThrowError(
407+
`Cannot find 'sentry.server.config.ts' or 'sentry.server.config.js' in '${tempDir}'`,
408+
);
409+
expect(() => getUserConfigFile(tempDir, 'client')).toThrowError(
410+
`Cannot find 'sentry.client.config.ts' or 'sentry.client.config.js' in '${tempDir}'`,
411+
);
412+
});
413+
});
343414
});

0 commit comments

Comments
 (0)