Environment
@opennextjs/cloudflare: 1.20.2
@opennextjs/aws: 4.1.0 (content-updater / aggregate-on-load)
- Next.js: 16.2.11
- Node: 22
- glob: 13.0.6
- esbuild: 0.25.0
Description
getLoadManifestRule() in packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts globs for manifest files:
const manifests = await glob(
join(dotNextDir, "**/{*-manifest,required-server-files,prefetch-hints}.json"),
{ windowsPathsNoEscape: true }
);
Without nodir: true, this matches App Router route directories whose path segments end in *-manifest.json. For example, a Next.js API route at src/app/mail-manifest.json/route.ts creates:
.next/server/app/mail-manifest.json/
├── route.js
└── route.js.nft.json
This directory matches *-manifest...json and is included in the manifests array. The subsequent await readFile(manifest, "utf-8") throws:
EISDIR: illegal operation on a directory, read
The same issue affects the getEvalManifestRule() glob call (which uses the same { windowsPathsNoEscape: true } options without nodir).
Stack trace
[ERROR] EISDIR: illegal operation on a directory, read [plugin aggregate-on-load]
at async readFileHandle (node:internal/fs/promises:555:24)
at async .../plugins/load-manifest.js:39:12
at async getLoadManifestRule (.../plugins/load-manifest.js:37:30)
at async callback (.../plugins/load-manifest.js:23:54)
at async .../content-updater.js:57:30
at setup (.../content-updater.js:40:23)
Reproduction
- Create an App Router route whose path ends in
*-manifest.json:
// src/app/mail-manifest.json/route.ts
import { NextResponse } from 'next/server';
export function GET() {
return NextResponse.json({ name: 'My App' });
}
- Run
opennextjs-cloudflare build
- Build fails with EISDIR
Any route whose directory name matches {*-manifest,required-server-files,prefetch-hints}.json will trigger this. The application route is legitimate — the bug is in the glob, not the route.
Proposed fix
Add nodir: true to both glob calls in load-manifest.ts:
const manifests = await glob(
join(dotNextDir, "**/{*-manifest,required-server-files,prefetch-hints}.json"),
- { windowsPathsNoEscape: true }
+ { windowsPathsNoEscape: true, nodir: true }
);
This excludes directories from glob results while retaining all genuine JSON manifest files. It is the smallest possible fix — no error handling, no path filtering, no route renaming.
Regression test
A test could create a .next-like tree with both:
server/app/some-manifest.json/route.js (directory — should be excluded)
app-paths-manifest.json (file — should be retained)
And assert the glob with nodir: true excludes the directory and includes the file.
Environment
@opennextjs/cloudflare: 1.20.2@opennextjs/aws: 4.1.0 (content-updater / aggregate-on-load)Description
getLoadManifestRule()inpackages/cloudflare/src/cli/build/patches/plugins/load-manifest.tsglobs for manifest files:Without
nodir: true, this matches App Router route directories whose path segments end in*-manifest.json. For example, a Next.js API route atsrc/app/mail-manifest.json/route.tscreates:This directory matches
*-manifest...jsonand is included in themanifestsarray. The subsequentawait readFile(manifest, "utf-8")throws:The same issue affects the
getEvalManifestRule()glob call (which uses the same{ windowsPathsNoEscape: true }options withoutnodir).Stack trace
Reproduction
*-manifest.json:opennextjs-cloudflare buildAny route whose directory name matches
{*-manifest,required-server-files,prefetch-hints}.jsonwill trigger this. The application route is legitimate — the bug is in the glob, not the route.Proposed fix
Add
nodir: trueto both glob calls inload-manifest.ts:const manifests = await glob( join(dotNextDir, "**/{*-manifest,required-server-files,prefetch-hints}.json"), - { windowsPathsNoEscape: true } + { windowsPathsNoEscape: true, nodir: true } );This excludes directories from glob results while retaining all genuine JSON manifest files. It is the smallest possible fix — no error handling, no path filtering, no route renaming.
Regression test
A test could create a
.next-like tree with both:server/app/some-manifest.json/route.js(directory — should be excluded)app-paths-manifest.json(file — should be retained)And assert the glob with
nodir: trueexcludes the directory and includes the file.