Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse } from 'next/server';

export function middleware() {
// Basic middleware to ensure that the build works with edge runtime
return NextResponse.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ test('should create a transaction for a CJS pages router API endpoint', async ({
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
return (
transactionEvent.transaction === 'GET /api/cjs-api-endpoint' &&
transactionEvent.contexts?.trace?.op === 'http.server'
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction_info?.source === 'route'
);
});

Expand Down Expand Up @@ -73,7 +74,8 @@ test('should not mess up require statements in CJS API endpoints', async ({ requ
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
return (
transactionEvent.transaction === 'GET /api/cjs-api-endpoint-with-require' &&
transactionEvent.contexts?.trace?.op === 'http.server'
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction_info?.source === 'route'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ const cases = [
cases.forEach(({ name, url, transactionName }) => {
test(`Should capture transactions for routes with various shapes (${name})`, async ({ request }) => {
const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => {
return transactionEvent.transaction === transactionName && transactionEvent.contexts?.trace?.op === 'http.server';
return (
transactionEvent.transaction === transactionName &&
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction_info?.source === 'route'
);
});

request.get(url).catch(() => {
Expand Down
6 changes: 6 additions & 0 deletions packages/nextjs/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ module.exports = {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['src/config/polyfills/perf_hooks.js'],
globals: {
globalThis: 'readonly',
},
},
],
};
15 changes: 15 additions & 0 deletions packages/nextjs/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,20 @@ export default [
},
}),
),
...makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/config/polyfills/perf_hooks.js'],

packageSpecificConfig: {
output: {
// Preserve the original file structure (i.e., so that everything is still relative to `src`)
entryFileNames: 'config/polyfills/[name].js',

// make it so Rollup calms down about the fact that we're combining default and named exports
exports: 'named',
},
},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
];
25 changes: 25 additions & 0 deletions packages/nextjs/src/config/polyfills/perf_hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Polyfill for Node.js perf_hooks module in edge runtime
// This mirrors the polyfill from packages/vercel-edge/rollup.npm.config.mjs

// Ensure performance global is available
if (typeof globalThis !== 'undefined' && globalThis.performance === undefined) {
globalThis.performance = {
timeOrigin: 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe do:

 const timeOrigin = Date.now();
  return {
    timeOrigin,
    now: () => Date.now() - timeOrigin,
  };

I think anyone doing timing assertions in dev might see different values

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, updated.

now: function () {
return Date.now();
},
};
}

// Export the performance object for perf_hooks compatibility
export const performance = globalThis.performance || {
timeOrigin: 0,
now: function () {
return Date.now();
},
};

// Default export for CommonJS compatibility
export default {
performance,
};
1 change: 1 addition & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ export type BuildContext = {
webpack: {
version: string;
DefinePlugin: new (values: Record<string, string | boolean>) => WebpackPluginInstance;
ProvidePlugin: new (values: Record<string, string | string[]>) => WebpackPluginInstance;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultLoaders: any; // needed for type tests (test:types)
Expand Down
27 changes: 25 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export function constructWebpackConfigFunction(
const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');
const nextJsVersion = getNextjsVersion();
const { major } = parseSemver(nextJsVersion || '');

// We add `.ts` and `.js` back in because `pageExtensions` might not be relevant to the instrumentation file
// e.g. user's setting `.mdx`. In that case we still want to default look up
Expand All @@ -70,8 +72,6 @@ export function constructWebpackConfigFunction(
warnAboutDeprecatedConfigFiles(projectDir, instrumentationFile, runtime);
}
if (runtime === 'server') {
const nextJsVersion = getNextjsVersion();
const { major } = parseSemver(nextJsVersion || '');
// was added in v15 (https://github.com/vercel/next.js/pull/67539)
if (major && major >= 15) {
warnAboutMissingOnRequestErrorHandler(instrumentationFile);
Expand Down Expand Up @@ -103,6 +103,11 @@ export function constructWebpackConfigFunction(

addOtelWarningIgnoreRule(newConfig);

// Add edge runtime polyfills when building for edge in dev mode
if (major && major === 13 && runtime === 'edge' && isDev) {
addEdgeRuntimePolyfills(newConfig, buildContext);
}

let pagesDirPath: string | undefined;
const maybePagesDirPath = path.join(projectDir, 'pages');
const maybeSrcPagesDirPath = path.join(projectDir, 'src', 'pages');
Expand Down Expand Up @@ -865,6 +870,24 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules)
}
}

function addEdgeRuntimePolyfills(newConfig: WebpackConfigObjectWithModuleRules, buildContext: BuildContext): void {
// Use ProvidePlugin to inject performance global only when accessed
newConfig.plugins = newConfig.plugins || [];
newConfig.plugins.push(
new buildContext.webpack.ProvidePlugin({
performance: [path.resolve(__dirname, 'polyfills', 'perf_hooks.js'), 'performance'],
}),
);

// Add module resolution aliases for problematic Node.js modules in edge runtime
newConfig.resolve = newConfig.resolve || {};
newConfig.resolve.alias = {
...newConfig.resolve.alias,
// Redirect perf_hooks imports to a polyfilled version
perf_hooks: path.resolve(__dirname, 'polyfills', 'perf_hooks.js'),
};
}

function _getModules(projectDir: string): Record<string, string> {
try {
const packageJson = path.join(projectDir, 'package.json');
Expand Down
8 changes: 7 additions & 1 deletion packages/nextjs/test/config/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ export function getBuildContext(
distDir: '.next',
...materializedNextConfig,
} as NextConfigObject,
webpack: { version: webpackVersion, DefinePlugin: class {} as any },
webpack: {
version: webpackVersion,
DefinePlugin: class {} as any,
ProvidePlugin: class {
constructor(public definitions: Record<string, any>) {}
} as any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we use a more precise type? or type this with unknown instead of any please?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just test fixtures, I don't think it's necessary to be more precise here.

},
defaultLoaders: true,
totalPages: 2,
isServer: buildTarget === 'server' || buildTarget === 'edge',
Expand Down
50 changes: 50 additions & 0 deletions packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CLIENT_SDK_CONFIG_FILE,
clientBuildContext,
clientWebpackConfig,
edgeBuildContext,
exportedNextConfig,
serverBuildContext,
serverWebpackConfig,
Expand Down Expand Up @@ -185,4 +186,53 @@ describe('constructWebpackConfigFunction()', () => {
});
});
});

describe('edge runtime polyfills', () => {
it('adds polyfills only for edge runtime in dev mode', async () => {
// Test edge runtime in dev mode - should add polyfills
const edgeDevBuildContext = { ...edgeBuildContext, dev: true };
const edgeDevConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeDevBuildContext,
});

const edgeProvidePlugin = edgeDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(edgeProvidePlugin).toBeDefined();
expect(edgeDevConfig.resolve?.alias?.perf_hooks).toMatch(/perf_hooks\.js$/);

// Test edge runtime in prod mode - should NOT add polyfills
const edgeProdBuildContext = { ...edgeBuildContext, dev: false };
const edgeProdConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeProdBuildContext,
});

const edgeProdProvidePlugin = edgeProdConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(edgeProdProvidePlugin).toBeUndefined();

// Test server runtime in dev mode - should NOT add polyfills
const serverDevBuildContext = { ...serverBuildContext, dev: true };
const serverDevConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: serverDevBuildContext,
});

const serverProvidePlugin = serverDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(serverProvidePlugin).toBeUndefined();

// Test client runtime in dev mode - should NOT add polyfills
const clientDevBuildContext = { ...clientBuildContext, dev: true };
const clientDevConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: clientWebpackConfig,
incomingWebpackBuildContext: clientDevBuildContext,
});

const clientProvidePlugin = clientDevConfig.plugins?.find(plugin => plugin.constructor.name === 'ProvidePlugin');
expect(clientProvidePlugin).toBeUndefined();
});
});
});
Loading