diff --git a/src/build/content/prerendered.ts b/src/build/content/prerendered.ts index 727044b69c..61027a8f5a 100644 --- a/src/build/content/prerendered.ts +++ b/src/build/content/prerendered.ts @@ -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')) @@ -169,6 +170,11 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise 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) }), ), diff --git a/src/build/content/static.ts b/src/build/content/static.ts index 7733c318ef..d5b418b438 100644 --- a/src/build/content/static.ts +++ b/src/build/content/static.ts @@ -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' @@ -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')) @@ -25,14 +26,14 @@ export const copyStaticContent = async (ctx: PluginContext): Promise => { }) try { + await mkdir(destDir, { recursive: true }) await Promise.all( paths .filter((path) => !paths.includes(`${path.slice(0, -5)}.json`)) .map(async (path): Promise => { - 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) { diff --git a/src/build/verification.ts b/src/build/verification.ts index 6589cb67c1..c6bcf929ef 100644 --- a/src/build/verification.ts +++ b/src/build/verification.ts @@ -8,6 +8,8 @@ import type { PluginContext } from './plugin-context.js' const SUPPORTED_NEXT_VERSIONS = '>=13.5.0' +const warnings = new Set() + export function verifyPublishDir(ctx: PluginContext) { if (!existsSync(ctx.publishDir)) { ctx.failBuild( @@ -85,3 +87,12 @@ export async function verifyNoAdvancedAPIRoutes(ctx: PluginContext) { ) } } + +export function verifyNoNetlifyForms(ctx: PluginContext, html: string) { + if (!warnings.has('netlifyForms') && /]*?\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') + } +} diff --git a/tests/fixtures/netlify-forms/app/layout.js b/tests/fixtures/netlify-forms/app/layout.js new file mode 100644 index 0000000000..60c789ba7a --- /dev/null +++ b/tests/fixtures/netlify-forms/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Netlify Forms', + description: 'Test for verifying Netlify Forms', +} + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/tests/fixtures/netlify-forms/app/page.js b/tests/fixtures/netlify-forms/app/page.js new file mode 100644 index 0000000000..591550885b --- /dev/null +++ b/tests/fixtures/netlify-forms/app/page.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( +
+ +
+ ) +} diff --git a/tests/fixtures/netlify-forms/next-env.d.ts b/tests/fixtures/netlify-forms/next-env.d.ts new file mode 100644 index 0000000000..4f11a03dc6 --- /dev/null +++ b/tests/fixtures/netlify-forms/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/tests/fixtures/netlify-forms/next.config.js b/tests/fixtures/netlify-forms/next.config.js new file mode 100644 index 0000000000..6346ab0742 --- /dev/null +++ b/tests/fixtures/netlify-forms/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + eslint: { + ignoreDuringBuilds: true, + }, + generateBuildId: () => 'build-id', +} + +module.exports = nextConfig diff --git a/tests/fixtures/netlify-forms/package.json b/tests/fixtures/netlify-forms/package.json new file mode 100644 index 0000000000..a95e5e097d --- /dev/null +++ b/tests/fixtures/netlify-forms/package.json @@ -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" + } +} diff --git a/tests/fixtures/netlify-forms/tsconfig.json b/tests/fixtures/netlify-forms/tsconfig.json new file mode 100644 index 0000000000..482806989f --- /dev/null +++ b/tests/fixtures/netlify-forms/tsconfig.json @@ -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"] +} diff --git a/tests/integration/advanced-api-routes.test.ts b/tests/integration/advanced-api-routes.test.ts index 7eb66199b0..41ad420936 100644 --- a/tests/integration/advanced-api-routes.test.ts +++ b/tests/integration/advanced-api-routes.test.ts @@ -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' diff --git a/tests/integration/netlify-forms.test.ts b/tests/integration/netlify-forms.test.ts new file mode 100644 index 0000000000..02073f955a --- /dev/null +++ b/tests/integration/netlify-forms.test.ts @@ -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(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('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', + ) +})