-
Notifications
You must be signed in to change notification settings - Fork 86
/
Copy pathnext.cts
152 lines (127 loc) · 5.8 KB
/
next.cts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import fs, { readFile } from 'fs/promises'
import { join, relative, resolve } from 'path'
// @ts-expect-error no types installed
import { patchFs } from 'fs-monkey'
import { getRequestContext } from './handlers/request-context.cjs'
import { getTracer } from './handlers/tracer.cjs'
import { getRegionalBlobStore } from './regional-blob-store.cjs'
// https://github.com/vercel/next.js/pull/68193/files#diff-37243d614f1f5d3f7ea50bbf2af263f6b1a9a4f70e84427977781e07b02f57f1R49
// This import resulted in importing unbundled React which depending if NODE_ENV is `production` or not would use
// either development or production version of React. When not set to `production` it would use development version
// which later cause mismatching problems when both development and production versions of React were loaded causing
// react errors.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ignoring readonly NODE_ENV
process.env.NODE_ENV = 'production'
console.time('import next server')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getRequestHandlers } = require('next/dist/server/lib/start-server.js')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ResponseCache = require('next/dist/server/response-cache/index.js').default
// Next.js standalone doesn't expose background work promises (such as generating fresh response
// while stale one is being served) that we could use so we regrettably have to use hacks to
// gain access to them so that we can explicitly track them to ensure they finish before function
// execution stops
const originalGet = ResponseCache.prototype.get
ResponseCache.prototype.get = function get(...getArgs: unknown[]) {
if (!this.didAddBackgroundWorkTracking) {
if (typeof this.batcher !== 'undefined') {
const originalBatcherBatch = this.batcher.batch
this.batcher.batch = async (key: string, fn: (...args: unknown[]) => unknown) => {
const trackedFn = async (...workFnArgs: unknown[]) => {
const workPromise = fn(...workFnArgs)
const requestContext = getRequestContext()
if (requestContext && workPromise instanceof Promise) {
requestContext.trackBackgroundWork(workPromise)
}
return await workPromise
}
return originalBatcherBatch.call(this.batcher, key, trackedFn)
}
} else if (typeof this.pendingResponses !== 'undefined') {
const backgroundWork = new Map<string, () => void>()
const originalPendingResponsesSet = this.pendingResponses.set
this.pendingResponses.set = async (key: string, value: unknown) => {
const requestContext = getRequestContext()
if (requestContext && !this.pendingResponses.has(key)) {
const workPromise = new Promise<void>((_resolve) => {
backgroundWork.set(key, _resolve)
})
requestContext.trackBackgroundWork(workPromise)
}
return originalPendingResponsesSet.call(this.pendingResponses, key, value)
}
const originalPendingResponsesDelete = this.pendingResponses.delete
this.pendingResponses.delete = async (key: string) => {
const _resolve = backgroundWork.get(key)
if (_resolve) {
_resolve()
}
return originalPendingResponsesDelete.call(this.pendingResponses, key)
}
}
this.didAddBackgroundWorkTracking = true
}
return originalGet.apply(this, getArgs)
}
console.timeEnd('import next server')
type FS = typeof import('fs')
export type FSBlobsManifest = {
fallbackPaths: string[]
outputRoot: string
}
function normalizeStaticAssetPath(path: string) {
return path.startsWith('/') ? path : `/${path}`
}
let fsBlobsManifestPromise: Promise<FSBlobsManifest> | undefined
const getFSBlobsManifest = (): Promise<FSBlobsManifest> => {
if (!fsBlobsManifestPromise) {
fsBlobsManifestPromise = (async () => {
const { FS_BLOBS_MANIFEST, PLUGIN_DIR } = await import('./constants.js')
return JSON.parse(await readFile(resolve(PLUGIN_DIR, FS_BLOBS_MANIFEST), 'utf-8'))
})()
}
return fsBlobsManifestPromise
}
export async function getMockedRequestHandlers(...args: Parameters<typeof getRequestHandlers>) {
const tracer = getTracer()
return tracer.withActiveSpan('mocked request handler', async () => {
const ofs = { ...fs }
const { encodeBlobKey } = await import('../shared/blobkey.js')
async function readFileFallbackBlobStore(...fsargs: Parameters<FS['promises']['readFile']>) {
const [path, options] = fsargs
try {
// Attempt to read from the disk
// important to use the `import * as fs from 'fs'` here to not end up in a endless loop
return await ofs.readFile(path, options)
} catch (error) {
// only try to get .html files from the blob store
if (typeof path === 'string' && path.endsWith('.html')) {
const fsBlobsManifest = await getFSBlobsManifest()
const store = getRegionalBlobStore()
const relPath = relative(resolve(join(fsBlobsManifest.outputRoot, '/server/pages')), path)
const file = await store.get(await encodeBlobKey(relPath))
if (file !== null) {
if (!fsBlobsManifest.fallbackPaths.includes(normalizeStaticAssetPath(relPath))) {
const requestContext = getRequestContext()
if (requestContext) {
requestContext.usedFsReadForNonFallback = true
}
}
return file
}
}
throw error
}
}
// patch the file system for fs.promises with operations to fallback on the blob store
patchFs(
{
readFile: readFileFallbackBlobStore,
},
// eslint-disable-next-line n/global-require, @typescript-eslint/no-var-requires
require('fs').promises,
)
return getRequestHandlers(...args)
})
}