Skip to content

feat: fail build when netlify form detected #2512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 27, 2024
6 changes: 6 additions & 0 deletions src/build/content/prerendered.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
NetlifyIncrementalCacheValue,
} from '../../shared/cache-types.cjs'
import type { PluginContext } from '../plugin-context.js'
import { verifyNoNetlifyForms } from '../verification.js'

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

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

// Netlify Forms are not support and require a workaround
if (value.kind === 'PAGE') {
verifyNoNetlifyForms(ctx, value.html)
}

await writeCacheEntry(key, value, lastModified, ctx)
}),
),
Expand Down
11 changes: 6 additions & 5 deletions src/build/content/static.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync } from 'node:fs'
import { cp, mkdir, rename, rm } from 'node:fs/promises'
import { cp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { basename, join } from 'node:path'

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

import { encodeBlobKey } from '../../shared/blobkey.js'
import { PluginContext } from '../plugin-context.js'
import { verifyNoNetlifyForms } from '../verification.js'

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

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

try {
await mkdir(destDir, { recursive: true })
await Promise.all(
paths
.filter((path) => !paths.includes(`${path.slice(0, -5)}.json`))
.map(async (path): Promise<void> => {
await cp(join(srcDir, path), join(destDir, await encodeBlobKey(path)), {
recursive: true,
force: true,
})
const html = await readFile(join(srcDir, path), 'utf-8')
verifyNoNetlifyForms(ctx, html)
await writeFile(join(destDir, await encodeBlobKey(path)), html, 'utf-8')
}),
)
} catch (error) {
Expand Down
11 changes: 11 additions & 0 deletions src/build/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { PluginContext } from './plugin-context.js'

const SUPPORTED_NEXT_VERSIONS = '>=13.5.0'

const warnings = new Set<string>()

export function verifyPublishDir(ctx: PluginContext) {
if (!existsSync(ctx.publishDir)) {
ctx.failBuild(
Expand Down Expand Up @@ -85,3 +87,12 @@ export async function verifyNoAdvancedAPIRoutes(ctx: PluginContext) {
)
}
}

export function verifyNoNetlifyForms(ctx: PluginContext, html: string) {
if (!warnings.has('netlifyForms') && /<form[^>]*?\s(netlify|data-netlify)[=>\s]/.test(html)) {
console.warn(
'@netlify/plugin-next@5 does not support Netlify Forms. Refer to https://ntl.fyi/next-runtime-forms-migration for migration example.',
)
warnings.add('netlifyForms')
}
}
12 changes: 12 additions & 0 deletions tests/fixtures/netlify-forms/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const metadata = {
title: 'Netlify Forms',
description: 'Test for verifying Netlify Forms',
}

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
7 changes: 7 additions & 0 deletions tests/fixtures/netlify-forms/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return (
<form data-netlify="true">
<button type="submit">Send</button>
</form>
)
}
5 changes: 5 additions & 0 deletions tests/fixtures/netlify-forms/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
10 changes: 10 additions & 0 deletions tests/fixtures/netlify-forms/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
},
generateBuildId: () => 'build-id',
}

module.exports = nextConfig
19 changes: 19 additions & 0 deletions tests/fixtures/netlify-forms/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "netlify-forms",
"version": "0.1.0",
"private": true,
"scripts": {
"postinstall": "next build",
"dev": "next dev",
"build": "next build"
},
"dependencies": {
"@netlify/functions": "^2.7.0",
"next": "latest",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/react": "18.2.75"
}
}
23 changes: 23 additions & 0 deletions tests/fixtures/netlify-forms/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
5 changes: 3 additions & 2 deletions tests/integration/advanced-api-routes.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getLogger } from 'lambda-local'
import { v4 } from 'uuid'
import { beforeEach, vi, it, expect } from 'vitest'
import { createFixture, runPlugin, type FixtureTestContext } from '../utils/fixture.js'
import { beforeEach, expect, it, vi } from 'vitest'
import { type FixtureTestContext } from '../utils/contexts.js'
import { createFixture, runPlugin } from '../utils/fixture.js'
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'

getLogger().level = 'alert'
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/netlify-forms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getLogger } from 'lambda-local'
import { v4 } from 'uuid'
import { beforeEach, expect, it, vi } from 'vitest'
import { type FixtureTestContext } from '../utils/contexts.js'
import { createFixture, runPlugin } from '../utils/fixture.js'
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'

getLogger().level = 'alert'

beforeEach<FixtureTestContext>(async (ctx) => {
// set for each test a new deployID and siteID
ctx.deployID = generateRandomObjectID()
ctx.siteID = v4()
vi.stubEnv('SITE_ID', ctx.siteID)
vi.stubEnv('DEPLOY_ID', ctx.deployID)
vi.stubEnv('NETLIFY_PURGE_API_TOKEN', 'fake-token')
// hide debug logs in tests
// vi.spyOn(console, 'debug').mockImplementation(() => {})

await startMockBlobStore(ctx)
})

it.skip<FixtureTestContext>('test', async (ctx) => {
await createFixture('netlify-forms', ctx)

const runPluginPromise = runPlugin(ctx)

await expect(runPluginPromise).rejects.toThrow(
'@netlify/plugin-next@5 does not support Netlify Forms',
)
})
Loading