Skip to content
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

feat: fail build when netlify form detected #2512

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' || value.kind === 'APP_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
32 changes: 32 additions & 0 deletions tests/integration/netlify-forms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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)
})

// test skipped until we actually start failing builds - right now we are just showing a warning
it.skip<FixtureTestContext>('should fail build when netlify forms are used', 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