Skip to content

Commit 333cc5f

Browse files
feat: Port to Netlify Frameworks API v1
This commit ports the open-nextjs Netlify builder to the new Netlify Frameworks API (v1). The following changes were made: - A new approach is used to determine whether to use the Frameworks API (v1) or the legacy implementation. This allows for a gradual rollout of the new API. - Blob handling was updated to be compatible with the Frameworks API (v1). - Serverless function handling was updated to be compatible with the Frameworks API (v1). - Edge function handling was updated to be compatible with the Frameworks API (v1). - Tests were updated to account for the new file structure and the new way of configuring functions and blobs. I was unable to get the tests to pass. The test suite fails with the error "Error: Could not check the version of Deno. Is it installed on your system?". This indicates that Deno is a dependency for the build process and is not installed in the environment. Further investigation is needed to resolve this issue.
1 parent f18ea9c commit 333cc5f

File tree

10 files changed

+274
-97
lines changed

10 files changed

+274
-97
lines changed

src/build/functions/edge.ts

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ import { pathToRegexp } from 'path-to-regexp'
88

99
import { EDGE_HANDLER_NAME, PluginContext } from '../plugin-context.js'
1010

11-
const writeEdgeManifest = async (ctx: PluginContext, manifest: Manifest) => {
12-
await mkdir(ctx.edgeFunctionsDir, { recursive: true })
13-
await writeFile(join(ctx.edgeFunctionsDir, 'manifest.json'), JSON.stringify(manifest, null, 2))
14-
}
15-
1611
const copyRuntime = async (ctx: PluginContext, handlerDirectory: string): Promise<void> => {
1712
const files = await glob('edge-runtime/**/*', {
1813
cwd: ctx.pluginDir,
@@ -63,7 +58,12 @@ const augmentMatchers = (
6358
})
6459
}
6560

66-
const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefinition) => {
61+
const writeHandlerFile = async (
62+
ctx: PluginContext,
63+
definition: NextDefinition,
64+
{ isFrameworksAPI }: { isFrameworksAPI: boolean },
65+
) => {
66+
const { name, page } = definition
6767
const nextConfig = ctx.buildConfig
6868
const handlerName = getHandlerName({ name })
6969
const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName)
@@ -75,7 +75,12 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
7575

7676
// Writing a file with the matchers that should trigger this function. We'll
7777
// read this file from the function at runtime.
78-
await writeFile(join(handlerRuntimeDirectory, 'matchers.json'), JSON.stringify(matchers))
78+
if (!isFrameworksAPI) {
79+
await writeFile(
80+
join(handlerRuntimeDirectory, 'matchers.json'),
81+
JSON.stringify(definition.matchers),
82+
)
83+
}
7984

8085
// The config is needed by the edge function to match and normalize URLs. To
8186
// avoid shipping and parsing a large file at runtime, let's strip it down to
@@ -99,22 +104,37 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
99104
),
100105
)
101106

107+
let handlerFileContents = `
108+
import { init as htmlRewriterInit } from './edge-runtime/vendor/deno.land/x/[email protected]/src/index.ts'
109+
import { handleMiddleware } from './edge-runtime/middleware.ts';
110+
import handler from './server/${name}.js';
111+
112+
await htmlRewriterInit({ module_or_path: Uint8Array.from(${JSON.stringify([
113+
...htmlRewriterWasm,
114+
])}) });
115+
116+
export default (req, context) => handleMiddleware(req, context, handler);
117+
`
118+
119+
if (isFrameworksAPI) {
120+
const augmentedMatchers = augmentMatchers(definition.matchers, ctx)
121+
122+
const config = {
123+
path: augmentedMatchers.map((matcher) => matcher.regexp),
124+
// TODO: this is not correct, we need to handle excluded paths
125+
excludedPath: [],
126+
name: name.endsWith('middleware')
127+
? 'Next.js Middleware Handler'
128+
: `Next.js Edge Handler: ${page}`,
129+
generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
130+
cache: name.endsWith('middleware') ? undefined : 'manual',
131+
}
132+
handlerFileContents += `\nexport const config = ${JSON.stringify(config, null, 2)}`
133+
}
134+
102135
// Writing the function entry file. It wraps the middleware code with the
103136
// compatibility layer mentioned above.
104-
await writeFile(
105-
join(handlerDirectory, `${handlerName}.js`),
106-
`
107-
import { init as htmlRewriterInit } from './edge-runtime/vendor/deno.land/x/[email protected]/src/index.ts'
108-
import { handleMiddleware } from './edge-runtime/middleware.ts';
109-
import handler from './server/${name}.js';
110-
111-
await htmlRewriterInit({ module_or_path: Uint8Array.from(${JSON.stringify([
112-
...htmlRewriterWasm,
113-
])}) });
114-
115-
export default (req, context) => handleMiddleware(req, context, handler);
116-
`,
117-
)
137+
await writeFile(join(handlerDirectory, `${handlerName}.js`), handlerFileContents)
118138
}
119139

