Skip to content

Commit e2737bc

Browse files
authored
feat: support static export (#349)
* feat: upload static export to cdn * feat: ensure out directory * fix: check standalone only in standalone mode * test: add export fixture * test: ignore out dir in test fixtures * test: add e2e test for output export * fix: check for undefined output config * fix: check properly for undefined output config
1 parent f497b8b commit e2737bc

File tree

12 files changed

+96
-4
lines changed

12 files changed

+96
-4
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ deno.lock
1717
tests/**/package-lock.json
1818
tests/**/pnpm-lock.yaml
1919
tests/**/yarn.lock
20+
tests/**/out/

src/build/content/static.ts

+9
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ export const copyStaticAssets = async (ctx: PluginContext): Promise<void> => {
5757
}
5858
}
5959

60+
export const copyStaticExport = async (ctx: PluginContext): Promise<void> => {
61+
try {
62+
await rm(ctx.staticDir, { recursive: true, force: true })
63+
await cp(ctx.resolveFromSiteDir('out'), ctx.staticDir, { recursive: true })
64+
} catch (error) {
65+
ctx.failBuild('Failed copying static export', error)
66+
}
67+
}
68+
6069
/**
6170
* Swap the static dir with the publish dir so it is uploaded to the CDN
6271
*/

src/build/verification.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,21 @@ export function verifyPublishDir(ctx: PluginContext) {
3434
'Your publish directory does not contain expected Next.js build output, please check your build settings',
3535
)
3636
}
37-
if (!existsSync(ctx.standaloneRootDir)) {
37+
if (
38+
(ctx.buildConfig.output === 'standalone' || ctx.buildConfig.output === undefined) &&
39+
!existsSync(ctx.standaloneRootDir)
40+
) {
3841
ctx.failBuild(
3942
`Your publish directory does not contain expected Next.js build output, please make sure you are using Next.js version (${SUPPORTED_NEXT_VERSIONS})`,
4043
)
4144
}
45+
if (ctx.buildConfig.output === 'export' && !existsSync(ctx.resolveFromSiteDir('out'))) {
46+
ctx.failBuild(
47+
`Your export directory was not found at: ${ctx.resolveFromSiteDir(
48+
'out',
49+
)}, please check your build settings`,
50+
)
51+
}
4252
}
4353

4454
export function verifyNextVersion(ctx: PluginContext, nextVersion: string): void | never {

src/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { copyPrerenderedContent } from './build/content/prerendered.js'
55
import {
66
copyStaticAssets,
77
copyStaticContent,
8+
copyStaticExport,
89
publishStaticDir,
910
unpublishStaticDir,
1011
} from './build/content/static.js'
@@ -32,6 +33,11 @@ export const onBuild = async (options: NetlifyPluginOptions) => {
3233
await saveBuildCache(ctx)
3334
}
3435

36+
// static exports only need to be uploaded to the CDN
37+
if (ctx.buildConfig.output === 'export') {
38+
return copyStaticExport(ctx)
39+
}
40+
3541
await Promise.all([
3642
copyStaticAssets(ctx),
3743
copyStaticContent(ctx),

tests/e2e/simple-app.test.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Locator, expect } from '@playwright/test'
1+
import { expect, type Locator } from '@playwright/test'
22
import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'
33
import { test } from '../utils/playwright-helpers.js'
44

@@ -7,10 +7,13 @@ const expectImageWasLoaded = async (locator: Locator) => {
77
}
88

99
test('Renders the Home page correctly', async ({ page, simpleNextApp }) => {
10-
await page.goto(simpleNextApp.url)
10+
const response = await page.goto(simpleNextApp.url)
11+
const headers = response?.headers() || {}
1112

1213
await expect(page).toHaveTitle('Simple Next App')
1314

15+
expect(headers['cache-status']).toBe('"Next.js"; hit\n"Netlify Edge"; fwd=miss')
16+
1417
const h1 = page.locator('h1')
1518
await expect(h1).toHaveText('Home')
1619

@@ -22,6 +25,23 @@ test('Renders the Home page correctly', async ({ page, simpleNextApp }) => {
2225
expect(body).toBe('{"words":"hello world"}')
2326
})
2427

28+
test('Renders the Home page correctly with output export', async ({
29+
page,
30+
simpleNextAppExport,
31+
}) => {
32+
const response = await page.goto(simpleNextAppExport.url)
33+
const headers = response?.headers() || {}
34+
35+
await expect(page).toHaveTitle('Simple Next App')
36+
37+
expect(headers['cache-status']).toBe('"Netlify Edge"; fwd=miss')
38+
39+
const h1 = page.locator('h1')
40+
await expect(h1).toHaveText('Home')
41+
42+
await expectImageWasLoaded(page.locator('img'))
43+
})
44+
2545
test('Renders the Home page correctly with distDir', async ({ page, simpleNextAppDistDir }) => {
2646
await page.goto(simpleNextAppDistDir.url)
2747

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const metadata = {
2+
title: 'Simple Next App',
3+
description: 'Description for Simple Next App',
4+
}
5+
6+
export default function RootLayout({ children }) {
7+
return (
8+
<html lang="en">
9+
<body>{children}</body>
10+
</html>
11+
)
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function Home() {
2+
return (
3+
<main>
4+
<h1>Home</h1>
5+
<img src="/squirrel.jpg" alt="a cute squirrel" width="300px" />
6+
</main>
7+
)
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
output: 'export',
4+
eslint: {
5+
ignoreDuringBuilds: true,
6+
},
7+
}
8+
9+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "simple-next-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"postinstall": "next build",
7+
"dev": "next dev",
8+
"build": "next build"
9+
},
10+
"dependencies": {
11+
"next": "^14.1.1-canary.66",
12+
"react": "18.2.0",
13+
"react-dom": "18.2.0"
14+
}
15+
}
Loading
Loading

tests/utils/create-e2e-fixture.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { execaCommand } from 'execa'
22
import fg from 'fast-glob'
33
import { exec } from 'node:child_process'
44
import { existsSync } from 'node:fs'
5-
import { appendFile, copyFile, mkdir, mkdtemp, readFile, writeFile, rm } from 'node:fs/promises'
5+
import { appendFile, copyFile, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
66
import { tmpdir } from 'node:os'
77
import { dirname, join } from 'node:path'
88
import process from 'node:process'
@@ -272,6 +272,7 @@ async function cleanup(dest: string, deployId?: string): Promise<void> {
272272

273273
export const fixtureFactories = {
274274
simpleNextApp: () => createE2EFixture('simple-next-app'),
275+
simpleNextAppExport: () => createE2EFixture('simple-next-app-export'),
275276
simpleNextAppDistDir: () =>
276277
createE2EFixture('simple-next-app-dist-dir', {
277278
publishDirectory: 'cool/output',

0 commit comments

Comments
 (0)