-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(nextjs): Support distDir Next.js option
#3990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
4d88272
cb94618
042223b
8014413
37e15ee
9295eaa
1a3aed0
344fd39
a940c07
f5f1f25
ef4cffd
3120b7c
718b269
de8ff3e
ca0a409
3f1df7c
3ee1215
3ed2d01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { NextConfigObject, SentryWebpackPluginOptions } from './types'; | ||
|
|
||
| /** | ||
| * About types: | ||
| * It's not possible to set strong types because they end up forcing you to explicitly | ||
| * set `undefined` for properties you don't want to include, which is quite | ||
| * inconvenient. The workaround to this is to relax type requirements at some point, | ||
| * which means not enforcing types (why have strong typing then?) and still having code | ||
| * that is hard to read. | ||
| */ | ||
|
|
||
| /** | ||
| * Next.js properties that should modify the webpack plugin properties. | ||
| * They should have an includer function in the map. | ||
| */ | ||
| export const SUPPORTED_NEXTJS_PROPERTIES = ['distDir']; | ||
|
|
||
| type PropIncluderFn = ( | ||
| nextConfig: NextConfigObject, | ||
| sentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions>, | ||
| ) => Partial<SentryWebpackPluginOptions>; | ||
|
|
||
| export type PropsIncluderMapType = Record<string, PropIncluderFn>; | ||
| export const PROPS_INCLUDER_MAP: PropsIncluderMapType = { | ||
| distDir: includeDistDir, | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a new Sentry Webpack Plugin config from the given one, including all available | ||
| * properties in the Nextjs Config. | ||
| * | ||
| * @param nextConfig User's Next.js config. | ||
| * @param sentryWebpackPluginOptions User's Sentry Webpack Plugin config. | ||
| * @returns New Sentry Webpack Plugin Config. | ||
| */ | ||
| export default function includeAllNextjsProps( | ||
| nextConfig: NextConfigObject, | ||
| sentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions>, | ||
| ): Partial<SentryWebpackPluginOptions> { | ||
| return includeNextjsProps(nextConfig, sentryWebpackPluginOptions, PROPS_INCLUDER_MAP, SUPPORTED_NEXTJS_PROPERTIES); | ||
iker-barriocanal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Creates a new Sentry Webpack Plugin config from the given one, and applying the corresponding | ||
| * modifications to the given next properties. If more than one option generates the same | ||
| * properties, the values generated last will override previous ones. | ||
| * | ||
| * @param nextConfig User's Next.js config. | ||
| * @param sentryWebpackPluginOptions User's Sentry Webapck Plugin config. | ||
| * @param nextProps Next.js config's properties that should modify webpack plugin properties. | ||
| * @returns New Sentry Webpack Plugin config. | ||
| */ | ||
| export function includeNextjsProps( | ||
| nextConfig: NextConfigObject, | ||
| sentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions>, | ||
| propsIncluderMap: Record<string, PropIncluderFn>, | ||
| nextProps: string[], | ||
| ): Partial<SentryWebpackPluginOptions> { | ||
| // @ts-ignore '__spreadArray' import from tslib, ts(2343) | ||
| const propsToInclude = [...new Set(nextProps)]; | ||
|
||
| return ( | ||
| propsToInclude | ||
| // Types are not strict enought to ensure there's a function in the map | ||
| .filter(prop => propsIncluderMap[prop]) | ||
| .map(prop => propsIncluderMap[prop](nextConfig, sentryWebpackPluginOptions)) | ||
| .reduce((prev, current) => ({ ...prev, ...current }), {}) | ||
| ); | ||
iker-barriocanal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Creates a new Sentry Webpack Plugin config with the `distDir` option from Next.js config | ||
| * in the `include` property. | ||
| * | ||
| * If no `distDir` is provided, the Webpack Plugin config doesn't change. | ||
| * If no `include` has been defined defined, the `distDir` value is assigned. | ||
| * The `distDir` directory is merged to the directories in `include`, if defined. | ||
| * Duplicated paths are removed while merging. | ||
| * | ||
| * @param nextConfig User's Next.js config | ||
| * @param sentryWebpackPluginOptions User's Sentry Webpack Plugin config | ||
| * @returns New Sentry Webpack Plugin config | ||
| */ | ||
| export function includeDistDir( | ||
| nextConfig: NextConfigObject, | ||
| sentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions>, | ||
| ): Partial<SentryWebpackPluginOptions> { | ||
| if (!nextConfig.distDir) { | ||
| return { ...sentryWebpackPluginOptions }; | ||
iker-barriocanal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| // It's assumed `distDir` is a string as that's what Next.js is expecting. If it's not, Next.js itself will complain | ||
| const usersInclude = sentryWebpackPluginOptions.include; | ||
|
|
||
| let sourcesToInclude; | ||
| if (typeof usersInclude === 'undefined') { | ||
| sourcesToInclude = nextConfig.distDir; | ||
| } else if (typeof usersInclude === 'string') { | ||
| sourcesToInclude = usersInclude === nextConfig.distDir ? usersInclude : [usersInclude, nextConfig.distDir]; | ||
| } else if (Array.isArray(usersInclude)) { | ||
| // @ts-ignore '__spreadArray' import from tslib, ts(2343) | ||
| sourcesToInclude = [...new Set(usersInclude.concat(nextConfig.distDir))]; | ||
iker-barriocanal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } else { | ||
| // Object | ||
| if (Array.isArray(usersInclude.paths)) { | ||
| const uniquePaths = [...new Set(usersInclude.paths.concat(nextConfig.distDir as string))]; | ||
| sourcesToInclude = { ...usersInclude, paths: uniquePaths }; | ||
| } else if (typeof usersInclude.paths === 'undefined') { | ||
| // eslint-disable-next-line no-console | ||
| console.warn( | ||
| 'Sentry Logger [Warn]:', | ||
| `An object was set in \`include\` but no \`paths\` was provided, so added the \`distDir\`: "${nextConfig.distDir}"\n` + | ||
| 'See https://github.com/getsentry/sentry-webpack-plugin#optionsinclude', | ||
| ); | ||
| sourcesToInclude = { ...usersInclude, paths: [nextConfig.distDir] }; | ||
| } else { | ||
| // eslint-disable-next-line no-console | ||
| console.error( | ||
| 'Sentry Logger [Error]:', | ||
| 'Found unexpected object in `include.paths`\n' + | ||
| 'See https://github.com/getsentry/sentry-webpack-plugin#optionsinclude', | ||
| ); | ||
| // Keep the same object even if it's incorrect, so that the user can get a more precise error from sentry-cli | ||
| // Casting to `any` for TS not complaining about it being `unknown` | ||
| sourcesToInclude = usersInclude as any; | ||
| } | ||
| } | ||
|
|
||
| return { ...sentryWebpackPluginOptions, include: sourcesToInclude }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import includeAllNextjsProps, { | ||
| includeDistDir, | ||
| includeNextjsProps, | ||
| PropsIncluderMapType, | ||
| } from '../../src/config/nextConfigToWebpackPluginConfig'; | ||
| import { SentryWebpackPluginOptions } from '../../src/config/types'; | ||
|
|
||
| test('includeAllNextjsProps', () => { | ||
| expect(includeAllNextjsProps({ distDir: 'test' }, {})).toMatchObject({ include: 'test' }); | ||
| }); | ||
|
|
||
| describe('includeNextjsProps', () => { | ||
| const includerMap: PropsIncluderMapType = { | ||
| test: includeEverythingFn, | ||
| }; | ||
| const includeEverything = { | ||
| include: 'everything', | ||
| }; | ||
| function includeEverythingFn(): Partial<SentryWebpackPluginOptions> { | ||
| return includeEverything; | ||
| } | ||
|
|
||
| test('a prop and an includer', () => { | ||
| expect(includeNextjsProps({ test: true }, {}, includerMap, ['test'])).toMatchObject(includeEverything); | ||
| }); | ||
|
|
||
| test('a prop without includer', () => { | ||
| expect(includeNextjsProps({ noExist: false }, {}, includerMap, ['noExist'])).toMatchObject({}); | ||
| }); | ||
|
|
||
| test('an includer without a prop', () => { | ||
| expect(includeNextjsProps({ noExist: false }, {}, includerMap, ['test'])).toMatchObject({}); | ||
| }); | ||
|
|
||
| test('neither prop nor includer', () => { | ||
| expect(includeNextjsProps({}, {}, {}, [])).toMatchObject({}); | ||
| }); | ||
| }); | ||
|
|
||
| describe('next config to webpack plugin config', () => { | ||
| describe('includeDistDir', () => { | ||
| const consoleWarnMock = jest.fn(); | ||
| const consoleErrorMock = jest.fn(); | ||
|
|
||
| beforeAll(() => { | ||
| global.console.warn = consoleWarnMock; | ||
| global.console.error = consoleErrorMock; | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| test.each([ | ||
| [{}, {}, {}], | ||
| [{}, { include: 'path' }, { include: 'path' }], | ||
| [{}, { include: [] }, { include: [] }], | ||
| [{}, { include: ['path'] }, { include: ['path'] }], | ||
| [{}, { include: { paths: ['path'] } }, { include: { paths: ['path'] } }], | ||
| ])('without `distDir`', (nextConfig, webpackPluginConfig, expectedConfig) => { | ||
| expect(includeDistDir(nextConfig, webpackPluginConfig)).toMatchObject(expectedConfig); | ||
| }); | ||
|
|
||
| test.each([ | ||
| [{ distDir: 'test' }, {}, { include: 'test' }], | ||
| [{ distDir: 'test' }, { include: 'path' }, { include: ['path', 'test'] }], | ||
| [{ distDir: 'test' }, { include: [] }, { include: ['test'] }], | ||
| [{ distDir: 'test' }, { include: ['path'] }, { include: ['path', 'test'] }], | ||
| [{ distDir: 'test' }, { include: { paths: ['path'] } }, { include: { paths: ['path', 'test'] } }], | ||
| ])('with `distDir`, different paths', (nextConfig, webpackPluginConfig, expectedConfig) => { | ||
| expect(includeDistDir(nextConfig, webpackPluginConfig)).toMatchObject(expectedConfig); | ||
| }); | ||
|
|
||
| test.each([ | ||
| [{ distDir: 'path' }, { include: 'path' }, { include: 'path' }], | ||
| [{ distDir: 'path' }, { include: ['path'] }, { include: ['path'] }], | ||
| [{ distDir: 'path' }, { include: { paths: ['path'] } }, { include: { paths: ['path'] } }], | ||
| ])('with `distDir`, same path', (nextConfig, webpackPluginConfig, expectedConfig) => { | ||
| expect(includeDistDir(nextConfig, webpackPluginConfig)).toMatchObject(expectedConfig); | ||
| }); | ||
|
|
||
| test.each([ | ||
| [{ distDir: 'path' }, { include: {} }, { include: { paths: ['path'] } }], | ||
| [{ distDir: 'path' }, { include: { prop: 'val' } }, { include: { prop: 'val', paths: ['path'] } }], | ||
| ])('webpack plugin config as object with other prop', (nextConfig, webpackPluginConfig, expectedConfig) => { | ||
| // @ts-ignore Other props don't match types | ||
| expect(includeDistDir(nextConfig, webpackPluginConfig)).toMatchObject(expectedConfig); | ||
| expect(consoleWarnMock).toHaveBeenCalledTimes(1); | ||
| consoleWarnMock.mockClear(); | ||
| }); | ||
|
|
||
| test.each([ | ||
| [{ distDir: 'path' }, { include: { paths: {} } }, { include: { paths: {} } }], | ||
| [{ distDir: 'path' }, { include: { paths: { badObject: true } } }, { include: { paths: { badObject: true } } }], | ||
| ])('webpack plugin config as object with bad structure', (nextConfig, webpackPluginConfig, expectedConfig) => { | ||
| // @ts-ignore Bad structures don't match types | ||
| expect(includeDistDir(nextConfig, webpackPluginConfig)).toMatchObject(expectedConfig); | ||
| expect(consoleErrorMock).toHaveBeenCalledTimes(1); | ||
| consoleErrorMock.mockClear(); | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.