Skip to content

File tree

5 files changed

+163
-66
lines changed

5 files changed

+163
-66
lines changed
 

‎packages/runtime/src/helpers/files.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable max-lines */
22
import { cpus } from 'os'
33

4+
import type { NetlifyConfig } from '@netlify/build'
45
import { yellowBright } from 'chalk'
56
import { existsSync, readJson, move, copy, writeJson, readFile, writeFile, ensureDir, readFileSync } from 'fs-extra'
67
import globby from 'globby'
@@ -59,11 +60,11 @@ export const matchesRewrite = (file: string, rewrites: Rewrites): boolean => {
5960
return matchesRedirect(file, rewrites.beforeFiles)
6061
}
6162

62-
export const getMiddleware = async (distDir: string): Promise<Array<string>> => {
63+
export const getMiddleware = async (publish: string): Promise<Array<string>> => {
6364
if (process.env.NEXT_DISABLE_NETLIFY_EDGE !== 'true' && process.env.NEXT_DISABLE_NETLIFY_EDGE !== '1') {
6465
return []
6566
}
66-
const manifestPath = join(distDir, 'server', 'middleware-manifest.json')
67+
const manifestPath = join(publish, 'server', 'middleware-manifest.json')
6768
if (existsSync(manifestPath)) {
6869
const manifest = await readJson(manifestPath, { throws: false })
6970
return manifest?.sortedMiddleware ?? []
@@ -73,28 +74,32 @@ export const getMiddleware = async (distDir: string): Promise<Array<string>> =>
7374

7475
// eslint-disable-next-line max-lines-per-function
7576
export const moveStaticPages = async ({
76-
distDir,
77+
netlifyConfig,
78+
target,
7779
i18n,
7880
basePath,
79-
publishDir,
8081
}: {
81-
distDir: string
82+
netlifyConfig: NetlifyConfig
83+
target: 'server' | 'serverless' | 'experimental-serverless-trace'
8284
i18n: NextConfig['i18n']
8385
basePath?: string
84-
publishDir
8586
}): Promise<void> => {
8687
console.log('Moving static page files to serve from CDN...')
87-
const outputDir = join(distDir, 'server')
88+
const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless')
8889
const root = join(outputDir, 'pages')
89-
const buildId = readFileSync(join(distDir, 'BUILD_ID'), 'utf8').trim()
90+
const buildId = readFileSync(join(netlifyConfig.build.publish, 'BUILD_ID'), 'utf8').trim()
9091
const dataDir = join('_next', 'data', buildId)
91-
await ensureDir(join(publishDir, dataDir))
92+
await ensureDir(join(netlifyConfig.build.publish, dataDir))
9293
// Load the middleware manifest so we can check if a file matches it before moving
93-
const middlewarePaths = await getMiddleware(distDir)
94+
const middlewarePaths = await getMiddleware(netlifyConfig.build.publish)
9495
const middleware = middlewarePaths.map((path) => path.slice(1))
9596

96-
const prerenderManifest: PrerenderManifest = await readJson(join(distDir, 'prerender-manifest.json'))
97-
const { redirects, rewrites }: RoutesManifest = await readJson(join(distDir, 'routes-manifest.json'))
97+
const prerenderManifest: PrerenderManifest = await readJson(
98+
join(netlifyConfig.build.publish, 'prerender-manifest.json'),
99+
)
100+
const { redirects, rewrites }: RoutesManifest = await readJson(
101+
join(netlifyConfig.build.publish, 'routes-manifest.json'),
102+
)
98103

99104
const isrFiles = new Set<string>()
100105

@@ -123,7 +128,7 @@ export const moveStaticPages = async ({
123128
files.push(file)
124129
filesManifest[file] = targetPath
125130

126-
const dest = join(publishDir, targetPath)
131+
const dest = join(netlifyConfig.build.publish, targetPath)
127132

128133
try {
129134
await move(source, dest)
@@ -237,10 +242,10 @@ export const moveStaticPages = async ({
237242
}
238243

239244
// Write the manifest for use in the serverless functions
240-
await writeJson(join(distDir, 'static-manifest.json'), Object.entries(filesManifest))
245+
await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), Object.entries(filesManifest))
241246

242247
if (i18n?.defaultLocale) {
243-
const rootPath = basePath ? join(publishDir, basePath) : publishDir
248+
const rootPath = basePath ? join(netlifyConfig.build.publish, basePath) : netlifyConfig.build.publish
244249
// Copy the default locale into the root
245250
const defaultLocaleDir = join(rootPath, i18n.defaultLocale)
246251
if (existsSync(defaultLocaleDir)) {
@@ -422,13 +427,12 @@ export const unpatchNextFiles = async (root: string): Promise<void> => {
422427
export const movePublicFiles = async ({
423428
appDir,
424429
outdir,
425-
publishDir,
430+
publish,
426431
}: {
427432
appDir: string
428433
outdir?: string
429-
publishDir: string
434+
publish: string
430435
}): Promise<void> => {
431-
await ensureDir(publishDir)
432436
// `outdir` is a config property added when using Next.js with Nx. It's typically
433437
// a relative path outside of the appDir, e.g. '../../dist/apps/<app-name>', and
434438
// the parent directory of the .next directory.
@@ -437,7 +441,7 @@ export const movePublicFiles = async ({
437441
// directory from the original app directory.
438442
const publicDir = outdir ? join(appDir, outdir, 'public') : join(appDir, 'public')
439443
if (existsSync(publicDir)) {
440-
await copy(publicDir, `${publishDir}/`)
444+
await copy(publicDir, `${publish}/`)
441445
}
442446
}
443447
/* eslint-enable max-lines */

‎packages/runtime/src/helpers/redirects.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { PrerenderManifest, SsgRoute } from 'next/dist/build'
77
import { outdent } from 'outdent'
88
import { join } from 'pathe'
99

10-
import { HANDLER_FUNCTION_PATH, ODB_FUNCTION_PATH } from '../constants'
10+
import { HANDLER_FUNCTION_PATH, HIDDEN_PATHS, ODB_FUNCTION_PATH } from '../constants'
1111

1212
import { getMiddleware } from './files'
1313
import { ApiRouteConfig } from './functions'
@@ -25,6 +25,14 @@ import {
2525
const matchesMiddleware = (middleware: Array<string>, route: string): boolean =>
2626
middleware.some((middlewarePath) => route.startsWith(middlewarePath))
2727

28+
const generateHiddenPathRedirects = ({ basePath }: Pick<NextConfig, 'basePath'>): NetlifyConfig['redirects'] =>
29+
HIDDEN_PATHS.map((path) => ({
30+
from: `${basePath}${path}`,
31+
to: '/404.html',
32+
status: 404,
33+
force: true,
34+
}))
35+
2836
const generateLocaleRedirects = ({
2937
i18n,
3038
basePath,
@@ -58,6 +66,21 @@ const generateLocaleRedirects = ({
5866
return redirects
5967
}
6068

69+
export const generateStaticRedirects = ({
70+
netlifyConfig,
71+
nextConfig: { i18n, basePath },
72+
}: {
73+
netlifyConfig: NetlifyConfig
74+
nextConfig: Pick<NextConfig, 'i18n' | 'basePath'>
75+
}) => {
76+
// Static files are in `static`
77+
netlifyConfig.redirects.push({ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 })
78+
79+
if (i18n) {
80+
netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 })
81+
}
82+
}
83+
6184
/**
6285
* Routes that match middleware need to always use the SSR function
6386
* This generates a rewrite for every middleware in every locale, both with and without a splat
@@ -220,6 +243,8 @@ export const generateRedirects = async ({
220243
join(netlifyConfig.build.publish, 'routes-manifest.json'),
221244
)
222245

246+
netlifyConfig.redirects.push(...generateHiddenPathRedirects({ basePath }))
247+
223248
if (i18n && i18n.localeDetection !== false) {
224249
netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash }))
225250
}

‎packages/runtime/src/index.ts

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { join, relative } from 'path'
44
import type { NetlifyPlugin } from '@netlify/build'
55
import { bold, redBright } from 'chalk'
66
import destr from 'destr'
7-
import { copy, ensureDir, existsSync, readFileSync } from 'fs-extra'
7+
import { existsSync, readFileSync } from 'fs-extra'
88
import { outdent } from 'outdent'
99

1010
import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } from './constants'
@@ -26,7 +26,7 @@ import {
2626
getExtendedApiRouteConfigs,
2727
warnOnApiRoutes,
2828
} from './helpers/functions'
29-
import { generateRedirects } from './helpers/redirects'
29+
import { generateRedirects, generateStaticRedirects } from './helpers/redirects'
3030
import { shouldSkip, isNextAuthInstalled, getCustomImageResponseHeaders, getRemotePatterns } from './helpers/utils'
3131
import {
3232
verifyNetlifyBuildVersion,
@@ -80,18 +80,12 @@ const plugin: NetlifyPlugin = {
8080

8181
checkNextSiteHasBuilt({ publish, failBuild })
8282

83-
const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir, experimental, distDir } =
84-
await getNextConfig({
83+
const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir, experimental } = await getNextConfig(
84+
{
8585
publish,
8686
failBuild,
87-
})
88-
89-
const dotNextDir = join(appDir, distDir)
90-
91-
// This is the *generated* publish dir. The user specifies .next, be we actually use this subdirectory
92-
const publishDir = join(dotNextDir, 'dist')
93-
await ensureDir(publishDir)
94-
87+
},
88+
)
9589
await cleanupEdgeFunctions(constants)
9690

9791
const middlewareManifest = await loadMiddlewareManifest(netlifyConfig)
@@ -123,7 +117,7 @@ const plugin: NetlifyPlugin = {
123117
}
124118

125119
if (isNextAuthInstalled()) {
126-
const config = await getRequiredServerFiles(dotNextDir)
120+
const config = await getRequiredServerFiles(publish)
127121

128122
const userDefinedNextAuthUrl = config.config.env.NEXTAUTH_URL
129123

@@ -140,7 +134,7 @@ const plugin: NetlifyPlugin = {
140134
)
141135
config.config.env.NEXTAUTH_URL = nextAuthUrl
142136

143-
await updateRequiredServerFiles(dotNextDir, config)
137+
await updateRequiredServerFiles(publish, config)
144138
} else {
145139
// Using the deploy prime url in production leads to issues because the unique deploy ID is part of the generated URL
146140
// and will not match the expected URL in the callback URL of an OAuth application.
@@ -151,27 +145,30 @@ const plugin: NetlifyPlugin = {
151145
console.log(`NextAuth package detected, setting NEXTAUTH_URL environment variable to ${nextAuthUrl}`)
152146
config.config.env.NEXTAUTH_URL = nextAuthUrl
153147

154-
await updateRequiredServerFiles(dotNextDir, config)
148+
await updateRequiredServerFiles(publish, config)
155149
}
156150
}
157151

158-
const buildId = readFileSync(join(dotNextDir, 'BUILD_ID'), 'utf8').trim()
152+
const buildId = readFileSync(join(publish, 'BUILD_ID'), 'utf8').trim()
159153

160-
await configureHandlerFunctions({ netlifyConfig, ignore, publish: relative(process.cwd(), dotNextDir) })
161-
const apiRoutes = await getExtendedApiRouteConfigs(dotNextDir, appDir)
154+
await configureHandlerFunctions({ netlifyConfig, ignore, publish: relative(process.cwd(), publish) })
155+
const apiRoutes = await getExtendedApiRouteConfigs(publish, appDir)
162156

163157
await generateFunctions(constants, appDir, apiRoutes)
164158
await generatePagesResolver({ target, constants })
165159

166-
await movePublicFiles({ appDir, outdir, publishDir })
160+
await movePublicFiles({ appDir, outdir, publish })
167161

168162
await patchNextFiles(appDir)
169163

170164
if (!destr(process.env.SERVE_STATIC_FILES_FROM_ORIGIN)) {
171-
await moveStaticPages({ distDir: dotNextDir, i18n, basePath, publishDir })
165+
await moveStaticPages({ target, netlifyConfig, i18n, basePath })
172166
}
173167

174-
await copy(join(dotNextDir, 'static'), join(publishDir, '_next', 'static'))
168+
await generateStaticRedirects({
169+
netlifyConfig,
170+
nextConfig: { basePath, i18n },
171+
})
175172

176173
await setupImageFunction({
177174
constants,
@@ -193,16 +190,20 @@ const plugin: NetlifyPlugin = {
193190
},
194191

195192
async onPostBuild({
196-
netlifyConfig,
193+
netlifyConfig: {
194+
build: { publish },
195+
redirects,
196+
headers,
197+
},
197198
utils: {
198199
status,
199200
cache,
200201
functions,
201202
build: { failBuild },
202203
},
203-
constants: { FUNCTIONS_DIST, PUBLISH_DIR },
204+
constants: { FUNCTIONS_DIST },
204205
}) {
205-
await saveCache({ cache, publish: netlifyConfig.build.publish })
206+
await saveCache({ cache, publish })
206207

207208
if (shouldSkip()) {
208209
status.show({
@@ -218,16 +219,15 @@ const plugin: NetlifyPlugin = {
218219

219220
await checkForOldFunctions({ functions })
220221
await checkZipSize(join(FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`))
221-
const nextConfig = await getNextConfig({ publish: netlifyConfig.build.publish, failBuild })
222+
const nextConfig = await getNextConfig({ publish, failBuild })
222223

223224
const { basePath, appDir } = nextConfig
224225

225-
generateCustomHeaders(nextConfig, netlifyConfig.headers)
226+
generateCustomHeaders(nextConfig, headers)
226227

227-
warnForProblematicUserRewrites({ basePath, redirects: netlifyConfig.redirects })
228+
warnForProblematicUserRewrites({ basePath, redirects })
228229
warnForRootRedirects({ appDir })
229230
await warnOnApiRoutes({ FUNCTIONS_DIST })
230-
netlifyConfig.build.publish = join(PUBLISH_DIR, 'dist')
231231
},
232232
}
233233
// The types haven't been updated yet

‎test/__snapshots__/index.js.snap

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,16 @@ Array [
10861086
"status": 301,
10871087
"to": "/_ipx/w_:width,q_:quality/:url",
10881088
},
1089+
Object {
1090+
"from": "/_next/static/*",
1091+
"status": 200,
1092+
"to": "/static/:splat",
1093+
},
1094+
Object {
1095+
"from": "/:locale/_next/static/*",
1096+
"status": 200,
1097+
"to": "/static/:splat",
1098+
},
10891099
Object {
10901100
"conditions": Object {
10911101
"Cookie": Array [
@@ -1130,6 +1140,24 @@ Array [
11301140
"status": 200,
11311141
"to": "/.netlify/functions/___netlify-handler",
11321142
},
1143+
Object {
1144+
"force": true,
1145+
"from": "/BUILD_ID",
1146+
"status": 404,
1147+
"to": "/404.html",
1148+
},
1149+
Object {
1150+
"force": true,
1151+
"from": "/build-manifest.json",
1152+
"status": 404,
1153+
"to": "/404.html",
1154+
},
1155+
Object {
1156+
"force": true,
1157+
"from": "/cache/*",
1158+
"status": 404,
1159+
"to": "/404.html",
1160+
},
11331161
Object {
11341162
"force": false,
11351163
"from": "/css",
@@ -1682,24 +1710,54 @@ Array [
16821710
"status": 200,
16831711
"to": "/.netlify/functions/___netlify-handler",
16841712
},
1713+
Object {
1714+
"force": true,
1715+
"from": "/prerender-manifest.json",
1716+
"status": 404,
1717+
"to": "/404.html",
1718+
},
16851719
Object {
16861720
"force": false,
16871721
"from": "/previewTest",
16881722
"status": 200,
16891723
"to": "/.netlify/functions/___netlify-handler",
16901724
},
1725+
Object {
1726+
"force": true,
1727+
"from": "/react-loadable-manifest.json",
1728+
"status": 404,
1729+
"to": "/404.html",
1730+
},
16911731
Object {
16921732
"force": false,
16931733
"from": "/redirectme",
16941734
"status": 200,
16951735
"to": "/.netlify/functions/___netlify-handler",
16961736
},
1737+
Object {
1738+
"force": true,
1739+
"from": "/routes-manifest.json",
1740+
"status": 404,
1741+
"to": "/404.html",
1742+
},
16971743
Object {
16981744
"force": false,
16991745
"from": "/script",
17001746
"status": 200,
17011747
"to": "/.netlify/functions/___netlify-handler",
17021748
},
1749+
Object {
1750+
"force": true,
1751+
"from": "/server/*",
1752+
"status": 404,
1753+
"to": "/404.html",
1754+
},
1755+
Object {
1756+
"force": true,
1757+
"from": "/serverless/*",
1758+
"status": 404,
1759+
"to": "/404.html",
1760+
},
17031761
Object {
17041762
"force": false,
17051763
"from": "/shows/:id",
@@ -1724,5 +1782,17 @@ Array [
17241782
"status": 200,
17251783
"to": "/.netlify/functions/___netlify-handler",
17261784
},
1785+
Object {
1786+
"force": true,
1787+
"from": "/trace",
1788+
"status": 404,
1789+
"to": "/404.html",
1790+
},
1791+
Object {
1792+
"force": true,
1793+
"from": "/traces",
1794+
"status": 404,
1795+
"to": "/404.html",
1796+
},
17271797
]
17281798
`;

‎test/index.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,21 @@ const changeCwd = function (cwd) {
107107
const onBuildHasRun = (netlifyConfig) =>
108108
Boolean(netlifyConfig.functions[HANDLER_FUNCTION_NAME]?.included_files?.some((file) => file.includes('BUILD_ID')))
109109

110-
const rewriteAppDir = async function (dir = '.next', appDir) {
111-
const manifest = path.join(appDir, dir, 'required-server-files.json')
110+
const rewriteAppDir = async function (dir = '.next') {
111+
const manifest = path.join(dir, 'required-server-files.json')
112112
const manifestContent = await readJson(manifest)
113-
manifestContent.appDir = appDir
113+
manifestContent.appDir = process.cwd()
114114

115115
await writeJSON(manifest, manifestContent)
116116
}
117117

118118
// Move .next from sample project to current directory
119-
export const moveNextDist = async function (dotNext = '.next', app = '.') {
120-
const appDir = path.join(process.cwd(), app)
119+
export const moveNextDist = async function (dir = '.next') {
121120
await stubModules(['next', 'sharp'])
122-
await ensureDir(dirname(dotNext))
123-
await copy(path.join(SAMPLE_PROJECT_DIR, '.next'), path.join(appDir, dotNext))
124-
await copy(path.join(SAMPLE_PROJECT_DIR, 'pages'), path.join(appDir, 'pages'))
125-
await rewriteAppDir(dotNext, appDir)
121+
await ensureDir(dirname(dir))
122+
await copy(path.join(SAMPLE_PROJECT_DIR, '.next'), path.join(process.cwd(), dir))
123+
await copy(path.join(SAMPLE_PROJECT_DIR, 'pages'), path.join(process.cwd(), 'pages'))
124+
await rewriteAppDir(dir)
126125
}
127126

128127
const stubModules = async function (modules) {
@@ -467,17 +466,17 @@ describe('onBuild()', () => {
467466
expect(data).toMatchSnapshot()
468467
})
469468

470-
test('moves static files to dist', async () => {
469+
test('moves static files to root', async () => {
471470
await moveNextDist()
472471
await nextRuntime.onBuild(defaultArgs)
473472
const data = JSON.parse(readFileSync(path.resolve('.next/static-manifest.json'), 'utf8'))
474473
data.forEach(([_, file]) => {
475-
expect(existsSync(path.resolve(path.join('.next', 'dist', file)))).toBeTruthy()
474+
expect(existsSync(path.resolve(path.join('.next', file)))).toBeTruthy()
476475
expect(existsSync(path.resolve(path.join('.next', 'server', 'pages', file)))).toBeFalsy()
477476
})
478477
})
479478

480-
test('copies default locale files to top level in dist', async () => {
479+
test('copies default locale files to top level', async () => {
481480
await moveNextDist()
482481
await nextRuntime.onBuild(defaultArgs)
483482
const data = JSON.parse(readFileSync(path.resolve('.next/static-manifest.json'), 'utf8'))
@@ -489,7 +488,7 @@ describe('onBuild()', () => {
489488
return
490489
}
491490
const trimmed = file.substring(locale.length)
492-
expect(existsSync(path.resolve(path.join('.next', 'dist', trimmed)))).toBeTruthy()
491+
expect(existsSync(path.resolve(path.join('.next', trimmed)))).toBeTruthy()
493492
})
494493
})
495494

@@ -597,14 +596,13 @@ describe('onBuild()', () => {
597596
})
598597

599598
test('generates a file referencing all when publish dir is a subdirectory', async () => {
600-
const dotNext = '.next'
601-
const app = 'web'
602-
await moveNextDist(dotNext, app)
603-
netlifyConfig.build.publish = path.resolve(app, dotNext)
599+
const dir = 'web/.next'
600+
await moveNextDist(dir)
601+
netlifyConfig.build.publish = path.resolve(dir)
604602
const config = {
605603
...defaultArgs,
606604
netlifyConfig,
607-
constants: { ...constants, PUBLISH_DIR: path.join(app, dotNext) },
605+
constants: { ...constants, PUBLISH_DIR: dir },
608606
}
609607
await nextRuntime.onBuild(config)
610608
const handlerPagesFile = path.join(constants.INTERNAL_FUNCTIONS_SRC, HANDLER_FUNCTION_NAME, 'pages.js')

0 commit comments

Comments
 (0)
Please sign in to comment.