Skip to content

Commit 273fd1c

Browse files
committed
refactor adapter a bit and start creating modules per concern
1 parent ce070d6 commit 273fd1c

File tree

8 files changed

+193
-125
lines changed

8 files changed

+193
-125
lines changed

adapters-notes.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Feedback
2+
3+
- Files from `public` not in `outputs.staticFiles`
4+
- In `onBuildComplete` - `config.images.remotePatterns` type is `(RemotePattern | URL)[]` but in
5+
reality `URL` inputs are converted to `RemotePattern` so type should be just `RemotePattern[]`
6+
7+
## Plan
8+
9+
1. There are some operations that are easier to do in a build plugin context due to helpers, so some
10+
handling will remain in build plugin (cache save/restore, moving static assets dirs for
11+
publishing them etc).
12+
13+
2. We will use adapters API where it's most helpful:
14+
15+
- adjusting next config:
16+
- set standalone mode instead of using "private" env var (for now at least we will continue with
17+
standalone mode as using outputs other than middleware require bigger changes which will be
18+
explored in later phases)
19+
- set image loader (url generator) to use Netlify Image CDN directly (no need for \_next/image
20+
rewrite then)
21+
- (maybe/explore) set build time cache handler to avoid having to read output of default cache
22+
handler and convert those files into blobs to upload later
23+
- use middleware output to generate middleware edge function
24+
- don't glob for static files and use `outputs.staticFiles` instead
25+
- don't read various manifest files manually and use provided context in `onBuildComplete` instead
26+
27+
## To figure out
28+
29+
- Can we export build time otel spans from adapter similarly how we do that now in a build plugin?
30+
- Expose some constants from build plugin to adapter - what's best way to do that? (things like
31+
packagePath, publishDir etc)

src/adapter.ts

Lines changed: 0 additions & 112 deletions
This file was deleted.

src/adapter/adapter.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { mkdir, writeFile } from 'node:fs/promises'
2+
import { dirname } from 'node:path'
3+
4+
import type { NextAdapter } from 'next-with-adapters'
5+
6+
import {
7+
modifyConfig as modifyConfigForImageCDN,
8+
onBuildComplete as onBuildCompleteForImageCDN,
9+
} from './image-cdn.js'
10+
11+
const NETLIFY_FRAMEWORKS_API_CONFIG_PATH = '.netlify/v1/config.json'
12+
13+
const adapter: NextAdapter = {
14+
name: 'Netlify',
15+
modifyConfig(config) {
16+
// Enable Next.js standalone mode at build time
17+
config.output = 'standalone'
18+
19+
modifyConfigForImageCDN(config)
20+
21+
return config
22+
},
23+
async onBuildComplete(ctx) {
24+
console.log('onBuildComplete hook called')
25+
26+
// TODO: do we have a type for this? https://docs.netlify.com/build/frameworks/frameworks-api/#netlifyv1configjson
27+
let frameworksAPIConfig: any = null
28+
29+
frameworksAPIConfig = onBuildCompleteForImageCDN(ctx, frameworksAPIConfig)
30+
31+
if (frameworksAPIConfig) {
32+
// write out config if there is any
33+
await mkdir(dirname(NETLIFY_FRAMEWORKS_API_CONFIG_PATH), { recursive: true })
34+
await writeFile(
35+
NETLIFY_FRAMEWORKS_API_CONFIG_PATH,
36+
JSON.stringify(frameworksAPIConfig, null, 2),
37+
)
38+
}
39+
40+
// for dev/debugging purposes only
41+
await writeFile('./onBuildComplete.json', JSON.stringify(ctx, null, 2))
42+
debugger
43+
},
44+
}
45+
46+
export default adapter

