Skip to content

Commit c893ad1

Browse files
authored
fix: requesting page router static assets (#58)
* fix: requesting page router static assets * chore: update test
1 parent 1271d73 commit c893ad1

20 files changed

+327
-113
lines changed

package-lock.json

+30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@netlify/build": "^29.20.6",
4242
"@netlify/functions": "^2.0.1",
4343
"@vercel/nft": "^0.24.3",
44+
"fs-monkey": "^1.0.5",
4445
"globby": "^13.2.2",
4546
"os": "^0.1.2",
4647
"outdent": "^0.8.0",
@@ -58,6 +59,7 @@
5859
"memfs": "^4.6.0",
5960
"next": "^13.5.4",
6061
"typescript": "^5.1.6",
62+
"unionfs": "^4.5.1",
6163
"uuid": "^9.0.1",
6264
"vitest": "^0.34.6"
6365
}

src/build/content/static-pages.test.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { expect, test, vi, afterEach } from 'vitest'
1+
import { expect, test, vi, afterEach, beforeEach } from 'vitest'
22
import { join } from 'node:path'
33
import { BUILD_DIR } from '../constants.js'
44
import { copyStaticContent } from './static.js'
55
import { createFsFixture, FixtureTestContext } from '../../../tests/utils/fixture.js'
66
import { globby } from 'globby'
7+
import { getDeployStore } from '@netlify/blobs'
78

89
// Globby wasn't easy to mock so instead we're using a temporary
910
// directory and the actual file system to test static pages
@@ -12,28 +13,33 @@ afterEach(() => {
1213
vi.restoreAllMocks()
1314
})
1415

16+
let fakeBlob: ReturnType<typeof getDeployStore>
17+
18+
beforeEach(() => {
19+
fakeBlob = {
20+
set: vi.fn(),
21+
} as unknown as ReturnType<typeof getDeployStore>
22+
})
23+
1524
test<FixtureTestContext>('should copy the static pages to the publish directory if they have no corresponding JSON files', async (ctx) => {
1625
const { cwd } = await createFsFixture(
1726
{
1827
[`${BUILD_DIR}/.next/static/test.js`]: '',
19-
[`${BUILD_DIR}/.next/server/pages/test.html`]: '',
20-
[`${BUILD_DIR}/.next/server/pages/test2.html`]: '',
28+
[`${BUILD_DIR}/.next/server/pages/test.html`]: 'test-1',
29+
[`${BUILD_DIR}/.next/server/pages/test2.html`]: 'test-2',
2130
},
2231
ctx,
2332
)
2433

2534
const PUBLISH_DIR = join(cwd, 'publish')
26-
await copyStaticContent({ PUBLISH_DIR })
35+
await copyStaticContent({ PUBLISH_DIR }, fakeBlob)
2736

2837
const files = await globby('**/*', { cwd, extglob: true })
2938

30-
expect(files).toEqual(
31-
expect.arrayContaining([
32-
'publish/_next/static/test.js',
33-
'publish/test.html',
34-
'publish/test2.html',
35-
]),
36-
)
39+
expect(files).toEqual(['publish/_next/static/test.js'])
40+
expect(fakeBlob.set).toHaveBeenCalledTimes(2)
41+
expect(fakeBlob.set).toHaveBeenCalledWith('server/pages/test.html', 'test-1')
42+
expect(fakeBlob.set).toHaveBeenCalledWith('server/pages/test2.html', 'test-2')
3743
})
3844

