diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 2fabbd856b91..21017abec545 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -43,7 +43,7 @@ export type WebpackConfigObject = { }; // Information about the current build environment -export type BuildContext = { dev: boolean; isServer: boolean; buildId: string }; +export type BuildContext = { dev: boolean; isServer: boolean; buildId: string; dir: string }; /** * Webpack `entry` config diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 9e7650412766..5545d40b50c0 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -1,6 +1,8 @@ import { getSentryRelease } from '@sentry/node'; import { dropUndefinedKeys, logger } from '@sentry/utils'; import * as SentryWebpackPlugin from '@sentry/webpack-plugin'; +import * as fs from 'fs'; +import * as path from 'path'; import { BuildContext, @@ -19,9 +21,6 @@ export { SentryWebpackPlugin }; // TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include // TODO: drop merged keys from override check? `includeDefaults` option? -export const CLIENT_SDK_CONFIG_FILE = './sentry.client.config.js'; -export const SERVER_SDK_CONFIG_FILE = './sentry.server.config.js'; - const defaultSentryWebpackPluginOptions = dropUndefinedKeys({ url: process.env.SENTRY_URL, org: process.env.SENTRY_ORG, @@ -132,17 +131,40 @@ async function addSentryToEntryProperty( const newEntryProperty = typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty }; - const userConfigFile = buildContext.isServer ? SERVER_SDK_CONFIG_FILE : CLIENT_SDK_CONFIG_FILE; + const userConfigFile = buildContext.isServer + ? getUserConfigFile(buildContext.dir, 'server') + : getUserConfigFile(buildContext.dir, 'client'); for (const entryPointName in newEntryProperty) { if (entryPointName === 'pages/_app' || entryPointName.includes('pages/api')) { - addFileToExistingEntryPoint(newEntryProperty, entryPointName, userConfigFile); + // we need to turn the filename into a path so webpack can find it + addFileToExistingEntryPoint(newEntryProperty, entryPointName, `./${userConfigFile}`); } } return newEntryProperty; } +/** + * Search the project directory for a valid user config file for the given platform, allowing for it to be either a + * TypeScript or JavaScript file. + * + * @param projectDir The root directory of the project, where the file should be located + * @param platform Either "server" or "client", so that we know which file to look for + * @returns The name of the relevant file. If no file is found, this method throws an error. + */ +export function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string { + const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; + + for (const filename of possibilities) { + if (fs.existsSync(path.resolve(projectDir, filename))) { + return filename; + } + } + + throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); +} + /** * Add a file to a specific element of the given `entry` webpack config property. * diff --git a/packages/nextjs/test/config.test.ts b/packages/nextjs/test/config.test.ts index efc465e70108..90e3cc5a6e47 100644 --- a/packages/nextjs/test/config.test.ts +++ b/packages/nextjs/test/config.test.ts @@ -1,3 +1,8 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; + import { withSentryConfig } from '../src/config'; import { BuildContext, @@ -7,12 +12,24 @@ import { SentryWebpackPluginOptions, WebpackConfigObject, } from '../src/config/types'; -import { - CLIENT_SDK_CONFIG_FILE, - constructWebpackConfigFunction, - SentryWebpackPlugin, - SERVER_SDK_CONFIG_FILE, -} from '../src/config/webpack'; +import { constructWebpackConfigFunction, getUserConfigFile, SentryWebpackPlugin } from '../src/config/webpack'; + +const SERVER_SDK_CONFIG_FILE = 'sentry.server.config.js'; +const CLIENT_SDK_CONFIG_FILE = 'sentry.client.config.js'; + +// We use `fs.existsSync()` in `getUserConfigFile()`. When we're not testing `getUserConfigFile()` specifically, all we +// 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 +// built-in, though, which jest itself uses, otherwise let it do the normal thing. Storing the real version of the +// function also lets us restore the original when we do want to test `getUserConfigFile()`. +const realExistsSync = jest.requireActual('fs').existsSync; +const mockExistsSync = (path: fs.PathLike) => { + if ((path as string).endsWith(SERVER_SDK_CONFIG_FILE) || (path as string).endsWith(CLIENT_SDK_CONFIG_FILE)) { + return true; + } + + return realExistsSync(path); +}; +const exitsSync = jest.spyOn(fs, 'existsSync').mockImplementation(mockExistsSync); /** Mocks of the arguments passed to `withSentryConfig` */ const userNextConfig = { @@ -63,8 +80,13 @@ const clientWebpackConfig = { target: 'web', context: '/Users/Maisey/projects/squirrelChasingSimulator', }; -const serverBuildContext = { isServer: true, dev: false, buildId: 'doGsaREgReaT' }; -const clientBuildContext = { isServer: false, dev: false, buildId: 'doGsaREgReaT' }; +const baseBuildContext = { + dev: false, + buildId: 'doGsaREgReaT', + dir: '/Users/Maisey/projects/squirrelChasingSimulator', +}; +const serverBuildContext = { isServer: true, ...baseBuildContext }; +const clientBuildContext = { isServer: false, ...baseBuildContext }; /** * 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', () => { }); describe('webpack `entry` property config', () => { + const serverConfigFilePath = `./${SERVER_SDK_CONFIG_FILE}`; + const clientConfigFilePath = `./${CLIENT_SDK_CONFIG_FILE}`; + it('handles various entrypoint shapes', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ userNextConfig, @@ -234,23 +259,23 @@ describe('webpack config', () => { expect.objectContaining({ // original entry point value is a string // (was 'private-next-pages/api/dogs/[name].js') - 'pages/api/dogs/[name]': [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/dogs/[name].js'], + 'pages/api/dogs/[name]': [serverConfigFilePath, 'private-next-pages/api/dogs/[name].js'], // original entry point value is a string array // (was ['./node_modules/smellOVision/index.js', 'private-next-pages/_app.js']) - 'pages/_app': [SERVER_SDK_CONFIG_FILE, './node_modules/smellOVision/index.js', 'private-next-pages/_app.js'], + 'pages/_app': [serverConfigFilePath, './node_modules/smellOVision/index.js', 'private-next-pages/_app.js'], // original entry point value is an object containing a string `import` value // (`import` was 'private-next-pages/api/simulator/dogStats/[name].js') 'pages/api/simulator/dogStats/[name]': { - import: [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/simulator/dogStats/[name].js'], + import: [serverConfigFilePath, 'private-next-pages/api/simulator/dogStats/[name].js'], }, // original entry point value is an object containing a string array `import` value // (`import` was ['./node_modules/dogPoints/converter.js', 'private-next-pages/api/simulator/leaderboard.js']) 'pages/api/simulator/leaderboard': { import: [ - SERVER_SDK_CONFIG_FILE, + serverConfigFilePath, './node_modules/dogPoints/converter.js', 'private-next-pages/api/simulator/leaderboard.js', ], @@ -259,7 +284,7 @@ describe('webpack config', () => { // original entry point value is an object containg properties besides `import` // (`dependOn` remains untouched) 'pages/api/tricks/[trickName]': { - import: [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/tricks/[trickName].js'], + import: [serverConfigFilePath, 'private-next-pages/api/tricks/[trickName].js'], dependOn: 'treats', }, }), @@ -278,7 +303,7 @@ describe('webpack config', () => { // no injected file main: './src/index.ts', // was 'next-client-pages-loader?page=%2F_app' - 'pages/_app': [CLIENT_SDK_CONFIG_FILE, 'next-client-pages-loader?page=%2F_app'], + 'pages/_app': [clientConfigFilePath, 'next-client-pages-loader?page=%2F_app'], }), ); }); @@ -340,4 +365,50 @@ describe('Sentry webpack plugin config', () => { expect(finalWebpackConfig?.devtool).not.toEqual('source-map'); }); + + describe('getUserConfigFile', () => { + let tempDir: string; + + beforeAll(() => { + exitsSync.mockImplementation(realExistsSync); + }); + + beforeEach(() => { + const tempDirPathPrefix = path.join(os.tmpdir(), 'sentry-nextjs-test-'); + tempDir = fs.mkdtempSync(tempDirPathPrefix); + }); + + afterEach(() => { + rimraf.sync(tempDir); + }); + + afterAll(() => { + exitsSync.mockImplementation(mockExistsSync); + }); + + it('successfully finds js files', () => { + fs.writeFileSync(path.resolve(tempDir, 'sentry.server.config.js'), 'Dogs are great!'); + fs.writeFileSync(path.resolve(tempDir, 'sentry.client.config.js'), 'Squirrel!'); + + expect(getUserConfigFile(tempDir, 'server')).toEqual('sentry.server.config.js'); + expect(getUserConfigFile(tempDir, 'client')).toEqual('sentry.client.config.js'); + }); + + it('successfully finds ts files', () => { + fs.writeFileSync(path.resolve(tempDir, 'sentry.server.config.ts'), 'Sit. Stay. Lie Down.'); + fs.writeFileSync(path.resolve(tempDir, 'sentry.client.config.ts'), 'Good dog!'); + + expect(getUserConfigFile(tempDir, 'server')).toEqual('sentry.server.config.ts'); + expect(getUserConfigFile(tempDir, 'client')).toEqual('sentry.client.config.ts'); + }); + + it('errors when files are missing', () => { + expect(() => getUserConfigFile(tempDir, 'server')).toThrowError( + `Cannot find 'sentry.server.config.ts' or 'sentry.server.config.js' in '${tempDir}'`, + ); + expect(() => getUserConfigFile(tempDir, 'client')).toThrowError( + `Cannot find 'sentry.client.config.ts' or 'sentry.client.config.js' in '${tempDir}'`, + ); + }); + }); });