Skip to content

Commit 18dc7b5

Browse files
GregBrimblejrf0110
andauthored
Add wrangler pages project validate [directory] command (#3762)
* Add `wrangler pages project validate [directory]` command * Update packages/wrangler/src/pages/validate.tsx Co-authored-by: John Fawcett <[email protected]> * Add Pages team as codeowners of their own tests --------- Co-authored-by: John Fawcett <[email protected]>
1 parent 3bba1eb commit 18dc7b5

File tree

7 files changed

+213
-106
lines changed

7 files changed

+213
-106
lines changed

.changeset/dull-pets-search.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
feat: Add internal `wrangler pages project validate [directory]` command which validates an asset directory

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/packages/wrangler/pages/ @cloudflare/pages @cloudflare/wrangler
1010
/packages/wrangler/src/api/pages/ @cloudflare/pages @cloudflare/wrangler
1111
/packages/wrangler/src/pages/ @cloudflare/pages @cloudflare/wrangler
12+
/packages/wrangler/src/__tests__/pages/ @cloudflare/pages @cloudflare/wrangler
1213

1314
/packages/wrangler/src/api/d1/ @cloudflare/d1 @cloudflare/wrangler
1415
/packages/wrangler/src/d1/ @cloudflare/d1 @cloudflare/wrangler
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// /* eslint-disable no-shadow */
2+
import { writeFileSync } from "node:fs";
3+
import { endEventLoop } from "../helpers/end-event-loop";
4+
import { mockConsoleMethods } from "../helpers/mock-console";
5+
import { runInTempDir } from "../helpers/run-in-tmp";
6+
import { runWrangler } from "../helpers/run-wrangler";
7+
8+
jest.mock("../../pages/constants", () => ({
9+
...jest.requireActual("../../pages/constants"),
10+
MAX_ASSET_SIZE: 1 * 1024 * 1024,
11+
MAX_ASSET_COUNT: 10,
12+
}));
13+
14+
describe("project validate", () => {
15+
const std = mockConsoleMethods();
16+
17+
runInTempDir();
18+
19+
afterEach(async () => {
20+
// Force a tick to ensure that all promises resolve
21+
await endEventLoop();
22+
});
23+
24+
it("should exit cleanly for a good directory", async () => {
25+
writeFileSync("logo.png", "foobar");
26+
27+
await runWrangler("pages project validate .");
28+
29+
expect(std.out).toMatchInlineSnapshot(`""`);
30+
expect(std.err).toMatchInlineSnapshot(`""`);
31+
});
32+
33+
it("should error for a large file", async () => {
34+
writeFileSync("logo.png", Buffer.alloc(1 * 1024 * 1024 + 1));
35+
36+
await expect(() => runWrangler("pages project validate .")).rejects
37+
.toThrowErrorMatchingInlineSnapshot(`
38+
"Error: Pages only supports files up to 1.05 MB in size
39+
logo.png is 1.05 MB in size"
40+
`);
41+
});
42+
43+
it("should error for a large directory", async () => {
44+
for (let i = 0; i < 10 + 1; i++) {
45+
writeFileSync(`logo${i}.png`, Buffer.alloc(1));
46+
}
47+
48+
await expect(() =>
49+
runWrangler("pages project validate .")
50+
).rejects.toThrowErrorMatchingInlineSnapshot(
51+
`"Error: Pages only supports up to 10 files in a deployment. Ensure you have specified your build output directory correctly."`
52+
);
53+
});
54+
});

packages/wrangler/src/api/pages/deploy.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from "../../pages/functions/buildWorker";
1919
import { validateRoutes } from "../../pages/functions/routes-validation";
2020
import { upload } from "../../pages/upload";
21+
import { validate } from "../../pages/validate";
2122
import { createUploadWorkerBundleContents } from "./create-worker-bundle-contents";
2223
import type { BundleResult } from "../../deployment-bundle/bundle";
2324
import type { Project, Deployment } from "@cloudflare/types";
@@ -196,8 +197,10 @@ export async function deploy({
196197
}
197198
}
198199

200+
const fileMap = await validate({ directory });
201+
199202
const manifest = await upload({
200-
directory,
203+
fileMap,
201204
accountId,
202205
projectName,
203206
skipCaching: skipCaching ?? false,

packages/wrangler/src/pages/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as Functions from "./functions";
99
import * as Projects from "./projects";
1010
import * as Upload from "./upload";
1111
import { CLEANUP } from "./utils";
12+
import * as Validate from "./validate";
1213
import type { CommonYargsArgv } from "../yargs-types";
1314

1415
process.on("SIGINT", () => {
@@ -69,6 +70,12 @@ export function pages(yargs: CommonYargsArgv) {
6970
Projects.DeleteHandler
7071
)
7172
.command("upload [directory]", false, Upload.Options, Upload.Handler)
73+
.command(
74+
"validate [directory]",
75+
false,
76+
Validate.Options,
77+
Validate.Handler
78+
)
7279
)
7380
.command(
7481
"deployment",

packages/wrangler/src/pages/upload.tsx

+15-105
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,28 @@
1-
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2-
import { dirname, join, relative, resolve, sep } from "node:path";
1+
import { mkdir, readFile, writeFile } from "node:fs/promises";
2+
import { dirname } from "node:path";
33
import { render, Text } from "ink";
44
import Spinner from "ink-spinner";
5-
import { getType } from "mime";
6-
import { Minimatch } from "minimatch";
75
import PQueue from "p-queue";
8-
import prettyBytes from "pretty-bytes";
96
import React from "react";
107
import { fetchResult } from "../cfetch";
118
import { FatalError } from "../errors";
129
import isInteractive from "../is-interactive";
1310
import { logger } from "../logger";
1411
import {
15-
MAX_ASSET_COUNT,
16-
MAX_ASSET_SIZE,
1712
BULK_UPLOAD_CONCURRENCY,
1813
MAX_BUCKET_FILE_COUNT,
1914
MAX_BUCKET_SIZE,
2015
MAX_CHECK_MISSING_ATTEMPTS,
2116
MAX_UPLOAD_ATTEMPTS,
2217
} from "./constants";
23-
import { hashFile } from "./hash";
2418

19+
import { validate } from "./validate";
2520
import type {
2621
CommonYargsArgv,
2722
StrictYargsOptionsToInterface,
2823
} from "../yargs-types";
2924
import type { UploadPayloadFile } from "./types";
25+
import type { FileContainer } from "./validate";
3026

3127
type UploadArgs = StrictYargsOptionsToInterface<typeof Options>;
3228

@@ -62,8 +58,10 @@ export const Handler = async ({
6258
throw new FatalError("No JWT given.", 1);
6359
}
6460

61+
const fileMap = await validate({ directory });
62+
6563
const manifest = await upload({
66-
directory,
64+
fileMap,
6765
jwt: process.env.CF_PAGES_UPLOAD_JWT,
6866
skipCaching: skipCaching ?? false,
6967
});
@@ -79,12 +77,12 @@ export const Handler = async ({
7977
export const upload = async (
8078
args:
8179
| {
82-
directory: string;
80+
fileMap: Map<string, FileContainer>;
8381
jwt: string;
8482
skipCaching: boolean;
8583
}
8684
| {
87-
directory: string;
85+
fileMap: Map<string, FileContainer>;
8886
accountId: string;
8987
projectName: string;
9088
skipCaching: boolean;
@@ -102,95 +100,7 @@ export const upload = async (
102100
}
103101
}
104102

105-
type FileContainer = {
106-
path: string;
107-
contentType: string;
108-
sizeInBytes: number;
109-
hash: string;
110-
};
111-
112-
const IGNORE_LIST = [
113-
"_worker.js",
114-
"_redirects",
115-
"_headers",
116-
"_routes.json",
117-
"functions",
118-
"**/.DS_Store",
119-
"**/node_modules",
120-
"**/.git",
121-
].map((pattern) => new Minimatch(pattern));
122-
123-
const directory = resolve(args.directory);
124-
125-
// TODO(future): Use this to more efficiently load files in and speed up uploading
126-
// Limit memory to 1 GB unless more is specified
127-
// let maxMemory = 1_000_000_000;
128-
// if (process.env.NODE_OPTIONS && (process.env.NODE_OPTIONS.includes('--max-old-space-size=') || process.env.NODE_OPTIONS.includes('--max_old_space_size='))) {
129-
// const parsed = parser(process.env.NODE_OPTIONS);
130-
// maxMemory = (parsed['max-old-space-size'] ? parsed['max-old-space-size'] : parsed['max_old_space_size']) * 1000 * 1000; // Turn MB into bytes
131-
// }
132-
133-
const walk = async (
134-
dir: string,
135-
fileMap: Map<string, FileContainer> = new Map(),
136-
startingDir: string = dir
137-
) => {
138-
const files = await readdir(dir);
139-
140-
await Promise.all(
141-
files.map(async (file) => {
142-
const filepath = join(dir, file);
143-
const relativeFilepath = relative(startingDir, filepath);
144-
const filestat = await stat(filepath);
145-
146-
for (const minimatch of IGNORE_LIST) {
147-
if (minimatch.match(relativeFilepath)) {
148-
return;
149-
}
150-
}
151-
152-
if (filestat.isSymbolicLink()) {
153-
return;
154-
}
155-
156-
if (filestat.isDirectory()) {
157-
fileMap = await walk(filepath, fileMap, startingDir);
158-
} else {
159-
const name = relativeFilepath.split(sep).join("/");
160-
161-
if (filestat.size > MAX_ASSET_SIZE) {
162-
throw new FatalError(
163-
`Error: Pages only supports files up to ${prettyBytes(
164-
MAX_ASSET_SIZE
165-
)} in size\n${name} is ${prettyBytes(filestat.size)} in size`,
166-
1
167-
);
168-
}
169-
170-
// We don't want to hold the content in memory. We instead only want to read it when it's needed
171-
fileMap.set(name, {
172-
path: filepath,
173-
contentType: getType(name) || "application/octet-stream",
174-
sizeInBytes: filestat.size,
175-
hash: hashFile(filepath),
176-
});
177-
}
178-
})
179-
);
180-
181-
return fileMap;
182-
};
183-
184-
const fileMap = await walk(directory);
185-
186-
if (fileMap.size > MAX_ASSET_COUNT) {
187-
throw new FatalError(
188-
`Error: Pages only supports up to ${MAX_ASSET_COUNT.toLocaleString()} files in a deployment. Ensure you have specified your build output directory correctly.`,
189-
1
190-
);
191-
}
192-
193-
const files = [...fileMap.values()];
103+
const files = [...args.fileMap.values()];
194104

195105
let jwt = await fetchJwt();
196106

@@ -274,8 +184,8 @@ export const upload = async (
274184
bucketOffset++;
275185
}
276186

277-
let counter = fileMap.size - sortedFiles.length;
278-
const { rerender, unmount } = renderProgress(counter, fileMap.size);
187+
let counter = args.fileMap.size - sortedFiles.length;
188+
const { rerender, unmount } = renderProgress(counter, args.fileMap.size);
279189

280190
const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
281191

@@ -333,7 +243,7 @@ export const upload = async (
333243
doUpload().then(
334244
() => {
335245
counter += bucket.files.length;
336-
rerender(counter, fileMap.size);
246+
rerender(counter, args.fileMap.size);
337247
},
338248
(error) => {
339249
return Promise.reject(
@@ -355,7 +265,7 @@ export const upload = async (
355265

356266
const uploadMs = Date.now() - start;
357267

358-
const skipped = fileMap.size - missingHashes.length;
268+
const skipped = args.fileMap.size - missingHashes.length;
359269
const skippedMessage = skipped > 0 ? `(${skipped} already uploaded) ` : "";
360270

361271
logger.log(
@@ -406,7 +316,7 @@ export const upload = async (
406316
}
407317

408318
return Object.fromEntries(
409-
[...fileMap.entries()].map(([fileName, file]) => [
319+
[...args.fileMap.entries()].map(([fileName, file]) => [
410320
`/${fileName}`,
411321
file.hash,
412322
])

0 commit comments

Comments
 (0)