Skip to content

Commit 2aa508b

Browse files
authored
fix: publish purely static pages router files to the CDN (#29)
* chore: simplify get/remove prerendered content * chore: pin fastly module * chore: add a couple comments to storing of prerendered content * fix: avoid overawaiting when globbing * feat: add static pages content to CDN * fix: ensure json data is not bundled
1 parent 9746cf5 commit 2aa508b

File tree

5 files changed

+98
-43
lines changed

5 files changed

+98
-43
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/helpers/files.ts

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,77 @@
11
import { cpus } from 'os'
2-
import path from 'path'
2+
import { parse, ParsedPath } from 'path'
33

44
import { getDeployStore } from '@netlify/blobs'
55
import { NetlifyPluginConstants } from '@netlify/build'
6-
import { copy, move, remove } from 'fs-extra/esm'
6+
import { copy, move, mkdirp } from 'fs-extra/esm'
77
import { globby } from 'globby'
88
import pLimit from 'p-limit'
99

1010
import { buildCacheValue } from './cache.js'
1111
import { BUILD_DIR } from './constants.js'
12+
import { EnhancedNetlifyPluginConstants } from './types.js'
13+
14+
type ContentPath = ParsedPath & {
15+
relative: string
16+
absolute: string
17+
publish: string
18+
}
1219

1320
/**
1421
* Move the Next.js build output from the publish dir to a temp dir
1522
*/
16-
export const stashBuildOutput = async ({ PUBLISH_DIR }: NetlifyPluginConstants) => {
17-
await move(PUBLISH_DIR, `${BUILD_DIR}/.next`, { overwrite: true })
23+
export const stashBuildOutput = async ({ PUBLISH_DIR }: NetlifyPluginConstants): Promise<void> => {
24+
return move(PUBLISH_DIR, `${BUILD_DIR}/.next`, { overwrite: true })
25+
}
1826

19-
// remove prerendered content from the standalone build (it's also in the main build dir)
20-
const prerenderedContent = await getPrerenderedContent(`${BUILD_DIR}/.next/standalone`, false)
21-
await Promise.all(
22-
prerenderedContent.map((file: string) => remove(`${BUILD_DIR}/.next/standalone/${file}`)),
23-
).catch((error) => console.error(error))
27+
/**
28+
* Glob the build output for static page content we can upload to the CDN
29+
*/
30+
const getStaticContent = async (cwd: string): Promise<ContentPath[]> => {
31+
const content = await globby([`server/pages/**/*.+(html|json)`], {
32+
cwd,
33+
extglob: true,
34+
})
35+
return content
36+
.map((path) => parsePath(path, cwd))
37+
.filter((path) => filterStatic(path, content, 'keep'))
2438
}
2539

2640
/**
27-
* Glob for prerendered content in the build output
41+
* Glob the build output for prerendered content we can upload to the blob store
2842
*/
29-
const getPrerenderedContent = async (cwd: string, get = true): Promise<string[]> => {
30-
// TODO: test this
31-
return await globby(
32-
get
33-
? [`cache/fetch-cache/*`, `server/+(app|pages)/**/*.+(html|body)`]
34-
: [
35-
`cache/fetch-cache/*`,
36-
`server/+(app|pages)/**/*.+(html|json|rsc|body|meta)`,
37-
`!server/**/*.js.nft.{html,json}`,
38-
],
39-
{ cwd, extglob: true },
43+
const getPrerenderedContent = async (cwd: string): Promise<ContentPath[]> => {
44+
const content = await globby(
45+
[`cache/fetch-cache/*`, `server/+(app|pages)/**/*.+(html|body|json)`],
46+
{
47+
cwd,
48+
extglob: true,
49+
},
4050
)
51+
return content
52+
.map((path) => parsePath(path, cwd))
53+
.filter((path) => filterStatic(path, content, 'omit'))
4154
}
4255

4356
/**
44-
* Upload prerendered content from the main build dir to the blob store
57+
* Glob the build output for JS content we can bundle with the server handler
58+
*/
59+
export const getServerContent = async (cwd: string): Promise<ContentPath[]> => {
60+
const content = await globby([`**`, `!server/+(app|pages)/**/*.+(html|body|json|rsc|meta)`], {
61+
cwd,
62+
extglob: true,
63+
})
64+
return content.map((path) => parsePath(path, cwd))
65+
}
66+
67+
/**
68+
* Upload prerendered content to the blob store and remove it from the bundle
4569
*/
4670
export const storePrerenderedContent = async ({
4771
NETLIFY_API_TOKEN,
4872
NETLIFY_API_HOST,
4973
SITE_ID,
50-
}: NetlifyPluginConstants & { NETLIFY_API_TOKEN: string; NETLIFY_API_HOST: string }) => {
74+
}: EnhancedNetlifyPluginConstants): Promise<void[]> => {
5175
if (!process.env.DEPLOY_ID) {
5276
// TODO: maybe change to logging
5377
throw new Error(
@@ -61,28 +85,52 @@ export const storePrerenderedContent = async ({
6185
token: NETLIFY_API_TOKEN,
6286
apiURL: `https://${NETLIFY_API_HOST}`,
6387
})
64-
65-
// todo: Check out setFiles within Blobs.js to see how to upload files to blob storage
6688
const limit = pLimit(Math.max(2, cpus().length))
6789

68-
const prerenderedContent = await getPrerenderedContent(`${BUILD_DIR}/.next`)
90+
const content = await getPrerenderedContent(`${BUILD_DIR}/.next/standalone/.next`)
6991
return await Promise.all(
70-
prerenderedContent.map(async (rawPath: string) => {
71-
// TODO: test this with files that have a double extension
72-
const ext = path.extname(rawPath)
73-
const key = rawPath.replace(ext, '')
74-
const value = await buildCacheValue(key, ext)
92+
content.map((path: ContentPath) => {
93+
const { dir, name, ext } = path
94+
const key = `${dir}/${name}`
95+
const value = buildCacheValue(key, ext)
7596
return limit(() => blob.setJSON(key, value))
7697
}),
7798
)
7899
}
79100

80101
/**
81-
* Move static assets to the publish dir so they are uploaded to the CDN
102+
* Move static content to the publish dir so it is uploaded to the CDN
82103
*/
83-
export const publishStaticAssets = ({ PUBLISH_DIR }: NetlifyPluginConstants) => {
84-
return Promise.all([
104+
export const publishStaticContent = async ({
105+
PUBLISH_DIR,
106+
}: NetlifyPluginConstants): Promise<void[]> => {
107+
const content = await getStaticContent(`${BUILD_DIR}/.next/standalone/.next`)
108+
return await Promise.all([
109+
mkdirp(PUBLISH_DIR),
85110
copy('public', PUBLISH_DIR),
86111
copy(`${BUILD_DIR}/.next/static/`, `${PUBLISH_DIR}/_next/static`),
112+
...content.map((path: ContentPath) => copy(path.absolute, `${PUBLISH_DIR}/${path.publish}`)),
87113
])
88114
}
115+
116+
/**
117+
* Keep or remove static content based on whether it has a corresponding JSON file
118+
*/
119+
const filterStatic = (
120+
{ dir, name, ext }: ContentPath,
121+
content: string[],
122+
type: 'keep' | 'omit',
123+
): boolean =>
124+
type === 'keep'
125+
? dir.startsWith('server/pages') && !content.includes(`${dir}/${name}.json`)
126+
: ext !== '.json' &&
127+
(!dir.startsWith('server/pages') || content.includes(`${dir}/${name}.json`))
128+
/**
129+
* Parse a file path into an object with file path variants
130+
*/
131+
const parsePath = (path: string, cwd: string): ContentPath => ({
132+
...parse(path),
133+
relative: path,
134+
absolute: `${cwd}/${path}`,
135+
publish: path.replace(/^server\/(app|pages)\//, ''),
136+
})

src/helpers/functions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
SERVER_HANDLER_DIR,
1111
SERVER_HANDLER_NAME,
1212
} from './constants.js'
13+
import { getServerContent } from './files.js'
1314

1415
const pkg = await readJson(`${PLUGIN_DIR}/package.json`)
1516

@@ -32,7 +33,10 @@ export const createServerHandler = async () => {
3233
)
3334

3435
// copy the next.js standalone build output to the handler directory
35-
await copy(`${BUILD_DIR}/.next/standalone/.next`, `${SERVER_HANDLER_DIR}/.next`)
36+
const content = await getServerContent(`${BUILD_DIR}/.next/standalone/.next`)
37+
await Promise.all(
38+
content.map((path) => copy(path.absolute, `${SERVER_HANDLER_DIR}/.next/${path.relative}`)),
39+
)
3640
await copy(`${BUILD_DIR}/.next/standalone/node_modules`, `${SERVER_HANDLER_DIR}/node_modules`)
3741

3842
// create the handler metadata file

src/helpers/types.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import type { OutgoingHttpHeaders } from 'http'
33
import type { NetlifyPluginOptions, NetlifyPluginConstants } from '@netlify/build'
44
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
55

6+
type NetlifyPluginOptionsWithFlags = NetlifyPluginOptions & {
7+
featureFlags?: Record<string, unknown>
8+
}
69

7-
type NetlifyPluginOptionsWithFlags = NetlifyPluginOptions & { featureFlags?: Record<string, unknown> }
8-
9-
type EnhancedNetlifyPluginConstants = NetlifyPluginConstants & {
10+
export type EnhancedNetlifyPluginConstants = NetlifyPluginConstants & {
1011
NETLIFY_API_HOST: string
1112
NETLIFY_API_TOKEN: string
1213
}
1314

14-
export type EnhancedNetlifyPluginOptions = NetlifyPluginOptions & { constants: EnhancedNetlifyPluginConstants } & {
15+
export type EnhancedNetlifyPluginOptions = NetlifyPluginOptions & {
16+
constants: EnhancedNetlifyPluginConstants
17+
} & {
1518
featureFlags?: Record<string, unknown>
1619
}
1720

@@ -27,4 +30,4 @@ export interface RequiredServerFiles {
2730
export interface MetaFile {
2831
status?: number
2932
headers?: OutgoingHttpHeaders
30-
}
33+
}

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { setBuildConfig } from './helpers/config.js'
2-
import { publishStaticAssets, stashBuildOutput, storePrerenderedContent } from './helpers/files.js'
2+
import { publishStaticContent, stashBuildOutput, storePrerenderedContent } from './helpers/files.js'
33
import { createEdgeHandler, createServerHandler } from './helpers/functions.js'
44
import { EnhancedNetlifyPluginOptions } from './helpers/types.js'
55

@@ -11,7 +11,7 @@ export const onBuild = async ({ constants }: EnhancedNetlifyPluginOptions) => {
1111
await stashBuildOutput(constants)
1212

1313
return Promise.all([
14-
publishStaticAssets(constants),
14+
publishStaticContent(constants),
1515
storePrerenderedContent(constants),
1616
createServerHandler(),
1717
createEdgeHandler(),

0 commit comments

Comments
 (0)