-
Notifications
You must be signed in to change notification settings - Fork 87
/
Copy pathserver.ts
227 lines (193 loc) · 7.8 KB
/
server.ts
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import { join as posixJoin } from 'node:path/posix'
import { trace } from '@opentelemetry/api'
import { wrapTracer } from '@opentelemetry/api/experimental'
import { glob } from 'fast-glob'
import {
copyNextDependencies,
copyNextServerCode,
verifyHandlerDirStructure,
} from '../content/server.js'
import { PluginContext, SERVER_HANDLER_NAME } from '../plugin-context.js'
const tracer = wrapTracer(trace.getTracer('Next runtime'))
/** Copies the runtime dist folder to the lambda */
const copyHandlerDependencies = async (ctx: PluginContext) => {
await tracer.withActiveSpan('copyHandlerDependencies', async (span) => {
const promises: Promise<void>[] = []
// if the user specified some files to include in the lambda
// we need to copy them to the functions-internal folder
const { included_files: includedFiles = [] } = ctx.netlifyConfig.functions?.['*'] || {}
// we also force including the .env files to ensure those are available in the lambda
includedFiles.push(
posixJoin(ctx.relativeAppDir, '.env'),
posixJoin(ctx.relativeAppDir, '.env.production'),
posixJoin(ctx.relativeAppDir, '.env.local'),
posixJoin(ctx.relativeAppDir, '.env.production.local'),
)
span.setAttribute('next.includedFiles', includedFiles.join(','))
const resolvedFiles = await Promise.all(
includedFiles.map((globPattern) => glob(globPattern, { cwd: process.cwd() })),
)
for (const filePath of resolvedFiles.flat()) {
promises.push(
cp(
join(process.cwd(), filePath),
// the serverHandlerDir is aware of the dist dir.
// The distDir must not be the package path therefore we need to rely on the
// serverHandlerDir instead of the serverHandlerRootDir
// therefore we need to remove the package path from the filePath
join(ctx.serverHandlerDir, relative(ctx.relativeAppDir, filePath)),
{
recursive: true,
force: true,
},
),
)
}
// We need to create a package.json file with type: module to make sure that the runtime modules
// are handled correctly as ESM modules
promises.push(
writeFile(
join(ctx.serverHandlerRuntimeModulesDir, 'package.json'),
JSON.stringify({ type: 'module' }),
),
)
const fileList = await glob('dist/**/*', { cwd: ctx.pluginDir })
for (const filePath of fileList) {
promises.push(
cp(join(ctx.pluginDir, filePath), join(ctx.serverHandlerRuntimeModulesDir, filePath), {
recursive: true,
force: true,
}),
)
}
await Promise.all(promises)
})
}
const writeHandlerManifest = async (ctx: PluginContext) => {
await writeFile(
join(ctx.serverHandlerRootDir, `${SERVER_HANDLER_NAME}.json`),
JSON.stringify({
config: {
name: 'Next.js Server Handler',
generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
nodeBundler: 'none',
// the folders can vary in monorepos based on the folder structure of the user so we have to glob all
includedFiles: ['**'],
includedFilesBasePath: ctx.serverHandlerRootDir,
},
version: 1,
}),
'utf-8',
)
}
const applyTemplateVariables = (template: string, variables: Record<string, string>) => {
return Object.entries(variables).reduce((acc, [key, value]) => {
return acc.replaceAll(key, value)
}, template)
}
// Convert Next.js route syntax to URLPattern syntax and add basePath and locales if present
const transformRoutePatterns = (
route: string,
basePath: string,
locales: string[] = [],
): string[] => {
const transformedRoute = route
.replace(/\[\[\.\.\.(\w+)]]/g, ':$1*') // [[...slug]] -> :slug*
.replace(/\[\.{3}(\w+)]/g, ':$1+') // [...slug] -> :slug+
.replace(/\[(\w+)]/g, ':$1') // [id] -> :id
return [
posixJoin('/', basePath, transformedRoute),
...locales.map((locale) => posixJoin('/', basePath, locale, transformedRoute)),
]
}
export const getRoutes = async (ctx: PluginContext) => {
const routesManifest = await ctx.getRoutesManifest()
const prerenderManifest = await ctx.getPrerenderManifest()
// static routes
const staticRoutes = routesManifest.staticRoutes.map((route) => route.page)
// dynamic routes (no wildcard for routes without fallback)
const dynamicRoutes = routesManifest.dynamicRoutes
.filter((route) => prerenderManifest.dynamicRoutes[route.page]?.fallback !== false)
.map((route) => route.page)
const prerenderedDynamicRoutes = Object.keys(prerenderManifest.routes)
// static Route Handler routes (App Router)
const appPathRoutesManifest = await ctx.getAppPathRoutesManifest()
const appRoutes = Object.values(appPathRoutesManifest)
// API handler routes (Page Router)
const pagesManifest = await ctx.getPagesManifest()
const pagesRoutes = Object.keys(pagesManifest).filter((route) => route.startsWith('/api'))
const transformedRoutes = [
...staticRoutes,
...dynamicRoutes,
...prerenderedDynamicRoutes,
...appRoutes,
...pagesRoutes,
].flatMap((route) =>
transformRoutePatterns(route, ctx.buildConfig.basePath, ctx.buildConfig.i18n?.locales),
)
const internalRoutes = [
'/_next/static/*',
'/_next/data/*',
'/_next/image/*',
'/_next/postponed/*',
]
// route.source conforms to the URLPattern syntax, which will work with our redirect engine
// however this will be a superset of possible routes as it does not parse the
// header/cookie/query matching that Next.js offers
const redirects = routesManifest.redirects.map((route) => route.source)
const rewrites = Array.isArray(routesManifest.rewrites)
? routesManifest.rewrites.map((route) => route.source)
: []
const uniqueRoutes = new Set([
...transformedRoutes,
...internalRoutes,
...redirects,
...rewrites,
// '/*', // retain the catch-all route for our initial testing
])
return [...uniqueRoutes]
}
/** Get's the content of the handler file that will be written to the lambda */
const getHandlerFile = async (ctx: PluginContext): Promise<string> => {
const routes = await getRoutes(ctx)
const templatesDir = join(ctx.pluginDir, 'dist/build/templates')
const templateVariables: Record<string, string> = {
'{{useRegionalBlobs}}': ctx.useRegionalBlobs.toString(),
'{{paths}}': routes.join("','"),
}
// In this case it is a monorepo and we need to use a own template for it
// as we have to change the process working directory
if (ctx.relativeAppDir.length !== 0) {
const template = await readFile(join(templatesDir, 'handler-monorepo.tmpl.js'), 'utf-8')
templateVariables['{{cwd}}'] = posixJoin(ctx.lambdaWorkingDirectory)
templateVariables['{{nextServerHandler}}'] = posixJoin(ctx.nextServerHandler)
return applyTemplateVariables(template, templateVariables)
}
return applyTemplateVariables(
await readFile(join(templatesDir, 'handler.tmpl.js'), 'utf-8'),
templateVariables,
)
}
const writeHandlerFile = async (ctx: PluginContext) => {
const handler = await getHandlerFile(ctx)
await writeFile(join(ctx.serverHandlerRootDir, `${SERVER_HANDLER_NAME}.mjs`), handler)
}
export const clearStaleServerHandlers = async (ctx: PluginContext) => {
await rm(ctx.serverFunctionsDir, { recursive: true, force: true })
}
/**
* Create a Netlify function to run the Next.js server
*/
export const createServerHandler = async (ctx: PluginContext) => {
await tracer.withActiveSpan('createServerHandler', async () => {
await mkdir(join(ctx.serverHandlerRuntimeModulesDir), { recursive: true })
await copyNextServerCode(ctx)
await copyNextDependencies(ctx)
await copyHandlerDependencies(ctx)
await writeHandlerManifest(ctx)
await writeHandlerFile(ctx)
await verifyHandlerDirStructure(ctx)
})
}