From 75982baf72527e613712a6bc1d99b8c6bf1942a0 Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Tue, 21 Jan 2025 13:46:42 -0800 Subject: [PATCH] Split entrypoint/route handling into separate dev and prod versions Currently, for `handleEntrypoints`, `handlePagesErrorRoute`, `handleRouteType`, etc, both dev and prod use cases are combined into the same functions. This leads to a lot of branching, use-case-specific arguments, etc. for minimal opportunity for shared code. In a following PR, entrypoint writing for prod will be done through a single napi call to rust, further branching the two versions. This splits them formally, at the cost of duplicating a handful of lines in each case. Perhaps in the future we can develop a better system for sharing code, but this makes things far clearer and easier to maintain in this moment. Test Plan: CI --- packages/next/src/build/handle-entrypoints.ts | 334 ++++++++++++++ packages/next/src/build/index.ts | 36 +- packages/next/src/build/swc/index.ts | 2 +- packages/next/src/build/swc/types.ts | 46 +- .../src/server/dev/hot-reloader-turbopack.ts | 26 +- .../next/src/server/dev/turbopack-utils.ts | 431 ++++-------------- .../next/src/server/dev/turbopack/types.ts | 49 -- .../lib/router-utils/setup-dev-bundler.ts | 8 +- .../dev => shared/lib}/turbopack/entry-key.ts | 0 .../lib}/turbopack/manifest-loader.ts | 18 +- .../next/src/shared/lib/turbopack/utils.ts | 241 ++++++++++ test/development/basic/next-rs-api.test.ts | 8 +- 12 files changed, 754 insertions(+), 445 deletions(-) create mode 100644 packages/next/src/build/handle-entrypoints.ts delete mode 100644 packages/next/src/server/dev/turbopack/types.ts rename packages/next/src/{server/dev => shared/lib}/turbopack/entry-key.ts (100%) rename packages/next/src/{server/dev => shared/lib}/turbopack/manifest-loader.ts (97%) create mode 100644 packages/next/src/shared/lib/turbopack/utils.ts diff --git a/packages/next/src/build/handle-entrypoints.ts b/packages/next/src/build/handle-entrypoints.ts new file mode 100644 index 0000000000000..adcac7d45049f --- /dev/null +++ b/packages/next/src/build/handle-entrypoints.ts @@ -0,0 +1,334 @@ +import type { CustomRoutes } from '../lib/load-custom-routes' +import type { TurbopackManifestLoader } from '../shared/lib/turbopack/manifest-loader' +import type { + TurbopackResult, + RawEntrypoints, + Entrypoints, + PageRoute, + AppRoute, +} from './swc/types' +import * as Log from './output/log' +import { getEntryKey } from '../shared/lib/turbopack/entry-key' +import { + processIssues, + type EntryIssuesMap, +} from '../shared/lib/turbopack/utils' + +export async function handleEntrypoints({ + entrypoints, + currentEntrypoints, + currentEntryIssues, + manifestLoader, + productionRewrites, + logErrors, +}: { + entrypoints: TurbopackResult + currentEntrypoints: Entrypoints + currentEntryIssues: EntryIssuesMap + manifestLoader: TurbopackManifestLoader + productionRewrites: CustomRoutes['rewrites'] | undefined + logErrors: boolean +}) { + currentEntrypoints.global.app = entrypoints.pagesAppEndpoint + currentEntrypoints.global.document = entrypoints.pagesDocumentEndpoint + currentEntrypoints.global.error = entrypoints.pagesErrorEndpoint + + currentEntrypoints.global.instrumentation = entrypoints.instrumentation + + currentEntrypoints.page.clear() + currentEntrypoints.app.clear() + + for (const [pathname, route] of entrypoints.routes) { + switch (route.type) { + case 'page': + case 'page-api': + currentEntrypoints.page.set(pathname, route) + break + case 'app-page': { + route.pages.forEach((page) => { + currentEntrypoints.app.set(page.originalName, { + type: 'app-page', + ...page, + }) + }) + break + } + case 'app-route': { + currentEntrypoints.app.set(route.originalName, route) + break + } + default: + Log.info(`skipping ${pathname} (${route.type})`) + break + } + } + + const { middleware, instrumentation } = entrypoints + + // We check for explicit true/false, since it's initialized to + // undefined during the first loop (middlewareChanges event is + // unnecessary during the first serve) + if (currentEntrypoints.global.middleware && !middleware) { + const key = getEntryKey('root', 'server', 'middleware') + // Went from middleware to no middleware + currentEntryIssues.delete(key) + } + + currentEntrypoints.global.middleware = middleware + + if (instrumentation) { + const processInstrumentation = async ( + name: string, + prop: 'nodeJs' | 'edge' + ) => { + const key = getEntryKey('root', 'server', name) + + const writtenEndpoint = await instrumentation[prop].writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + await processInstrumentation('instrumentation.nodeJs', 'nodeJs') + await processInstrumentation('instrumentation.edge', 'edge') + await manifestLoader.loadMiddlewareManifest( + 'instrumentation', + 'instrumentation' + ) + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints: currentEntrypoints, + }) + } + + if (middleware) { + const key = getEntryKey('root', 'server', 'middleware') + + const endpoint = middleware.endpoint + + async function processMiddleware() { + const writtenEndpoint = await endpoint.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + await manifestLoader.loadMiddlewareManifest('middleware', 'middleware') + } + await processMiddleware() + } else { + manifestLoader.deleteMiddlewareManifest( + getEntryKey('root', 'server', 'middleware') + ) + } +} + +export async function handlePagesErrorRoute({ + currentEntryIssues, + entrypoints, + manifestLoader, + productionRewrites, + logErrors, +}: { + currentEntryIssues: EntryIssuesMap + entrypoints: Entrypoints + manifestLoader: TurbopackManifestLoader + productionRewrites: CustomRoutes['rewrites'] | undefined + logErrors: boolean +}) { + if (entrypoints.global.app) { + const key = getEntryKey('pages', 'server', '_app') + const writtenEndpoint = await entrypoints.global.app.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + await manifestLoader.loadBuildManifest('_app') + await manifestLoader.loadPagesManifest('_app') + await manifestLoader.loadFontManifest('_app') + + if (entrypoints.global.document) { + const key = getEntryKey('pages', 'server', '_document') + const writtenEndpoint = await entrypoints.global.document.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + await manifestLoader.loadPagesManifest('_document') + + if (entrypoints.global.error) { + const key = getEntryKey('pages', 'server', '_error') + const writtenEndpoint = await entrypoints.global.error.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + + await manifestLoader.loadBuildManifest('_error') + await manifestLoader.loadPagesManifest('_error') + await manifestLoader.loadFontManifest('_error') + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) +} + +export async function handleRouteType({ + page, + route, + currentEntryIssues, + entrypoints, + manifestLoader, + productionRewrites, + logErrors, +}: { + page: string + route: PageRoute | AppRoute + + currentEntryIssues: EntryIssuesMap + entrypoints: Entrypoints + manifestLoader: TurbopackManifestLoader + productionRewrites: CustomRoutes['rewrites'] | undefined + logErrors: boolean +}) { + const shouldCreateWebpackStats = process.env.TURBOPACK_STATS != null + + switch (route.type) { + case 'page': { + const serverKey = getEntryKey('pages', 'server', page) + + if (entrypoints.global.app) { + const key = getEntryKey('pages', 'server', '_app') + + const writtenEndpoint = await entrypoints.global.app.writeToDisk() + processIssues( + currentEntryIssues, + key, + writtenEndpoint, + false, + logErrors + ) + } + await manifestLoader.loadBuildManifest('_app') + await manifestLoader.loadPagesManifest('_app') + + if (entrypoints.global.document) { + const key = getEntryKey('pages', 'server', '_document') + + const writtenEndpoint = await entrypoints.global.document.writeToDisk() + processIssues( + currentEntryIssues, + key, + writtenEndpoint, + false, + logErrors + ) + } + await manifestLoader.loadPagesManifest('_document') + + const writtenEndpoint = await route.htmlEndpoint.writeToDisk() + + const type = writtenEndpoint?.type + + await manifestLoader.loadBuildManifest(page) + await manifestLoader.loadPagesManifest(page) + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'pages') + } else { + manifestLoader.deleteMiddlewareManifest(serverKey) + } + await manifestLoader.loadFontManifest('/_app', 'pages') + await manifestLoader.loadFontManifest(page, 'pages') + + if (shouldCreateWebpackStats) { + await manifestLoader.loadWebpackStats(page, 'pages') + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + + processIssues( + currentEntryIssues, + serverKey, + writtenEndpoint, + false, + logErrors + ) + + break + } + case 'page-api': { + const key = getEntryKey('pages', 'server', page) + + const writtenEndpoint = await route.endpoint.writeToDisk() + + const type = writtenEndpoint.type + + await manifestLoader.loadPagesManifest(page) + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'pages') + } else { + manifestLoader.deleteMiddlewareManifest(key) + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + + processIssues(currentEntryIssues, key, writtenEndpoint, true, logErrors) + + break + } + case 'app-page': { + const key = getEntryKey('app', 'server', page) + const writtenEndpoint = await route.htmlEndpoint.writeToDisk() + const type = writtenEndpoint.type + + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'app') + } else { + manifestLoader.deleteMiddlewareManifest(key) + } + + await manifestLoader.loadAppBuildManifest(page) + await manifestLoader.loadBuildManifest(page, 'app') + await manifestLoader.loadAppPathsManifest(page) + await manifestLoader.loadActionManifest(page) + await manifestLoader.loadFontManifest(page, 'app') + + if (shouldCreateWebpackStats) { + await manifestLoader.loadWebpackStats(page, 'app') + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + + break + } + case 'app-route': { + const key = getEntryKey('app', 'server', page) + const writtenEndpoint = await route.endpoint.writeToDisk() + const type = writtenEndpoint.type + + await manifestLoader.loadAppPathsManifest(page) + + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'app') + } else { + manifestLoader.deleteMiddlewareManifest(key) + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + processIssues(currentEntryIssues, key, writtenEndpoint, true, logErrors) + + break + } + default: { + throw new Error(`unknown route type ${(route as any).type} for ${page}`) + } + } +} diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 47798a405cd21..e4e055f2ac40e 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -186,18 +186,7 @@ import { import { getStartServerInfo, logStartInfo } from '../server/lib/app-info-log' import type { NextEnabledDirectories } from '../server/base-server' import { hasCustomExportOutput } from '../export/utils' -import { - getTurbopackJsConfig, - handleEntrypoints, - type EntryIssuesMap, - handleRouteType, - handlePagesErrorRoute, - formatIssue, - isRelevantWarning, - isPersistentCachingEnabled, -} from '../server/dev/turbopack-utils' -import { TurbopackManifestLoader } from '../server/dev/turbopack/manifest-loader' -import type { Entrypoints } from '../server/dev/turbopack/types' +import { TurbopackManifestLoader } from '../shared/lib/turbopack/manifest-loader' import { buildCustomRoute } from '../lib/build-custom-route' import { createProgress } from './progress' import { traceMemoryUsage } from '../lib/memory/trace' @@ -218,6 +207,19 @@ import { import { InvariantError } from '../shared/lib/invariant-error' import { HTML_LIMITED_BOT_UA_RE_STRING } from '../shared/lib/router/utils/is-bot' import type { UseCacheTrackerKey } from './webpack/plugins/telemetry-plugin/use-cache-tracker-utils' +import { + handleEntrypoints, + handlePagesErrorRoute, + handleRouteType, +} from './handle-entrypoints' +import type { Entrypoints } from './swc/types' +import { + formatIssue, + getTurbopackJsConfig, + isPersistentCachingEnabled, + isRelevantWarning, + type EntryIssuesMap, +} from '../shared/lib/turbopack/utils' type Fallback = null | boolean | string @@ -1489,7 +1491,6 @@ export default async function build( currentEntrypoints, currentEntryIssues, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) @@ -1530,15 +1531,11 @@ export default async function build( for (const [page, route] of currentEntrypoints.page) { enqueue(() => handleRouteType({ - dev, page, - pathname: page, route, - currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) @@ -1550,13 +1547,10 @@ export default async function build( enqueue(() => handleRouteType({ page, - dev: false, - pathname: normalizeAppPath(page), route, currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) @@ -1565,11 +1559,9 @@ export default async function build( enqueue(() => handlePagesErrorRoute({ - dev: false, currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 1640442bd2f4a..58e17490b974c 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -21,7 +21,6 @@ import { getDefineEnv, } from '../webpack/plugins/define-env-plugin' import { getReactCompilerLoader } from '../get-babel-loader-config' -import { TurbopackInternalError } from '../../server/dev/turbopack-utils' import type { NapiPartialProjectOptions, NapiProjectOptions, @@ -41,6 +40,7 @@ import type { UpdateMessage, WrittenEndpoint, } from './types' +import { TurbopackInternalError } from '../../shared/lib/turbopack/utils' type RawBindings = typeof import('./generated-native') type RawWasmBindings = typeof import('./generated-wasm') & { diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 67af229ca489b..8327e8bb5bb86 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -120,7 +120,7 @@ export interface Instrumentation { edge: Endpoint } -export interface Entrypoints { +export interface RawEntrypoints { routes: Map middleware?: Middleware instrumentation?: Instrumentation @@ -192,7 +192,7 @@ export interface UpdateInfo { export interface Project { update(options: Partial): Promise - entrypointsSubscribe(): AsyncIterableIterator> + entrypointsSubscribe(): AsyncIterableIterator> hmrEvents(identifier: string): AsyncIterableIterator> @@ -395,3 +395,45 @@ export interface DefineEnv { } export type RustifiedEnv = { name: string; value: string }[] + +export interface GlobalEntrypoints { + app: Endpoint | undefined + document: Endpoint | undefined + error: Endpoint | undefined + middleware: Middleware | undefined + instrumentation: Instrumentation | undefined +} + +export type PageRoute = + | { + type: 'page' + htmlEndpoint: Endpoint + dataEndpoint: Endpoint + } + | { + type: 'page-api' + endpoint: Endpoint + } + +export type AppRoute = + | { + type: 'app-page' + htmlEndpoint: Endpoint + rscEndpoint: Endpoint + } + | { + type: 'app-route' + endpoint: Endpoint + } + +// pathname -> route +export type PageEntrypoints = Map + +// originalName / page -> route +export type AppEntrypoints = Map + +export type Entrypoints = { + global: GlobalEntrypoints + page: PageEntrypoints + app: AppEntrypoints +} diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 47af297218d72..2b224932a9ae4 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -22,6 +22,7 @@ import type { WrittenEndpoint, TurbopackResult, Project, + Entrypoints, } from '../../build/swc/types' import { createDefineEnv } from '../../build/swc' import * as Log from '../../build/output/log' @@ -47,40 +48,31 @@ import { AssetMapper, type ChangeSubscriptions, type ClientState, - type EntryIssuesMap, - formatIssue, - getTurbopackJsConfig, handleEntrypoints, handlePagesErrorRoute, handleRouteType, hasEntrypointForKey, msToNs, - processIssues, type ReadyIds, - renderStyledStringToErrorAnsi, type SendHmr, type StartBuilding, processTopLevelIssues, - type TopLevelIssuesMap, - isWellKnownError, printNonFatalIssue, normalizedPageToTurbopackStructureRoute, - isPersistentCachingEnabled, } from './turbopack-utils' import { propagateServerField, type ServerFields, type SetupOpts, } from '../lib/router-utils/setup-dev-bundler' -import { TurbopackManifestLoader } from './turbopack/manifest-loader' -import type { Entrypoints } from './turbopack/types' +import { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loader' import { findPagePathData } from './on-demand-entry-handler' import type { RouteDefinition } from '../route-definitions/route-definition' import { type EntryKey, getEntryKey, splitEntryKey, -} from './turbopack/entry-key' +} from '../../shared/lib/turbopack/entry-key' import { FAST_REFRESH_RUNTIME_RELOAD } from './messages' import { generateEncryptionKeyBase64 } from '../app-render/encryption-utils-server' import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-definition' @@ -92,6 +84,16 @@ import { type ModernSourceMapPayload, } from '../patch-error-inspect' import { getNextErrorFeedbackMiddleware } from '../../client/components/react-dev-overlay/server/get-next-error-feedback-middleware' +import { + formatIssue, + getTurbopackJsConfig, + isPersistentCachingEnabled, + isWellKnownError, + processIssues, + renderStyledStringToErrorAnsi, + type EntryIssuesMap, + type TopLevelIssuesMap, +} from '../../shared/lib/turbopack/utils' // import { getSupportedBrowsers } from '../../build/utils' const wsServer = new ws.Server({ noServer: true }) @@ -973,14 +975,12 @@ export async function createHotReloaderTurbopack( let finishBuilding = startBuilding(pathname, requestUrl, false) try { await handlePagesErrorRoute({ - dev: true, currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, devRewrites: opts.fsChecker.rewrites, productionRewrites: undefined, logErrors: true, - hooks: { subscribeToChanges, handleWrittenEndpoint: (id, result) => { diff --git a/packages/next/src/server/dev/turbopack-utils.ts b/packages/next/src/server/dev/turbopack-utils.ts index e8b0ce1407a00..39ff45754e7f7 100644 --- a/packages/next/src/server/dev/turbopack-utils.ts +++ b/packages/next/src/server/dev/turbopack-utils.ts @@ -1,83 +1,40 @@ -import type { NextConfigComplete } from '../config-shared' -import loadJsConfig from '../../build/load-jsconfig' import type { ServerFields, SetupOpts, } from '../lib/router-utils/setup-dev-bundler' import type { Issue, - StyledString, TurbopackResult, Endpoint, - Entrypoints as RawEntrypoints, + RawEntrypoints, Update as TurbopackUpdate, WrittenEndpoint, } from '../../build/swc/types' -import { - decodeMagicIdentifier, - MAGIC_IDENTIFIER_REGEX, -} from '../../shared/lib/magic-identifier' -import { bold, green, magenta, red } from '../../lib/picocolors' import { type HMR_ACTION_TYPES, HMR_ACTIONS_SENT_TO_BROWSER, } from './hot-reloader-types' import * as Log from '../../build/output/log' import type { PropagateToWorkersField } from '../lib/router-utils/types' -import type { TurbopackManifestLoader } from './turbopack/manifest-loader' -import type { AppRoute, Entrypoints, PageRoute } from './turbopack/types' +import type { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loader' +import type { AppRoute, Entrypoints, PageRoute } from '../../build/swc/types' import { type EntryKey, getEntryKey, splitEntryKey, -} from './turbopack/entry-key' +} from '../../shared/lib/turbopack/entry-key' import type ws from 'next/dist/compiled/ws' -import isInternal from '../../shared/lib/is-internal' import { isMetadataRoute } from '../../lib/metadata/is-metadata-route' import type { CustomRoutes } from '../../lib/load-custom-routes' - -export async function getTurbopackJsConfig( - dir: string, - nextConfig: NextConfigComplete -) { - const { jsConfig } = await loadJsConfig(dir, nextConfig) - return jsConfig ?? { compilerOptions: {} } -} - -// An error generated from emitted Turbopack issues. This can include build -// errors caused by issues with user code. -export class ModuleBuildError extends Error { - name = 'ModuleBuildError' -} - -// An error caused by an internal issue in Turbopack. These should be written -// to a log file and details should not be shown to the user. -export class TurbopackInternalError extends Error { - name = 'TurbopackInternalError' - - constructor(cause: Error) { - super(cause.message) - this.stack = cause.stack - } -} - -/** - * Thin stopgap workaround layer to mimic existing wellknown-errors-plugin in webpack's build - * to emit certain type of errors into cli. - */ -export function isWellKnownError(issue: Issue): boolean { - const { title } = issue - const formattedTitle = renderStyledStringToErrorAnsi(title) - // TODO: add more well known errors - if ( - formattedTitle.includes('Module not found') || - formattedTitle.includes('Unknown module type') - ) { - return true - } - - return false -} +import { + formatIssue, + getIssueKey, + isRelevantWarning, + processIssues, + renderStyledStringToErrorAnsi, + type EntryIssuesMap, + type TopLevelIssuesMap, +} from '../../shared/lib/turbopack/utils' const onceErrorSet = new Set() /** @@ -116,121 +73,6 @@ export function printNonFatalIssue(issue: Issue) { } } -function isNodeModulesIssue(issue: Issue): boolean { - if (issue.severity === 'warning' && issue.stage === 'config') { - // Override for the externalize issue - // `Package foo (serverExternalPackages or default list) can't be external` - if ( - renderStyledStringToErrorAnsi(issue.title).includes("can't be external") - ) { - return false - } - } - - return ( - issue.severity === 'warning' && - (issue.filePath.match(/^(?:.*[\\/])?node_modules(?:[\\/].*)?$/) !== null || - // Ignore Next.js itself when running next directly in the monorepo where it is not inside - // node_modules anyway. - // TODO(mischnic) prevent matches when this is published to npm - issue.filePath.startsWith('[project]/packages/next/')) - ) -} - -export function isRelevantWarning(issue: Issue): boolean { - return issue.severity === 'warning' && !isNodeModulesIssue(issue) -} - -export function formatIssue(issue: Issue) { - const { filePath, title, description, source } = issue - let { documentationLink } = issue - let formattedTitle = renderStyledStringToErrorAnsi(title).replace( - /\n/g, - '\n ' - ) - - // TODO: Use error codes to identify these - // TODO: Generalize adapting Turbopack errors to Next.js errors - if (formattedTitle.includes('Module not found')) { - // For compatiblity with webpack - // TODO: include columns in webpack errors. - documentationLink = 'https://nextjs.org/docs/messages/module-not-found' - } - - let formattedFilePath = filePath - .replace('[project]/', './') - .replaceAll('/./', '/') - .replace('\\\\?\\', '') - - let message = '' - - if (source && source.range) { - const { start } = source.range - message = `${formattedFilePath}:${start.line + 1}:${ - start.column + 1 - }\n${formattedTitle}` - } else if (formattedFilePath) { - message = `${formattedFilePath}\n${formattedTitle}` - } else { - message = formattedTitle - } - message += '\n' - - if ( - source?.range && - source.source.content && - // ignore Next.js/React internals, as these can often be huge bundled files. - !isInternal(filePath) - ) { - const { start, end } = source.range - const { codeFrameColumns } = require('next/dist/compiled/babel/code-frame') - - message += - codeFrameColumns( - source.source.content, - { - start: { - line: start.line + 1, - column: start.column + 1, - }, - end: { - line: end.line + 1, - column: end.column + 1, - }, - }, - { forceColor: true } - ).trim() + '\n\n' - } - - if (description) { - message += renderStyledStringToErrorAnsi(description) + '\n\n' - } - - // TODO: make it possible to enable this for debugging, but not in tests. - // if (detail) { - // message += renderStyledStringToErrorAnsi(detail) + '\n\n' - // } - - // TODO: Include a trace from the issue. - - if (documentationLink) { - message += documentationLink + '\n\n' - } - - return message -} - -type IssueKey = `${Issue['severity']}-${Issue['filePath']}-${string}-${string}` -export type IssuesMap = Map -export type EntryIssuesMap = Map -export type TopLevelIssuesMap = IssuesMap - -function getIssueKey(issue: Issue): IssueKey { - return `${issue.severity}-${issue.filePath}-${JSON.stringify( - issue.title - )}-${JSON.stringify(issue.description)}` -} - export function processTopLevelIssues( currentTopLevelIssues: TopLevelIssuesMap, result: TurbopackResult @@ -243,74 +85,6 @@ export function processTopLevelIssues( } } -export function processIssues( - currentEntryIssues: EntryIssuesMap, - key: EntryKey, - result: TurbopackResult, - throwIssue: boolean, - logErrors: boolean -) { - const newIssues = new Map() - currentEntryIssues.set(key, newIssues) - - const relevantIssues = new Set() - - for (const issue of result.issues) { - if ( - issue.severity !== 'error' && - issue.severity !== 'fatal' && - issue.severity !== 'warning' - ) - continue - - const issueKey = getIssueKey(issue) - newIssues.set(issueKey, issue) - - if (issue.severity !== 'warning') { - if (throwIssue) { - const formatted = formatIssue(issue) - relevantIssues.add(formatted) - } - // if we throw the issue it will most likely get handed and logged elsewhere - else if (logErrors && isWellKnownError(issue)) { - const formatted = formatIssue(issue) - Log.error(formatted) - } - } - } - - if (relevantIssues.size && throwIssue) { - throw new ModuleBuildError([...relevantIssues].join('\n\n')) - } -} - -export function renderStyledStringToErrorAnsi(string: StyledString): string { - function decodeMagicIdentifiers(str: string): string { - return str.replaceAll(MAGIC_IDENTIFIER_REGEX, (ident) => { - try { - return magenta(`{${decodeMagicIdentifier(ident)}}`) - } catch (e) { - return magenta(`{${ident} (decoding failed: ${e})}`) - } - }) - } - - switch (string.type) { - case 'text': - return decodeMagicIdentifiers(string.value) - case 'strong': - return bold(red(decodeMagicIdentifiers(string.value))) - case 'code': - return green(decodeMagicIdentifiers(string.value)) - case 'line': - return string.value.map(renderStyledStringToErrorAnsi).join('') - case 'stack': - return string.value.map(renderStyledStringToErrorAnsi).join('\n') - default: - throw new Error('Unknown StyledString type', string) - } -} - const MILLISECONDS_IN_NANOSECOND = BigInt(1_000_000) export function msToNs(ms: number): bigint { @@ -789,7 +563,6 @@ export async function handleEntrypoints({ currentEntryIssues, manifestLoader, devRewrites, - productionRewrites, logErrors, dev, }: { @@ -803,7 +576,7 @@ export async function handleEntrypoints({ productionRewrites: CustomRoutes['rewrites'] | undefined logErrors: boolean - dev?: HandleEntrypointsDevOpts + dev: HandleEntrypointsDevOpts }) { currentEntrypoints.global.app = entrypoints.pagesAppEndpoint currentEntrypoints.global.document = entrypoints.pagesDocumentEndpoint @@ -858,12 +631,12 @@ export async function handleEntrypoints({ // Went from middleware to no middleware await dev?.hooks.unsubscribeFromChanges(key) currentEntryIssues.delete(key) - dev?.hooks.sendHmr('middleware', { + dev.hooks.sendHmr('middleware', { event: HMR_ACTIONS_SENT_TO_BROWSER.MIDDLEWARE_CHANGES, }) } else if (!currentEntrypoints.global.middleware && middleware) { // Went from no middleware to middleware - dev?.hooks.sendHmr('middleware', { + dev.hooks.sendHmr('middleware', { event: HMR_ACTIONS_SENT_TO_BROWSER.MIDDLEWARE_CHANGES, }) } @@ -878,7 +651,7 @@ export async function handleEntrypoints({ const key = getEntryKey('root', 'server', name) const writtenEndpoint = await instrumentation[prop].writeToDisk() - dev?.hooks.handleWrittenEndpoint(key, writtenEndpoint) + dev.hooks.handleWrittenEndpoint(key, writtenEndpoint) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await processInstrumentation('instrumentation.nodeJs', 'nodeJs') @@ -889,25 +662,21 @@ export async function handleEntrypoints({ ) await manifestLoader.writeManifests({ devRewrites, - productionRewrites, + productionRewrites: undefined, entrypoints: currentEntrypoints, }) - if (dev) { - dev.serverFields.actualInstrumentationHookFile = '/instrumentation' - await dev.hooks.propagateServerField( - 'actualInstrumentationHookFile', - dev.serverFields.actualInstrumentationHookFile - ) - } + dev.serverFields.actualInstrumentationHookFile = '/instrumentation' + await dev.hooks.propagateServerField( + 'actualInstrumentationHookFile', + dev.serverFields.actualInstrumentationHookFile + ) } else { - if (dev) { - dev.serverFields.actualInstrumentationHookFile = undefined - await dev.hooks.propagateServerField( - 'actualInstrumentationHookFile', - dev.serverFields.actualInstrumentationHookFile - ) - } + dev.serverFields.actualInstrumentationHookFile = undefined + await dev.hooks.propagateServerField( + 'actualInstrumentationHookFile', + dev.serverFields.actualInstrumentationHookFile + ) } if (middleware) { @@ -917,7 +686,7 @@ export async function handleEntrypoints({ async function processMiddleware() { const writtenEndpoint = await endpoint.writeToDisk() - dev?.hooks.handleWrittenEndpoint(key, writtenEndpoint) + dev.hooks.handleWrittenEndpoint(key, writtenEndpoint) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) await manifestLoader.loadMiddlewareManifest('middleware', 'middleware') if (dev) { @@ -953,7 +722,7 @@ export async function handleEntrypoints({ ) await manifestLoader.writeManifests({ devRewrites, - productionRewrites, + productionRewrites: undefined, entrypoints: currentEntrypoints, }) @@ -971,22 +740,18 @@ export async function handleEntrypoints({ manifestLoader.deleteMiddlewareManifest( getEntryKey('root', 'server', 'middleware') ) - if (dev) { - dev.serverFields.actualMiddlewareFile = undefined - dev.serverFields.middleware = undefined - } + dev.serverFields.actualMiddlewareFile = undefined + dev.serverFields.middleware = undefined } - if (dev) { - await dev.hooks.propagateServerField( - 'actualMiddlewareFile', - dev.serverFields.actualMiddlewareFile - ) - await dev.hooks.propagateServerField( - 'middleware', - dev.serverFields.middleware - ) - } + await dev.hooks.propagateServerField( + 'actualMiddlewareFile', + dev.serverFields.actualMiddlewareFile + ) + await dev.hooks.propagateServerField( + 'middleware', + dev.serverFields.middleware + ) } async function handleEntrypointsDevCleanup({ @@ -1050,49 +815,43 @@ async function handleEntrypointsDevCleanup({ } export async function handlePagesErrorRoute({ - dev, currentEntryIssues, entrypoints, manifestLoader, devRewrites, productionRewrites, logErrors, - hooks, }: { - dev: boolean currentEntryIssues: EntryIssuesMap entrypoints: Entrypoints manifestLoader: TurbopackManifestLoader devRewrites: SetupOpts['fsChecker']['rewrites'] | undefined productionRewrites: CustomRoutes['rewrites'] | undefined logErrors: boolean - - hooks?: HandleRouteTypeHooks // dev + hooks: HandleRouteTypeHooks }) { if (entrypoints.global.app) { const key = getEntryKey('pages', 'server', '_app') const writtenEndpoint = await entrypoints.global.app.writeToDisk() - hooks?.handleWrittenEndpoint(key, writtenEndpoint) - if (dev) { - hooks?.subscribeToChanges( - key, - false, - entrypoints.global.app, - () => { - // There's a special case for this in `../client/page-bootstrap.ts`. - // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 - return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } - }, - () => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: '_app has changed (error route)', - } + hooks.handleWrittenEndpoint(key, writtenEndpoint) + hooks.subscribeToChanges( + key, + false, + entrypoints.global.app, + () => { + // There's a special case for this in `../client/page-bootstrap.ts`. + // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 + return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } + }, + () => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: '_app has changed (error route)', } - ) - } + } + ) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await manifestLoader.loadBuildManifest('_app') @@ -1103,26 +862,24 @@ export async function handlePagesErrorRoute({ const key = getEntryKey('pages', 'server', '_document') const writtenEndpoint = await entrypoints.global.document.writeToDisk() - hooks?.handleWrittenEndpoint(key, writtenEndpoint) - if (dev) { - hooks?.subscribeToChanges( - key, - false, - entrypoints.global.document, - () => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: '_document has changed (error route)', - } - }, - (e) => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: `error in _document subscription (error route): ${e}`, - } + hooks.handleWrittenEndpoint(key, writtenEndpoint) + hooks.subscribeToChanges( + key, + false, + entrypoints.global.document, + () => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: '_document has changed (error route)', } - ) - } + }, + (e) => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: `error in _document subscription (error route): ${e}`, + } + } + ) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await manifestLoader.loadPagesManifest('_document') @@ -1131,25 +888,23 @@ export async function handlePagesErrorRoute({ const key = getEntryKey('pages', 'server', '_error') const writtenEndpoint = await entrypoints.global.error.writeToDisk() - hooks?.handleWrittenEndpoint(key, writtenEndpoint) - if (dev) { - hooks?.subscribeToChanges( - key, - false, - entrypoints.global.error, - () => { - // There's a special case for this in `../client/page-bootstrap.ts`. - // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 - return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } - }, - (e) => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: `error in _error subscription: ${e}`, - } + hooks.handleWrittenEndpoint(key, writtenEndpoint) + hooks.subscribeToChanges( + key, + false, + entrypoints.global.error, + () => { + // There's a special case for this in `../client/page-bootstrap.ts`. + // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 + return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } + }, + (e) => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: `error in _error subscription: ${e}`, } - ) - } + } + ) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await manifestLoader.loadBuildManifest('_error') @@ -1202,9 +957,3 @@ export function normalizedPageToTurbopackStructureRoute( } return entrypointKey } - -export function isPersistentCachingEnabled( - config: NextConfigComplete -): boolean { - return config.experimental.turbo?.unstablePersistentCaching || false -} diff --git a/packages/next/src/server/dev/turbopack/types.ts b/packages/next/src/server/dev/turbopack/types.ts deleted file mode 100644 index 31b26fa383157..0000000000000 --- a/packages/next/src/server/dev/turbopack/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { - Endpoint, - Instrumentation, - Middleware, -} from '../../../build/swc/types' - -export interface GlobalEntrypoints { - app: Endpoint | undefined - document: Endpoint | undefined - error: Endpoint | undefined - - middleware: Middleware | undefined - instrumentation: Instrumentation | undefined -} - -export type PageRoute = - | { - type: 'page' - htmlEndpoint: Endpoint - dataEndpoint: Endpoint - } - | { - type: 'page-api' - endpoint: Endpoint - } - -export type AppRoute = - | { - type: 'app-page' - htmlEndpoint: Endpoint - rscEndpoint: Endpoint - } - | { - type: 'app-route' - endpoint: Endpoint - } - -// pathname -> route -export type PageEntrypoints = Map - -// originalName / page -> route -export type AppEntrypoints = Map - -export type Entrypoints = { - global: GlobalEntrypoints - - page: PageEntrypoints - app: AppEntrypoints -} diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index cba059010c8fd..9ad83a12dba38 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -66,15 +66,15 @@ import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../dev/hot-reloader-types' import { PAGE_TYPES } from '../../../lib/page-types' import { createHotReloaderTurbopack } from '../../dev/hot-reloader-turbopack' import { generateEncryptionKeyBase64 } from '../../app-render/encryption-utils-server' -import { - ModuleBuildError, - TurbopackInternalError, -} from '../../dev/turbopack-utils' import { isMetadataRouteFile } from '../../../lib/metadata/is-metadata-route' import { normalizeMetadataPageToRoute } from '../../../lib/metadata/get-metadata-route' import { createEnvDefinitions } from '../experimental/create-env-definitions' import { JsConfigPathsPlugin } from '../../../build/webpack/plugins/jsconfig-paths-plugin' import { store as consoleStore } from '../../../build/output/store' +import { + ModuleBuildError, + TurbopackInternalError, +} from '../../../shared/lib/turbopack/utils' export type SetupOpts = { renderServer: LazyRenderServerInstance diff --git a/packages/next/src/server/dev/turbopack/entry-key.ts b/packages/next/src/shared/lib/turbopack/entry-key.ts similarity index 100% rename from packages/next/src/server/dev/turbopack/entry-key.ts rename to packages/next/src/shared/lib/turbopack/entry-key.ts diff --git a/packages/next/src/server/dev/turbopack/manifest-loader.ts b/packages/next/src/shared/lib/turbopack/manifest-loader.ts similarity index 97% rename from packages/next/src/server/dev/turbopack/manifest-loader.ts rename to packages/next/src/shared/lib/turbopack/manifest-loader.ts index 294f66139adcd..cd62e5b53f884 100644 --- a/packages/next/src/server/dev/turbopack/manifest-loader.ts +++ b/packages/next/src/shared/lib/turbopack/manifest-loader.ts @@ -3,14 +3,14 @@ import type { MiddlewareManifest, } from '../../../build/webpack/plugins/middleware-plugin' import type { StatsAsset, StatsChunk, StatsChunkGroup, StatsModule, StatsCompilation as WebpackStats } from 'webpack' -import type { BuildManifest } from '../../get-page-files' +import type { BuildManifest } from '../../../server/get-page-files' import type { AppBuildManifest } from '../../../build/webpack/plugins/app-build-manifest-plugin' import type { PagesManifest } from '../../../build/webpack/plugins/pages-manifest-plugin' import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' import type { ActionManifest } from '../../../build/webpack/plugins/flight-client-entry-plugin' import type { NextFontManifest } from '../../../build/webpack/plugins/next-font-manifest-plugin' import type { - REACT_LOADABLE_MANIFEST} from '../../../shared/lib/constants'; + REACT_LOADABLE_MANIFEST} from '../constants'; import { APP_BUILD_MANIFEST, APP_PATHS_MANIFEST, @@ -23,11 +23,11 @@ import { SERVER_REFERENCE_MANIFEST, TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST, WEBPACK_STATS, -} from '../../../shared/lib/constants' +} from '../constants' import { join, posix } from 'path' import { readFile } from 'fs/promises' -import type { SetupOpts } from '../../lib/router-utils/setup-dev-bundler' -import { deleteCache } from '../require-cache' +import type { SetupOpts } from '../../../server/lib/router-utils/setup-dev-bundler' +import { deleteCache } from '../../../server/dev/require-cache' import { writeFileAtomic } from '../../../lib/fs/write-atomic' import { isInterceptionRouteRewrite } from '../../../lib/generate-interception-routes-rewrites' import { @@ -36,14 +36,14 @@ import { srcEmptySsgManifest, processRoute, } from '../../../build/webpack/plugins/build-manifest-plugin' -import type { Entrypoints } from './types' -import getAssetPathFromRoute from '../../../shared/lib/router/utils/get-asset-path-from-route' +import getAssetPathFromRoute from '../router/utils/get-asset-path-from-route' import { getEntryKey, type EntryKey } from './entry-key' import type { CustomRoutes } from '../../../lib/load-custom-routes' -import { getSortedRoutes } from '../../../shared/lib/router/utils' +import { getSortedRoutes } from '../router/utils' import { existsSync } from 'fs' -import { addMetadataIdToRoute, addRouteSuffix, removeRouteSuffix } from '../turbopack-utils' +import { addMetadataIdToRoute, addRouteSuffix, removeRouteSuffix } from '../../../server/dev/turbopack-utils' import { tryToParsePath } from '../../../lib/try-to-parse-path' +import type { Entrypoints } from '../../../build/swc/types' interface InstrumentationDefinition { files: string[] diff --git a/packages/next/src/shared/lib/turbopack/utils.ts b/packages/next/src/shared/lib/turbopack/utils.ts new file mode 100644 index 0000000000000..e200f4a5d4d55 --- /dev/null +++ b/packages/next/src/shared/lib/turbopack/utils.ts @@ -0,0 +1,241 @@ +import type { Issue, StyledString, TurbopackResult } from '../../../build/swc/types' +import { bold, green, magenta, red } from '../../../lib/picocolors' +import isInternal from '../is-internal' +import { decodeMagicIdentifier, MAGIC_IDENTIFIER_REGEX } from '../magic-identifier' +import type { EntryKey } from './entry-key' +import * as Log from '../../../build/output/log' +import type { NextConfigComplete } from '../../../server/config-shared' +import loadJsConfig from '../../../build/load-jsconfig' + +type IssueKey = `${Issue['severity']}-${Issue['filePath']}-${string}-${string}` +export type IssuesMap = Map +export type EntryIssuesMap = Map +export type TopLevelIssuesMap = IssuesMap + +// An error generated from emitted Turbopack issues. This can include build +// errors caused by issues with user code. +export class ModuleBuildError extends Error { + name = 'ModuleBuildError' +} + +// An error caused by an internal issue in Turbopack. These should be written +// to a log file and details should not be shown to the user. +export class TurbopackInternalError extends Error { + name = 'TurbopackInternalError' + + constructor(cause: Error) { + super(cause.message) + this.stack = cause.stack + } +} + +/** + * Thin stopgap workaround layer to mimic existing wellknown-errors-plugin in webpack's build + * to emit certain type of errors into cli. + */ +export function isWellKnownError(issue: Issue): boolean { + const { title } = issue + const formattedTitle = renderStyledStringToErrorAnsi(title) + // TODO: add more well known errors + if ( + formattedTitle.includes('Module not found') || + formattedTitle.includes('Unknown module type') + ) { + return true + } + + return false +} + +export function getIssueKey(issue: Issue): IssueKey { + return `${issue.severity}-${issue.filePath}-${JSON.stringify( + issue.title + )}-${JSON.stringify(issue.description)}` +} + +export async function getTurbopackJsConfig( + dir: string, + nextConfig: NextConfigComplete +) { + const { jsConfig } = await loadJsConfig(dir, nextConfig) + return jsConfig ?? { compilerOptions: {} } +} + + +export function processIssues( + currentEntryIssues: EntryIssuesMap, + key: EntryKey, + result: TurbopackResult, + throwIssue: boolean, + logErrors: boolean +) { + const newIssues = new Map() + currentEntryIssues.set(key, newIssues) + + const relevantIssues = new Set() + + for (const issue of result.issues) { + if ( + issue.severity !== 'error' && + issue.severity !== 'fatal' && + issue.severity !== 'warning' + ) + continue + + const issueKey = getIssueKey(issue) + newIssues.set(issueKey, issue) + + if (issue.severity !== 'warning') { + if (throwIssue) { + const formatted = formatIssue(issue) + relevantIssues.add(formatted) + } + // if we throw the issue it will most likely get handed and logged elsewhere + else if (logErrors && isWellKnownError(issue)) { + const formatted = formatIssue(issue) + Log.error(formatted) + } + } + } + + if (relevantIssues.size && throwIssue) { + throw new ModuleBuildError([...relevantIssues].join('\n\n')) + } +} + +export function formatIssue(issue: Issue) { + const { filePath, title, description, source } = issue + let { documentationLink } = issue + let formattedTitle = renderStyledStringToErrorAnsi(title).replace( + /\n/g, + '\n ' + ) + + // TODO: Use error codes to identify these + // TODO: Generalize adapting Turbopack errors to Next.js errors + if (formattedTitle.includes('Module not found')) { + // For compatiblity with webpack + // TODO: include columns in webpack errors. + documentationLink = 'https://nextjs.org/docs/messages/module-not-found' + } + + let formattedFilePath = filePath + .replace('[project]/', './') + .replaceAll('/./', '/') + .replace('\\\\?\\', '') + + let message = '' + + if (source && source.range) { + const { start } = source.range + message = `${formattedFilePath}:${start.line + 1}:${ + start.column + 1 + }\n${formattedTitle}` + } else if (formattedFilePath) { + message = `${formattedFilePath}\n${formattedTitle}` + } else { + message = formattedTitle + } + message += '\n' + + if ( + source?.range && + source.source.content && + // ignore Next.js/React internals, as these can often be huge bundled files. + !isInternal(filePath) + ) { + const { start, end } = source.range + const { codeFrameColumns } = require('next/dist/compiled/babel/code-frame') + + message += + codeFrameColumns( + source.source.content, + { + start: { + line: start.line + 1, + column: start.column + 1, + }, + end: { + line: end.line + 1, + column: end.column + 1, + }, + }, + { forceColor: true } + ).trim() + '\n\n' + } + + if (description) { + message += renderStyledStringToErrorAnsi(description) + '\n\n' + } + + // TODO: make it possible to enable this for debugging, but not in tests. + // if (detail) { + // message += renderStyledStringToErrorAnsi(detail) + '\n\n' + // } + + // TODO: Include a trace from the issue. + + if (documentationLink) { + message += documentationLink + '\n\n' + } + + return message +} + +export function isRelevantWarning(issue: Issue): boolean { + return issue.severity === 'warning' && !isNodeModulesIssue(issue) +} + +function isNodeModulesIssue(issue: Issue): boolean { + if (issue.severity === 'warning' && issue.stage === 'config') { + // Override for the externalize issue + // `Package foo (serverExternalPackages or default list) can't be external` + if ( + renderStyledStringToErrorAnsi(issue.title).includes("can't be external") + ) { + return false + } + } + + return ( + issue.severity === 'warning' && + (issue.filePath.match(/^(?:.*[\\/])?node_modules(?:[\\/].*)?$/) !== null || + // Ignore Next.js itself when running next directly in the monorepo where it is not inside + // node_modules anyway. + // TODO(mischnic) prevent matches when this is published to npm + issue.filePath.startsWith('[project]/packages/next/')) + ) +} + +export function renderStyledStringToErrorAnsi(string: StyledString): string { + function decodeMagicIdentifiers(str: string): string { + return str.replaceAll(MAGIC_IDENTIFIER_REGEX, (ident) => { + try { + return magenta(`{${decodeMagicIdentifier(ident)}}`) + } catch (e) { + return magenta(`{${ident} (decoding failed: ${e})}`) + } + }) + } + + switch (string.type) { + case 'text': + return decodeMagicIdentifiers(string.value) + case 'strong': + return bold(red(decodeMagicIdentifiers(string.value))) + case 'code': + return green(decodeMagicIdentifiers(string.value)) + case 'line': + return string.value.map(renderStyledStringToErrorAnsi).join('') + case 'stack': + return string.value.map(renderStyledStringToErrorAnsi).join('\n') + default: + throw new Error('Unknown StyledString type', string) + } +} + +export function isPersistentCachingEnabled( + config: NextConfigComplete +): boolean { + return config.experimental.turbo?.unstablePersistentCaching || false +} diff --git a/test/development/basic/next-rs-api.test.ts b/test/development/basic/next-rs-api.test.ts index f97ab5866bbfc..e295487210668 100644 --- a/test/development/basic/next-rs-api.test.ts +++ b/test/development/basic/next-rs-api.test.ts @@ -4,9 +4,9 @@ import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' import { createDefineEnv, loadBindings } from 'next/dist/build/swc' import type { Diagnostics, - Entrypoints, Issue, Project, + RawEntrypoints, StyledString, TurbopackResult, UpdateInfo, @@ -329,7 +329,7 @@ describe('next.rs api', () => { // eslint-disable-next-line no-loop-func it(`should allow to write ${name} to disk`, async () => { const entrypointsSubscribtion = project.entrypointsSubscribe() - const entrypoints: TurbopackResult = ( + const entrypoints: TurbopackResult = ( await entrypointsSubscribtion.next() ).value const route = entrypoints.routes.get(path) @@ -465,7 +465,7 @@ describe('next.rs api', () => { console.log('start') await new Promise((r) => setTimeout(r, 1000)) const entrypointsSubscribtion = project.entrypointsSubscribe() - const entrypoints: TurbopackResult = ( + const entrypoints: TurbopackResult = ( await entrypointsSubscribtion.next() ).value const route = entrypoints.routes.get(path) @@ -608,7 +608,7 @@ describe('next.rs api', () => { console.log('start') await new Promise((r) => setTimeout(r, 1000)) const entrypointsSubscribtion = project.entrypointsSubscribe() - const entrypoints: TurbopackResult = ( + const entrypoints: TurbopackResult = ( await entrypointsSubscribtion.next() ).value const route = entrypoints.routes.get('/')