Skip to content

Commit 60594f1

Browse files
authoredDec 12, 2023
feat: use prerenderManifest data instead of globbing when copying prerendered content (#105)
* feat: use publish dir constant when getting middlewareManifest * feat: better error checking when copying static content * feat: simply static content copying * chore: better variable names for copying server content * feat: rework prerendered content method * chore: simplify static content logic * fix: normalize prerendered index routes * fix: explicity add prerendered not found routes * feat: better guard against wrongly unpublishing static on failed builds * fix: don't attempt to copy app router 404 page on page router site * chore: fix static content tests
1 parent 4886838 commit 60594f1

File tree

8 files changed

+222
-183
lines changed

8 files changed

+222
-183
lines changed
 

‎src/build/config.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { NetlifyPluginConstants, NetlifyPluginOptions } from '@netlify/build'
22
import type { PrerenderManifest } from 'next/dist/build/index.js'
33
import { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
4+
import { existsSync } from 'node:fs'
45
import { readFile } from 'node:fs/promises'
56
import { resolve } from 'node:path'
67
import { SERVER_HANDLER_NAME } from './constants.js'
@@ -14,8 +15,12 @@ export const getPrerenderManifest = async ({
1415
/**
1516
* Get Next.js middleware config from the build output
1617
*/
17-
export const getMiddlewareManifest = async (): Promise<MiddlewareManifest> => {
18-
return JSON.parse(await readFile(resolve('.next/server/middleware-manifest.json'), 'utf-8'))
18+
export const getMiddlewareManifest = async ({
19+
PUBLISH_DIR,
20+
}: Pick<NetlifyPluginConstants, 'PUBLISH_DIR'>): Promise<MiddlewareManifest> => {
21+
return JSON.parse(
22+
await readFile(resolve(PUBLISH_DIR, 'server/middleware-manifest.json'), 'utf-8'),
23+
)
1924
}
2025

2126
/**
@@ -35,3 +40,14 @@ export const setPostBuildConfig = ({
3540
status: 200,
3641
})
3742
}
43+
44+
export const verifyBuildConfig = ({
45+
constants: { PUBLISH_DIR },
46+
utils: {
47+
build: { failBuild },
48+
},
49+
}: Pick<NetlifyPluginOptions, 'constants' | 'utils'>) => {
50+
if (!existsSync(resolve(PUBLISH_DIR))) {
51+
failBuild('Publish directory not found, please check your netlify.toml')
52+
}
53+
}

‎src/build/constants.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ const pkg = JSON.parse(readFileSync(join(PLUGIN_DIR, 'package.json'), 'utf-8'))
1010
export const PLUGIN_NAME = pkg.name
1111
export const PLUGIN_VERSION = pkg.version
1212

13-
/** The directory that is published to Netlify */
13+
/** This directory is swapped with the publish dir and deployed to the Netlify CDN */
1414
export const STATIC_DIR = '.netlify/static'
15-
export const TEMP_DIR = '.netlify/temp'
16-
/** The directory inside the publish directory that will be uploaded by build to the blob store */
15+
16+
/** This directory will be deployed to the blob store */
1717
export const BLOB_DIR = '.netlify/blobs/deploy'
1818

19+
/** This directory contains files for the serverless lambda function */
1920
export const SERVER_FUNCTIONS_DIR = '.netlify/functions-internal'
2021
export const SERVER_HANDLER_NAME = '___netlify-server-handler'
2122
export const SERVER_HANDLER_DIR = join(SERVER_FUNCTIONS_DIR, SERVER_HANDLER_NAME)
2223

24+
/** This directory contains files for deno edge functions */
2325
export const EDGE_FUNCTIONS_DIR = '.netlify/edge-functions'
2426
export const EDGE_HANDLER_NAME = '___netlify-edge-handler'
2527
export const EDGE_HANDLER_DIR = join(EDGE_FUNCTIONS_DIR, EDGE_HANDLER_NAME)

‎src/build/content/prerendered.ts

+100-92
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import type { NetlifyPluginOptions } from '@netlify/build'
22
import glob from 'fast-glob'
3+
import { existsSync } from 'node:fs'
34
import { mkdir, readFile, writeFile } from 'node:fs/promises'
4-
import { basename, dirname, extname, resolve } from 'node:path'
5-
import { join as joinPosix } from 'node:path/posix'
5+
import { dirname, resolve } from 'node:path'
66
import { getPrerenderManifest } from '../config.js'
77
import { BLOB_DIR } from '../constants.js'
88

99
export type CacheEntry = {
10-
key: string
11-
value: CacheEntryValue
12-
}
13-
14-
export type CacheEntryValue = {
1510
lastModified: number
16-
value: PageCacheValue | RouteCacheValue | FetchCacheValue
11+
value: CacheValue
1712
}
1813

14+
type CacheValue = PageCacheValue | RouteCacheValue | FetchCacheValue
15+
1916
export type PageCacheValue = {
2017
kind: 'PAGE'
2118
html: string
@@ -42,110 +39,121 @@ type FetchCacheValue = {
4239
}
4340
}
4441

45-
// static prerendered pages content with JSON data
46-
const isPage = (key: string, routes: string[]) => {
47-
return key.startsWith('server/pages') && routes.includes(key.replace(/^server\/pages/, ''))
48-
}
49-
// static prerendered app content with RSC data
50-
const isApp = (path: string) => {
51-
return path.startsWith('server/app') && extname(path) === '.html'
52-
}
53-
// static prerendered app route handler
54-
const isRoute = (path: string) => {
55-
return path.startsWith('server/app') && extname(path) === '.body'
56-
}
57-
// fetch cache data (excluding tags manifest)
58-
const isFetch = (path: string) => {
59-
return path.startsWith('cache/fetch-cache') && extname(path) === ''
42+
const writeCacheEntry = async (key: string, value: CacheValue) => {
43+
await mkdir(dirname(resolve(BLOB_DIR, key)), { recursive: true })
44+
await writeFile(
45+
resolve(BLOB_DIR, key),
46+
JSON.stringify({ lastModified: Date.now(), value } satisfies CacheEntry),
47+
'utf-8',
48+
)
6049
}
6150

62-
/**
63-
* Transform content file paths into cache entries for the blob store
64-
*/
65-
const buildPrerenderedContentEntries = async (
66-
src: string,
67-
routes: string[],
68-
): Promise<Promise<CacheEntry>[]> => {
69-
const paths = await glob([`cache/fetch-cache/*`, `server/+(app|pages)/**/*.+(html|body)`], {
70-
cwd: resolve(src),
71-
extglob: true,
72-
})
73-
74-
return paths.map(async (path: string): Promise<CacheEntry> => {
75-
const key = joinPosix(dirname(path), basename(path, extname(path)))
76-
let value
77-
78-
if (isPage(key, routes)) {
79-
value = {
80-
kind: 'PAGE',
81-
html: await readFile(resolve(src, `${key}.html`), 'utf-8'),
82-
pageData: JSON.parse(await readFile(resolve(src, `${key}.json`), 'utf-8')),
83-
} satisfies PageCacheValue
84-
}
51+
const urlPathToFilePath = (path: string) => (path === '/' ? '/index' : path)
8552

86-
if (isApp(path)) {
87-
value = {
88-
kind: 'PAGE',
89-
html: await readFile(resolve(src, `${key}.html`), 'utf-8'),
90-
pageData: await readFile(resolve(src, `${key}.rsc`), 'utf-8'),
91-
...JSON.parse(await readFile(resolve(src, `${key}.meta`), 'utf-8')),
92-
} satisfies PageCacheValue
93-
}
53+
const buildPagesCacheValue = async (path: string): Promise<PageCacheValue> => ({
54+
kind: 'PAGE',
55+
html: await readFile(resolve(`${path}.html`), 'utf-8'),
56+
pageData: JSON.parse(await readFile(resolve(`${path}.json`), 'utf-8')),
57+
})
9458

95-
if (isRoute(path)) {
96-
value = {
97-
kind: 'ROUTE',
98-
body: await readFile(resolve(src, `${key}.body`), 'utf-8'),
99-
...JSON.parse(await readFile(resolve(src, `${key}.meta`), 'utf-8')),
100-
} satisfies RouteCacheValue
101-
}
59+
const buildAppCacheValue = async (path: string): Promise<PageCacheValue> => ({
60+
kind: 'PAGE',
61+
html: await readFile(resolve(`${path}.html`), 'utf-8'),
62+
pageData: await readFile(resolve(`${path}.rsc`), 'utf-8'),
63+
...JSON.parse(await readFile(resolve(`${path}.meta`), 'utf-8')),
64+
})
10265

103-
if (isFetch(path)) {
104-
value = {
105-
kind: 'FETCH',
106-
...JSON.parse(await readFile(resolve(src, key), 'utf-8')),
107-
} satisfies FetchCacheValue
108-
}
66+
const buildRouteCacheValue = async (path: string): Promise<RouteCacheValue> => ({
67+
kind: 'ROUTE',
68+
body: await readFile(resolve(`${path}.body`), 'utf-8'),
69+
...JSON.parse(await readFile(resolve(`${path}.meta`), 'utf-8')),
70+
})
10971

110-
return {
111-
key,
112-
value: {
113-
lastModified: Date.now(),
114-
value,
115-
},
116-
}
117-
})
118-
}
72+
const buildFetchCacheValue = async (path: string): Promise<FetchCacheValue> => ({
73+
kind: 'FETCH',
74+
...JSON.parse(await readFile(resolve(path), 'utf-8')),
75+
})
11976

12077
/**
121-
* Upload prerendered content to the blob store and remove it from the bundle
78+
* Upload prerendered content to the blob store
12279
*/
123-
export const uploadPrerenderedContent = async ({
80+
export const copyPrerenderedContent = async ({
12481
constants: { PUBLISH_DIR },
125-
utils,
82+
utils: {
83+
build: { failBuild },
84+
},
12685
}: Pick<NetlifyPluginOptions, 'constants' | 'utils'>) => {
12786
try {
12887
// read prerendered content and build JSON key/values for the blob store
12988
const manifest = await getPrerenderManifest({ PUBLISH_DIR })
130-
const entries = await Promise.all(
131-
await buildPrerenderedContentEntries(PUBLISH_DIR, Object.keys(manifest.routes)),
132-
)
89+
const routes = Object.entries(manifest.routes)
90+
const notFoundRoute = 'server/app/_not-found'
13391

134-
// movce JSON content to the blob store directory for upload
13592
await Promise.all(
136-
entries
137-
.filter((entry) => entry.value.value !== undefined)
138-
.map(async (entry) => {
139-
const dest = resolve(BLOB_DIR, entry.key)
140-
await mkdir(dirname(dest), { recursive: true })
141-
await writeFile(resolve(BLOB_DIR, entry.key), JSON.stringify(entry.value), 'utf-8')
142-
}),
93+
routes.map(async ([path, route]) => {
94+
let key, value
95+
96+
switch (true) {
97+
case route.dataRoute?.endsWith('.json'):
98+
key = `server/pages/${urlPathToFilePath(path)}`
99+
value = await buildPagesCacheValue(resolve(PUBLISH_DIR, key))
100+
break
101+
102+
case route.dataRoute?.endsWith('.rsc'):
103+
key = `server/app/${urlPathToFilePath(path)}`
104+
value = await buildAppCacheValue(resolve(PUBLISH_DIR, key))
105+
break
106+
107+
case route.dataRoute === null:
108+
key = `server/app/${urlPathToFilePath(path)}`
109+
value = await buildRouteCacheValue(resolve(PUBLISH_DIR, key))
110+
break
111+
112+
default:
113+
throw new Error(`Unrecognized prerendered content: ${path}`)
114+
}
115+
116+
await writeCacheEntry(key, value)
117+
}),
143118
)
119+
120+
// app router 404 pages are not in the prerender manifest
121+
// so we need to check for them manually
122+
if (existsSync(resolve(PUBLISH_DIR, `${notFoundRoute}.html`))) {
123+
await writeCacheEntry(
124+
notFoundRoute,
125+
await buildAppCacheValue(resolve(PUBLISH_DIR, notFoundRoute)),
126+
)
127+
}
144128
} catch (error) {
145-
utils.build.failBuild(
129+
failBuild(
146130
'Failed assembling prerendered content for upload',
147131
error instanceof Error ? { error } : {},
148132
)
149-
throw error
133+
}
134+
}
135+
136+
/**
137+
* Upload fetch content to the blob store
138+
*/
139+
export const copyFetchContent = async ({
140+
constants: { PUBLISH_DIR },
141+
utils: {
142+
build: { failBuild },
143+
},
144+
}: Pick<NetlifyPluginOptions, 'constants' | 'utils'>) => {
145+
try {
146+
const paths = await glob([`cache/fetch-cache/!(*.*)`], {
147+
cwd: resolve(PUBLISH_DIR),
148+
extglob: true,
149+
})
150+
151+
await Promise.all(
152+
paths.map(async (key) => {
153+
await writeCacheEntry(key, await buildFetchCacheValue(resolve(PUBLISH_DIR, key)))
154+
}),
155+
)
156+
} catch (error) {
157+
failBuild('Failed assembling fetch content for upload', error instanceof Error ? { error } : {})
150158
}
151159
}

‎src/build/content/server.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ import { SERVER_HANDLER_DIR } from '../constants.js'
1212
export const copyNextServerCode = async ({
1313
constants: { PUBLISH_DIR },
1414
}: Pick<NetlifyPluginOptions, 'constants'>): Promise<void> => {
15-
const src = resolve(PUBLISH_DIR, 'standalone/.next')
16-
const dest = resolve(SERVER_HANDLER_DIR, '.next')
15+
const srcDir = resolve(PUBLISH_DIR, 'standalone/.next')
16+
const destDir = resolve(SERVER_HANDLER_DIR, '.next')
1717

1818
const paths = await glob([`*`, `server/*`, `server/chunks/*`, `server/+(app|pages)/**/*.js`], {
19-
cwd: src,
19+
cwd: srcDir,
2020
extglob: true,
2121
})
2222

2323
await Promise.all(
2424
paths.map(async (path: string) => {
25-
await cp(join(src, path), join(dest, path), { recursive: true })
25+
await cp(join(srcDir, path), join(destDir, path), { recursive: true })
2626
}),
2727
)
2828
}
@@ -67,15 +67,15 @@ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: strin
6767
export const copyNextDependencies = async ({
6868
constants: { PUBLISH_DIR },
6969
}: Pick<NetlifyPluginOptions, 'constants'>): Promise<void> => {
70-
const src = resolve(PUBLISH_DIR, 'standalone/node_modules')
71-
const dest = resolve(SERVER_HANDLER_DIR, 'node_modules')
70+
const srcDir = resolve(PUBLISH_DIR, 'standalone/node_modules')
71+
const destDir = resolve(SERVER_HANDLER_DIR, 'node_modules')
7272

73-
await cp(src, dest, { recursive: true })
73+
await cp(srcDir, destDir, { recursive: true })
7474

7575
// use the node_modules tree from the process.cwd() and not the one from the standalone output
7676
// as the standalone node_modules are already wrongly assembled by Next.js.
7777
// see: https://github.com/vercel/next.js/issues/50072
78-
await recreateNodeModuleSymlinks(resolve('node_modules'), dest)
78+
await recreateNodeModuleSymlinks(resolve('node_modules'), destDir)
7979
}
8080

8181
export const writeTagsManifest = async ({

‎src/build/content/static.test.ts

+16-26
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import type {
2-
NetlifyPluginConstants,
3-
NetlifyPluginOptions,
4-
NetlifyPluginUtils,
5-
} from '@netlify/build'
1+
import type { NetlifyPluginConstants, NetlifyPluginUtils } from '@netlify/build'
62
import glob from 'fast-glob'
7-
import { Mock, afterEach, beforeEach, expect, test, vi } from 'vitest'
3+
import { join } from 'path'
4+
import { expect, test, vi } from 'vitest'
85
import { mockFileSystem } from '../../../tests/index.js'
96
import { FixtureTestContext, createFsFixture } from '../../../tests/utils/fixture.js'
107
import { BLOB_DIR, STATIC_DIR } from '../constants.js'
11-
import { copyStaticAssets, uploadStaticContent } from './static.js'
12-
import * as fs from 'fs'
13-
import { join } from 'path'
8+
import { copyStaticAssets, copyStaticContent } from './static.js'
149

1510
const utils = {
1611
build: { failBuild: vi.fn() },
@@ -25,6 +20,7 @@ test('should clear the static directory contents', async () => {
2520

2621
await copyStaticAssets({
2722
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
23+
utils,
2824
})
2925

3026
expect(Object.keys(vol.toJSON())).toEqual(
@@ -45,6 +41,7 @@ test<FixtureTestContext>('should link static content from the publish directory
4541

4642
await copyStaticAssets({
4743
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
44+
utils,
4845
})
4946

5047
const files = await glob('**/*', { cwd, dot: true })
@@ -72,6 +69,7 @@ test<FixtureTestContext>('should link static content from the public directory t
7269

7370
await copyStaticAssets({
7471
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
72+
utils,
7573
})
7674

7775
const files = await glob('**/*', { cwd, dot: true })
@@ -85,21 +83,18 @@ test<FixtureTestContext>('should link static content from the public directory t
8583
)
8684
})
8785

88-
test<FixtureTestContext>('should copy the static pages to the publish directory if the routes do not exist in the prerender-manifest', async (ctx) => {
86+
test<FixtureTestContext>('should copy the static pages to the publish directory if there are no corresponding JSON files', async (ctx) => {
8987
const PUBLISH_DIR = '.next'
9088
const { cwd } = await createFsFixture(
9189
{
92-
[`${PUBLISH_DIR}/prerender-manifest.json`]: JSON.stringify({
93-
routes: {},
94-
}),
95-
[`${PUBLISH_DIR}/static/test.js`]: '',
96-
[`${PUBLISH_DIR}/server/pages/test.html`]: 'test-1',
97-
[`${PUBLISH_DIR}/server/pages/test2.html`]: 'test-2',
90+
[`${PUBLISH_DIR}/server/pages/test.html`]: '',
91+
[`${PUBLISH_DIR}/server/pages/test2.html`]: '',
92+
[`${PUBLISH_DIR}/server/pages/test3.json`]: '',
9893
},
9994
ctx,
10095
)
10196

102-
await uploadStaticContent({
97+
await copyStaticContent({
10398
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
10499
utils,
105100
})
@@ -110,25 +105,20 @@ test<FixtureTestContext>('should copy the static pages to the publish directory
110105
])
111106
})
112107

113-
test<FixtureTestContext>('should not copy the static pages to the publish directory if the routes exist in the prerender-manifest', async (ctx) => {
108+
test<FixtureTestContext>('should not copy the static pages to the publish directory if there are corresponding JSON files', async (ctx) => {
114109
const PUBLISH_DIR = '.next'
115110

116111
const { cwd } = await createFsFixture(
117112
{
118-
[`${PUBLISH_DIR}/prerender-manifest.json`]: JSON.stringify({
119-
routes: {
120-
'/test': {},
121-
'/test2': {},
122-
},
123-
}),
124-
[`${PUBLISH_DIR}/static/test.js`]: '',
125113
[`${PUBLISH_DIR}/server/pages/test.html`]: '',
114+
[`${PUBLISH_DIR}/server/pages/test.json`]: '',
126115
[`${PUBLISH_DIR}/server/pages/test2.html`]: '',
116+
[`${PUBLISH_DIR}/server/pages/test2.json`]: '',
127117
},
128118
ctx,
129119
)
130120

131-
await uploadStaticContent({
121+
await copyStaticContent({
132122
constants: { PUBLISH_DIR } as NetlifyPluginConstants,
133123
utils,
134124
})

‎src/build/content/static.ts

+57-39
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,94 @@
11
import type { NetlifyPluginOptions } from '@netlify/build'
22
import glob from 'fast-glob'
3-
import type { PrerenderManifest } from 'next/dist/build/index.js'
43
import { existsSync } from 'node:fs'
54
import { cp, mkdir, rename, rm } from 'node:fs/promises'
6-
import { basename, dirname, resolve } from 'node:path'
7-
import { join as joinPosix } from 'node:path/posix'
8-
import { getPrerenderManifest } from '../config.js'
9-
import { BLOB_DIR, STATIC_DIR, TEMP_DIR } from '../constants.js'
5+
import { resolve } from 'node:path'
6+
import { BLOB_DIR, STATIC_DIR } from '../constants.js'
107

118
/**
129
* Assemble the static content for being uploaded to the blob storage
1310
*/
14-
export const uploadStaticContent = async ({
11+
export const copyStaticContent = async ({
1512
constants: { PUBLISH_DIR },
16-
utils,
13+
utils: {
14+
build: { failBuild },
15+
},
1716
}: Pick<NetlifyPluginOptions, 'constants' | 'utils'>): Promise<void> => {
18-
const dir = 'server/pages'
19-
const paths = await glob('**/*.html', {
20-
cwd: resolve(PUBLISH_DIR, dir),
17+
const srcDir = resolve(PUBLISH_DIR, 'server/pages')
18+
const destDir = resolve(BLOB_DIR, 'server/pages')
19+
20+
const paths = await glob('**/*.+(html|json)', {
21+
cwd: srcDir,
22+
extglob: true,
2123
})
2224

2325
try {
24-
const manifest = await getPrerenderManifest({ PUBLISH_DIR })
2526
await Promise.all(
2627
paths
27-
.filter((path) => {
28-
const route = '/' + joinPosix(dirname(path), basename(path, '.html'))
29-
return !Object.keys(manifest.routes).includes(route)
30-
})
28+
.filter((path) => !paths.includes(`${path.slice(0, -5)}.json`))
3129
.map(async (path) => {
32-
const dest = resolve(BLOB_DIR, dir, path)
33-
await mkdir(dirname(dest), { recursive: true })
34-
await cp(resolve(PUBLISH_DIR, dir, path), dest)
30+
await cp(resolve(srcDir, path), resolve(destDir, path), { recursive: true })
3531
}),
3632
)
3733
} catch (error) {
38-
utils.build.failBuild(
39-
'Failed assembling static assets for upload',
40-
error instanceof Error ? { error } : {},
41-
)
42-
throw error
34+
failBuild('Failed assembling static pages for upload', error instanceof Error ? { error } : {})
4335
}
4436
}
4537

4638
/**
47-
* Move static content to the publish dir so it is uploaded to the CDN
39+
* Copy static content to the static dir so it is uploaded to the CDN
4840
*/
4941
export const copyStaticAssets = async ({
5042
constants: { PUBLISH_DIR },
51-
}: Pick<NetlifyPluginOptions, 'constants'>): Promise<void> => {
52-
await rm(resolve(STATIC_DIR), { recursive: true, force: true })
53-
if (existsSync(resolve('public'))) {
54-
await cp(resolve('public'), resolve(STATIC_DIR), { recursive: true })
55-
}
56-
if (existsSync(resolve(PUBLISH_DIR, 'static'))) {
57-
await cp(resolve(PUBLISH_DIR, 'static'), resolve(STATIC_DIR, '_next/static'), {
58-
recursive: true,
59-
})
43+
utils: {
44+
build: { failBuild },
45+
},
46+
}: Pick<NetlifyPluginOptions, 'constants' | 'utils'>): Promise<void> => {
47+
try {
48+
await rm(resolve(STATIC_DIR), { recursive: true, force: true })
49+
if (existsSync(resolve('public'))) {
50+
await cp(resolve('public'), resolve(STATIC_DIR), { recursive: true })
51+
}
52+
if (existsSync(resolve(PUBLISH_DIR, 'static'))) {
53+
await cp(resolve(PUBLISH_DIR, 'static'), resolve(STATIC_DIR, '_next/static'), {
54+
recursive: true,
55+
})
56+
}
57+
} catch (error) {
58+
failBuild('Failed copying static assets', error instanceof Error ? { error } : {})
6059
}
6160
}
6261

62+
/**
63+
* Swap the static dir with the publish dir so it is uploaded to the CDN
64+
*/
6365
export const publishStaticDir = async ({
6466
constants: { PUBLISH_DIR },
65-
}: Pick<NetlifyPluginOptions, 'constants'>): Promise<void> => {
66-
await mkdir(resolve(TEMP_DIR, 'publish'), { recursive: true })
67-
await rename(resolve(PUBLISH_DIR), resolve(TEMP_DIR, 'publish'))
68-
await rename(resolve(STATIC_DIR), resolve(PUBLISH_DIR))
67+
utils: {
68+
build: { failBuild },
69+
},
70+
}: Pick<NetlifyPluginOptions, 'constants' | 'utils'>): Promise<void> => {
71+
try {
72+
await mkdir(resolve('.netlify/.next'), { recursive: true })
73+
await rename(resolve(PUBLISH_DIR), resolve('.netlify/.next'))
74+
await rename(resolve(STATIC_DIR), resolve(PUBLISH_DIR))
75+
} catch (error) {
76+
failBuild('Failed publishing static content', error instanceof Error ? { error } : {})
77+
}
6978
}
7079

80+
/**
81+
* Restore the publish dir that was swapped with the static dir
82+
*/
7183
export const unpublishStaticDir = async ({
7284
constants: { PUBLISH_DIR },
7385
}: Pick<NetlifyPluginOptions, 'constants'>): Promise<void> => {
74-
await rename(resolve(PUBLISH_DIR), resolve(STATIC_DIR))
75-
await rename(resolve(TEMP_DIR, 'publish'), resolve(PUBLISH_DIR))
86+
try {
87+
if (existsSync(resolve('.netlify/.next'))) {
88+
await rename(resolve(PUBLISH_DIR), resolve(STATIC_DIR))
89+
await rename(resolve('.netlify/.next'), resolve(PUBLISH_DIR))
90+
}
91+
} catch (error) {
92+
// ignore
93+
}
7694
}

‎src/build/functions/edge.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { NetlifyPluginOptions } from '@netlify/build'
12
import type { EdgeFunctionDefinition as NextDefinition } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
23
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
34
import { dirname, join, relative, resolve } from 'node:path'
@@ -116,10 +117,12 @@ const writeEdgeManifest = async (manifest: NetlifyManifest) => {
116117
await writeFile(resolve(EDGE_FUNCTIONS_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2))
117118
}
118119

119-
export const createEdgeHandlers = async () => {
120+
export const createEdgeHandlers = async ({
121+
constants,
122+
}: Pick<NetlifyPluginOptions, 'constants'>) => {
120123
await rm(EDGE_FUNCTIONS_DIR, { recursive: true, force: true })
121124

122-
const nextManifest = await getMiddlewareManifest()
125+
const nextManifest = await getMiddlewareManifest(constants)
123126
const nextDefinitions = [
124127
...Object.values(nextManifest.middleware),
125128
// ...Object.values(nextManifest.functions)

‎src/index.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
11
import type { NetlifyPluginOptions } from '@netlify/build'
22
import { restoreBuildCache, saveBuildCache } from './build/cache.js'
3-
import { setPostBuildConfig, setPreBuildConfig } from './build/config.js'
4-
import { uploadPrerenderedContent } from './build/content/prerendered.js'
3+
import { setPostBuildConfig, setPreBuildConfig, verifyBuildConfig } from './build/config.js'
4+
import { copyFetchContent, copyPrerenderedContent } from './build/content/prerendered.js'
55
import {
66
copyStaticAssets,
7+
copyStaticContent,
78
publishStaticDir,
89
unpublishStaticDir,
9-
uploadStaticContent,
1010
} from './build/content/static.js'
11-
import { createServerHandler } from './build/functions/server.js'
1211
import { createEdgeHandlers } from './build/functions/edge.js'
12+
import { createServerHandler } from './build/functions/server.js'
1313

1414
export const onPreBuild = async ({ constants, utils }: NetlifyPluginOptions) => {
15-
await restoreBuildCache({ constants, utils })
1615
setPreBuildConfig()
16+
await restoreBuildCache({ constants, utils })
1717
}
1818

1919
export const onBuild = async ({ constants, utils }: NetlifyPluginOptions) => {
20+
verifyBuildConfig({ constants, utils })
2021
await saveBuildCache({ constants, utils })
2122

2223
await Promise.all([
23-
copyStaticAssets({ constants }),
24-
uploadStaticContent({ constants }),
25-
uploadPrerenderedContent({ constants }),
24+
copyStaticAssets({ constants, utils }),
25+
copyStaticContent({ constants, utils }),
26+
copyPrerenderedContent({ constants, utils }),
27+
copyFetchContent({ constants, utils }),
2628
createServerHandler({ constants }),
27-
createEdgeHandlers(),
29+
createEdgeHandlers({ constants }),
2830
])
2931
}
3032

31-
export const onPostBuild = async ({ constants, netlifyConfig }: NetlifyPluginOptions) => {
32-
await publishStaticDir({ constants })
33+
export const onPostBuild = async ({ constants, utils, netlifyConfig }: NetlifyPluginOptions) => {
3334
setPostBuildConfig({ netlifyConfig })
35+
await publishStaticDir({ constants, utils })
3436
}
3537

3638
export const onEnd = async ({ constants }: NetlifyPluginOptions) => {

0 commit comments

Comments
 (0)
Please sign in to comment.