diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index b6faa2670c4e..1b5ebf5da4c1 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -11,7 +11,6 @@ }, "main": "build/cjs/index.js", "module": "build/esm/index.js", - "browser": "build/esm/client/index.js", "types": "build/types/index.types.d.ts", "publishConfig": { "access": "public" diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index 2329f9eafed1..db1fa9c1fde1 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -5,7 +5,7 @@ export default [ makeBaseNPMConfig({ // We need to include `instrumentServer.ts` separately because it's only conditionally required, and so rollup // doesn't automatically include it when calculating the module dependency tree. - entrypoints: ['src/index.ts', 'src/client/index.ts', 'src/config/webpack.ts'], + entrypoints: ['src/index.ts', 'src/client/index.ts', 'src/edge/index.ts', 'src/config/webpack.ts'], // prevent this internal nextjs code from ending up in our built package (this doesn't happen automatially because // the name doesn't match an SDK dependency) diff --git a/packages/nextjs/src/config/loaders/index.ts b/packages/nextjs/src/config/loaders/index.ts index 322567c1495b..27620e004f39 100644 --- a/packages/nextjs/src/config/loaders/index.ts +++ b/packages/nextjs/src/config/loaders/index.ts @@ -1,3 +1,4 @@ export { default as valueInjectionLoader } from './valueInjectionLoader'; export { default as prefixLoader } from './prefixLoader'; export { default as wrappingLoader } from './wrappingLoader'; +export { default as sdkMultiplexerLoader } from './sdkMultiplexerLoader'; diff --git a/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts b/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts new file mode 100644 index 000000000000..9241def48a80 --- /dev/null +++ b/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts @@ -0,0 +1,24 @@ +import type { LoaderThis } from './types'; + +type LoaderOptions = { + importTarget: string; +}; + +/** + * This loader allows us to multiplex SDKs depending on what is passed to the `importTarget` loader option. + * If this loader encounters a file that contains the string "__SENTRY_SDK_MULTIPLEXER__" it will replace it's entire + * content with an "export all"-statement that points to `importTarget`. + * + * In our case we use this to multiplex different SDKs depending on whether we're bundling browser code, server code, + * or edge-runtime code. + */ +export default function sdkMultiplexerLoader(this: LoaderThis, userCode: string): string { + if (!userCode.includes('__SENTRY_SDK_MULTIPLEXER__')) { + return userCode; + } + + // We know one or the other will be defined, depending on the version of webpack being used + const { importTarget } = 'getOptions' in this ? this.getOptions() : this.query; + + return `export * from "${importTarget}";`; +} diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 35c017e732e4..404dec749cb9 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -85,12 +85,17 @@ export function constructWebpackConfigFunction( // Add a loader which will inject code that sets global values addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions); - if (buildContext.nextRuntime === 'edge') { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] You are using edge functions or middleware. Please note that Sentry does not yet support error monitoring for these features.', - ); - } + newConfig.module.rules.push({ + test: /node_modules\/@sentry\/nextjs/, + use: [ + { + loader: path.resolve(__dirname, 'loaders/sdkMultiplexerLoader.js'), + options: { + importTarget: buildContext.nextRuntime === 'edge' ? './edge' : './client', + }, + }, + ], + }); if (isServer) { if (userSentryOptions.autoInstrumentServerFunctions !== false) { @@ -301,28 +306,25 @@ async function addSentryToEntryProperty( // we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function // options. See https://webpack.js.org/configuration/entry-context/#entry. - const { isServer, dir: projectDir, dev: isDev } = buildContext; + const { isServer, dir: projectDir, dev: isDev, nextRuntime } = buildContext; const newEntryProperty = typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty }; // `sentry.server.config.js` or `sentry.client.config.js` (or their TS equivalents) - const userConfigFile = isServer ? getUserConfigFile(projectDir, 'server') : getUserConfigFile(projectDir, 'client'); + const userConfigFile = + nextRuntime === 'edge' + ? getUserConfigFile(projectDir, 'edge') + : isServer + ? getUserConfigFile(projectDir, 'server') + : getUserConfigFile(projectDir, 'client'); // we need to turn the filename into a path so webpack can find it - const filesToInject = [`./${userConfigFile}`]; + const filesToInject = userConfigFile ? [`./${userConfigFile}`] : []; // inject into all entry points which might contain user's code for (const entryPointName in newEntryProperty) { - if ( - shouldAddSentryToEntryPoint( - entryPointName, - isServer, - userSentryOptions.excludeServerRoutes, - isDev, - buildContext.nextRuntime === 'edge', - ) - ) { + if (shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev)) { addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject); } else { if ( @@ -348,7 +350,7 @@ async function addSentryToEntryProperty( * @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 { +export function getUserConfigFile(projectDir: string, platform: 'server' | 'client' | 'edge'): string | undefined { const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; for (const filename of possibilities) { @@ -357,7 +359,16 @@ export function getUserConfigFile(projectDir: string, platform: 'server' | 'clie } } - throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); + // Edge config file is optional + if (platform === 'edge') { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] You are using Next.js features that run on the Edge Runtime. Please add a "sentry.edge.config.js" or a "sentry.edge.config.ts" file to your project root in which you initialize the Sentry SDK with "Sentry.init()".', + ); + return; + } else { + throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); + } } /** @@ -449,11 +460,9 @@ function shouldAddSentryToEntryPoint( isServer: boolean, excludeServerRoutes: Array = [], isDev: boolean, - isEdgeRuntime: boolean, ): boolean { - // We don't support the Edge runtime yet - if (isEdgeRuntime) { - return false; + if (entryPointName === 'middleware') { + return true; } // On the server side, by default we inject the `Sentry.init()` code into every page (with a few exceptions). @@ -479,9 +488,6 @@ function shouldAddSentryToEntryPoint( // versions.) entryPointRoute === '/_app' || entryPointRoute === '/_document' || - // While middleware was in beta, it could be anywhere (at any level) in the `pages` directory, and would be called - // `_middleware.js`. Until the SDK runs successfully in the lambda edge environment, we have to exclude these. - entryPointName.includes('_middleware') || // Newer versions of nextjs are starting to introduce things outside the `pages/` folder (middleware, an `app/` // directory, etc), but until those features are stable and we know how we want to support them, the safest bet is // not to inject anywhere but inside `pages/`. @@ -552,13 +558,7 @@ export function getWebpackPluginOptions( stripPrefix: ['webpack://_N_E/'], urlPrefix, entries: (entryPointName: string) => - shouldAddSentryToEntryPoint( - entryPointName, - isServer, - userSentryOptions.excludeServerRoutes, - isDev, - buildContext.nextRuntime === 'edge', - ), + shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev), release: getSentryRelease(buildId), dryRun: isDev, }); diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts new file mode 100644 index 000000000000..a5b38d651aed --- /dev/null +++ b/packages/nextjs/src/edge/edgeclient.ts @@ -0,0 +1,69 @@ +import type { Scope } from '@sentry/core'; +import { BaseClient, SDK_VERSION } from '@sentry/core'; +import type { ClientOptions, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; + +import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; +import type { EdgeTransportOptions } from './transport'; + +export type EdgeClientOptions = ClientOptions; + +/** + * The Sentry Edge SDK Client. + */ +export class EdgeClient extends BaseClient { + /** + * Creates a new Edge SDK instance. + * @param options Configuration options for this SDK. + */ + public constructor(options: EdgeClientOptions) { + options._metadata = options._metadata || {}; + options._metadata.sdk = options._metadata.sdk || { + name: 'sentry.javascript.nextjs', + packages: [ + { + name: 'npm:@sentry/nextjs', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; + + super(options); + } + + /** + * @inheritDoc + */ + public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { + return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint)); + } + + /** + * @inheritDoc + */ + public eventFromMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + ): PromiseLike { + return Promise.resolve( + eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), + ); + } + + /** + * @inheritDoc + */ + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + event.platform = event.platform || 'edge'; + event.contexts = { + ...event.contexts, + runtime: event.contexts?.runtime || { + name: 'edge', + }, + }; + event.server_name = event.server_name || process.env.SENTRY_NAME; + return super._prepareEvent(event, hint, scope); + } +} diff --git a/packages/nextjs/src/edge/eventbuilder.ts b/packages/nextjs/src/edge/eventbuilder.ts new file mode 100644 index 000000000000..4e483fce3ff7 --- /dev/null +++ b/packages/nextjs/src/edge/eventbuilder.ts @@ -0,0 +1,130 @@ +import { getCurrentHub } from '@sentry/core'; +import type { + Event, + EventHint, + Exception, + Mechanism, + Severity, + SeverityLevel, + StackFrame, + StackParser, +} from '@sentry/types'; +import { + addExceptionMechanism, + addExceptionTypeValue, + extractExceptionKeysForMessage, + isError, + isPlainObject, + normalizeToSize, +} from '@sentry/utils'; + +/** + * Extracts stack frames from the error.stack string + */ +export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { + return stackParser(error.stack || '', 1); +} + +/** + * Extracts stack frames from the error and builds a Sentry Exception + */ +export function exceptionFromError(stackParser: StackParser, error: Error): Exception { + const exception: Exception = { + type: error.name || error.constructor.name, + value: error.message, + }; + + const frames = parseStackFrames(stackParser, error); + if (frames.length) { + exception.stacktrace = { frames }; + } + + return exception; +} + +/** + * Builds and Event from a Exception + * @hidden + */ +export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { + let ex: unknown = exception; + const providedMechanism: Mechanism | undefined = + hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; + const mechanism: Mechanism = providedMechanism || { + handled: true, + type: 'generic', + }; + + if (!isError(exception)) { + if (isPlainObject(exception)) { + // This will allow us to group events based on top-level keys + // which is much better than creating new group when any key/value change + const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; + + const hub = getCurrentHub(); + const client = hub.getClient(); + const normalizeDepth = client && client.getOptions().normalizeDepth; + hub.configureScope(scope => { + scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); + }); + + ex = (hint && hint.syntheticException) || new Error(message); + (ex as Error).message = message; + } else { + // This handles when someone does: `throw "something awesome";` + // We use synthesized Error here so we can extract a (rough) stack trace. + ex = (hint && hint.syntheticException) || new Error(exception as string); + (ex as Error).message = exception as string; + } + mechanism.synthetic = true; + } + + const event = { + exception: { + values: [exceptionFromError(stackParser, ex as Error)], + }, + }; + + addExceptionTypeValue(event, undefined, undefined); + addExceptionMechanism(event, mechanism); + + return { + ...event, + event_id: hint && hint.event_id, + }; +} + +/** + * Builds and Event from a Message + * @hidden + */ +export function eventFromMessage( + stackParser: StackParser, + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + attachStacktrace?: boolean, +): Event { + const event: Event = { + event_id: hint && hint.event_id, + level, + message, + }; + + if (attachStacktrace && hint && hint.syntheticException) { + const frames = parseStackFrames(stackParser, hint.syntheticException); + if (frames.length) { + event.exception = { + values: [ + { + value: message, + stacktrace: { frames }, + }, + ], + }; + } + } + + return event; +} diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts new file mode 100644 index 000000000000..4f4d6a51799d --- /dev/null +++ b/packages/nextjs/src/edge/index.ts @@ -0,0 +1,148 @@ +import '@sentry/tracing'; // Allow people to call tracing API methods without explicitly importing the tracing package. + +import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import type { Options } from '@sentry/types'; +import { + createStackParser, + GLOBAL_OBJ, + logger, + nodeStackLineParser, + stackParserFromStackParserOptions, +} from '@sentry/utils'; + +import { EdgeClient } from './edgeclient'; +import { makeEdgeTransport } from './transport'; + +const nodeStackParser = createStackParser(nodeStackLineParser()); + +export const defaultIntegrations = [new CoreIntegrations.InboundFilters(), new CoreIntegrations.FunctionToString()]; + +export type EdgeOptions = Options; + +/** Inits the Sentry NextJS SDK on the Edge Runtime. */ +export function init(options: EdgeOptions = {}): void { + if (options.defaultIntegrations === undefined) { + options.defaultIntegrations = defaultIntegrations; + } + + if (options.dsn === undefined && process.env.SENTRY_DSN) { + options.dsn = process.env.SENTRY_DSN; + } + + if (options.tracesSampleRate === undefined && process.env.SENTRY_TRACES_SAMPLE_RATE) { + const tracesSampleRate = parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE); + if (isFinite(tracesSampleRate)) { + options.tracesSampleRate = tracesSampleRate; + } + } + + if (options.release === undefined) { + const detectedRelease = getSentryRelease(); + if (detectedRelease !== undefined) { + options.release = detectedRelease; + } else { + // If release is not provided, then we should disable autoSessionTracking + options.autoSessionTracking = false; + } + } + + if (options.environment === undefined && process.env.SENTRY_ENVIRONMENT) { + options.environment = process.env.SENTRY_ENVIRONMENT; + } + + if (options.autoSessionTracking === undefined && options.dsn !== undefined) { + options.autoSessionTracking = true; + } + + if (options.instrumenter === undefined) { + options.instrumenter = 'sentry'; + } + // TODO(v7): Refactor this to reduce the logic above + const clientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeEdgeTransport, + }; + + initAndBind(EdgeClient, clientOptions); + + // TODO?: Sessiontracking +} + +/** + * Returns a release dynamically from environment variables. + */ +export function getSentryRelease(fallback?: string): string | undefined { + // Always read first as Sentry takes this as precedence + if (process.env.SENTRY_RELEASE) { + return process.env.SENTRY_RELEASE; + } + + // This supports the variable that sentry-webpack-plugin injects + if (GLOBAL_OBJ.SENTRY_RELEASE && GLOBAL_OBJ.SENTRY_RELEASE.id) { + return GLOBAL_OBJ.SENTRY_RELEASE.id; + } + + return ( + // GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables + process.env.GITHUB_SHA || + // Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata + process.env.COMMIT_REF || + // Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables + process.env.VERCEL_GIT_COMMIT_SHA || + process.env.VERCEL_GITHUB_COMMIT_SHA || + process.env.VERCEL_GITLAB_COMMIT_SHA || + process.env.VERCEL_BITBUCKET_COMMIT_SHA || + // Zeit (now known as Vercel) + process.env.ZEIT_GITHUB_COMMIT_SHA || + process.env.ZEIT_GITLAB_COMMIT_SHA || + process.env.ZEIT_BITBUCKET_COMMIT_SHA || + fallback + ); +} + +/** + * Call `close()` on the current client, if there is one. See {@link Client.close}. + * + * @param timeout Maximum time in ms the client should wait to flush its event queue before shutting down. Omitting this + * parameter will cause the client to wait until all events are sent before disabling itself. + * @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it + * doesn't (or if there's no client defined). + */ +export async function close(timeout?: number): Promise { + const client = getCurrentHub().getClient(); + if (client) { + return client.close(timeout); + } + __DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. No client defined.'); + return Promise.resolve(false); +} + +/** + * Call `flush()` on the current client, if there is one. See {@link Client.flush}. + * + * @param timeout Maximum time in ms the client should wait to flush its event queue. Omitting this parameter will cause + * the client to wait until all events are sent before resolving the promise. + * @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it + * doesn't (or if there's no client defined). + */ +export async function flush(timeout?: number): Promise { + const client = getCurrentHub().getClient(); + if (client) { + return client.flush(timeout); + } + __DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.'); + return Promise.resolve(false); +} + +/** + * This is the getter for lastEventId. + * + * @returns The last event id of a captured event. + */ +export function lastEventId(): string | undefined { + return getCurrentHub().lastEventId(); +} + +export * from '@sentry/core'; diff --git a/packages/nextjs/src/edge/transport.ts b/packages/nextjs/src/edge/transport.ts new file mode 100644 index 000000000000..3fc4b8e101c3 --- /dev/null +++ b/packages/nextjs/src/edge/transport.ts @@ -0,0 +1,38 @@ +import { createTransport } from '@sentry/core'; +import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; + +export interface EdgeTransportOptions extends BaseTransportOptions { + /** Fetch API init parameters. Used by the FetchTransport */ + fetchOptions?: RequestInit; + /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ + headers?: { [key: string]: string }; +} + +/** + * Creates a Transport that uses the Edge Runtimes native fetch API to send events to Sentry. + */ +export function makeEdgeTransport(options: EdgeTransportOptions): Transport { + function makeRequest(request: TransportRequest): PromiseLike { + const requestOptions: RequestInit = { + body: request.body, + method: 'POST', + referrerPolicy: 'origin', + headers: options.headers, + ...options.fetchOptions, + }; + + try { + return fetch(options.url, requestOptions).then(response => ({ + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + })); + } catch (e) { + return Promise.reject(e); + } + } + + return createTransport(options, makeRequest); +} diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 133b6ecf1da0..4133c06089d5 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -1,2 +1,4 @@ export * from './config'; export * from './server'; + +// __SENTRY_SDK_MULTIPLEXER__ diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 5df30dffd580..fcce6708a293 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -5,28 +5,27 @@ export * from './config'; export * from './client'; export * from './server'; +export * from './edge'; import type { Integration, Options, StackParser } from '@sentry/types'; import type { BrowserOptions } from './client'; import * as clientSdk from './client'; +import type { EdgeOptions } from './edge'; +import * as edgeSdk from './edge'; import type { NodeOptions } from './server'; import * as serverSdk from './server'; /** Initializes Sentry Next.js SDK */ -export declare function init(options: Options | BrowserOptions | NodeOptions): void; +export declare function init(options: Options | BrowserOptions | NodeOptions | EdgeOptions): void; // We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. -export const Integrations = { ...clientSdk.Integrations, ...serverSdk.Integrations }; +export const Integrations = { ...clientSdk.Integrations, ...serverSdk.Integrations, ...edgeSdk.Integrations }; export declare const defaultIntegrations: Integration[]; export declare const defaultStackParser: StackParser; -// This variable is not a runtime variable but just a type to tell typescript that the methods below can either come -// from the client SDK or from the server SDK. TypeScript is smart enough to understand that these resolve to the same -// methods from `@sentry/core`. -declare const runtime: 'client' | 'server'; - -export const close = runtime === 'client' ? clientSdk.close : serverSdk.close; -export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush; -export const lastEventId = runtime === 'client' ? clientSdk.lastEventId : serverSdk.lastEventId; +export declare function close(timeout?: number | undefined): PromiseLike; +export declare function flush(timeout?: number | undefined): PromiseLike; +export declare function lastEventId(): string | undefined; +export declare function getSentryRelease(fallback?: string): string | undefined; diff --git a/packages/nextjs/test/config/fixtures.ts b/packages/nextjs/test/config/fixtures.ts index 8edf88a8caf9..f747edbc2be9 100644 --- a/packages/nextjs/test/config/fixtures.ts +++ b/packages/nextjs/test/config/fixtures.ts @@ -9,6 +9,7 @@ import type { export const SERVER_SDK_CONFIG_FILE = 'sentry.server.config.js'; export const CLIENT_SDK_CONFIG_FILE = 'sentry.client.config.js'; +export const EDGE_SDK_CONFIG_FILE = 'sentry.edge.config.js'; /** Mock next config object */ export const userNextConfig: NextConfigObject = { @@ -43,7 +44,7 @@ export const serverWebpackConfig: WebpackConfigObject = { 'pages/_error': 'private-next-pages/_error.js', 'pages/_app': 'private-next-pages/_app.js', 'pages/sniffTour': ['./node_modules/smellOVision/index.js', 'private-next-pages/sniffTour.js'], - 'pages/api/_middleware': 'private-next-pages/api/_middleware.js', + middleware: 'private-next-pages/middleware.js', 'pages/api/simulator/dogStats/[name]': { import: 'private-next-pages/api/simulator/dogStats/[name].js' }, 'pages/simulator/leaderboard': { import: ['./node_modules/dogPoints/converter.js', 'private-next-pages/simulator/leaderboard.js'], @@ -84,7 +85,7 @@ export const clientWebpackConfig: WebpackConfigObject = { * @returns A mock build context for the given target */ export function getBuildContext( - buildTarget: 'server' | 'client', + buildTarget: 'server' | 'client' | 'edge', materializedNextConfig: ExportedNextConfig, webpackVersion: string = '5.4.15', ): BuildContext { @@ -101,9 +102,11 @@ export function getBuildContext( webpack: { version: webpackVersion }, defaultLoaders: true, totalPages: 2, - isServer: buildTarget === 'server', + isServer: buildTarget === 'server' || buildTarget === 'edge', + nextRuntime: ({ server: 'nodejs', client: undefined, edge: 'edge' } as const)[buildTarget], }; } export const serverBuildContext = getBuildContext('server', exportedNextConfig); export const clientBuildContext = getBuildContext('client', exportedNextConfig); +export const edgeBuildContext = getBuildContext('edge', exportedNextConfig); diff --git a/packages/nextjs/test/config/mocks.ts b/packages/nextjs/test/config/mocks.ts index 581b7d2bbbd1..ddf4ce4d1553 100644 --- a/packages/nextjs/test/config/mocks.ts +++ b/packages/nextjs/test/config/mocks.ts @@ -6,7 +6,7 @@ import * as os from 'os'; import * as path from 'path'; import * as rimraf from 'rimraf'; -import { CLIENT_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures'; +import { CLIENT_SDK_CONFIG_FILE, EDGE_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures'; // 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 @@ -14,7 +14,11 @@ import { CLIENT_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures'; // function also lets us restore the original when we do want to test `getUserConfigFile()`. export const realExistsSync = jest.requireActual('fs').existsSync; export const mockExistsSync = (path: fs.PathLike): ReturnType => { - if ((path as string).endsWith(SERVER_SDK_CONFIG_FILE) || (path as string).endsWith(CLIENT_SDK_CONFIG_FILE)) { + if ( + (path as string).endsWith(SERVER_SDK_CONFIG_FILE) || + (path as string).endsWith(CLIENT_SDK_CONFIG_FILE) || + (path as string).endsWith(EDGE_SDK_CONFIG_FILE) + ) { return true; } diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 206050d56d38..bee971f104e6 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -6,6 +6,8 @@ import { CLIENT_SDK_CONFIG_FILE, clientBuildContext, clientWebpackConfig, + EDGE_SDK_CONFIG_FILE, + edgeBuildContext, exportedNextConfig, SERVER_SDK_CONFIG_FILE, serverBuildContext, @@ -87,6 +89,7 @@ describe('constructWebpackConfigFunction()', () => { describe('webpack `entry` property config', () => { const serverConfigFilePath = `./${SERVER_SDK_CONFIG_FILE}`; const clientConfigFilePath = `./${CLIENT_SDK_CONFIG_FILE}`; + const edgeConfigFilePath = `./${EDGE_SDK_CONFIG_FILE}`; it('handles various entrypoint shapes', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ @@ -207,17 +210,16 @@ describe('constructWebpackConfigFunction()', () => { ); }); - it('does not inject user config file into API middleware', async () => { + it('injects user config file into API middleware', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, - incomingWebpackBuildContext: serverBuildContext, + incomingWebpackBuildContext: edgeBuildContext, }); expect(finalWebpackConfig.entry).toEqual( expect.objectContaining({ - // no injected file - 'pages/api/_middleware': 'private-next-pages/api/_middleware.js', + middleware: [edgeConfigFilePath, 'private-next-pages/middleware.js'], }), ); });