Skip to content

Commit e7e9357

Browse files
committed
refactor: use native Node glob APIs
1 parent df7f890 commit e7e9357

10 files changed

Lines changed: 175 additions & 115 deletions

File tree

.changeset/glob.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
refactor: use native Node glob APIs
6+
7+
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.

packages/cloudflare/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
"cloudflare": "^4.4.1",
6060
"comment-json": "^4.5.1",
6161
"enquirer": "^2.4.1",
62-
"glob": "catalog:",
6362
"ts-tqdm": "^0.8.6",
6463
"yargs": "catalog:"
6564
},

packages/cloudflare/src/cli/build/bundle-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function bundleServer(buildOpts: BuildOptions, projectOpts: Project
6363
console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
6464

6565
await patchWebpackRuntime(buildOpts);
66-
const useOg = patchVercelOgLibrary(buildOpts);
66+
const useOg = await patchVercelOgLibrary(buildOpts);
6767

6868
const outputPath = path.join(outputDir, "server-functions", "default");
6969
const packagePath = getPackagePath(buildOpts);

packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ describe("patchVercelOgLibrary", () => {
4343

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

46-
it("should patch the open-next files correctly", () => {
47-
patchVercelOgLibrary(buildOpts);
46+
it("should patch the open-next files correctly", async () => {
47+
await patchVercelOgLibrary(buildOpts);
4848

4949
expect(readdirSync(openNextVercelOgDir)).toMatchInlineSnapshot(`
5050
[

packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2+
import { glob } from "node:fs/promises";
23
import path from "node:path";
34

45
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
56
import { getPackagePath } from "@opennextjs/aws/build/helper.js";
67
import { parseFile } from "@opennextjs/aws/build/patch/astCodePatcher.js";
7-
import { globSync } from "glob";
88

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

@@ -16,19 +16,19 @@ type TraceInfo = { version: number; files: string[] };
1616
* @param buildOpts Build options.
1717
* @returns Whether the @vercel/og library is used.
1818
*/
19-
export function patchVercelOgLibrary(buildOpts: BuildOptions): boolean {
19+
export async function patchVercelOgLibrary(buildOpts: BuildOptions): Promise<boolean> {
2020
const { appBuildOutputPath, outputDir } = buildOpts;
2121

2222
const functionsPath = path.join(outputDir, "server-functions/default");
2323
const packagePath = path.join(functionsPath, getPackagePath(buildOpts));
2424

2525
let useOg = false;
2626

27-
for (const traceInfoPath of globSync(path.join(appBuildOutputPath, ".next/server/**/*.nft.json"), {
28-
windowsPathsNoEscape: true,
29-
})) {
27+
for await (const traceInfoPath of glob(".next/server/**/*.nft.json", { cwd: appBuildOutputPath })) {
28+
const fullTraceInfoPath = path.join(appBuildOutputPath, traceInfoPath);
29+
3030
// Look for the Node version of the traced @vercel/og files
31-
const traceInfo: TraceInfo = JSON.parse(readFileSync(traceInfoPath, { encoding: "utf8" }));
31+
const traceInfo: TraceInfo = JSON.parse(readFileSync(fullTraceInfoPath, { encoding: "utf8" }));
3232
const tracedNodePath = traceInfo.files.find((p) => p.endsWith("@vercel/og/index.node.js"));
3333
if (!tracedNodePath) continue;
3434

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

4949
copyFileSync(tracedEdgePath, outputEdgePath);
5050

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

7878
const ast = parseFile(routeFilePath);
7979
const { edits } = patchVercelOgImport(ast);

packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55
*/
66

77
import crypto from "node:crypto";
8-
import { readFile } from "node:fs/promises";
8+
import { glob, readFile } from "node:fs/promises";
99
import { join, posix, relative, sep } from "node:path";
1010

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

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

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

42-
const manifests = await glob(
43-
join(dotNextDir, "**/{*-manifest,required-server-files,prefetch-hints}.json"),
44-
{
45-
windowsPathsNoEscape: true,
46-
}
41+
const manifests = await Array.fromAsync(
42+
glob("**/{*-manifest,required-server-files,prefetch-hints}.json", { cwd: dotNextDir })
4743
);
4844

4945
const returnManifests = (
5046
await Promise.all(
51-
manifests.map(
52-
async (manifest) => `
53-
if ($PATH.endsWith("${normalizePath("/" + relative(dotNextDir, manifest))}")) {
54-
return ${await readFile(manifest, "utf-8")};
55-
}`
56-
)
47+
manifests.map(async (manifestPath) => {
48+
const fullManifestPath = join(dotNextDir, manifestPath);
49+
return `
50+
if ($PATH.endsWith("${normalizePath("/" + manifestPath)}")) {
51+
return ${await readFile(fullManifestPath, "utf-8")};
52+
}`;
53+
})
5754
)
5855
).join("\n");
5956

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

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

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

112-
for (const path of manifestPaths) {
113-
if (path.endsWith("page_client-reference-manifest.js")) {
107+
for (const manifestPath of manifestPaths) {
108+
const fullManifestPath = join(baseDir, manifestPath);
109+
110+
if (manifestPath.endsWith("page_client-reference-manifest.js")) {
114111
// `page_client-reference-manifest.js` files could contain large repeated values.
115112
// Factor out large values into separate variables to reduce the overall size of the generated code.
116-
let manifest = await readFile(path, "utf-8");
113+
let manifest = await readFile(fullManifestPath, "utf-8");
117114
for (const key of [
118115
"clientModules",
119116
"ssrModuleMapping",
@@ -124,7 +121,7 @@ async function getEvalManifestRule(buildOpts: BuildOptions) {
124121
]) {
125122
manifest = factorManifestValue(manifest, key, factoredObjects, prefixMap);
126123
}
127-
factoredManifest.set(path, manifest);
124+
factoredManifest.set(manifestPath, manifest);
128125
}
129126
}
130127

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

154-
if (factoredManifest.has(path)) {
155-
manifest = factoredManifest.get(path)!;
152+
if (factoredManifest.has(manifestPath)) {
153+
manifest = factoredManifest.get(manifestPath)!;
156154
} else {
157-
manifest = `require(${JSON.stringify(path)});`;
155+
manifest = `require(${JSON.stringify(fullManifestPath)});`;
158156
}
159157

160-
const endsWith = normalizePath(relative(baseDir, path));
161-
const key = normalizePath("/" + relative(appDir, path)).replace("_client-reference-manifest.js", "");
158+
const endsWith = normalizePath(manifestPath);
159+
const key = normalizePath("/" + relative(appDir, fullManifestPath)).replace(
160+
"_client-reference-manifest.js",
161+
""
162+
);
162163
return `
163164
if ($PATH.endsWith("${endsWith}")) {
164165
${manifest}

packages/cloudflare/src/cli/commands/populate-cache.spec.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ describe("getCacheAssets", () => {
3131

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

34-
test("list cache assets", () => {
35-
expect(getCacheAssets({ outputDir: "/base/path" } as BuildOptions)).toMatchInlineSnapshot(`
34+
test("list cache assets", async () => {
35+
await expect(getCacheAssets({ outputDir: "/base/path" } as BuildOptions)).resolves.toMatchInlineSnapshot(`
3636
[
3737
{
3838
"buildId": "buildID",
39-
"fullPath": "/base/path/cache/buildID/path/to/2.cache",
39+
"fullPath": "/base/path/cache/buildID/path/to/0.cache",
4040
"isFetch": false,
41-
"key": "/path/to/2",
41+
"key": "/path/to/0",
4242
},
4343
{
4444
"buildId": "buildID",
@@ -48,15 +48,15 @@ describe("getCacheAssets", () => {
4848
},
4949
{
5050
"buildId": "buildID",
51-
"fullPath": "/base/path/cache/buildID/path/to/0.cache",
51+
"fullPath": "/base/path/cache/buildID/path/to/2.cache",
5252
"isFetch": false,
53-
"key": "/path/to/0",
53+
"key": "/path/to/2",
5454
},
5555
{
5656
"buildId": "buildID",
57-
"fullPath": "/base/path/cache/__fetch/buildID/2",
57+
"fullPath": "/base/path/cache/__fetch/buildID/0",
5858
"isFetch": true,
59-
"key": "/2",
59+
"key": "/0",
6060
},
6161
{
6262
"buildId": "buildID",
@@ -66,9 +66,9 @@ describe("getCacheAssets", () => {
6666
},
6767
{
6868
"buildId": "buildID",
69-
"fullPath": "/base/path/cache/__fetch/buildID/0",
69+
"fullPath": "/base/path/cache/__fetch/buildID/2",
7070
"isFetch": true,
71-
"key": "/0",
71+
"key": "/2",
7272
},
7373
]
7474
`);
@@ -144,7 +144,6 @@ describe("populateCache", () => {
144144
const mockWorkerDispose = vi.fn();
145145

146146
setupMockFileSystem();
147-
vi.useFakeTimers();
148147
// @ts-expect-error - Mock unstable_startWorker to return a mock worker instance
149148
vi.mocked(unstable_startWorker).mockResolvedValueOnce({
150149
ready: Promise.resolve(),

packages/cloudflare/src/cli/commands/populate-cache.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type {
1515
OpenNextConfig,
1616
} from "@opennextjs/aws/types/open-next.js";
1717
import type { IncrementalCache, TagCache } from "@opennextjs/aws/types/overrides.js";
18-
import { globSync } from "glob";
1918
import { tqdm } from "ts-tqdm";
2019
import type { Unstable_Config as WranglerConfig } from "wrangler";
2120
import { unstable_startWorker } from "wrangler";
@@ -148,17 +147,16 @@ async function resolveCacheName(
148147

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

151-
export function getCacheAssets(opts: BuildOptions): CacheAsset[] {
152-
const allFiles = globSync(path.join(opts.outputDir, "cache/**/*"), {
153-
withFileTypes: true,
154-
windowsPathsNoEscape: true,
155-
}).filter((f) => f.isFile());
156-
150+
export async function getCacheAssets(opts: BuildOptions): Promise<CacheAsset[]> {
157151
const baseCacheDir = path.join(opts.outputDir, "cache");
158152
const assets: CacheAsset[] = [];
159153

160-
for (const file of allFiles) {
161-
const fullPath = file.fullpath();
154+
for await (const file of fsp.glob("**/*", { cwd: baseCacheDir, withFileTypes: true })) {
155+
if (!file.isFile()) {
156+
continue;
157+
}
158+
159+
const fullPath = path.join(file.parentPath, file.name);
162160
const relativePath = normalizePath(path.relative(baseCacheDir, fullPath));
163161

164162
if (relativePath.startsWith("__fetch")) {
@@ -253,7 +251,7 @@ async function populateR2IncrementalCache(
253251
? binding.preview_bucket_name
254252
: binding.bucket_name;
255253
const prefix = envVars[R2_CACHE_PREFIX_ENV_NAME];
256-
const assets = getCacheAssets(buildOpts);
254+
const assets = await getCacheAssets(buildOpts);
257255

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

497495
const prefix = envVars[KV_CACHE_PREFIX_ENV_NAME];
498-
const assets = getCacheAssets(buildOpts);
496+
const assets = await getCacheAssets(buildOpts);
499497

500498
if (assets.length === 0) {
501499
logger.info("No cache assets to populate");

0 commit comments

Comments
 (0)