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 },
+);