Dev Service (#56442)
This replaces the `(global as any)._nextDevHandlers` invocation with references to a specific service instance while also removing the module scoped `devInstances`. This ensures that correct types are used. This was done while changing the `match` parameter in `ensurePage` to `definition` which didn't cause a typescript error (it should have).
This commit is contained in:
parent
d21025cc3a
commit
a44b4f85b5
10 changed files with 203 additions and 191 deletions
20
.github/CODEOWNERS
vendored
20
.github/CODEOWNERS
vendored
|
@ -28,13 +28,13 @@
|
|||
|
||||
# Tooling & Telemetry
|
||||
|
||||
/packages/next/src/build/ @timneutkens @ijjk @shuding @huozhi @ztanner @feedthejim @vercel/web-tooling
|
||||
/packages/next/src/server/lib/router-utils/setup-dev.ts @timneutkens @ijjk @shuding @huozhi @feedthejim @ztanner @wyattjoh @vercel/web-tooling
|
||||
/packages/next/src/telemetry/ @timneutkens @ijjk @shuding @padmaia
|
||||
/packages/next-swc/ @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
Cargo.toml @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
Cargo.lock @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/.cargo/config.toml @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/.config/nextest.toml @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/test/build-turbopack-tests-manifest.js @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/test/turbopack-tests-manifest.json @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/packages/next/src/build/ @timneutkens @ijjk @shuding @huozhi @ztanner @feedthejim @vercel/web-tooling
|
||||
/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @timneutkens @ijjk @shuding @huozhi @feedthejim @ztanner @wyattjoh @vercel/web-tooling
|
||||
/packages/next/src/telemetry/ @timneutkens @ijjk @shuding @padmaia
|
||||
/packages/next-swc/ @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
Cargo.toml @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
Cargo.lock @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/.cargo/config.toml @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/.config/nextest.toml @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/test/build-turbopack-tests-manifest.js @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
/test/turbopack-tests-manifest.json @timneutkens @ijjk @shuding @huozhi @vercel/web-tooling
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { UrlObject } from 'url'
|
|||
import type { Duplex } from 'stream'
|
||||
import type { webpack } from 'next/dist/compiled/webpack/webpack'
|
||||
import type getBaseWebpackConfig from '../../build/webpack-config'
|
||||
import type { RouteMatch } from '../future/route-matches/route-match'
|
||||
import type { RouteDefinition } from '../future/route-definitions/route-definition'
|
||||
import type { Project, Update as TurbopackUpdate } from '../../build/swc'
|
||||
import type { VersionInfo } from './parse-version-info'
|
||||
|
||||
|
@ -134,13 +134,13 @@ export interface NextJsHotReloaderInterface {
|
|||
page,
|
||||
clientOnly,
|
||||
appPaths,
|
||||
match,
|
||||
definition,
|
||||
isApp,
|
||||
}: {
|
||||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths?: ReadonlyArray<string> | null
|
||||
isApp?: boolean
|
||||
match?: RouteMatch
|
||||
definition: RouteDefinition | undefined
|
||||
}): Promise<void>
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { Duplex } from 'stream'
|
|||
import type { Telemetry } from '../../telemetry/storage'
|
||||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import type { UrlObject } from 'url'
|
||||
import type { RouteDefinition } from '../future/route-definitions/route-definition'
|
||||
|
||||
import { webpack, StringXor } from 'next/dist/compiled/webpack/webpack'
|
||||
import { getOverlayMiddleware } from 'next/dist/compiled/@next/react-dev-overlay/dist/middleware'
|
||||
|
@ -60,7 +61,6 @@ import ws from 'next/dist/compiled/ws'
|
|||
import { existsSync, promises as fs } from 'fs'
|
||||
import { UnwrapPromise } from '../../lib/coalesced-function'
|
||||
import { getRegistry } from '../../lib/helpers/get-registry'
|
||||
import { RouteMatch } from '../future/route-matches/route-match'
|
||||
import { parseVersionInfo, VersionInfo } from './parse-version-info'
|
||||
import { isAPIRoute } from '../../lib/is-api-route'
|
||||
import { getRouteLoaderEntry } from '../../build/webpack/loaders/next-route-loader'
|
||||
|
@ -1466,14 +1466,14 @@ export default class HotReloader implements NextJsHotReloaderInterface {
|
|||
page,
|
||||
clientOnly,
|
||||
appPaths,
|
||||
match,
|
||||
definition,
|
||||
isApp,
|
||||
}: {
|
||||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths?: ReadonlyArray<string> | null
|
||||
isApp?: boolean
|
||||
match?: RouteMatch
|
||||
definition?: RouteDefinition
|
||||
}): Promise<void> {
|
||||
// Make sure we don't re-build or dispose prebuilt pages
|
||||
if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) {
|
||||
|
@ -1490,7 +1490,7 @@ export default class HotReloader implements NextJsHotReloaderInterface {
|
|||
page,
|
||||
clientOnly,
|
||||
appPaths,
|
||||
match,
|
||||
definition,
|
||||
isApp,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,12 +8,13 @@ import type { UrlWithParsedQuery } from 'url'
|
|||
import type { BaseNextRequest, BaseNextResponse } from '../base-http'
|
||||
import type { FallbackMode, MiddlewareRoutingItem } from '../base-server'
|
||||
import type { FunctionComponent } from 'react'
|
||||
import type { RouteMatch } from '../future/route-matches/route-match'
|
||||
import type { RouteDefinition } from '../future/route-definitions/route-definition'
|
||||
import type { RouteMatcherManager } from '../future/route-matcher-managers/route-matcher-manager'
|
||||
import type {
|
||||
NextParsedUrlQuery,
|
||||
NextUrlWithParsedQuery,
|
||||
} from '../request-meta'
|
||||
import type { DevBundlerService } from '../lib/dev-bundler-service'
|
||||
|
||||
import fs from 'fs'
|
||||
import { Worker } from 'next/dist/compiled/jest-worker'
|
||||
|
@ -80,6 +81,11 @@ export interface Options extends ServerOptions {
|
|||
* Tells of Next.js is running from the `next dev` command
|
||||
*/
|
||||
isNextDevCommand?: boolean
|
||||
|
||||
/**
|
||||
* Interface to the development bundler.
|
||||
*/
|
||||
bundlerService: DevBundlerService
|
||||
}
|
||||
|
||||
export default class DevServer extends Server {
|
||||
|
@ -92,15 +98,12 @@ export default class DevServer extends Server {
|
|||
private actualInstrumentationHookFile?: string
|
||||
private middleware?: MiddlewareRoutingItem
|
||||
private originalFetch: typeof fetch
|
||||
private readonly bundlerService: DevBundlerService
|
||||
private staticPathsCache: LRUCache<
|
||||
string,
|
||||
UnwrapPromise<ReturnType<DevServer['getStaticPaths']>>
|
||||
>
|
||||
|
||||
private invokeDevMethod({ method, args }: { method: string; args: any[] }) {
|
||||
return (global as any)._nextDevHandlers[method](this.dir, ...args)
|
||||
}
|
||||
|
||||
protected staticPathsWorker?: { [key: string]: any } & {
|
||||
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
|
||||
}
|
||||
|
@ -140,6 +143,7 @@ export default class DevServer extends Server {
|
|||
Error.stackTraceLimit = 50
|
||||
} catch {}
|
||||
super({ ...options, dev: true })
|
||||
this.bundlerService = options.bundlerService
|
||||
this.originalFetch = global.fetch
|
||||
this.renderOpts.dev = true
|
||||
this.renderOpts.appDirDevErrorLogger = (err: any) =>
|
||||
|
@ -190,7 +194,7 @@ export default class DevServer extends Server {
|
|||
const ensurer: RouteEnsurer = {
|
||||
ensure: async (match) => {
|
||||
await this.ensurePage({
|
||||
match,
|
||||
definition: match.definition,
|
||||
page: match.definition.page,
|
||||
clientOnly: false,
|
||||
})
|
||||
|
@ -487,10 +491,7 @@ export default class DevServer extends Server {
|
|||
err?: unknown,
|
||||
type?: 'unhandledRejection' | 'uncaughtException' | 'warning' | 'app-dir'
|
||||
): Promise<void> {
|
||||
await this.invokeDevMethod({
|
||||
method: 'logErrorWithOriginalStack',
|
||||
args: [err, type],
|
||||
})
|
||||
await this.bundlerService.logErrorWithOriginalStack(err, type)
|
||||
}
|
||||
|
||||
protected getPagesManifest(): PagesManifest | undefined {
|
||||
|
@ -534,6 +535,7 @@ export default class DevServer extends Server {
|
|||
return this.ensurePage({
|
||||
page: this.actualMiddlewareFile!,
|
||||
clientOnly: false,
|
||||
definition: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -543,6 +545,7 @@ export default class DevServer extends Server {
|
|||
(await this.ensurePage({
|
||||
page: this.actualInstrumentationHookFile!,
|
||||
clientOnly: false,
|
||||
definition: undefined,
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
|
@ -570,7 +573,12 @@ export default class DevServer extends Server {
|
|||
page: string
|
||||
appPaths: string[] | null
|
||||
}) {
|
||||
return this.ensurePage({ page, appPaths, clientOnly: false })
|
||||
return this.ensurePage({
|
||||
page,
|
||||
appPaths,
|
||||
clientOnly: false,
|
||||
definition: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
generateRoutes(_dev?: boolean) {
|
||||
|
@ -723,12 +731,9 @@ export default class DevServer extends Server {
|
|||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths?: ReadonlyArray<string> | null
|
||||
match?: RouteMatch
|
||||
definition: RouteDefinition | undefined
|
||||
}): Promise<void> {
|
||||
await this.invokeDevMethod({
|
||||
method: 'ensurePage',
|
||||
args: [opts],
|
||||
})
|
||||
await this.bundlerService.ensurePage(opts)
|
||||
}
|
||||
|
||||
protected async findPageComponents({
|
||||
|
@ -759,6 +764,7 @@ export default class DevServer extends Server {
|
|||
page,
|
||||
appPaths,
|
||||
clientOnly: false,
|
||||
definition: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -785,17 +791,11 @@ export default class DevServer extends Server {
|
|||
}
|
||||
|
||||
protected async getFallbackErrorComponents(): Promise<LoadComponentsReturnType | null> {
|
||||
await this.invokeDevMethod({
|
||||
method: 'getFallbackErrorComponents',
|
||||
args: [],
|
||||
})
|
||||
await this.bundlerService.getFallbackErrorComponents()
|
||||
return await loadDefaultErrorComponents(this.distDir)
|
||||
}
|
||||
|
||||
async getCompilationError(page: string): Promise<any> {
|
||||
return await this.invokeDevMethod({
|
||||
method: 'getCompilationError',
|
||||
args: [page],
|
||||
})
|
||||
return await this.bundlerService.getCompilationError(page)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ import {
|
|||
COMPILER_NAMES,
|
||||
RSC_MODULE_TYPES,
|
||||
} from '../../shared/lib/constants'
|
||||
import { RouteMatch } from '../future/route-matches/route-match'
|
||||
import { HMR_ACTIONS_SENT_TO_BROWSER } from './hot-reloader-types'
|
||||
import HotReloader from './hot-reloader-webpack'
|
||||
import { isAppPageRouteDefinition } from '../future/route-definitions/app-page-route-definition'
|
||||
|
@ -677,13 +676,13 @@ export function onDemandEntryHandler({
|
|||
page,
|
||||
clientOnly,
|
||||
appPaths,
|
||||
match,
|
||||
definition,
|
||||
isApp,
|
||||
}: {
|
||||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths: ReadonlyArray<string> | null
|
||||
match: Pick<RouteMatch, 'definition'> | undefined
|
||||
definition: RouteDefinition | undefined
|
||||
isApp: boolean | undefined
|
||||
}): Promise<void> {
|
||||
const stalledTime = 60
|
||||
|
@ -694,11 +693,11 @@ export function onDemandEntryHandler({
|
|||
}, stalledTime * 1000)
|
||||
|
||||
try {
|
||||
let definition: Pick<RouteDefinition, 'filename' | 'bundlePath' | 'page'>
|
||||
if (match?.definition) {
|
||||
definition = match.definition
|
||||
let route: Pick<RouteDefinition, 'filename' | 'bundlePath' | 'page'>
|
||||
if (definition) {
|
||||
route = definition
|
||||
} else {
|
||||
definition = await findPagePathData(
|
||||
route = await findPagePathData(
|
||||
rootDir,
|
||||
page,
|
||||
nextConfig.pageExtensions,
|
||||
|
@ -707,18 +706,18 @@ export function onDemandEntryHandler({
|
|||
)
|
||||
}
|
||||
|
||||
const isInsideAppDir = !!appDir && definition.filename.startsWith(appDir)
|
||||
const isInsideAppDir = !!appDir && route.filename.startsWith(appDir)
|
||||
|
||||
if (typeof isApp === 'boolean' && isApp !== isInsideAppDir) {
|
||||
Error.stackTraceLimit = 15
|
||||
throw new Error(
|
||||
`Ensure bailed, found path "${
|
||||
definition.page
|
||||
route.page
|
||||
}" does not match ensure type (${isApp ? 'app' : 'pages'})`
|
||||
)
|
||||
}
|
||||
|
||||
const pageBundleType = getPageBundleType(definition.bundlePath)
|
||||
const pageBundleType = getPageBundleType(route.bundlePath)
|
||||
const addEntry = (
|
||||
compilerType: CompilerNameValues
|
||||
): {
|
||||
|
@ -726,11 +725,7 @@ export function onDemandEntryHandler({
|
|||
newEntry: boolean
|
||||
shouldInvalidate: boolean
|
||||
} => {
|
||||
const entryKey = getEntryKey(
|
||||
compilerType,
|
||||
pageBundleType,
|
||||
definition.page
|
||||
)
|
||||
const entryKey = getEntryKey(compilerType, pageBundleType, route.page)
|
||||
if (
|
||||
curEntries[entryKey] &&
|
||||
// there can be an overlap in the entryKey for the instrumentation hook file and a page named the same
|
||||
|
@ -758,9 +753,9 @@ export function onDemandEntryHandler({
|
|||
curEntries[entryKey] = {
|
||||
type: EntryTypes.ENTRY,
|
||||
appPaths,
|
||||
absolutePagePath: definition.filename,
|
||||
request: definition.filename,
|
||||
bundlePath: definition.bundlePath,
|
||||
absolutePagePath: route.filename,
|
||||
request: route.filename,
|
||||
bundlePath: route.bundlePath,
|
||||
dispose: false,
|
||||
lastActiveTime: Date.now(),
|
||||
status: ADDED,
|
||||
|
@ -774,7 +769,7 @@ export function onDemandEntryHandler({
|
|||
|
||||
const staticInfo = await getStaticInfoIncludingLayouts({
|
||||
page,
|
||||
pageFilePath: definition.filename,
|
||||
pageFilePath: route.filename,
|
||||
isInsideAppDir,
|
||||
pageExtensions: nextConfig.pageExtensions,
|
||||
isDev: true,
|
||||
|
@ -787,7 +782,7 @@ export function onDemandEntryHandler({
|
|||
isInsideAppDir && staticInfo.rsc !== RSC_MODULE_TYPES.client
|
||||
|
||||
runDependingOnPageType({
|
||||
page: definition.page,
|
||||
page: route.page,
|
||||
pageRuntime: staticInfo.runtime,
|
||||
pageType: pageBundleType,
|
||||
onClient: () => {
|
||||
|
@ -802,11 +797,11 @@ export function onDemandEntryHandler({
|
|||
const edgeServerEntry = getEntryKey(
|
||||
COMPILER_NAMES.edgeServer,
|
||||
pageBundleType,
|
||||
definition.page
|
||||
route.page
|
||||
)
|
||||
if (
|
||||
curEntries[edgeServerEntry] &&
|
||||
!isInstrumentationHookFile(definition.page)
|
||||
!isInstrumentationHookFile(route.page)
|
||||
) {
|
||||
// Runtime switched from edge to server
|
||||
delete curEntries[edgeServerEntry]
|
||||
|
@ -820,11 +815,11 @@ export function onDemandEntryHandler({
|
|||
const serverEntry = getEntryKey(
|
||||
COMPILER_NAMES.server,
|
||||
pageBundleType,
|
||||
definition.page
|
||||
route.page
|
||||
)
|
||||
if (
|
||||
curEntries[serverEntry] &&
|
||||
!isInstrumentationHookFile(definition.page)
|
||||
!isInstrumentationHookFile(route.page)
|
||||
) {
|
||||
// Runtime switched from server to edge
|
||||
delete curEntries[serverEntry]
|
||||
|
@ -839,9 +834,7 @@ export function onDemandEntryHandler({
|
|||
const hasNewEntry = addedValues.some((entry) => entry.newEntry)
|
||||
|
||||
if (hasNewEntry) {
|
||||
reportTrigger(
|
||||
!clientOnly && hasNewEntry ? `${definition.page}` : definition.page
|
||||
)
|
||||
reportTrigger(!clientOnly && hasNewEntry ? `${route.page}` : route.page)
|
||||
}
|
||||
|
||||
if (entriesThatShouldBeInvalidated.length > 0) {
|
||||
|
@ -883,18 +876,12 @@ export function onDemandEntryHandler({
|
|||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths?: ReadonlyArray<string> | null
|
||||
match?: RouteMatch
|
||||
definition?: RouteDefinition
|
||||
isApp?: boolean
|
||||
}
|
||||
|
||||
// Make sure that we won't have multiple invalidations ongoing concurrently.
|
||||
const batcher = Batcher.create<
|
||||
Omit<EnsurePageOptions, 'match'> & {
|
||||
definition?: RouteDefinition
|
||||
},
|
||||
void,
|
||||
string
|
||||
>({
|
||||
const batcher = Batcher.create<EnsurePageOptions, void, string>({
|
||||
// The cache key here is composed of the elements that affect the
|
||||
// compilation, namely, the page, whether it's client only, and whether
|
||||
// it's an app page. This ensures that we don't have multiple compilations
|
||||
|
@ -913,26 +900,28 @@ export function onDemandEntryHandler({
|
|||
page,
|
||||
clientOnly,
|
||||
appPaths = null,
|
||||
match,
|
||||
definition,
|
||||
isApp,
|
||||
}: EnsurePageOptions) {
|
||||
// If the route is actually an app page route, then we should have access
|
||||
// to the app route match, and therefore, the appPaths from it.
|
||||
if (
|
||||
!appPaths &&
|
||||
match?.definition &&
|
||||
isAppPageRouteDefinition(match.definition)
|
||||
) {
|
||||
appPaths = match.definition.appPaths
|
||||
// to the app route definition, and therefore, the appPaths from it.
|
||||
if (!appPaths && definition && isAppPageRouteDefinition(definition)) {
|
||||
appPaths = definition.appPaths
|
||||
}
|
||||
|
||||
// Wrap the invocation of the ensurePageImpl function in the pending
|
||||
// wrapper, which will ensure that we don't have multiple compilations
|
||||
// for the same page happening concurrently.
|
||||
return batcher.batch(
|
||||
{ page, clientOnly, appPaths, definition: match?.definition, isApp },
|
||||
{ page, clientOnly, appPaths, definition, isApp },
|
||||
async () => {
|
||||
await ensurePageImpl({ page, clientOnly, appPaths, match, isApp })
|
||||
await ensurePageImpl({
|
||||
page,
|
||||
clientOnly,
|
||||
appPaths,
|
||||
definition,
|
||||
isApp,
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
|
74
packages/next/src/server/lib/dev-bundler-service.ts
Normal file
74
packages/next/src/server/lib/dev-bundler-service.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import type { IncomingMessage } from 'http'
|
||||
import type { DevBundler } from './router-utils/setup-dev-bundler'
|
||||
import type { WorkerRequestHandler } from './types'
|
||||
|
||||
import { createRequestResponseMocks } from './mock-request'
|
||||
|
||||
/**
|
||||
* The DevBundlerService provides an interface to perform tasks with the
|
||||
* bundler while in development.
|
||||
*/
|
||||
export class DevBundlerService {
|
||||
constructor(
|
||||
private readonly bundler: DevBundler,
|
||||
private readonly handler: WorkerRequestHandler
|
||||
) {}
|
||||
|
||||
public ensurePage: typeof this.bundler.hotReloader.ensurePage = async (
|
||||
definition
|
||||
) => {
|
||||
// TODO: remove after ensure is pulled out of server
|
||||
return await this.bundler.hotReloader.ensurePage(definition)
|
||||
}
|
||||
|
||||
public logErrorWithOriginalStack: typeof this.bundler.logErrorWithOriginalStack =
|
||||
async (...args) => {
|
||||
return await this.bundler.logErrorWithOriginalStack(...args)
|
||||
}
|
||||
|
||||
public async getFallbackErrorComponents() {
|
||||
await this.bundler.hotReloader.buildFallbackError()
|
||||
// Build the error page to ensure the fallback is built too.
|
||||
// TODO: See if this can be moved into hotReloader or removed.
|
||||
await this.bundler.hotReloader.ensurePage({
|
||||
page: '/_error',
|
||||
clientOnly: false,
|
||||
definition: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
public async getCompilationError(page: string) {
|
||||
const errors = await this.bundler.hotReloader.getCompilationErrors(page)
|
||||
if (!errors) return
|
||||
|
||||
// Return the very first error we found.
|
||||
return errors[0]
|
||||
}
|
||||
|
||||
public async revalidate({
|
||||
urlPath,
|
||||
revalidateHeaders,
|
||||
opts: revalidateOpts,
|
||||
}: {
|
||||
urlPath: string
|
||||
revalidateHeaders: IncomingMessage['headers']
|
||||
opts: any
|
||||
}) {
|
||||
const mocked = createRequestResponseMocks({
|
||||
url: urlPath,
|
||||
headers: revalidateHeaders,
|
||||
})
|
||||
|
||||
await this.handler(mocked.req, mocked.res)
|
||||
await mocked.res.hasStreamed
|
||||
|
||||
if (
|
||||
mocked.res.getHeader('x-nextjs-cache') !== 'REVALIDATED' &&
|
||||
!(mocked.res.statusCode === 404 && revalidateOpts.unstable_onlyGenerated)
|
||||
) {
|
||||
throw new Error(`Invalid response ${mocked.res.statusCode}`)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import type { NextServer, RequestHandler } from '../next'
|
||||
import type { DevBundlerService } from './dev-bundler-service'
|
||||
|
||||
import next from '../next'
|
||||
import { PropagateToWorkersField } from './router-utils/types'
|
||||
|
@ -79,6 +80,7 @@ async function initializeImpl(opts: {
|
|||
experimentalHttpsServer: boolean
|
||||
_ipcPort?: string
|
||||
_ipcKey?: string
|
||||
bundlerService: DevBundlerService | undefined
|
||||
}) {
|
||||
const type = process.env.__NEXT_PRIVATE_RENDER_WORKER
|
||||
if (type) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { IncomingMessage } from 'http'
|
||||
|
||||
// this must come first as it includes require hooks
|
||||
import type { WorkerRequestHandler, WorkerUpgradeHandler } from './types'
|
||||
import type { DevBundler } from './router-utils/setup-dev-bundler'
|
||||
|
||||
// This is required before other imports to ensure the require hook is setup.
|
||||
import '../node-polyfill-fetch'
|
||||
|
@ -20,8 +19,6 @@ import { findPagesDir } from '../../lib/find-pages-dir'
|
|||
import { setupFsCheck } from './router-utils/filesystem'
|
||||
import { proxyRequest } from './router-utils/proxy-request'
|
||||
import { isAbortError, pipeReadable } from '../pipe-readable'
|
||||
import { createRequestResponseMocks } from './mock-request'
|
||||
import { UnwrapPromise } from '../../lib/coalesced-function'
|
||||
import { getResolveRoutes } from './router-utils/resolve-routes'
|
||||
import { NextUrlWithParsedQuery, getRequestMeta } from '../request-meta'
|
||||
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
|
||||
|
@ -35,7 +32,7 @@ import {
|
|||
PHASE_DEVELOPMENT_SERVER,
|
||||
PERMANENT_REDIRECT_STATUS,
|
||||
} from '../../shared/lib/constants'
|
||||
import type { NextJsHotReloaderInterface } from '../dev/hot-reloader-types'
|
||||
import { DevBundlerService } from './dev-bundler-service'
|
||||
|
||||
const debug = setupDebug('next:router-server:main')
|
||||
|
||||
|
@ -48,11 +45,6 @@ export type RenderServer = Pick<
|
|||
| 'propagateServerField'
|
||||
>
|
||||
|
||||
const devInstances: Record<
|
||||
string,
|
||||
UnwrapPromise<ReturnType<typeof import('./router-utils/setup-dev').setupDev>>
|
||||
> = {}
|
||||
|
||||
export interface LazyRenderServerInstance {
|
||||
instance?: RenderServer
|
||||
}
|
||||
|
@ -100,11 +92,9 @@ export async function initialize(opts: {
|
|||
|
||||
const renderServer: LazyRenderServerInstance = {}
|
||||
|
||||
let devInstance:
|
||||
| UnwrapPromise<
|
||||
ReturnType<typeof import('./router-utils/setup-dev').setupDev>
|
||||
>
|
||||
| undefined
|
||||
let developmentBundler: DevBundler | undefined
|
||||
|
||||
let devBundlerService: DevBundlerService | undefined
|
||||
|
||||
if (opts.dev) {
|
||||
const telemetry = new Telemetry({
|
||||
|
@ -112,10 +102,10 @@ export async function initialize(opts: {
|
|||
})
|
||||
const { pagesDir, appDir } = findPagesDir(opts.dir)
|
||||
|
||||
const { setupDev } =
|
||||
(await require('./router-utils/setup-dev')) as typeof import('./router-utils/setup-dev')
|
||||
const { setupDevBundler } =
|
||||
require('./router-utils/setup-dev-bundler') as typeof import('./router-utils/setup-dev-bundler')
|
||||
|
||||
devInstance = await setupDev({
|
||||
developmentBundler = await setupDevBundler({
|
||||
// Passed here but the initialization of this object happens below, doing the initialization before the setupDev call breaks.
|
||||
renderServer,
|
||||
appDir,
|
||||
|
@ -128,74 +118,15 @@ export async function initialize(opts: {
|
|||
turbo: !!process.env.TURBOPACK,
|
||||
port: opts.port,
|
||||
})
|
||||
devInstances[opts.dir] = devInstance
|
||||
;(global as any)._nextDevHandlers = {
|
||||
async ensurePage(
|
||||
dir: string,
|
||||
match: Parameters<NextJsHotReloaderInterface['ensurePage']>[0]
|
||||
) {
|
||||
const curDevInstance = devInstances[dir]
|
||||
// TODO: remove after ensure is pulled out of server
|
||||
return await curDevInstance?.hotReloader.ensurePage(match)
|
||||
},
|
||||
async logErrorWithOriginalStack(dir: string, ...args: any[]) {
|
||||
const curDevInstance = devInstances[dir]
|
||||
// @ts-ignore
|
||||
return await curDevInstance?.logErrorWithOriginalStack(...args)
|
||||
},
|
||||
async getFallbackErrorComponents(dir: string) {
|
||||
const curDevInstance = devInstances[dir]
|
||||
await curDevInstance.hotReloader.buildFallbackError()
|
||||
// Build the error page to ensure the fallback is built too.
|
||||
// TODO: See if this can be moved into hotReloader or removed.
|
||||
await curDevInstance.hotReloader.ensurePage({
|
||||
page: '/_error',
|
||||
clientOnly: false,
|
||||
})
|
||||
},
|
||||
async getCompilationError(dir: string, page: string) {
|
||||
const curDevInstance = devInstances[dir]
|
||||
const errors = await curDevInstance?.hotReloader?.getCompilationErrors(
|
||||
page
|
||||
)
|
||||
if (!errors) return
|
||||
|
||||
// Return the very first error we found.
|
||||
return errors[0]
|
||||
},
|
||||
async revalidate(
|
||||
dir: string,
|
||||
{
|
||||
urlPath,
|
||||
revalidateHeaders,
|
||||
opts: revalidateOpts,
|
||||
}: {
|
||||
urlPath: string
|
||||
revalidateHeaders: IncomingMessage['headers']
|
||||
opts: any
|
||||
}
|
||||
) {
|
||||
const mocked = createRequestResponseMocks({
|
||||
url: urlPath,
|
||||
headers: revalidateHeaders,
|
||||
})
|
||||
const curRequestHandler = requestHandlers[dir]
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
await curRequestHandler(mocked.req, mocked.res)
|
||||
await mocked.res.hasStreamed
|
||||
|
||||
if (
|
||||
mocked.res.getHeader('x-nextjs-cache') !== 'REVALIDATED' &&
|
||||
!(
|
||||
mocked.res.statusCode === 404 &&
|
||||
revalidateOpts.unstable_onlyGenerated
|
||||
)
|
||||
) {
|
||||
throw new Error(`Invalid response ${mocked.res.statusCode}`)
|
||||
}
|
||||
return {}
|
||||
},
|
||||
} as any
|
||||
devBundlerService = new DevBundlerService(
|
||||
developmentBundler,
|
||||
// The request handler is assigned below, this allows us to create a lazy
|
||||
// reference to it.
|
||||
(req, res) => {
|
||||
return requestHandlers[opts.dir](req, res)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderServer.instance =
|
||||
|
@ -209,9 +140,10 @@ export async function initialize(opts: {
|
|||
dev: !!opts.dev,
|
||||
server: opts.server,
|
||||
isNodeDebugging: !!opts.isNodeDebugging,
|
||||
serverFields: devInstance?.serverFields || {},
|
||||
serverFields: developmentBundler?.serverFields || {},
|
||||
experimentalTestProxy: !!opts.experimentalTestProxy,
|
||||
experimentalHttpsServer: !!opts.experimentalHttpsServer,
|
||||
bundlerService: devBundlerService,
|
||||
}
|
||||
|
||||
// pre-initialize workers
|
||||
|
@ -221,7 +153,7 @@ export async function initialize(opts: {
|
|||
type: 'uncaughtException' | 'unhandledRejection',
|
||||
err: Error | undefined
|
||||
) => {
|
||||
await devInstance?.logErrorWithOriginalStack(err, type)
|
||||
await developmentBundler?.logErrorWithOriginalStack(err, type)
|
||||
}
|
||||
|
||||
process.on('uncaughtException', logError.bind(null, 'uncaughtException'))
|
||||
|
@ -233,7 +165,7 @@ export async function initialize(opts: {
|
|||
opts,
|
||||
renderServer.instance,
|
||||
renderServerOpts,
|
||||
devInstance?.ensureMiddleware
|
||||
developmentBundler?.ensureMiddleware
|
||||
)
|
||||
|
||||
const requestHandlerImpl: WorkerRequestHandler = async (req, res) => {
|
||||
|
@ -328,7 +260,7 @@ export async function initialize(opts: {
|
|||
}
|
||||
|
||||
// handle hot-reloader first
|
||||
if (devInstance) {
|
||||
if (developmentBundler) {
|
||||
const origUrl = req.url || '/'
|
||||
|
||||
if (config.basePath && pathHasPrefix(origUrl, config.basePath)) {
|
||||
|
@ -336,7 +268,7 @@ export async function initialize(opts: {
|
|||
}
|
||||
const parsedUrl = url.parse(req.url || '/')
|
||||
|
||||
const hotReloaderResult = await devInstance.hotReloader.run(
|
||||
const hotReloaderResult = await developmentBundler.hotReloader.run(
|
||||
req,
|
||||
res,
|
||||
parsedUrl
|
||||
|
@ -367,7 +299,7 @@ export async function initialize(opts: {
|
|||
return
|
||||
}
|
||||
|
||||
if (devInstance && matchedOutput?.type === 'devVirtualFsItem') {
|
||||
if (developmentBundler && matchedOutput?.type === 'devVirtualFsItem') {
|
||||
const origUrl = req.url || '/'
|
||||
|
||||
if (config.basePath && pathHasPrefix(origUrl, config.basePath)) {
|
||||
|
@ -379,7 +311,7 @@ export async function initialize(opts: {
|
|||
res.setHeader(key, resHeaders[key])
|
||||
}
|
||||
}
|
||||
const result = await devInstance.requestHandler(req, res)
|
||||
const result = await developmentBundler.requestHandler(req, res)
|
||||
|
||||
if (result.finished) {
|
||||
return
|
||||
|
@ -571,7 +503,7 @@ export async function initialize(opts: {
|
|||
}
|
||||
|
||||
const appNotFound = opts.dev
|
||||
? devInstance?.serverFields.hasAppNotFound
|
||||
? developmentBundler?.serverFields.hasAppNotFound
|
||||
: await fsChecker.getItem('/_not-found')
|
||||
|
||||
res.statusCode = 404
|
||||
|
@ -640,9 +572,9 @@ export async function initialize(opts: {
|
|||
// console.error(_err);
|
||||
})
|
||||
|
||||
if (opts.dev && devInstance) {
|
||||
if (opts.dev && developmentBundler) {
|
||||
if (req.url?.includes(`/_next/webpack-hmr`)) {
|
||||
return devInstance.hotReloader.onHMR(req, socket, head)
|
||||
return developmentBundler.hotReloader.onHMR(req, socket, head)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1065,6 +1065,7 @@ async function startWatcher(opts: SetupOpts) {
|
|||
.ensurePage({
|
||||
page: decodedPagePath,
|
||||
clientOnly: false,
|
||||
definition: undefined,
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
@ -1166,10 +1167,10 @@ async function startWatcher(opts: SetupOpts) {
|
|||
// Unused parameters
|
||||
// clientOnly,
|
||||
// appPaths,
|
||||
match,
|
||||
definition,
|
||||
isApp,
|
||||
}) {
|
||||
let page = match?.definition?.pathname ?? inputPage
|
||||
let page = definition?.pathname ?? inputPage
|
||||
|
||||
if (page === '/_error') {
|
||||
if (globalEntries.app) {
|
||||
|
@ -1218,7 +1219,7 @@ async function startWatcher(opts: SetupOpts) {
|
|||
curEntries.get(page) ??
|
||||
curEntries.get(
|
||||
normalizeAppPath(
|
||||
normalizeMetadataRoute(match?.definition?.page ?? inputPage)
|
||||
normalizeMetadataRoute(definition?.page ?? inputPage)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1457,6 +1458,7 @@ async function startWatcher(opts: SetupOpts) {
|
|||
clientOnly: false,
|
||||
page: item.itemPath,
|
||||
isApp: item.type === 'appFile',
|
||||
definition: undefined,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -2236,12 +2238,13 @@ async function startWatcher(opts: SetupOpts) {
|
|||
return hotReloader.ensurePage({
|
||||
page: serverFields.actualMiddlewareFile,
|
||||
clientOnly: false,
|
||||
definition: undefined,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupDev(opts: SetupOpts) {
|
||||
export async function setupDevBundler(opts: SetupOpts) {
|
||||
const isSrcDir = path
|
||||
.relative(opts.dir, opts.pagesDir || opts.appDir || '')
|
||||
.startsWith('src')
|
||||
|
@ -2266,3 +2269,5 @@ export async function setupDev(opts: SetupOpts) {
|
|||
)
|
||||
return result
|
||||
}
|
||||
|
||||
export type DevBundler = Awaited<ReturnType<typeof setupDevBundler>>
|
|
@ -1,5 +1,8 @@
|
|||
import type { Options as DevServerOptions } from './dev/next-dev-server'
|
||||
import type { NodeRequestHandler } from './next-server'
|
||||
import type {
|
||||
NodeRequestHandler,
|
||||
Options as ServerOptions,
|
||||
} from './next-server'
|
||||
import type { UrlWithParsedQuery } from 'url'
|
||||
import type { NextConfigComplete } from './config-shared'
|
||||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
|
@ -34,10 +37,15 @@ const getServerImpl = async () => {
|
|||
return ServerImpl
|
||||
}
|
||||
|
||||
export type NextServerOptions = Partial<DevServerOptions> & {
|
||||
preloadedConfig?: NextConfigComplete
|
||||
internal_setStandaloneConfig?: boolean
|
||||
}
|
||||
export type NextServerOptions = Omit<
|
||||
ServerOptions | DevServerOptions,
|
||||
// This is assigned in this server abstraction.
|
||||
'conf'
|
||||
> &
|
||||
Partial<Pick<ServerOptions | DevServerOptions, 'conf'>> & {
|
||||
preloadedConfig?: NextConfigComplete
|
||||
internal_setStandaloneConfig?: boolean
|
||||
}
|
||||
|
||||
export interface RequestHandler {
|
||||
(
|
||||
|
@ -153,7 +161,9 @@ export class NextServer {
|
|||
return (server as any).close()
|
||||
}
|
||||
|
||||
private async createServer(options: DevServerOptions): Promise<Server> {
|
||||
private async createServer(
|
||||
options: ServerOptions | DevServerOptions
|
||||
): Promise<Server> {
|
||||
let ServerImplementation: typeof Server
|
||||
if (options.dev) {
|
||||
ServerImplementation = require('./dev/next-dev-server').default
|
||||
|
|
Loading…
Reference in a new issue