Skip to content

Commit 075854e

Browse files
committed
fixup! add asset handling
1 parent 610ada9 commit 075854e

File tree

7 files changed

+381
-21
lines changed

7 files changed

+381
-21
lines changed

packages/cloudflare/src/cli/build/build.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,6 @@ export async function build(
6060
buildHelper.initOutputDir(options);
6161

6262
compileCache(options);
63-
compileEnvFiles(options);
64-
compileInit(options);
65-
compileImages(options);
66-
compileSkewProtection(options, config);
67-
6863
// Compile middleware
6964
await createMiddleware(options, { forceOnlyBuildOnce: true });
7065

@@ -78,6 +73,12 @@ export async function build(
7873
}
7974
}
8075

76+
compileEnvFiles(options);
77+
compileInit(options);
78+
compileImages(options);
79+
// Compile skew protection, needs the assets to be copied first (see `createStaticAssets`)
80+
compileSkewProtection(options, config);
81+
8182
await createServerBundle(options);
8283

8384
await compileDurableObjects(options);
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { filesToTree } from "./compile-skew-protection";
4+
5+
describe("filesToTree", () => {
6+
it("should return an empty tree for an empty array of paths", () => {
7+
const paths: string[] = [];
8+
const tree = filesToTree(paths);
9+
expect(tree).toEqual({ f: [], d: {} });
10+
});
11+
12+
it("should correctly add a single file at the root", () => {
13+
const paths = ["file.txt"];
14+
const tree = filesToTree(paths);
15+
expect(tree).toEqual({
16+
f: ["file.txt"],
17+
d: {},
18+
});
19+
});
20+
21+
it("should correctly add multiple files at the root", () => {
22+
const paths = ["file1.txt", "file2.txt"];
23+
const tree = filesToTree(paths);
24+
expect(tree).toEqual({
25+
f: ["file1.txt", "file2.txt"],
26+
d: {},
27+
});
28+
});
29+
30+
it("should correctly add a single file in a single folder", () => {
31+
const paths = ["folder/file.txt"];
32+
const tree = filesToTree(paths);
33+
expect(tree).toEqual({
34+
f: [],
35+
d: {
36+
folder: {
37+
f: ["file.txt"],
38+
d: {},
39+
},
40+
},
41+
});
42+
});
43+
44+
it("should correctly add multiple files in the same folder", () => {
45+
const paths = ["folder/file1.txt", "folder/file2.txt"];
46+
const tree = filesToTree(paths);
47+
expect(tree).toEqual({
48+
f: [],
49+
d: {
50+
folder: {
51+
f: ["file1.txt", "file2.txt"],
52+
d: {},
53+
},
54+
},
55+
});
56+
});
57+
58+
it("should correctly add files in nested folders", () => {
59+
const paths = ["folder1/folder2/file.txt"];
60+
const tree = filesToTree(paths);
61+
expect(tree).toEqual({
62+
f: [],
63+
d: {
64+
folder1: {
65+
f: [],
66+
d: { folder2: { f: ["file.txt"], d: {} } },
67+
},
68+
},
69+
});
70+
});
71+
72+
it("should handle mixed files and folders at different levels", () => {
73+
const paths = ["root_file.txt", "folderA/fileA.txt", "folderA/subfolderB/fileB.txt", "folderC/fileC.txt"];
74+
const tree = filesToTree(paths);
75+
expect(tree).toEqual({
76+
f: ["root_file.txt"],
77+
d: {
78+
folderA: {
79+
f: ["fileA.txt"],
80+
d: {
81+
subfolderB: {
82+
f: ["fileB.txt"],
83+
d: {},
84+
},
85+
},
86+
},
87+
folderC: {
88+
f: ["fileC.txt"],
89+
d: {},
90+
},
91+
},
92+
});
93+
});
94+
95+
it("should handle paths with leading/trailing slashes gracefully", () => {
96+
const paths = ["/folder/file.txt", "another_folder/file.txt/"];
97+
const tree = filesToTree(paths);
98+
expect(tree).toEqual({
99+
f: [],
100+
d: {
101+
folder: {
102+
f: ["file.txt"],
103+
d: {},
104+
},
105+
another_folder: {
106+
f: ["file.txt"], // Trailing slash on file name is removed by filter(Boolean)
107+
d: {},
108+
},
109+
},
110+
});
111+
});
112+
113+
it("should handle duplicate file names in different folders", () => {
114+
const paths = ["folder1/file.txt", "folder2/file.txt"];
115+
const tree = filesToTree(paths);
116+
expect(tree).toEqual({
117+
f: [],
118+
d: {
119+
folder1: {
120+
f: ["file.txt"],
121+
d: {},
122+
},
123+
folder2: {
124+
f: ["file.txt"],
125+
d: {},
126+
},
127+
},
128+
});
129+
});
130+
131+
it("should handle folders with the same name but different parents", () => {
132+
const paths = ["a/b/file1.txt", "c/b/file2.txt"];
133+
const tree = filesToTree(paths);
134+
expect(tree).toEqual({
135+
f: [],
136+
d: {
137+
a: {
138+
f: [],
139+
d: {
140+
b: {
141+
f: ["file1.txt"],
142+
d: {},
143+
},
144+
},
145+
},
146+
c: {
147+
f: [],
148+
d: {
149+
b: {
150+
f: ["file2.txt"],
151+
d: {},
152+
},
153+
},
154+
},
155+
},
156+
});
157+
});
158+
});