src/adapter/image-cdn.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { fileURLToPath } from 'node:url'
2+
3+
import type { RemotePattern } from 'next-with-adapters/dist/shared/lib/image-config.js'
4+
import { makeRe } from 'picomatch'
5+
6+
import type { NextConfigComplete, OnBuildCompleteContext } from './types.js'
7+
8+
const NETLIFY_IMAGE_LOADER_FILE = fileURLToPath(import.meta.resolve(`./next-image-loader.cjs`))
9+
10+
export function modifyConfig(config: NextConfigComplete) {
11+
if (config.images.loader === 'default') {
12+
// Set up Netlify Image CDN image's loaderFile
13+
// see https://nextjs.org/docs/app/api-reference/config/next-config-js/images
14+
config.images.loader = 'custom'
15+
config.images.loaderFile = NETLIFY_IMAGE_LOADER_FILE
16+
}
17+
}
18+
19+
function generateRegexFromPattern(pattern: string): string {
20+
return makeRe(pattern).source
21+
}
22+
23+
export function onBuildComplete(ctx: OnBuildCompleteContext, frameworksAPIConfigArg: any) {
24+
let frameworksAPIConfig: any = frameworksAPIConfigArg
25+
26+
const { images } = ctx.config
27+
if (images.loader === 'custom' && images.loaderFile === NETLIFY_IMAGE_LOADER_FILE) {
28+
const { remotePatterns, domains } = images
29+
// if Netlify image loader is used, configure allowed remote image sources
30+
const remoteImageSources: string[] = []
31+
if (remotePatterns && remotePatterns.length !== 0) {
32+
// convert images.remotePatterns to regexes for Frameworks API
33+
for (const remotePattern of remotePatterns) {
34+
if (remotePattern instanceof URL) {
35+
// Note: even if URL notation is used in next.config.js, This will result in RemotePattern
36+
// object here, so types for the complete config should not have URL as an possible type
37+
throw new TypeError('Received not supported URL instance in remotePatterns')
38+
}
39+
let { protocol, hostname, port, pathname }: RemotePattern = remotePattern
40+
41+
if (pathname) {
42+
pathname = pathname.startsWith('/') ? pathname : `/${pathname}`
43+
}
44+
45+
const combinedRemotePattern = `${protocol ?? 'http?(s)'}://${hostname}${
46+
port ? `:${port}` : ''
47+
}${pathname ?? '/**'}`
48+
49+
try {
50+
remoteImageSources.push(generateRegexFromPattern(combinedRemotePattern))
51+
} catch (error) {
52+
throw new Error(
53+
`Failed to generate Image CDN remote image regex from Next.js remote pattern: ${JSON.stringify(
54+
{ remotePattern, combinedRemotePattern },
55+
null,
56+
2,
57+
)}`,
58+
{
59+
cause: error,
60+
},
61+
)
62+
}
63+
}
64+
}
65+
66+
if (domains && domains.length !== 0) {
67+
for (const domain of domains) {
68+
const patternFromDomain = `http?(s)://${domain}/**`
69+
try {
70+
remoteImageSources.push(generateRegexFromPattern(patternFromDomain))
71+
} catch (error) {
72+
throw new Error(
73+
`Failed to generate Image CDN remote image regex from Next.js domain: ${JSON.stringify(
74+
{ domain, patternFromDomain },
75+
null,
76+
2,
77+
)}`,
78+
{ cause: error },
79+
)
80+
}
81+
}
82+
}
83+
84+
if (remoteImageSources.length !== 0) {
85+
// https://docs.netlify.com/build/frameworks/frameworks-api/#images
86+
frameworksAPIConfig ??= {}
87+
frameworksAPIConfig.images ??= {}
88+
frameworksAPIConfig.images.remote_images = remoteImageSources
89+
}
90+
}
91+
return frameworksAPIConfig
92+
}

src/adapter/next-image-loader.cts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// this file is CJS because we add a `require` polyfill banner that attempt to use node:module in ESM modules
2+
// this later cause problems because Next.js will use this file in browser context where node:module is not available
3+
// ideally we would not add banner for this file and the we could make it ESM, but currently there is no conditional banners
4+
// in esbuild, only workaround in form of this proof of concept https://www.npmjs.com/package/esbuild-plugin-transform-hook
5+
// (or rolling our own esbuild plugin for that)
6+
7+
import type { ImageLoader } from 'next/dist/shared/lib/image-external.js'
8+
9+
const netlifyImageLoader: ImageLoader = ({ src, width, quality }) => {
10+
const url = new URL(`.netlify/images`, 'http://n')
11+
url.searchParams.set('url', src)
12+
url.searchParams.set('w', width.toString())
13+
url.searchParams.set('q', (quality || 75).toString())
14+
return url.pathname + url.search
15+
}
16+
17+
export default netlifyImageLoader

src/adapter/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { NextAdapter } from 'next-with-adapters'
2+
3+
export type OnBuildCompleteContext = Parameters<Required<NextAdapter>['onBuildComplete']>[0]
4+
export type NextConfigComplete = OnBuildCompleteContext['config']

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { rm } from 'fs/promises'
1+
import { rm } from 'node:fs/promises'
2+
import { fileURLToPath } from 'node:url'
23

34
import type { NetlifyPluginOptions } from '@netlify/build'
45
import { trace } from '@opentelemetry/api'
@@ -65,7 +66,7 @@ export const onPreBuild = async (options: NetlifyPluginOptions) => {
6566
// We will have a build plugin that will contain the adapter, we will still use some build plugin features
6667
// for operations that are more idiomatic to do in build plugin rather than adapter due to helpers we can
6768
// use in a build plugin context.
68-
process.env.NEXT_ADAPTER_PATH = `@netlify/plugin-nextjs/dist/adapter.js`
69+
process.env.NEXT_ADAPTER_PATH = fileURLToPath(import.meta.resolve(`./adapter/adapter.js`))
6970
}
7071

7172
export const onBuild = async (options: NetlifyPluginOptions) => {

src/next-image-loader.cts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)