Skip to content

Commit 195b8b2

Browse files
orinokaipieh
andauthoredJun 27, 2024··
feat: fail build when netlify form detected (#2512)
* feat: verify no netlify forms * test: ensure build is failed when netlify forms detected * chore: format with prettier * feat: make verification passive by outputing warning * feat: include static content for forms verification * test: skip forms verification test until we are failing the build * feat: also verify no netlify forms for APP_PAGE * Update tests/integration/netlify-forms.test.ts Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com> --------- Co-authored-by: orinokai <orinokai@users.noreply.github.com> Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com>
1 parent 62ab214 commit 195b8b2

File tree

11 files changed

+134
-7
lines changed

11 files changed

+134
-7
lines changed
 

‎src/build/content/prerendered.ts

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
NetlifyIncrementalCacheValue,
1919
} from '../../shared/cache-types.cjs'
2020
import type { PluginContext } from '../plugin-context.js'
21+
import { verifyNoNetlifyForms } from '../verification.js'
2122

2223
const tracer = wrapTracer(trace.getTracer('Next runtime'))
2324

@@ -169,6 +170,11 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
169170
throw new Error(`Unrecognized content: ${route}`)
170171
}
171172

173+
// Netlify Forms are not support and require a workaround
174+
if (value.kind === 'PAGE' || value.kind === 'APP_PAGE') {
175+
verifyNoNetlifyForms(ctx, value.html)
176+
}
177+
172178
await writeCacheEntry(key, value, lastModified, ctx)
173179
}),
174180
),

‎src/build/content/static.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { existsSync } from 'node:fs'
2-
import { cp, mkdir, rename, rm } from 'node:fs/promises'
2+
import { cp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
33
import { basename, join } from 'node:path'
44

55
import { trace } from '@opentelemetry/api'
@@ -8,6 +8,7 @@ import glob from 'fast-glob'
88

99
import { encodeBlobKey } from '../../shared/blobkey.js'
1010
import { PluginContext } from '../plugin-context.js'
11+
import { verifyNoNetlifyForms } from '../verification.js'
1112

1213
const tracer = wrapTracer(trace.getTracer('Next runtime'))
1314

@@ -25,14 +26,14 @@ export const copyStaticContent = async (ctx: PluginContext): Promise<void> => {
2526
})
2627

2728
try {
29+
await mkdir(destDir, { recursive: true })
2830
await Promise.all(
2931
paths
3032
.filter((path) => !paths.includes(`${path.slice(0, -5)}.json`))
3133
.map(async (path): Promise<void> => {
32-
await cp(join(srcDir, path), join(destDir, await encodeBlobKey(path)), {
33-
recursive: true,
34-
force: true,
35-
})
34+
const html = await readFile(join(srcDir, path), 'utf-8')
35+
verifyNoNetlifyForms(ctx, html)
36+
await writeFile(join(destDir, await encodeBlobKey(path)), html, 'utf-8')
3637
}),
3738
)
3839
} catch (error) {

‎src/build/verification.ts

+11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type { PluginContext } from './plugin-context.js'
88

99
const SUPPORTED_NEXT_VERSIONS = '>=13.5.0'
1010

11+
const warnings = new Set<string>()
12+
1113
export function verifyPublishDir(ctx: PluginContext) {
1214
if (!existsSync(ctx.publishDir)) {
1315
ctx.failBuild(
@@ -85,3 +87,12 @@ export async function verifyNoAdvancedAPIRoutes(ctx: PluginContext) {
8587
)
8688
}
8789
}
90+
91+
export function verifyNoNetlifyForms(ctx: PluginContext, html: string) {
92+
if (!warnings.has('netlifyForms') && /<form[^>]*?\s(netlify|data-netlify)[=>\s]/.test(html)) {
93+
console.warn(
94+
'@netlify/plugin-next@5 does not support Netlify Forms. Refer to https://ntl.fyi/next-runtime-forms-migration for migration example.',
95+
)
96+
warnings.add('netlifyForms')
97+
}
98+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const metadata = {
2+
title: 'Netlify Forms',
3+
description: 'Test for verifying Netlify Forms',
4+
}
5+
6+
export default function RootLayout({ children }) {
7+
return (
8+
<html lang="en">
9+
<body>{children}</body>
10+
</html>
11+
)
12+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Page() {
2+
return (
3+
<form data-netlify="true">
4+
<button type="submit">Send</button>
5+
</form>
6+
)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/basic-features/typescript for more information.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
output: 'standalone',
4+
eslint: {
5+
ignoreDuringBuilds: true,
6+
},
7+
generateBuildId: () => 'build-id',
8+
}
9+
10+
module.exports = nextConfig
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "netlify-forms",
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+
"@netlify/functions": "^2.7.0",
12+
"next": "latest",
13+
"react": "18.2.0",
14+
"react-dom": "18.2.0"
15+
},
16+
"devDependencies": {
17+
"@types/react": "18.2.75"
18+
}
19+
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["dom", "dom.iterable", "esnext"],
4+
"allowJs": true,
5+
"skipLibCheck": true,
6+
"strict": false,
7+
"noEmit": true,
8+
"incremental": true,
9+
"esModuleInterop": true,
10+
"module": "esnext",
11+
"moduleResolution": "node",
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"jsx": "preserve",
15+
"plugins": [
16+
{
17+
"name": "next"
18+
}
19+
]
20+
},
21+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22+
"exclude": ["node_modules"]
23+
}

‎tests/integration/advanced-api-routes.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getLogger } from 'lambda-local'
22
import { v4 } from 'uuid'
3-
import { beforeEach, vi, it, expect } from 'vitest'
4-
import { createFixture, runPlugin, type FixtureTestContext } from '../utils/fixture.js'
3+
import { beforeEach, expect, it, vi } from 'vitest'
4+
import { type FixtureTestContext } from '../utils/contexts.js'
5+
import { createFixture, runPlugin } from '../utils/fixture.js'
56
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
67

78
getLogger().level = 'alert'
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { getLogger } from 'lambda-local'
2+
import { v4 } from 'uuid'
3+
import { beforeEach, expect, it, vi } from 'vitest'
4+
import { type FixtureTestContext } from '../utils/contexts.js'
5+
import { createFixture, runPlugin } from '../utils/fixture.js'
6+
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
7+
8+
getLogger().level = 'alert'
9+
10+
beforeEach<FixtureTestContext>(async (ctx) => {
11+
// set for each test a new deployID and siteID
12+
ctx.deployID = generateRandomObjectID()
13+
ctx.siteID = v4()
14+
vi.stubEnv('SITE_ID', ctx.siteID)
15+
vi.stubEnv('DEPLOY_ID', ctx.deployID)
16+
vi.stubEnv('NETLIFY_PURGE_API_TOKEN', 'fake-token')
17+
// hide debug logs in tests
18+
// vi.spyOn(console, 'debug').mockImplementation(() => {})
19+
20+
await startMockBlobStore(ctx)
21+
})
22+
23+
// test skipped until we actually start failing builds - right now we are just showing a warning
24+
it.skip<FixtureTestContext>('should fail build when netlify forms are used', async (ctx) => {
25+
await createFixture('netlify-forms', ctx)
26+
27+
const runPluginPromise = runPlugin(ctx)
28+
29+
await expect(runPluginPromise).rejects.toThrow(
30+
'@netlify/plugin-next@5 does not support Netlify Forms',
31+
)
32+
})

0 commit comments

Comments
 (0)
Please sign in to comment.