Skip to content

Commit 97a5c4a

Browse files
committed
offline navigations: emit fallback manifest data (2/13)
1 parent f0fc5c0 commit 97a5c4a

4 files changed

Lines changed: 155 additions & 18 deletions

File tree

packages/next/src/build/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ import {
233233
createOfflineNavigationFallbackDocument,
234234
getOfflineNavigationFallbackFilePath,
235235
} from './offline-navigation-fallback'
236+
import {
237+
createOfflineNavigationManifest,
238+
getOfflineNavigationManifestFilePath,
239+
} from './offline-navigation-manifest'
236240

237241
type Fallback = null | boolean | string
238242

@@ -4097,7 +4101,7 @@ export default async function build(
40974101

40984102
if (config.experimental.offlineNavigations && appDir) {
40994103
await nextBuildSpan
4100-
.traceChild('write-offline-navigation-fallback')
4104+
.traceChild('write-offline-navigation-artifacts')
41014105
.traceAsyncFn(async () => {
41024106
const fallbackDocument = createOfflineNavigationFallbackDocument({
41034107
assetPrefix: config.assetPrefix,
@@ -4116,6 +4120,17 @@ export default async function build(
41164120
)
41174121
await mkdir(path.dirname(fallbackPath), { recursive: true })
41184122
await writeFileUtf8(fallbackPath, fallbackDocument)
4123+
4124+
await writeManifest(
4125+
path.join(distDir, getOfflineNavigationManifestFilePath(buildId)),
4126+
createOfflineNavigationManifest({
4127+
assetPrefix: config.assetPrefix,
4128+
basePath: config.basePath,
4129+
buildId,
4130+
output: config.output,
4131+
trailingSlash: config.trailingSlash,
4132+
})
4133+
)
41194134
})
41204135
}
41214136

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import path from 'node:path'
2+
3+
import type { NextConfigComplete } from '../server/config-shared'
4+
import { CLIENT_STATIC_FILES_PATH } from '../shared/lib/constants'
5+
import { encodeURIPath } from '../shared/lib/encode-uri-path'
6+
import { getOfflineNavigationFallbackFilePath } from './offline-navigation-fallback'
7+
8+
export const OFFLINE_NAVIGATION_MANIFEST = '_offline-navigation-manifest.json'
9+
10+
export interface OfflineNavigationManifest {
11+
version: 1
12+
buildId: string
13+
basePath: string
14+
assetPrefix: string
15+
trailingSlash: boolean
16+
output: NonNullable<NextConfigComplete['output']> | 'default'
17+
scope: string
18+
cacheNamespace: string
19+
manifest: {
20+
path: string
21+
href: string
22+
}
23+
fallbackDocument: {
24+
path: string
25+
href: string
26+
}
27+
}
28+
29+
export function getOfflineNavigationManifestFilePath(buildId: string): string {
30+
return path.join(
31+
CLIENT_STATIC_FILES_PATH,
32+
buildId,
33+
OFFLINE_NAVIGATION_MANIFEST
34+
)
35+
}
36+
37+
function getStaticHref(basePath: string, filePath: string): string {
38+
return `${basePath}/_next/${encodeURIPath(filePath)}`
39+
}
40+
41+
function getScope(basePath: string): string {
42+
return basePath ? `${basePath}/` : '/'
43+
}
44+
45+
// Describe the build-scoped offline navigation artifacts with URLs that honor
46+
// the app basePath. Later slices use this manifest as the service worker's
47+
// source of truth for what bootstrap artifacts to cache.
48+
export function createOfflineNavigationManifest({
49+
assetPrefix,
50+
basePath,
51+
buildId,
52+
output,
53+
trailingSlash,
54+
}: {
55+
assetPrefix: string
56+
basePath: string
57+
buildId: string
58+
output: NextConfigComplete['output']
59+
trailingSlash: boolean
60+
}): OfflineNavigationManifest {
61+
const manifestPath = getOfflineNavigationManifestFilePath(buildId)
62+
const fallbackDocumentPath = getOfflineNavigationFallbackFilePath(buildId)
63+
64+
return {
65+
version: 1,
66+
buildId,
67+
basePath,
68+
assetPrefix,
69+
trailingSlash,
70+
output: output ?? 'default',
71+
scope: getScope(basePath),
72+
// Scope caches by build and basePath so a new deployment never reuses an
73+
// older fallback document.
74+
cacheNamespace: `next-offline-navigation-v1:${buildId}:${basePath || '/'}`,
75+
manifest: {
76+
path: manifestPath,
77+
href: getStaticHref(basePath, manifestPath),
78+
},
79+
fallbackDocument: {
80+
path: fallbackDocumentPath,
81+
href: getStaticHref(basePath, fallbackDocumentPath),
82+
},
83+
}
84+
}

test/production/app-dir/offline-navigations/next.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
*/
44
const nextConfig = {
55
cacheComponents: true,
6+
assetPrefix: 'https://cdn.example.com/app-assets',
7+
basePath: '/docs',
68
experimental: {
79
offlineNavigations: true,
810
},
11+
trailingSlash: true,
912
}
1013

1114
module.exports = nextConfig

test/production/app-dir/offline-navigations/offline-navigations.test.ts

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,87 @@ import { existsSync } from 'fs'
22
import { join } from 'path'
33
import { nextTestSetup } from 'e2e-utils'
44

5-
describe('offlineNavigations fallback document', () => {
5+
describe('offlineNavigations build artifacts', () => {
66
const { next } = nextTestSetup({
77
files: __dirname,
88
skipStart: true,
99
})
1010

11-
async function getFallbackDocumentPath() {
11+
async function getOfflineNavigationArtifactPaths() {
1212
const buildId = (await next.readFile('.next/BUILD_ID')).trim()
1313

1414
return {
1515
buildId,
16-
absolutePath: join(
17-
next.testDir,
18-
'.next',
19-
'static',
20-
buildId,
21-
'_offline-navigation-fallback.html'
22-
),
23-
relativePath: `.next/static/${buildId}/_offline-navigation-fallback.html`,
16+
fallbackDocument: {
17+
absolutePath: join(
18+
next.testDir,
19+
'.next',
20+
'static',
21+
buildId,
22+
'_offline-navigation-fallback.html'
23+
),
24+
relativePath: `.next/static/${buildId}/_offline-navigation-fallback.html`,
25+
},
26+
manifest: {
27+
absolutePath: join(
28+
next.testDir,
29+
'.next',
30+
'static',
31+
buildId,
32+
'_offline-navigation-manifest.json'
33+
),
34+
relativePath: `.next/static/${buildId}/_offline-navigation-manifest.json`,
35+
},
2436
}
2537
}
2638

27-
it('emits a request-invariant fallback document when enabled', async () => {
39+
it('emits request-invariant offline navigation artifacts when enabled', async () => {
2840
const buildResult = await next.build()
2941
expect(buildResult.exitCode).toBe(0)
3042

31-
const { buildId, relativePath } = await getFallbackDocumentPath()
32-
const html = await next.readFile(relativePath)
43+
const { buildId, fallbackDocument, manifest } =
44+
await getOfflineNavigationArtifactPaths()
45+
const html = await next.readFile(fallbackDocument.relativePath)
46+
const manifestJson = JSON.parse(await next.readFile(manifest.relativePath))
3347

3448
expect(html).toContain('data-next-offline-navigation-fallback')
3549
expect(html).toContain('id="__NEXT_OFFLINE_NAVIGATION_FALLBACK"')
3650
expect(html).toContain(`"buildId":"${buildId}"`)
3751
expect(html).toContain('self.__next_f')
38-
expect(html).toContain('/_next/static/')
52+
expect(html).toContain('https://cdn.example.com/app-assets/_next/static/')
3953
expect(html).not.toContain('offline navigations page')
54+
55+
expect(manifestJson).toEqual({
56+
version: 1,
57+
buildId,
58+
basePath: '/docs',
59+
assetPrefix: 'https://cdn.example.com/app-assets',
60+
trailingSlash: true,
61+
output: 'default',
62+
scope: '/docs/',
63+
cacheNamespace: `next-offline-navigation-v1:${buildId}:/docs`,
64+
manifest: {
65+
path: `static/${buildId}/_offline-navigation-manifest.json`,
66+
href: `/docs/_next/static/${buildId}/_offline-navigation-manifest.json`,
67+
},
68+
fallbackDocument: {
69+
path: `static/${buildId}/_offline-navigation-fallback.html`,
70+
href: `/docs/_next/static/${buildId}/_offline-navigation-fallback.html`,
71+
},
72+
})
4073
})
4174

42-
it('does not emit a fallback document when disabled', async () => {
75+
it('does not emit offline navigation artifacts when disabled', async () => {
4376
await next.patchFile('next.config.js', (content) =>
4477
content.replace('offlineNavigations: true', 'offlineNavigations: false')
4578
)
4679

4780
const buildResult = await next.build()
4881
expect(buildResult.exitCode).toBe(0)
4982

50-
const { absolutePath } = await getFallbackDocumentPath()
51-
expect(existsSync(absolutePath)).toBe(false)
83+
const { fallbackDocument, manifest } =
84+
await getOfflineNavigationArtifactPaths()
85+
expect(existsSync(fallbackDocument.absolutePath)).toBe(false)
86+
expect(existsSync(manifest.absolutePath)).toBe(false)
5287
})
5388
})

0 commit comments

Comments
 (0)