packages/cloudflare/src/cli/build/open-next/compile-skew-protection.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,32 @@ import { fileURLToPath } from "node:url";
33

44
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
55
import { build } from "esbuild";
6+
import { glob } from "glob";
67

78
import type { OpenNextConfig } from "../../../api";
9+
import type { FolderNode } from "../../templates/skew-protection";
810

911
export async function compileSkewProtection(options: BuildOptions, config: OpenNextConfig) {
1012
const currentDir = path.join(path.dirname(fileURLToPath(import.meta.url)));
1113
const templatesDir = path.join(currentDir, "../../templates");
1214
const initPath = path.join(templatesDir, "skew-protection.js");
1315

16+
const skewProtectionEnabled = config.cloudflare?.skewProtectionEnabled ?? false;
17+
18+
// Create a tree of assets located inside the base path
19+
const assetPath = path.join(options.outputDir, "assets", globalThis.__NEXT_BASE_PATH__ ?? "");
20+
const files = await glob(`**/*`, {
21+
windowsPathsNoEscape: true,
22+
posix: true,
23+
nodir: true,
24+
absolute: false,
25+
cwd: assetPath,
26+
});
27+
// All files located inside `_next/static` are static assets, no need to list them
28+
const assetTree = filesToTree(
29+
skewProtectionEnabled ? files.filter((path) => !path.startsWith("_next/static/")) : []
30+
);
31+
1432
await build({
1533
entryPoints: [initPath],
1634
outdir: path.join(options.outputDir, "cloudflare"),
@@ -20,7 +38,40 @@ export async function compileSkewProtection(options: BuildOptions, config: OpenN
2038
target: "esnext",
2139
platform: "node",
2240
define: {
23-
__SKEW_PROTECTION_ENABLED__: JSON.stringify(config.cloudflare?.skewProtectionEnabled ?? false),
41+
__SKEW_PROTECTION_ENABLED__: JSON.stringify(skewProtectionEnabled),
42+
__CF_ASSETS_TREE__: JSON.stringify(assetTree),
2443
},
2544
});
2645
}
46+
47+
/**
48+
* Converts a list a file to tree of `FolderNode`
49+
*
50+
* @param paths The list of path
51+
* @returns The root node of the tree
52+
*/
53+
export function filesToTree(paths: string[]): FolderNode {
54+
const root: FolderNode = {
55+
f: [],
56+
d: {},
57+
};
58+
59+
for (const filePath of paths) {
60+
// Split the path into components, filtering out empty strings from potential leading/trailing slashes
61+
const parts = filePath.split("/").filter(Boolean);
62+
63+
let currentNode: FolderNode = root;
64+
65+
// Traverse through folder parts, creating new nodes as needed
66+
for (let i = 0; i < parts.length - 1; i++) {
67+
const folderName = parts[i] as string;
68+
if (!currentNode.d[folderName]) {
69+
currentNode.d[folderName] = { f: [], d: {} };
70+
}
71+
currentNode = currentNode.d[folderName];
72+
}
73+
// Add the file to the current node's files array
74+
currentNode.f.push(parts[parts.length - 1] as string);
75+
}
76+
return root;
77+
}

packages/cloudflare/src/cli/build/open-next/compileDurableObjects.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,15 @@ export function compileDurableObjects(buildOpts: BuildOptions) {
1212
_require.resolve("@opennextjs/cloudflare/durable-objects/bucket-cache-purge"),
1313
];
1414

15-
const { outputDir } = buildOpts;
16-
1715
const baseManifestPath = path.join(
18-
outputDir,
16+
buildOpts.outputDir,
1917
"server-functions/default",
2018
getPackagePath(buildOpts),
2119
".next"
2220
);
2321

24-
// We need to change the type in aws
25-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26-
const prerenderManifest = loadPrerenderManifest(baseManifestPath) as any;
22+
const prerenderManifest = loadPrerenderManifest(baseManifestPath);
2723
const previewModeId = prerenderManifest.preview.previewModeId;
28-
2924
const BUILD_ID = loadBuildId(baseManifestPath);
3025

3126
return esbuildSync(
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { filesToTree } from "../build/open-next/compile-skew-protection.js";
4+
import { isFileInTree } from "./skew-protection.js";
5+
6+
describe("isFileInTree", () => {
7+
const samplePaths = [
8+
"file1.txt",
9+
"folderA/fileA.txt",
10+
"folderA/subfolderB/fileB.txt",
11+
"folderC/fileC.txt",
12+
"folderC/nested/fileD.txt",
13+
"folderC/nested/anotherFile.txt",
14+
"/root_file_with_slash.txt",
15+
];
16+
const tree = filesToTree(samplePaths);
17+
18+
it("should return true for a file existing at the root", () => {
19+
expect(isFileInTree("file1.txt", tree)).toBe(true);
20+
});
21+
22+
it("should return true for a file existing in a single folder", () => {
23+
expect(isFileInTree("folderA/fileA.txt", tree)).toBe(true);
24+
});
25+
26+
it("should return true for a file existing in a nested folder", () => {
27+
expect(isFileInTree("folderA/subfolderB/fileB.txt", tree)).toBe(true);
28+
});
29+
30+
it("should return true for another file in a nested folder", () => {
31+
expect(isFileInTree("folderC/nested/anotherFile.txt", tree)).toBe(true);
32+
});
33+
34+
it("should return true for a file with a leading slash if it's at the root", () => {
35+
expect(isFileInTree("/root_file_with_slash.txt", tree)).toBe(true);
36+
});
37+
38+
it("should return false for a file that does not exist at the root", () => {
39+
expect(isFileInTree("nonexistent.txt", tree)).toBe(false);
40+
});
41+
42+
it("should return false for a file that does not exist in an existing folder", () => {
43+
expect(isFileInTree("folderA/nonexistent.txt", tree)).toBe(false);
44+
});
45+
46+
it("should return false for a file that does not exist in a nonexistent folder", () => {
47+
expect(isFileInTree("nonexistentFolder/file.txt", tree)).toBe(false);
48+
});
49+
50+
it("should return false for a file that does not exist in a nested nonexistent folder", () => {
51+
expect(isFileInTree("folderA/nonexistentSubfolder/file.txt", tree)).toBe(false);
52+
});
53+
54+
it("should return false for an empty filename", () => {
55+
expect(isFileInTree("", tree)).toBe(false);
56+
});
57+
58+
it("should return false for a filename that is just a folder name", () => {
59+
expect(isFileInTree("folderA", tree)).toBe(false);
60+
expect(isFileInTree("folderA/subfolderB", tree)).toBe(false);
61+
});
62+
63+
it("should return false for a filename that is just a folder name with a trailing slash", () => {
64+
expect(isFileInTree("folderA/", tree)).toBe(false);
65+
expect(isFileInTree("folderA/subfolderB/", tree)).toBe(false);
66+
});
67+
68+
it("should return false for a file in an incorrect path segment", () => {
69+
expect(isFileInTree("folderC/fileA.txt", tree)).toBe(false); // fileA.txt is in folderA
70+
});
71+
72+
it("should handle a tree with no files gracefully", () => {
73+
const emptyTree = filesToTree([]);
74+
expect(isFileInTree("anyfile.txt", emptyTree)).toBe(false);
75+
expect(isFileInTree("folder/file.txt", emptyTree)).toBe(false);
76+
});
77+
});

0 commit comments

Comments
 (0)