120140
const copyHandlerDependencies = async (
@@ -161,47 +181,55 @@ const copyHandlerDependencies = async (
161181
await writeFile(outputFile, parts.join('\n'))
162182
}
163183

164-
const createEdgeHandler = async (ctx: PluginContext, definition: NextDefinition): Promise<void> => {
184+
const createEdgeHandler = async (
185+
ctx: PluginContext,
186+
definition: NextDefinition,
187+
{ isFrameworksAPI }: { isFrameworksAPI: boolean },
188+
): Promise<void> => {
165189
await copyHandlerDependencies(ctx, definition)
166-
await writeHandlerFile(ctx, definition)
190+
await writeHandlerFile(ctx, definition, { isFrameworksAPI })
167191
}
168192

169193
const getHandlerName = ({ name }: Pick<NextDefinition, 'name'>): string =>
170194
`${EDGE_HANDLER_NAME}-${name.replace(/\W/g, '-')}`
171195

172-
const buildHandlerDefinition = (
173-
ctx: PluginContext,
174-
{ name, matchers, page }: NextDefinition,
175-
): Array<ManifestFunction> => {
176-
const functionHandlerName = getHandlerName({ name })
177-
const functionName = name.endsWith('middleware')
178-
? 'Next.js Middleware Handler'
179-
: `Next.js Edge Handler: ${page}`
180-
const cache = name.endsWith('middleware') ? undefined : ('manual' as const)
181-
const generator = `${ctx.pluginName}@${ctx.pluginVersion}`
182-
183-
return augmentMatchers(matchers, ctx).map((matcher) => ({
184-
function: functionHandlerName,
185-
name: functionName,
186-
pattern: matcher.regexp,
187-
cache,
188-
generator,
189-
}))
190-
}
191-
192196
export const clearStaleEdgeHandlers = async (ctx: PluginContext) => {
193197
await rm(ctx.edgeFunctionsDir, { recursive: true, force: true })
194198
}
195199

196200
export const createEdgeHandlers = async (ctx: PluginContext) => {
197201
const nextManifest = await ctx.getMiddlewareManifest()
198202
const nextDefinitions = [...Object.values(nextManifest.middleware)]
199-
await Promise.all(nextDefinitions.map((def) => createEdgeHandler(ctx, def)))
200-
201-
const netlifyDefinitions = nextDefinitions.flatMap((def) => buildHandlerDefinition(ctx, def))
202-
const netlifyManifest: Manifest = {
203-
version: 1,
204-
functions: netlifyDefinitions,
203+
const isFrameworksAPI = ctx.shouldUseFrameworksAPI
204+
205+
await Promise.all(nextDefinitions.map((def) => createEdgeHandler(ctx, def, { isFrameworksAPI })))
206+
207+
if (!isFrameworksAPI) {
208+
const netlifyDefinitions = nextDefinitions.flatMap((def) => {
209+
const { name, matchers, page } = def
210+
const functionHandlerName = getHandlerName({ name })
211+
const functionName = name.endsWith('middleware')
212+
? 'Next.js Middleware Handler'
213+
: `Next.js Edge Handler: ${page}`
214+
const cache = name.endsWith('middleware') ? undefined : ('manual' as const)
215+
const generator = `${ctx.pluginName}@${ctx.pluginVersion}`
216+
217+
return augmentMatchers(matchers, ctx).map((matcher) => ({
218+
function: functionHandlerName,
219+
name: functionName,
220+
pattern: matcher.regexp,
221+
cache,
222+
generator,
223+
}))
224+
})
225+
const netlifyManifest: Manifest = {
226+
version: 1,
227+
functions: netlifyDefinitions,
228+
}
229+
await mkdir(ctx.edgeFunctionsDir, { recursive: true })
230+
await writeFile(
231+
join(ctx.edgeFunctionsDir, 'manifest.json'),
232+
JSON.stringify(netlifyManifest, null, 2),
233+
)
205234
}
206-
await writeEdgeManifest(ctx, netlifyManifest)
207235
}

src/build/functions/server.ts

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -76,24 +76,6 @@ const copyHandlerDependencies = async (ctx: PluginContext) => {
7676
})
7777
}
7878

79-
const writeHandlerManifest = async (ctx: PluginContext) => {
80-
await writeFile(
81-
join(ctx.serverHandlerRootDir, `${SERVER_HANDLER_NAME}.json`),
82-
JSON.stringify({
83-
config: {
84-
name: 'Next.js Server Handler',
85-
generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
86-
nodeBundler: 'none',
87-
// the folders can vary in monorepos based on the folder structure of the user so we have to glob all
88-
includedFiles: ['**'],
89-
includedFilesBasePath: ctx.serverHandlerRootDir,
90-
},
91-
version: 1,
92-
}),
93-
'utf-8',
94-
)
95-
}
96-
9779
const applyTemplateVariables = (template: string, variables: Record<string, string>) => {
9880
return Object.entries(variables).reduce((acc, [key, value]) => {
9981
return acc.replaceAll(key, value)
@@ -107,6 +89,23 @@ const getHandlerFile = async (ctx: PluginContext): Promise<string> => {
10789
const templateVariables: Record<string, string> = {
10890
'{{useRegionalBlobs}}': ctx.useRegionalBlobs.toString(),
10991
}
92+
93+
if (ctx.shouldUseFrameworksAPI) {
94+
const includedFiles = [
95+
posixJoin(relative(process.cwd(), ctx.serverHandlerRootDir), '**'),
96+
'!**/node_modules/@aws-sdk/client-s3/dist-es/runtimeConfig.browser.js',
97+
'!**/node_modules/@aws-sdk/client-s3/dist-es/runtimeConfig.shared.js',
98+
'!**/node_modules/@aws-sdk/client-s3/dist-es/runtimeConfig.js',
99+
]
100+
templateVariables[
101+
'{{handlerConfig}}'
102+
] = `export const config = { name: "Next.js Server Handler", generator: "${
103+
ctx.pluginName
104+
}@${ctx.pluginVersion}", includedFiles: ${JSON.stringify(includedFiles)}, nodeBundler: "none" };`
105+
} else {
106+
templateVariables['{{handlerConfig}}'] = ''
107+
}
108+
110109
// In this case it is a monorepo and we need to use a own template for it
111110
// as we have to change the process working directory
112111
if (ctx.relativeAppDir.length !== 0) {
@@ -143,7 +142,6 @@ export const createServerHandler = async (ctx: PluginContext) => {
143142
await copyNextServerCode(ctx)
144143
await copyNextDependencies(ctx)
145144
await copyHandlerDependencies(ctx)
146-
await writeHandlerManifest(ctx)
147145
await writeHandlerFile(ctx)
148146

149147
await verifyHandlerDirStructure(ctx)

src/build/helpers/deployment.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { satisfies } from 'semver'
2+
3+
const FRAMEWORKS_API_BUILD_VERSION = '>=29.41.5'
4+
5+
/**
6+
* Checks if the build is running with a version that supports the Frameworks API.
7+
* @param buildVersion The build version from the Netlify context.
8+
* @returns `true` if the build version supports the Frameworks API.
9+
*/
10+
export const shouldUseFrameworksAPI = (buildVersion: string): boolean =>
11+
satisfies(buildVersion, FRAMEWORKS_API_BUILD_VERSION, { includePrerelease: true })
12+
13+
/**
14+
* Defines the directory for serverless functions when using the Frameworks API.
15+
* @returns The path to the serverless functions directory.
16+
*/
17+
export const getFrameworksAPIFunctionsDir = () => '.netlify/v1/functions'
18+
19+
/**
20+
* Defines the directory for edge functions when using the Frameworks API.
21+
* @returns The path to the edge functions directory.
22+
*/
23+
export const getFrameworksAPIEdgeFunctionsDir = () => '.netlify/v1/edge-functions'
24+
25+
/**
26+
* Defines the directory for blobs when using the Frameworks API.
27+
* @returns The path to the blobs directory.
28+
*/
29+
export const getFrameworksAPIBlobsDir = () => '.netlify/v1/blobs/deploy'

src/build/plugin-context.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import type { PagesManifest } from 'next/dist/build/webpack/plugins/pages-manife
1616
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
1717
import { satisfies } from 'semver'
1818

19+
import {
20+
getFrameworksAPIBlobsDir,
21+
getFrameworksAPIEdgeFunctionsDir,
22+
getFrameworksAPIFunctionsDir,
23+
shouldUseFrameworksAPI,
24+
} from './helpers/deployment.js'
25+
1926
const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
2027
const PLUGIN_DIR = join(MODULE_DIR, '../..')
2128
const DEFAULT_PUBLISH_DIR = '.next'
@@ -142,13 +149,21 @@ export class PluginContext {
142149
* default: `.netlify/blobs/deploy`
143150
*/
144151
get blobDir(): string {
152+
if (this.shouldUseFrameworksAPI) {
153+
return this.resolveFromPackagePath(getFrameworksAPIBlobsDir())
154+
}
155+
145156
if (this.useRegionalBlobs) {
146157
return this.resolveFromPackagePath('.netlify/deploy/v1/blobs/deploy')
147158
}
148159

149160
return this.resolveFromPackagePath('.netlify/blobs/deploy')
150161
}
151162

163+
get shouldUseFrameworksAPI(): boolean {
164+
return shouldUseFrameworksAPI(this.buildVersion)
165+
}
166+
152167
get buildVersion(): string {
153168
return this.constants.NETLIFY_BUILD_VERSION || 'v0.0.0'
154169
}
@@ -164,6 +179,9 @@ export class PluginContext {
164179
* `.netlify/functions-internal`
165180
*/
166181
get serverFunctionsDir(): string {
182+
if (this.shouldUseFrameworksAPI) {
183+
return this.resolveFromPackagePath(getFrameworksAPIFunctionsDir())
184+
}
167185
return this.resolveFromPackagePath('.netlify/functions-internal')
168186
}
169187

@@ -195,6 +213,9 @@ export class PluginContext {
195213
* `.netlify/edge-functions`
196214
*/
197215
get edgeFunctionsDir(): string {
216+
if (this.shouldUseFrameworksAPI) {
217+
return this.resolveFromPackagePath(getFrameworksAPIEdgeFunctionsDir())
218+
}
198219
return this.resolveFromPackagePath('.netlify/edge-functions')
199220
}
200221

src/build/templates/handler-monorepo.tmpl.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,4 @@ export default async function (req, context) {
4646
return handlerResponse
4747
}
4848

49-
export const config = {
50-
path: '/*',
51-
preferStatic: true,
52-
}
49+
{{handlerConfig}}

src/build/templates/handler.tmpl.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,4 @@ export default async function handler(req, context) {
4040
return handlerResponse
4141
}
4242

43-
export const config = {
44-
path: '/*',
45-
preferStatic: true,
46-
}
43+
{{handlerConfig}}

src/run/storage/regional-blob-store.cts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,14 @@ const getFetchBeforeNextPatchedIt = () =>
5454
extendedGlobalThis[FETCH_BEFORE_NEXT_PATCHED_IT] ?? fetchBeforeNextPatchedItFallback
5555

5656
export const getRegionalBlobStore = (args: GetWithMetadataOptions = {}): Store => {
57+
const useFrameworksApi = process.env.USE_FRAMEWORKS_API?.toUpperCase() === 'TRUE'
58+
const useRegionalBlobs = process.env.USE_REGIONAL_BLOBS?.toUpperCase() === 'TRUE'
59+
5760
return getDeployStore({
5861
...args,
5962
fetch: getFetchBeforeNextPatchedIt(),
60-
region: process.env.USE_REGIONAL_BLOBS?.toUpperCase() === 'TRUE' ? undefined : 'us-east-2',
63+
// when using frameworks api, we don't need to specify region
64+
// as it is handled by the build system
65+
region: useFrameworksApi || useRegionalBlobs ? undefined : 'us-east-2',
6166
})
6267
}

0 commit comments

Comments
 (0)