diff --git a/.changeset/little-hotels-wave.md b/.changeset/little-hotels-wave.md
new file mode 100644
index 000000000..bbed87498
--- /dev/null
+++ b/.changeset/little-hotels-wave.md
@@ -0,0 +1,20 @@
+---
+'eslint-plugin-next-on-pages': patch
+'@cloudflare/next-on-pages': patch
+---
+
+Add workaround so that people can use standard not-found routes in their app directory applications
+
+The problem is that:
+
+- if there's a static not-found route in app dir, that generates a serverless (edge incompatible) function (\_not-found)
+- if there's a dynamic not-found route in app dir, that generates two serverless (edge incompatible) functions (\_not-found, \_error)
+
+The workaround being introduced here is:
+
+- if there's a static not-found route in app dir, we delete the generated \_not-found serverless function
+ (which is not needed as we can just fallback to the static 404 html)
+- if there's a dynamic not-found route in app dir, we can't actually fix it but provide a warning for the user
+
+Besides the above the `no-app-not-found-runtime` eslint rule has been introduced to try to help developers avoid
+the issue
diff --git a/packages/eslint-plugin-next-on-pages/docs/rules/no-app-not-found-runtime.md b/packages/eslint-plugin-next-on-pages/docs/rules/no-app-not-found-runtime.md
new file mode 100644
index 000000000..e00aa4dcc
--- /dev/null
+++ b/packages/eslint-plugin-next-on-pages/docs/rules/no-app-not-found-runtime.md
@@ -0,0 +1,51 @@
+# `next-on-pages/no-app-not-found-runtime`
+
+[Not found](https://nextjs.org/docs/app/api-reference/file-conventions/not-found) app router components are not currently supported with `@cloudflare/next-on-pages`, the only current alternative is not to export a runtime and making sure that the component doesn't have runtime logic. In such a way a static 404 page gets generated during the build time and served to users.
+
+> **Note**
+> This restriction applies only to top-level not-found pages (present in the `app` directory), not-found pages
+> nested inside routes are handled correctly and can contain runtime logic.
+
+## ❌ Invalid Code
+
+```js
+// app/not-found.jsx
+
+export const runtime = 'edge';
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+> **Warning**
+> The following code is invalid but not caught by the rule, please be aware of such limitation
+
+```js
+// app/not-found.jsx
+
+import { cookies } from 'next/headers';
+
+export default function NotFound() {
+ // notice the runtime cookie retrieval
+ const cookieStore = cookies();
+ const theme = cookieStore.get('theme');
+
+ return (
+
+
Not Found
+
+ );
+}
+```
+
+## ✅ Valid Code
+
+```js
+// app/not-found.jsx
+
+export default function NotFound() {
+ return (
+
+
Not Found
+
+ );
+}
+```
diff --git a/packages/eslint-plugin-next-on-pages/src/index.ts b/packages/eslint-plugin-next-on-pages/src/index.ts
index 17baa167b..161b600e9 100644
--- a/packages/eslint-plugin-next-on-pages/src/index.ts
+++ b/packages/eslint-plugin-next-on-pages/src/index.ts
@@ -1,5 +1,6 @@
import noNodeJsRuntime from './rules/no-nodejs-runtime';
import noUnsupportedConfigs from './rules/no-unsupported-configs';
+import noAppNotFound from './rules/no-app-not-found-runtime';
import type { ESLint } from 'eslint';
@@ -7,11 +8,13 @@ const config: ESLint.Plugin = {
rules: {
'no-nodejs-runtime': noNodeJsRuntime,
'no-unsupported-configs': noUnsupportedConfigs,
+ 'no-app-not-found': noAppNotFound,
},
configs: {
recommended: {
plugins: ['eslint-plugin-next-on-pages'],
rules: {
+ 'next-on-pages/no-app-not-found': 'error',
'next-on-pages/no-nodejs-runtime': 'error',
'next-on-pages/no-unsupported-configs': 'error',
},
diff --git a/packages/eslint-plugin-next-on-pages/src/rules/no-app-not-found-runtime.ts b/packages/eslint-plugin-next-on-pages/src/rules/no-app-not-found-runtime.ts
new file mode 100644
index 000000000..e3deb2780
--- /dev/null
+++ b/packages/eslint-plugin-next-on-pages/src/rules/no-app-not-found-runtime.ts
@@ -0,0 +1,42 @@
+import type { Rule } from 'eslint';
+
+const rule: Rule.RuleModule = {
+ create: context => {
+ const isAppNotFoundRoute = new RegExp(
+ `${context.cwd}/app/not-found\\.(tsx|jsx)`,
+ ).test(context.filename);
+ return {
+ ExportNamedDeclaration: node => {
+ if (!isAppNotFoundRoute) {
+ // This rule only applies to app/not-found routes
+ return;
+ }
+
+ const declaration = node.declaration;
+ if (
+ declaration?.type === 'VariableDeclaration' &&
+ declaration.declarations.length === 1 &&
+ declaration.declarations[0]?.id.type === 'Identifier' &&
+ declaration.declarations[0].id.name === 'runtime' &&
+ declaration.declarations[0].init?.type === 'Literal'
+ ) {
+ context.report({
+ message:
+ 'Only static not-found pages are currently supported, please remove the runtime export.' +
+ context.filename,
+ node,
+ fix: fixer => fixer.remove(node),
+ });
+ }
+ },
+ };
+ },
+ meta: {
+ fixable: 'code',
+ docs: {
+ url: 'https://github.com/cloudflare/next-on-pages/blob/main/packages/eslint-plugin-next-on-pages/docs/rules/no-app-not-found-runtime.md',
+ },
+ },
+};
+
+export = rule;
diff --git a/packages/next-on-pages/src/buildApplication/processVercelFunctions/index.ts b/packages/next-on-pages/src/buildApplication/processVercelFunctions/index.ts
index 55546aaef..11ed8be32 100644
--- a/packages/next-on-pages/src/buildApplication/processVercelFunctions/index.ts
+++ b/packages/next-on-pages/src/buildApplication/processVercelFunctions/index.ts
@@ -4,7 +4,7 @@ import { processEdgeFunctions } from './edgeFunctions';
import { processPrerenderFunctions } from './prerenderFunctions';
import type { CollectedFunctionIdentifiers } from './dedupeEdgeFunctions';
import { dedupeEdgeFunctions } from './dedupeEdgeFunctions';
-import { checkForInvalidFunctions } from './invalidFunctions';
+import { checkInvalidFunctions } from './invalidFunctions';
/**
* Processes and dedupes the Vercel build output functions directory.
@@ -23,7 +23,7 @@ export async function processVercelFunctions(
await processEdgeFunctions(collectedFunctions);
- await checkForInvalidFunctions(collectedFunctions);
+ await checkInvalidFunctions(collectedFunctions);
const identifiers = await dedupeEdgeFunctions(collectedFunctions, opts);
diff --git a/packages/next-on-pages/src/buildApplication/processVercelFunctions/invalidFunctions.ts b/packages/next-on-pages/src/buildApplication/processVercelFunctions/invalidFunctions.ts
index af0aa501b..0d68e67aa 100644
--- a/packages/next-on-pages/src/buildApplication/processVercelFunctions/invalidFunctions.ts
+++ b/packages/next-on-pages/src/buildApplication/processVercelFunctions/invalidFunctions.ts
@@ -1,19 +1,25 @@
import { gtr as versionGreaterThan, coerce } from 'semver';
-import { cliError } from '../../cli';
+import { cliError, cliWarn } from '../../cli';
import { getPackageVersion } from '../packageManagerUtils';
import { stripFuncExtension } from '../../utils';
import type { CollectedFunctions, FunctionInfo } from './configs';
+import { join, resolve } from 'path';
/**
* Checks if there are any invalid functions from the Vercel build output.
*
- * If there are any invalid functions, an error message will be printed and the process will exit.
+ * If there are any invalid functions it will try to see if they are amendable and if the
+ * build output can still be used.
+ *
+ * If however the build output can't be used, an error message will be printed and the process will exit.
*
* @param collectedFunctions Collected functions from the Vercel build output.
*/
-export async function checkForInvalidFunctions(
+export async function checkInvalidFunctions(
collectedFunctions: CollectedFunctions,
): Promise {
+ await tryToFixNotFoundRoute(collectedFunctions);
+
if (collectedFunctions.invalidFunctions.size > 0) {
await printInvalidFunctionsErrorMessage(
collectedFunctions.invalidFunctions,
@@ -22,6 +28,49 @@ export async function checkForInvalidFunctions(
}
}
+/**
+ * Tries to fix potential not-found invalid functions from the Vercel build output.
+ *
+ * Static app/not-found.(jsx|tsx) pages generate an _not-found.func serverless function,
+ * that can be removed as we can fallback to the statically generated 404 page
+ *
+ * If the app/not-found.(jsx|tsx) contains runtime logic alongside the _not-found.func serverless
+ * function also an _error.func will be generated, in such a case we can only warn the user about
+ * it.
+ * (
+ * That's the only option because:
+ * - removing the _not-found.func and _error.func doesn't result in a working application
+ * - we don't have a guarantee that the _error.func hasn't been generated by something else
+ * and that the _not-found.func is that of a static app/not-found route
+ * )
+ *
+ * @param collectedFunctions Collected functions from the Vercel build output.
+ */
+async function tryToFixNotFoundRoute(
+ collectedFunctions: CollectedFunctions,
+): Promise {
+ const functionsDir = resolve('.vercel', 'output', 'functions');
+ const notFoundDir = join(functionsDir, '_not-found.func');
+ const errorDir = join(functionsDir, '_error.func');
+
+ const invalidNotFound = collectedFunctions.invalidFunctions.get(notFoundDir);
+ const invalidError = collectedFunctions.invalidFunctions.get(errorDir);
+
+ if (invalidNotFound && !invalidError) {
+ collectedFunctions.invalidFunctions.delete(notFoundDir);
+ const notFoundRscDir = join(functionsDir, '_not-found.rsc.func');
+ collectedFunctions.invalidFunctions.delete(notFoundRscDir);
+ }
+
+ if (invalidNotFound && invalidError) {
+ cliWarn(`
+ Warning: your app/not-found route might contain runtime logic, this is currently
+ not supported by @cloudflare/next-on-pages, if that's actually the case please
+ remove the runtime logic from your not-found route
+ `);
+ }
+}
+
/**
* Prints an error message for the invalid functions from the Vercel build output.
*