3945
test<FixtureTestContext>('should not copy the static pages to the publish directory if they have corresponding JSON files', async (ctx) => {
@@ -49,7 +55,7 @@ test<FixtureTestContext>('should not copy the static pages to the publish direct
4955
)
5056

5157
const PUBLISH_DIR = join(cwd, 'publish')
52-
await copyStaticContent({ PUBLISH_DIR })
58+
await copyStaticContent({ PUBLISH_DIR }, fakeBlob)
5359

5460
const files = await globby('**/*', { cwd, extglob: true })
5561

src/build/content/static.test.ts

+35-21
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,49 @@
1-
import { fs, vol } from 'memfs'
1+
import { type getDeployStore } from '@netlify/blobs'
22
import { join } from 'node:path'
3-
import { afterEach, expect, test, vi } from 'vitest'
4-
import { fsCpHelper, mockFileSystem } from '../../../tests/index.js'
3+
import { beforeEach, expect, test, vi } from 'vitest'
4+
import { mockFileSystem } from '../../../tests/index.js'
55
import { BUILD_DIR } from '../constants.js'
66
import { copyStaticContent } from './static.js'
77

8-
vi.mock('node:fs', () => fs)
9-
vi.mock('node:fs/promises', () => {
8+
vi.mock('node:fs', async () => {
9+
const unionFs: any = (await import('unionfs')).default
10+
const fs = await vi.importActual<typeof import('fs')>('node:fs')
11+
unionFs.reset = () => {
12+
unionFs.fss = [fs]
13+
}
14+
const united = unionFs.use(fs)
15+
return { default: united, ...united }
16+
})
17+
18+
vi.mock('node:fs/promises', async () => {
19+
const fs = await import('node:fs')
20+
const { fsCpHelper, rmHelper } = await import('../../../tests/utils/fs-helper.js')
1021
return {
1122
...fs.promises,
12-
cp: (src, dest, options) => fsCpHelper(src, dest, options),
23+
rm: rmHelper,
24+
cp: fsCpHelper,
1325
}
1426
})
1527

16-
afterEach(() => {
17-
vol.reset()
18-
vi.restoreAllMocks()
28+
let fakeBlob: ReturnType<typeof getDeployStore>
29+
30+
beforeEach(() => {
31+
fakeBlob = {
32+
set: vi.fn(),
33+
} as unknown as ReturnType<typeof getDeployStore>
1934
})
2035

2136
test('should copy the static assets from the build to the publish directory', async () => {
22-
const cwd = mockFileSystem({
37+
const { cwd, vol } = mockFileSystem({
2338
[`${BUILD_DIR}/.next/static/test.js`]: '',
2439
[`${BUILD_DIR}/.next/static/sub-dir/test2.js`]: '',
2540
})
2641

2742
const PUBLISH_DIR = join(cwd, 'publish')
28-
await copyStaticContent({ PUBLISH_DIR })
43+
await copyStaticContent({ PUBLISH_DIR }, fakeBlob)
2944

30-
const filenamesInVolume = Object.keys(vol.toJSON())
31-
expect(filenamesInVolume).toEqual(
45+
expect(fakeBlob.set).toHaveBeenCalledTimes(0)
46+
expect(Object.keys(vol.toJSON())).toEqual(
3247
expect.arrayContaining([
3348
`${PUBLISH_DIR}/_next/static/test.js`,
3449
`${PUBLISH_DIR}/_next/static/sub-dir/test2.js`,
@@ -37,39 +52,38 @@ test('should copy the static assets from the build to the publish directory', as
3752
})
3853

3954
test('should throw expected error if no static assets directory exists', async () => {
40-
const cwd = mockFileSystem({})
55+
const { cwd } = mockFileSystem({})
4156

4257
const PUBLISH_DIR = join(cwd, 'publish')
4358
const staticDirectory = join(cwd, '.netlify/.next/static')
4459

45-
await expect(copyStaticContent({ PUBLISH_DIR })).rejects.toThrowError(
60+
await expect(copyStaticContent({ PUBLISH_DIR }, fakeBlob)).rejects.toThrowError(
4661
`Failed to copy static assets: Error: ENOENT: no such file or directory, readdir '${staticDirectory}'`,
4762
)
4863
})
4964

5065
test('should copy files from the public directory to the publish directory', async () => {
51-
const cwd = mockFileSystem({
66+
const { cwd, vol } = mockFileSystem({
5267
[`${BUILD_DIR}/.next/static/test.js`]: '',
5368
'public/fake-image.svg': '',
5469
'public/another-asset.json': '',
5570
})
5671

5772
const PUBLISH_DIR = join(cwd, 'publish')
58-
await copyStaticContent({ PUBLISH_DIR })
73+
await copyStaticContent({ PUBLISH_DIR }, fakeBlob)
5974

60-
const filenamesInVolume = Object.keys(vol.toJSON())
61-
expect(filenamesInVolume).toEqual(
75+
expect(Object.keys(vol.toJSON())).toEqual(
6276
expect.arrayContaining([`${PUBLISH_DIR}/fake-image.svg`, `${PUBLISH_DIR}/another-asset.json`]),
6377
)
6478
})
6579

6680
test('should not copy files if the public directory does not exist', async () => {
67-
const cwd = mockFileSystem({
81+
const { cwd, vol } = mockFileSystem({
6882
[`${BUILD_DIR}/.next/static/test.js`]: '',
6983
})
7084

7185
const PUBLISH_DIR = join(cwd, 'publish')
72-
await expect(copyStaticContent({ PUBLISH_DIR })).resolves.toBeUndefined()
86+
await expect(copyStaticContent({ PUBLISH_DIR }, fakeBlob)).resolves.toBeUndefined()
7387

7488
expect(vol.toJSON()).toEqual({
7589
[join(cwd, `${BUILD_DIR}/.next/static/test.js`)]: '',

src/build/content/static.ts

+19-14
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import { getDeployStore } from '@netlify/blobs'
12
import { NetlifyPluginConstants } from '@netlify/build'
23
import { globby } from 'globby'
34
import { existsSync } from 'node:fs'
4-
import { copyFile, cp, mkdir } from 'node:fs/promises'
5-
import { ParsedPath, dirname, join, parse } from 'node:path'
5+
import { cp, mkdir, readFile } from 'node:fs/promises'
6+
import { ParsedPath, join, parse } from 'node:path'
67
import { BUILD_DIR } from '../constants.js'
78

89
/**
910
* Copy static pages (HTML without associated JSON data)
1011
*/
11-
const copyStaticPages = async (src: string, dest: string): Promise<void> => {
12+
const copyStaticPages = async (
13+
blob: ReturnType<typeof getDeployStore>,
14+
src: string,
15+
): Promise<void> => {
1216
const paths = await globby([`server/pages/**/*.+(html|json)`], {
1317
cwd: src,
1418
extglob: true,
@@ -20,11 +24,9 @@ const copyStaticPages = async (src: string, dest: string): Promise<void> => {
2024
// keep only static files that do not have JSON data
2125
.filter(({ dir, name }: ParsedPath) => !paths.includes(`${dir}/${name}.json`))
2226
.map(async ({ dir, base }: ParsedPath) => {
23-
const srcPath = join(src, dir, base)
24-
const destPath = join(dest, dir.replace(/^server\/(app|pages)/, ''), base)
25-
26-
await mkdir(dirname(destPath), { recursive: true })
27-
await copyFile(srcPath, destPath)
27+
const relPath = join(dir, base)
28+
const srcPath = join(src, relPath)
29+
await blob.set(relPath, await readFile(srcPath, 'utf-8'))
2830
}),
2931
)
3032
}
@@ -64,10 +66,13 @@ const copyPublicAssets = async ({
6466
/**
6567
* Move static content to the publish dir so it is uploaded to the CDN
6668
*/
67-
export const copyStaticContent = async ({
68-
PUBLISH_DIR,
69-
}: Pick<NetlifyPluginConstants, 'PUBLISH_DIR'>): Promise<void> => {
70-
await copyPublicAssets({ PUBLISH_DIR })
71-
await copyStaticAssets({ PUBLISH_DIR })
72-
await copyStaticPages(join(process.cwd(), BUILD_DIR, '.next'), PUBLISH_DIR)
69+
export const copyStaticContent = async (
70+
{ PUBLISH_DIR }: Pick<NetlifyPluginConstants, 'PUBLISH_DIR'>,
71+
blob: ReturnType<typeof getDeployStore>,
72+
): Promise<void> => {
73+
await Promise.all([
74+
copyStaticPages(blob, join(process.cwd(), BUILD_DIR, '.next')),
75+
copyStaticAssets({ PUBLISH_DIR }),
76+
copyPublicAssets({ PUBLISH_DIR }),
77+
])
7378
}

src/build/functions/server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const createServerHandler = async () => {
2020
[
2121
join(PLUGIN_DIR, 'dist/run/handlers/server.js'),
2222
join(PLUGIN_DIR, 'dist/run/handlers/cache.cjs'),
23+
join(PLUGIN_DIR, 'dist/run/handlers/next.cjs'),
2324
],
2425
{ base: PLUGIN_DIR, ignore: ['package.json', 'node_modules/next/**'] },
2526
)

src/build/move-build-output.test.ts

+21-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
1-
import { fs, vol } from 'memfs'
21
import { Mock, TestContext, afterEach, assert, beforeEach, expect, test, vi } from 'vitest'
32

43
import { NetlifyPluginUtils } from '@netlify/build'
54
import { join } from 'node:path'
65
import { mockFileSystem } from '../../tests/index.js'
76
import { moveBuildOutput } from './move-build-output.js'
87

9-
vi.mock('node:fs', () => fs)
10-
vi.mock('node:fs/promises', () => fs.promises)
8+
vi.mock('node:fs', async () => {
9+
const unionFs: any = (await import('unionfs')).default
10+
const fs = await vi.importActual<typeof import('fs')>('node:fs')
11+
unionFs.reset = () => {
12+
unionFs.fss = [fs]
13+
}
14+
const united = unionFs.use(fs)
15+
return { default: united, ...united }
16+
})
17+
18+
vi.mock('node:fs/promises', async () => {
19+
const fs = await import('node:fs')
20+
const { fsCpHelper, rmHelper } = await import('../../tests/utils/fs-helper.js')
21+
return {
22+
...fs.promises,
23+
rm: rmHelper,
24+
cp: fsCpHelper,
25+
}
26+
})
1127

1228
type Context = TestContext & {
1329
utils: {
@@ -27,13 +43,8 @@ beforeEach<Context>((ctx) => {
2743
}
2844
})
2945

30-
afterEach(() => {
31-
vol.reset()
32-
vi.restoreAllMocks()
33-
})
34-
3546
test<Context>('should fail the build and throw an error if the PUBLISH_DIR does not exist', async (ctx) => {
36-
const cwd = mockFileSystem({})
47+
const { cwd } = mockFileSystem({})
3748
ctx.utils.build.failBuild.mockImplementation((msg) => {
3849
throw new Error(msg)
3950
})
@@ -57,9 +68,7 @@ test<Context>('should fail the build and throw an error if the PUBLISH_DIR does
5768
})
5869

5970
test<Context>('should move the build output to the `.netlify/.next` folder', async (ctx) => {
60-
const cwd = mockFileSystem({
61-
'out/fake-file.js': '',
62-
})
71+
const { cwd, vol } = mockFileSystem({ 'out/fake-file.js': '' })
6372

6473
await moveBuildOutput(
6574
{ PUBLISH_DIR: join(cwd, 'out') },

0 commit comments

Comments
 (0)