diff --git a/.eslintrc.js b/.eslintrc.js index f50b3ae03..bdc8463d4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,6 +15,7 @@ module.exports = { 'assets/**', 'scripts/**', 'coverage/**', + 'lib/Helper/test-fixtures/**', ], overrides: [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index a330522d1..d88e88103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ # Changelog +## Unreleased + +- feat: Merge next.config.js files automatically (#222) + ## 2.4.2 - feat(nextjs): Add sentry.edge.config.js template (#227) ## 2.4.1 -- feat: Add logic to add @sentry/nextjs if it's missing when running the wizard (#219) +- feat: Add logic to add @sentry/nextjs if it's missing when running the wizard + (#219) - fix: Print localhost with `http` instead of `https` (#212) - feat: Add project_platform as query param if -s and -i are set (#221) - feat: Add promo code option used for signup flows (#223) @@ -22,12 +27,14 @@ ## 2.3.1 -- fix(nextjs): Always check for both `next` and `@sentry/nextjs` presence and version (#209) +- fix(nextjs): Always check for both `next` and `@sentry/nextjs` presence and + version (#209) - fix: `cli.executable` property should be resolved from cwd (#211) ## 2.3.0 -- feat(react-native): Xcode plugin debug files upload can include source using env +- feat(react-native): Xcode plugin debug files upload can include source using + env - chore(ci): remove jira workflow (#204) ## 2.2.2 @@ -36,7 +43,8 @@ ## 2.2.1 -- feat(nextjs): Add option to auto-wrap data fetchers and API routes to Next.js config (#194) +- feat(nextjs): Add option to auto-wrap data fetchers and API routes to Next.js + config (#194) ## 2.2.0 diff --git a/lib/Helper/MergeConfig.ts b/lib/Helper/MergeConfig.ts new file mode 100644 index 000000000..608db2a8a --- /dev/null +++ b/lib/Helper/MergeConfig.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; + +// merges the config files +export function mergeConfigFile( + sourcePath: string, + templatePath: string, +): boolean { + try { + const templateFile = fs.readFileSync(templatePath, 'utf8'); + const sourceFile = fs.readFileSync(sourcePath, 'utf8'); + const newText = templateFile.replace('// ORIGINAL CONFIG', sourceFile); + Function(newText); // check if the file is valid javascript + fs.writeFileSync(sourcePath, newText); + return true; + } catch (error) { + return false; + } +} diff --git a/lib/Helper/__tests__/MergeConfig.ts b/lib/Helper/__tests__/MergeConfig.ts new file mode 100644 index 000000000..27b4a4288 --- /dev/null +++ b/lib/Helper/__tests__/MergeConfig.ts @@ -0,0 +1,77 @@ +/// +import * as fs from 'fs'; +import * as path from 'path'; + +import { mergeConfigFile } from '../MergeConfig'; + +const configPath = path.join(__dirname, '..', 'test-fixtures/next.config.js'); +const templatePath = path.join( + __dirname, + '..', + '..', + '..', + 'scripts/NextJS/configs/next.config.template.js', +); + +function configFileNames(num: number): { + sourcePath: string; + mergedPath: string; +} { + const sourcePath = path.join( + __dirname, + '..', + `test-fixtures/next.config.${num}.js`, + ); + const mergedPath = path.join( + __dirname, + '..', + `test-fixtures/next.config.${num}-merged.js`, + ); + return { sourcePath, mergedPath }; +} + +describe('Merging next.config.js', () => { + test('merge basic next.config.js', () => { + const { sourcePath, mergedPath } = configFileNames(1); + fs.copyFileSync(sourcePath, configPath); + + expect(mergeConfigFile(configPath, templatePath)).toBe(true); + expect( + fs.readFileSync(configPath, 'utf8') === + fs.readFileSync(mergedPath, 'utf8'), + ).toBe(true); + fs.unlinkSync(configPath); + }); + + test('merge invalid javascript config', () => { + const { sourcePath } = configFileNames(2); + fs.copyFileSync(sourcePath, configPath); + + expect(mergeConfigFile(configPath, templatePath)).toBe(false); + fs.unlinkSync(configPath); + }); + + test('merge more complicated next.config.js', () => { + const { sourcePath, mergedPath } = configFileNames(3); + fs.copyFileSync(sourcePath, configPath); + + expect(mergeConfigFile(configPath, templatePath)).toBe(true); + expect( + fs.readFileSync(configPath, 'utf8') === + fs.readFileSync(mergedPath, 'utf8'), + ).toBe(true); + fs.unlinkSync(configPath); + }); + + test('merge next.config.js with function', () => { + const { sourcePath, mergedPath } = configFileNames(4); + fs.copyFileSync(sourcePath, configPath); + + expect(mergeConfigFile(configPath, templatePath)).toBe(true); + expect( + fs.readFileSync(configPath, 'utf8') === + fs.readFileSync(mergedPath, 'utf8'), + ).toBe(true); + fs.unlinkSync(configPath); + }); +}); diff --git a/lib/Helper/test-fixtures/next.config.1-merged.js b/lib/Helper/test-fixtures/next.config.1-merged.js new file mode 100644 index 000000000..300e63aa9 --- /dev/null +++ b/lib/Helper/test-fixtures/next.config.1-merged.js @@ -0,0 +1,18 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +module.exports = nextConfig; + +module.exports = withSentryConfig( + module.exports, + { silent: true }, + { hideSourcemaps: true }, +); diff --git a/lib/Helper/test-fixtures/next.config.1.js b/lib/Helper/test-fixtures/next.config.1.js new file mode 100644 index 000000000..91ef62f0d --- /dev/null +++ b/lib/Helper/test-fixtures/next.config.1.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +module.exports = nextConfig; diff --git a/lib/Helper/test-fixtures/next.config.2.js b/lib/Helper/test-fixtures/next.config.2.js new file mode 100644 index 000000000..ee823eeee --- /dev/null +++ b/lib/Helper/test-fixtures/next.config.2.js @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + + + + +module.exports = nextConfig; diff --git a/lib/Helper/test-fixtures/next.config.3-merged.js b/lib/Helper/test-fixtures/next.config.3-merged.js new file mode 100644 index 000000000..9b8bbe2b4 --- /dev/null +++ b/lib/Helper/test-fixtures/next.config.3-merged.js @@ -0,0 +1,21 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + images: { + domains: [], + }, +}; + +module.exports = nextConfig; + +module.exports = withSentryConfig( + module.exports, + { silent: true }, + { hideSourcemaps: true }, +); diff --git a/lib/Helper/test-fixtures/next.config.3.js b/lib/Helper/test-fixtures/next.config.3.js new file mode 100644 index 000000000..01fce3847 --- /dev/null +++ b/lib/Helper/test-fixtures/next.config.3.js @@ -0,0 +1,9 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + images: { + domains: [], + }, +}; + +module.exports = nextConfig; diff --git a/lib/Helper/test-fixtures/next.config.4-merged.js b/lib/Helper/test-fixtures/next.config.4-merged.js new file mode 100644 index 000000000..0ac26e7aa --- /dev/null +++ b/lib/Helper/test-fixtures/next.config.4-merged.js @@ -0,0 +1,21 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ +const { withSentryConfig } = require('@sentry/nextjs'); + +module.exports = (phase, { defaultConfig }) => { + /** + * @type {import('next').NextConfig} + */ + const nextConfig = { + /* config options here */ + }; + return nextConfig; +}; + +module.exports = withSentryConfig( + module.exports, + { silent: true }, + { hideSourcemaps: true }, +); diff --git a/lib/Helper/test-fixtures/next.config.4.js b/lib/Helper/test-fixtures/next.config.4.js new file mode 100644 index 000000000..233223978 --- /dev/null +++ b/lib/Helper/test-fixtures/next.config.4.js @@ -0,0 +1,9 @@ +module.exports = (phase, { defaultConfig }) => { + /** + * @type {import('next').NextConfig} + */ + const nextConfig = { + /* config options here */ + }; + return nextConfig; +}; diff --git a/lib/Steps/Integrations/NextJs.ts b/lib/Steps/Integrations/NextJs.ts index d342c0c7c..b8b0d7b33 100644 --- a/lib/Steps/Integrations/NextJs.ts +++ b/lib/Steps/Integrations/NextJs.ts @@ -10,6 +10,7 @@ import { promisify } from 'util'; import { Args } from '../../Constants'; import { debug, green, l, nl, red } from '../../Helper/Logging'; +import { mergeConfigFile } from '../../Helper/MergeConfig'; import { SentryCli, SentryCliProps } from '../../Helper/SentryCli'; import { BaseIntegration } from './BaseIntegration'; @@ -60,7 +61,7 @@ export class NextJs extends BaseIntegration { const configDirectory = path.join(templateDirectory, CONFIG_DIR); if (fs.existsSync(configDirectory)) { - this._createNextConfig(configDirectory, dsn); + await this._createNextConfig(configDirectory, dsn); } else { debug( `Couldn't find ${configDirectory}, probably because you ran this from inside of \`/lib\` rather than \`/dist\``, @@ -74,7 +75,7 @@ export class NextJs extends BaseIntegration { (p: { slug: string }) => p.slug === selectedProjectSlug, )?.firstEvent; if (!hasFirstEvent) { - this._setTemplate( + await this._setTemplate( templateDirectory, 'sentry_sample_error.js', ['pages', 'src/pages'], @@ -243,10 +244,18 @@ export class NextJs extends BaseIntegration { } } - private _createNextConfig(configDirectory: string, dsn: any): void { + private async _createNextConfig( + configDirectory: string, + dsn: any, + ): Promise { const templates = fs.readdirSync(configDirectory); - for (const template of templates) { - this._setTemplate( + // next.config.template.js used for merging next.config.js , not its own template, + // so it shouldn't have a setTemplate call + const filteredTemplates = templates.filter( + (template) => template !== 'next.config.template.js', + ); + for (const template of filteredTemplates) { + await this._setTemplate( configDirectory, template, TEMPLATE_DESTINATIONS[template], @@ -260,19 +269,18 @@ export class NextJs extends BaseIntegration { nl(); } - private _setTemplate( + private async _setTemplate( configDirectory: string, templateFile: string, destinationOptions: string[], dsn: string, - ): void { + ): Promise { const templatePath = path.join(configDirectory, templateFile); for (const destinationDir of destinationOptions) { if (!fs.existsSync(destinationDir)) { continue; } - const destinationPath = path.join(destinationDir, templateFile); // in case the file in question already exists, we'll make a copy with // `MERGEABLE_CONFIG_INFIX` inserted just before the extension, so as not @@ -287,23 +295,35 @@ export class NextJs extends BaseIntegration { ).join('.'), ); - if (!fs.existsSync(destinationPath)) { - this._fillAndCopyTemplate(templatePath, destinationPath, dsn); - } else if (!fs.existsSync(mergeableFilePath)) { - this._fillAndCopyTemplate(templatePath, mergeableFilePath, dsn); - red( - `File \`${templateFile}\` already exists, so created \`${mergeableFilePath}\`.\n` + - 'Please merge those files.', + if (templateFile === 'next.config.js') { + await this._mergeNextConfig( + destinationPath, + templatePath, + destinationDir, + templateFile, + configDirectory, + mergeableFilePath, ); - nl(); + return; } else { - red( - `Both \`${templateFile}\` and \`${mergeableFilePath}\` already exist.\n` + - 'Please merge those files.', - ); - nl(); + if (!fs.existsSync(destinationPath)) { + this._fillAndCopyTemplate(templatePath, destinationPath, dsn); + } else if (!fs.existsSync(mergeableFilePath)) { + this._fillAndCopyTemplate(templatePath, mergeableFilePath, dsn); + red( + `File \`${templateFile}\` already exists, so created \`${mergeableFilePath}\`.\n` + + 'Please merge those files.', + ); + nl(); + } else { + red( + `Both \`${templateFile}\` and \`${mergeableFilePath}\` already exist.\n` + + 'Please merge those files.', + ); + nl(); + } + return; } - return; } red( @@ -437,4 +457,61 @@ export class NextJs extends BaseIntegration { arr.splice(start, deleteCount, ...inserts); return arr; } + + private async _mergeNextConfig( + destinationPath: string, + templatePath: string, + destinationDir: string, + templateFile: string, + configDirectory: string, + mergeableFilePath: string, + ): Promise { + // if no next.config.js exists, we'll create one + if (!fs.existsSync(destinationPath)) { + fs.copyFileSync(templatePath, destinationPath); + green('Created File `next.config.js`'); + nl(); + } else { + // creates a file name for the copy of the original next.config.js file + // with the name `next.config.original.js` + const originalFileName = this._spliceInPlace( + templateFile.split('.'), + -1, + 0, + 'original', + ).join('.'); + const originalFilePath = path.join(destinationDir, originalFileName); + // makes copy of original next.config.js + fs.writeFileSync(originalFilePath, fs.readFileSync(destinationPath)); + await this._addToGitignore( + originalFilePath, + 'Unable to add next.config.original.js to gitignore', + ); + + const mergedTemplatePath = path.join( + configDirectory, + 'next.config.template.js', + ); + // attempts to merge with existing next.config.js, if true -> success + if (mergeConfigFile(destinationPath, mergedTemplatePath)) { + green( + `Updated \`${templateFile}\` with Sentry. The original ${templateFile} was saved as \`next.config.original.js\`.\n` + + 'Information on the changes made to the Next.js configuration file an be found at https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/', + ); + nl(); + } else { + // if merge fails, we'll create a copy of the `next.config.js` template and ask them to merge + fs.copyFileSync(templatePath, mergeableFilePath); + await this._addToGitignore( + mergeableFilePath, + 'Unable to add next.config.wizard.js template to gitignore', + ); + red( + `Unable to merge \`${templateFile}\`, so created \`${mergeableFilePath}\`.\n` + + 'Please integrate next.config.wizardcopy.js into your next.config.js or next.config.ts file', + ); + nl(); + } + } + } } diff --git a/scripts/NextJs/configs/next.config.template.js b/scripts/NextJs/configs/next.config.template.js new file mode 100644 index 000000000..0e8aafdc7 --- /dev/null +++ b/scripts/NextJs/configs/next.config.template.js @@ -0,0 +1,12 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ +const { withSentryConfig } = require('@sentry/nextjs'); + +// ORIGINAL CONFIG +module.exports = withSentryConfig( + module.exports, + { silent: true }, + { hideSourcemaps: true }, +);