Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/glob.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not really need a changeset for that if there are no user facing change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are no changes facing users directly.

However, since this PR defines engines, I thought it should be submitted as a patch.

"@opennextjs/cloudflare": patch
---

refactor: use native Node glob APIs

Use Node.js native glob APIs for cache population and build-time patch discovery instead of the `glob` package. This keeps path handling based on explicit working directories and removes the direct `glob` dependency.

Node.js `>=22.17.0 <23 || >=24.1.0` is now required because `fs.promises.glob` — used for cache population and patch discovery — was stabilized in those releases.
4 changes: 3 additions & 1 deletion packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
"cloudflare": "^4.4.1",
"comment-json": "^4.5.1",
"enquirer": "^2.4.1",
"glob": "catalog:",
"ts-tqdm": "^0.8.6",
"yargs": "catalog:"
},
Expand Down Expand Up @@ -89,5 +88,8 @@
"peerDependencies": {
"next": ">=15.5.15 <16 || >=16.2.3",
"wrangler": "catalog:"
},
"engines": {
"node": ">=22.17.0 <23 || >=24.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { copyFileSync, existsSync, globSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import path from "node:path";

import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
import { getPackagePath } from "@opennextjs/aws/build/helper.js";
import { parseFile } from "@opennextjs/aws/build/patch/astCodePatcher.js";
import { globSync } from "glob";

import { patchVercelOgFallbackFont, patchVercelOgImport } from "./vercel-og.js";

Expand All @@ -24,11 +23,11 @@ export function patchVercelOgLibrary(buildOpts: BuildOptions): boolean {

let useOg = false;

for (const traceInfoPath of globSync(path.join(appBuildOutputPath, ".next/server/**/*.nft.json"), {
windowsPathsNoEscape: true,
})) {
for (const traceInfoPath of globSync(".next/server/**/*.nft.json", { cwd: appBuildOutputPath })) {
const fullTraceInfoPath = path.join(appBuildOutputPath, traceInfoPath);

// Look for the Node version of the traced @vercel/og files
const traceInfo: TraceInfo = JSON.parse(readFileSync(traceInfoPath, { encoding: "utf8" }));
const traceInfo: TraceInfo = JSON.parse(readFileSync(fullTraceInfoPath, { encoding: "utf8" }));
const tracedNodePath = traceInfo.files.find((p) => p.endsWith("@vercel/og/index.node.js"));
if (!tracedNodePath) continue;

Expand All @@ -42,15 +41,15 @@ export function patchVercelOgLibrary(buildOpts: BuildOptions): boolean {
// Ensure the edge version is available in the OpenNext node_modules.
if (!existsSync(outputEdgePath)) {
const tracedEdgePath = path.join(
path.dirname(traceInfoPath),
path.dirname(fullTraceInfoPath),
tracedNodePath.replace("index.node.js", "index.edge.js")
);

copyFileSync(tracedEdgePath, outputEdgePath);

// On Next 16.2 and above, we also need to copy the yoga.wasm file used by the library.
const tracedWasmPath = path.join(
path.dirname(traceInfoPath),
path.dirname(fullTraceInfoPath),
tracedNodePath.replace("index.node.js", "yoga.wasm")
);
if (existsSync(tracedWasmPath)) {
Expand All @@ -73,7 +72,7 @@ export function patchVercelOgLibrary(buildOpts: BuildOptions): boolean {
// Change node imports for the library to edge imports.
// This is only useful when turbopack is not used to bundle the function.
{
const routeFilePath = traceInfoPath.replace(appBuildOutputPath, packagePath).replace(".nft.json", "");
const routeFilePath = path.join(packagePath, traceInfoPath.replace(".nft.json", ""));

const ast = parseFile(routeFilePath);
const { edits } = patchVercelOgImport(ast);
Expand Down
53 changes: 27 additions & 26 deletions packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
*/

import crypto from "node:crypto";
import { readFile } from "node:fs/promises";
import { glob, readFile } from "node:fs/promises";
import { join, posix, relative, sep } from "node:path";

import { Lang, parse, type SgNode } from "@ast-grep/napi";
import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js";
import { applyRule, patchCode, type RuleConfig } from "@opennextjs/aws/build/patch/astCodePatcher.js";
import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js";
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
import { glob } from "glob";

import { normalizePath } from "../../../utils/normalize-path.js";

Expand All @@ -39,21 +38,19 @@ async function getLoadManifestRule(buildOpts: BuildOptions) {
const baseDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts));
const dotNextDir = join(baseDir, ".next");

const manifests = await glob(
join(dotNextDir, "**/{*-manifest,required-server-files,prefetch-hints}.json"),
{
windowsPathsNoEscape: true,
}
const manifests = await Array.fromAsync(
glob("**/{*-manifest,required-server-files,prefetch-hints}.json", { cwd: dotNextDir })
);

const returnManifests = (
await Promise.all(
manifests.map(
async (manifest) => `
if ($PATH.endsWith("${normalizePath("/" + relative(dotNextDir, manifest))}")) {
return ${await readFile(manifest, "utf-8")};
}`
)
manifests.map(async (manifestPath) => {
const fullManifestPath = join(dotNextDir, manifestPath);
return `
if ($PATH.endsWith("${normalizePath("/" + manifestPath)}")) {
return ${await readFile(fullManifestPath, "utf-8")};
}`;
})
)
).join("\n");

Expand Down Expand Up @@ -98,9 +95,7 @@ async function getEvalManifestRule(buildOpts: BuildOptions) {

const baseDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
const appDir = join(baseDir, "server/app");
const manifestPaths = await glob(join(baseDir, "**/*_client-reference-manifest.js"), {
windowsPathsNoEscape: true,
});
const manifestPaths = await Array.fromAsync(glob("**/*_client-reference-manifest.js", { cwd: baseDir }));

// Map of factored large objects (variable name -> {...})
const factoredObjects = new Map<string, string>();
Expand All @@ -109,11 +104,13 @@ async function getEvalManifestRule(buildOpts: BuildOptions) {
// Shared map of short hash prefix -> full SHA1 hash, used for collision resolution.
const prefixMap = new Map<string, string>();

for (const path of manifestPaths) {
if (path.endsWith("page_client-reference-manifest.js")) {
for (const manifestPath of manifestPaths) {
const fullManifestPath = join(baseDir, manifestPath);

if (manifestPath.endsWith("page_client-reference-manifest.js")) {
// `page_client-reference-manifest.js` files could contain large repeated values.
// Factor out large values into separate variables to reduce the overall size of the generated code.
let manifest = await readFile(path, "utf-8");
let manifest = await readFile(fullManifestPath, "utf-8");
for (const key of [
"clientModules",
"ssrModuleMapping",
Expand All @@ -124,7 +121,7 @@ async function getEvalManifestRule(buildOpts: BuildOptions) {
]) {
manifest = factorManifestValue(manifest, key, factoredObjects, prefixMap);
}
factoredManifest.set(path, manifest);
factoredManifest.set(manifestPath, manifest);
}
}

Expand All @@ -148,17 +145,21 @@ async function getEvalManifestRule(buildOpts: BuildOptions) {
// Sort by path length descending so longer (more specific) paths match first,
// preventing suffix collisions in the `.endsWith()` chain (see #1156).
.toSorted((a, b) => b.length - a.length)
.map((path) => {
.map((manifestPath) => {
const fullManifestPath = join(baseDir, manifestPath);
let manifest: string;

if (factoredManifest.has(path)) {
manifest = factoredManifest.get(path)!;
if (factoredManifest.has(manifestPath)) {
manifest = factoredManifest.get(manifestPath)!;
} else {
manifest = `require(${JSON.stringify(path)});`;
manifest = `require(${JSON.stringify(fullManifestPath)});`;
}

const endsWith = normalizePath(relative(baseDir, path));
const key = normalizePath("/" + relative(appDir, path)).replace("_client-reference-manifest.js", "");
const endsWith = normalizePath(manifestPath);
const key = normalizePath("/" + relative(appDir, fullManifestPath)).replace(
"_client-reference-manifest.js",
""
);
return `
if ($PATH.endsWith("${endsWith}")) {
${manifest}
Expand Down
21 changes: 10 additions & 11 deletions packages/cloudflare/src/cli/commands/populate-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ describe("getCacheAssets", () => {

afterAll(() => mockFs.restore());

test("list cache assets", () => {
expect(getCacheAssets({ outputDir: "/base/path" } as BuildOptions)).toMatchInlineSnapshot(`
test("list cache assets", async () => {
await expect(getCacheAssets({ outputDir: "/base/path" } as BuildOptions)).resolves.toMatchInlineSnapshot(`
[
{
"buildId": "buildID",
"fullPath": "/base/path/cache/buildID/path/to/2.cache",
"fullPath": "/base/path/cache/buildID/path/to/0.cache",
"isFetch": false,
"key": "/path/to/2",
"key": "/path/to/0",
},
{
"buildId": "buildID",
Expand All @@ -48,15 +48,15 @@ describe("getCacheAssets", () => {
},
{
"buildId": "buildID",
"fullPath": "/base/path/cache/buildID/path/to/0.cache",
"fullPath": "/base/path/cache/buildID/path/to/2.cache",
"isFetch": false,
"key": "/path/to/0",
"key": "/path/to/2",
},
{
"buildId": "buildID",
"fullPath": "/base/path/cache/__fetch/buildID/2",
"fullPath": "/base/path/cache/__fetch/buildID/0",
"isFetch": true,
"key": "/2",
"key": "/0",
},
{
"buildId": "buildID",
Expand All @@ -66,9 +66,9 @@ describe("getCacheAssets", () => {
},
{
"buildId": "buildID",
"fullPath": "/base/path/cache/__fetch/buildID/0",
"fullPath": "/base/path/cache/__fetch/buildID/2",
"isFetch": true,
"key": "/0",
"key": "/2",
},
]
`);
Expand Down Expand Up @@ -144,7 +144,6 @@ describe("populateCache", () => {
const mockWorkerDispose = vi.fn();

setupMockFileSystem();
vi.useFakeTimers();
// @ts-expect-error - Mock unstable_startWorker to return a mock worker instance
vi.mocked(unstable_startWorker).mockResolvedValueOnce({
ready: Promise.resolve(),
Expand Down
20 changes: 9 additions & 11 deletions packages/cloudflare/src/cli/commands/populate-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type {
OpenNextConfig,
} from "@opennextjs/aws/types/open-next.js";
import type { IncrementalCache, TagCache } from "@opennextjs/aws/types/overrides.js";
import { globSync } from "glob";
import { tqdm } from "ts-tqdm";
import type { Unstable_Config as WranglerConfig } from "wrangler";
import { unstable_startWorker } from "wrangler";
Expand Down Expand Up @@ -148,17 +147,16 @@ async function resolveCacheName(

export type CacheAsset = { isFetch: boolean; fullPath: string; key: string; buildId: string };

export function getCacheAssets(opts: BuildOptions): CacheAsset[] {
const allFiles = globSync(path.join(opts.outputDir, "cache/**/*"), {
withFileTypes: true,
windowsPathsNoEscape: true,
}).filter((f) => f.isFile());

export async function getCacheAssets(opts: BuildOptions): Promise<CacheAsset[]> {
const baseCacheDir = path.join(opts.outputDir, "cache");
const assets: CacheAsset[] = [];

for (const file of allFiles) {
const fullPath = file.fullpath();
for await (const file of fsp.glob("**/*", { cwd: baseCacheDir, withFileTypes: true })) {
if (!file.isFile()) {
continue;
}

const fullPath = path.join(file.parentPath, file.name);
const relativePath = normalizePath(path.relative(baseCacheDir, fullPath));

if (relativePath.startsWith("__fetch")) {
Expand Down Expand Up @@ -253,7 +251,7 @@ async function populateR2IncrementalCache(
? binding.preview_bucket_name
: binding.bucket_name;
const prefix = envVars[R2_CACHE_PREFIX_ENV_NAME];
const assets = getCacheAssets(buildOpts);
const assets = await getCacheAssets(buildOpts);

if (assets.length === 0) {
logger.info("No cache assets to populate");
Expand Down Expand Up @@ -495,7 +493,7 @@ async function populateKVIncrementalCache(
}

const prefix = envVars[KV_CACHE_PREFIX_ENV_NAME];
const assets = getCacheAssets(buildOpts);
const assets = await getCacheAssets(buildOpts);

if (assets.length === 0) {
logger.info("No cache assets to populate");
Expand Down
Loading
Loading