wrangler 0.0.0-e2b7352 → 0.0.0-e2f9bb2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -13
- package/bin/wrangler.js +3 -0
- package/miniflare-dist/index.mjs +4828 -21
- package/package.json +29 -22
- package/pages/functions/buildPlugin.ts +92 -0
- package/pages/functions/buildWorker.ts +80 -4
- package/pages/functions/filepath-routing.test.ts +11 -0
- package/pages/functions/filepath-routing.ts +8 -0
- package/pages/functions/routes.ts +6 -1
- package/pages/functions/template-plugin.ts +146 -0
- package/pages/functions/template-worker.ts +21 -8
- package/src/__tests__/config-cache.test.ts +36 -0
- package/src/__tests__/configuration.test.ts +386 -81
- package/src/__tests__/dev.test.tsx +242 -21
- package/src/__tests__/guess-worker-format.test.ts +18 -1
- package/src/__tests__/helpers/mock-cfetch.ts +5 -1
- package/src/__tests__/helpers/mock-console.ts +20 -3
- package/src/__tests__/helpers/mock-dialogs.ts +24 -0
- package/src/__tests__/helpers/mock-http-server.ts +46 -0
- package/src/__tests__/helpers/mock-istty.ts +27 -0
- package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
- package/src/__tests__/helpers/run-in-tmp.ts +7 -0
- package/src/__tests__/helpers/write-worker-source.ts +31 -0
- package/src/__tests__/https-options.test.ts +5 -2
- package/src/__tests__/index.test.ts +25 -759
- package/src/__tests__/init.test.ts +1042 -0
- package/src/__tests__/jest.setup.ts +4 -4
- package/src/__tests__/kv.test.ts +130 -56
- package/src/__tests__/logger.test.ts +115 -0
- package/src/__tests__/pages.test.ts +290 -43
- package/src/__tests__/publish.test.ts +1699 -333
- package/src/__tests__/r2.test.ts +24 -8
- package/src/__tests__/route.test.ts +4 -4
- package/src/__tests__/secret.test.ts +55 -34
- package/src/__tests__/tail.test.ts +97 -45
- package/src/__tests__/user.test.ts +131 -0
- package/src/__tests__/whoami.test.tsx +14 -9
- package/src/bundle.ts +15 -1
- package/src/cfetch/internal.ts +0 -2
- package/src/cli.ts +5 -11
- package/src/config/README.md +1 -1
- package/src/config/config.ts +20 -4
- package/src/config/environment.ts +26 -3
- package/src/config/index.ts +2 -1
- package/src/config/validation-helpers.ts +26 -5
- package/src/config/validation.ts +233 -41
- package/src/config-cache.ts +45 -0
- package/src/create-worker-preview.ts +22 -13
- package/src/create-worker-upload-form.ts +32 -9
- package/src/dev/dev-vars.ts +36 -0
- package/src/dev/dev.tsx +60 -34
- package/src/dev/local.tsx +101 -21
- package/src/dev/remote.tsx +23 -13
- package/src/dev/use-esbuild.ts +19 -5
- package/src/dialogs.tsx +16 -6
- package/src/entry.ts +38 -11
- package/src/environment-variables.ts +3 -1
- package/src/errors.ts +11 -0
- package/src/https-options.ts +3 -2
- package/src/index.tsx +529 -303
- package/src/inspect.ts +17 -11
- package/src/logger.ts +66 -0
- package/src/miniflare-cli/index.ts +4 -0
- package/src/miniflare-cli/request-context.ts +40 -0
- package/src/module-collection.ts +7 -7
- package/src/open-in-browser.ts +5 -4
- package/src/package-manager.ts +9 -8
- package/src/pages.tsx +860 -55
- package/src/parse.ts +1 -1
- package/src/proxy.ts +25 -11
- package/src/publish.ts +260 -141
- package/src/sites.tsx +27 -14
- package/src/tail/index.ts +1 -1
- package/src/tail/printing.ts +13 -8
- package/src/update-check.ts +19 -0
- package/src/user.tsx +104 -38
- package/src/whoami.tsx +2 -1
- package/src/worker.ts +15 -6
- package/templates/gitignore +171 -0
- package/templates/static-asset-facade.js +1 -1
- package/wrangler-dist/cli.js +20948 -50169
- package/miniflare-dist/index.mjs.map +0 -7
- package/src/__tests__/logout.test.ts +0 -52
- package/src/__tests__/sentry.test.ts +0 -146
- package/src/reporting.ts +0 -131
- package/wrangler-dist/cli.js.map +0 -7
package/src/pages.tsx
CHANGED
|
@@ -2,21 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
import { execSync, spawn } from "node:child_process";
|
|
4
4
|
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
+
import { dirname, join, sep } from "node:path";
|
|
8
|
+
import { cwd } from "node:process";
|
|
7
9
|
import { URL } from "node:url";
|
|
10
|
+
import { hash } from "blake3-wasm";
|
|
8
11
|
import { watch } from "chokidar";
|
|
12
|
+
import { render, Text } from "ink";
|
|
13
|
+
import SelectInput from "ink-select-input";
|
|
14
|
+
import Spinner from "ink-spinner";
|
|
15
|
+
import Table from "ink-table";
|
|
9
16
|
import { getType } from "mime";
|
|
17
|
+
import prettyBytes from "pretty-bytes";
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { format as timeagoFormat } from "timeago.js";
|
|
20
|
+
import { File, FormData } from "undici";
|
|
21
|
+
import { buildPlugin } from "../pages/functions/buildPlugin";
|
|
10
22
|
import { buildWorker } from "../pages/functions/buildWorker";
|
|
11
23
|
import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
|
|
12
24
|
import { writeRoutesModule } from "../pages/functions/routes";
|
|
25
|
+
import { fetchResult } from "./cfetch";
|
|
26
|
+
import { getConfigCache, saveToConfigCache } from "./config-cache";
|
|
27
|
+
import { prompt } from "./dialogs";
|
|
28
|
+
import { FatalError } from "./errors";
|
|
29
|
+
import { logger } from "./logger";
|
|
30
|
+
import { getRequestContextCheckOptions } from "./miniflare-cli/request-context";
|
|
13
31
|
import openInBrowser from "./open-in-browser";
|
|
14
32
|
import { toUrlPath } from "./paths";
|
|
33
|
+
import { requireAuth } from "./user";
|
|
15
34
|
import type { Config } from "../pages/functions/routes";
|
|
16
35
|
import type { Headers, Request, fetch } from "@miniflare/core";
|
|
17
36
|
import type { BuildResult } from "esbuild";
|
|
18
37
|
import type { MiniflareOptions } from "miniflare";
|
|
19
|
-
import type { BuilderCallback } from "yargs";
|
|
38
|
+
import type { BuilderCallback, CommandModule } from "yargs";
|
|
39
|
+
|
|
40
|
+
export type Project = {
|
|
41
|
+
name: string;
|
|
42
|
+
subdomain: string;
|
|
43
|
+
domains: Array<string>;
|
|
44
|
+
source?: {
|
|
45
|
+
type: string;
|
|
46
|
+
};
|
|
47
|
+
latest_deployment?: {
|
|
48
|
+
modified_on: string;
|
|
49
|
+
};
|
|
50
|
+
created_on: string;
|
|
51
|
+
production_branch: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type Deployment = {
|
|
55
|
+
id: string;
|
|
56
|
+
environment: string;
|
|
57
|
+
deployment_trigger: {
|
|
58
|
+
metadata: {
|
|
59
|
+
commit_hash: string;
|
|
60
|
+
branch: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
url: string;
|
|
64
|
+
latest_stage: {
|
|
65
|
+
status: string;
|
|
66
|
+
ended_on: string;
|
|
67
|
+
};
|
|
68
|
+
project_name: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
interface PagesConfigCache {
|
|
72
|
+
account_id?: string;
|
|
73
|
+
project_name?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PAGES_CONFIG_CACHE_FILENAME = "pages.json";
|
|
20
77
|
|
|
21
78
|
// Defer importing miniflare until we really need it. This takes ~0.5s
|
|
22
79
|
// and also modifies some `stream/web` and `undici` prototypes, so we
|
|
@@ -25,17 +82,20 @@ import type { BuilderCallback } from "yargs";
|
|
|
25
82
|
export const pagesBetaWarning =
|
|
26
83
|
"🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose";
|
|
27
84
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
if (code) process.exitCode = code;
|
|
32
|
-
EXIT_CALLBACKS.forEach((callback) => callback());
|
|
85
|
+
const CLEANUP_CALLBACKS: (() => void)[] = [];
|
|
86
|
+
const CLEANUP = () => {
|
|
87
|
+
CLEANUP_CALLBACKS.forEach((callback) => callback());
|
|
33
88
|
RUNNING_BUILDERS.forEach((builder) => builder.stop?.());
|
|
34
|
-
process.exit(code);
|
|
35
89
|
};
|
|
36
90
|
|
|
37
|
-
process.on("SIGINT", () =>
|
|
38
|
-
|
|
91
|
+
process.on("SIGINT", () => {
|
|
92
|
+
CLEANUP();
|
|
93
|
+
process.exit();
|
|
94
|
+
});
|
|
95
|
+
process.on("SIGTERM", () => {
|
|
96
|
+
CLEANUP();
|
|
97
|
+
process.exit();
|
|
98
|
+
});
|
|
39
99
|
|
|
40
100
|
function isWindows() {
|
|
41
101
|
return process.platform === "win32";
|
|
@@ -95,7 +155,7 @@ function getPort(pid: number) {
|
|
|
95
155
|
const match = matches[0];
|
|
96
156
|
if (match) return parseInt(match[1]);
|
|
97
157
|
} catch (thrown) {
|
|
98
|
-
|
|
158
|
+
logger.error(
|
|
99
159
|
`Error scanning for ports of process with PID ${pid}: ${thrown}`
|
|
100
160
|
);
|
|
101
161
|
}
|
|
@@ -108,13 +168,15 @@ async function spawnProxyProcess({
|
|
|
108
168
|
port?: number;
|
|
109
169
|
command: (string | number)[];
|
|
110
170
|
}): Promise<void | number> {
|
|
111
|
-
if (command.length === 0)
|
|
112
|
-
|
|
171
|
+
if (command.length === 0) {
|
|
172
|
+
CLEANUP();
|
|
173
|
+
throw new FatalError(
|
|
113
174
|
"Must specify a directory of static assets to serve or a command to run.",
|
|
114
175
|
1
|
|
115
176
|
);
|
|
177
|
+
}
|
|
116
178
|
|
|
117
|
-
|
|
179
|
+
logger.log(`Running ${command.join(" ")}...`);
|
|
118
180
|
const proxy = spawn(
|
|
119
181
|
command[0].toString(),
|
|
120
182
|
command.slice(1).map((value) => value.toString()),
|
|
@@ -126,30 +188,30 @@ async function spawnProxyProcess({
|
|
|
126
188
|
},
|
|
127
189
|
}
|
|
128
190
|
);
|
|
129
|
-
|
|
191
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
130
192
|
proxy.kill();
|
|
131
193
|
});
|
|
132
194
|
|
|
133
195
|
proxy.stdout.on("data", (data) => {
|
|
134
|
-
|
|
196
|
+
logger.log(`[proxy]: ${data}`);
|
|
135
197
|
});
|
|
136
198
|
|
|
137
199
|
proxy.stderr.on("data", (data) => {
|
|
138
|
-
|
|
200
|
+
logger.error(`[proxy]: ${data}`);
|
|
139
201
|
});
|
|
140
202
|
|
|
141
203
|
proxy.on("close", (code) => {
|
|
142
|
-
|
|
204
|
+
logger.error(`Proxy exited with status ${code}.`);
|
|
143
205
|
});
|
|
144
206
|
|
|
145
207
|
// Wait for proxy process to start...
|
|
146
208
|
while (!proxy.pid) {}
|
|
147
209
|
|
|
148
210
|
if (port === undefined) {
|
|
149
|
-
|
|
211
|
+
logger.log(
|
|
150
212
|
`Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
|
|
151
213
|
);
|
|
152
|
-
|
|
214
|
+
logger.log("To skip, specify the proxy port with --proxy.");
|
|
153
215
|
await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
|
|
154
216
|
|
|
155
217
|
port = getPids(proxy.pid)
|
|
@@ -157,12 +219,13 @@ async function spawnProxyProcess({
|
|
|
157
219
|
.filter((port) => port !== undefined)[0];
|
|
158
220
|
|
|
159
221
|
if (port === undefined) {
|
|
160
|
-
|
|
222
|
+
CLEANUP();
|
|
223
|
+
throw new FatalError(
|
|
161
224
|
"Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
|
|
162
225
|
1
|
|
163
226
|
);
|
|
164
227
|
} else {
|
|
165
|
-
|
|
228
|
+
logger.log(`Automatically determined the proxy port to be ${port}.`);
|
|
166
229
|
}
|
|
167
230
|
}
|
|
168
231
|
|
|
@@ -429,12 +492,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
429
492
|
}).on("change", (path) => {
|
|
430
493
|
switch (path) {
|
|
431
494
|
case headersFile: {
|
|
432
|
-
|
|
495
|
+
logger.log("_headers modified. Re-evaluating...");
|
|
433
496
|
headersMatcher = generateHeadersMatcher(headersFile);
|
|
434
497
|
break;
|
|
435
498
|
}
|
|
436
499
|
case redirectsFile: {
|
|
437
|
-
|
|
500
|
+
logger.log("_redirects modified. Re-evaluating...");
|
|
438
501
|
redirectsMatcher = generateRedirectsMatcher(redirectsFile);
|
|
439
502
|
break;
|
|
440
503
|
}
|
|
@@ -647,7 +710,7 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
647
710
|
const RUNNING_BUILDERS: BuildResult[] = [];
|
|
648
711
|
|
|
649
712
|
async function buildFunctions({
|
|
650
|
-
|
|
713
|
+
outfile,
|
|
651
714
|
outputConfigPath,
|
|
652
715
|
functionsDirectory,
|
|
653
716
|
minify = false,
|
|
@@ -655,8 +718,10 @@ async function buildFunctions({
|
|
|
655
718
|
fallbackService = "ASSETS",
|
|
656
719
|
watch = false,
|
|
657
720
|
onEnd,
|
|
721
|
+
plugin = false,
|
|
722
|
+
buildOutputDirectory,
|
|
658
723
|
}: {
|
|
659
|
-
|
|
724
|
+
outfile: string;
|
|
660
725
|
outputConfigPath?: string;
|
|
661
726
|
functionsDirectory: string;
|
|
662
727
|
minify?: boolean;
|
|
@@ -664,6 +729,8 @@ async function buildFunctions({
|
|
|
664
729
|
fallbackService?: string;
|
|
665
730
|
watch?: boolean;
|
|
666
731
|
onEnd?: () => void;
|
|
732
|
+
plugin?: boolean;
|
|
733
|
+
buildOutputDirectory?: string;
|
|
667
734
|
}) {
|
|
668
735
|
RUNNING_BUILDERS.forEach(
|
|
669
736
|
(runningBuilder) => runningBuilder.stop && runningBuilder.stop()
|
|
@@ -690,19 +757,483 @@ async function buildFunctions({
|
|
|
690
757
|
outfile: routesModule,
|
|
691
758
|
});
|
|
692
759
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
760
|
+
if (plugin) {
|
|
761
|
+
RUNNING_BUILDERS.push(
|
|
762
|
+
await buildPlugin({
|
|
763
|
+
routesModule,
|
|
764
|
+
outfile,
|
|
765
|
+
minify,
|
|
766
|
+
sourcemap,
|
|
767
|
+
watch,
|
|
768
|
+
onEnd,
|
|
769
|
+
})
|
|
770
|
+
);
|
|
771
|
+
} else {
|
|
772
|
+
RUNNING_BUILDERS.push(
|
|
773
|
+
await buildWorker({
|
|
774
|
+
routesModule,
|
|
775
|
+
outfile,
|
|
776
|
+
minify,
|
|
777
|
+
sourcemap,
|
|
778
|
+
fallbackService,
|
|
779
|
+
watch,
|
|
780
|
+
onEnd,
|
|
781
|
+
buildOutputDirectory,
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
interface CreateDeploymentArgs {
|
|
788
|
+
directory: string;
|
|
789
|
+
projectName?: string;
|
|
790
|
+
branch?: string;
|
|
791
|
+
commitHash?: string;
|
|
792
|
+
commitMessage?: string;
|
|
793
|
+
commitDirty?: boolean;
|
|
704
794
|
}
|
|
705
795
|
|
|
796
|
+
const createDeployment: CommandModule<
|
|
797
|
+
CreateDeploymentArgs,
|
|
798
|
+
CreateDeploymentArgs
|
|
799
|
+
> = {
|
|
800
|
+
describe: "🆙 Publish a directory of static assets as a Pages deployment",
|
|
801
|
+
builder: (yargs) => {
|
|
802
|
+
return yargs
|
|
803
|
+
.positional("directory", {
|
|
804
|
+
type: "string",
|
|
805
|
+
default: "functions",
|
|
806
|
+
description: "The directory of Pages Functions",
|
|
807
|
+
})
|
|
808
|
+
.options({
|
|
809
|
+
"project-name": {
|
|
810
|
+
type: "string",
|
|
811
|
+
description:
|
|
812
|
+
"The name of the project you want to list deployments for",
|
|
813
|
+
},
|
|
814
|
+
branch: {
|
|
815
|
+
type: "string",
|
|
816
|
+
description:
|
|
817
|
+
"The branch of the project you want to list deployments for",
|
|
818
|
+
},
|
|
819
|
+
"commit-hash": {
|
|
820
|
+
type: "string",
|
|
821
|
+
description:
|
|
822
|
+
"The branch of the project you want to list deployments for",
|
|
823
|
+
},
|
|
824
|
+
"commit-message": {
|
|
825
|
+
type: "string",
|
|
826
|
+
description:
|
|
827
|
+
"The branch of the project you want to list deployments for",
|
|
828
|
+
},
|
|
829
|
+
"commit-dirty": {
|
|
830
|
+
type: "boolean",
|
|
831
|
+
description:
|
|
832
|
+
"The branch of the project you want to list deployments for",
|
|
833
|
+
},
|
|
834
|
+
})
|
|
835
|
+
.epilogue(pagesBetaWarning);
|
|
836
|
+
},
|
|
837
|
+
handler: async ({
|
|
838
|
+
directory,
|
|
839
|
+
projectName,
|
|
840
|
+
branch,
|
|
841
|
+
commitHash,
|
|
842
|
+
commitMessage,
|
|
843
|
+
commitDirty,
|
|
844
|
+
}) => {
|
|
845
|
+
const config = getConfigCache<PagesConfigCache>(
|
|
846
|
+
PAGES_CONFIG_CACHE_FILENAME
|
|
847
|
+
);
|
|
848
|
+
const accountId = await requireAuth(config);
|
|
849
|
+
|
|
850
|
+
projectName ??= config.project_name;
|
|
851
|
+
|
|
852
|
+
const isInteractive = process.stdin.isTTY;
|
|
853
|
+
if (!projectName && isInteractive) {
|
|
854
|
+
const existingOrNew = await new Promise<"new" | "existing">((resolve) => {
|
|
855
|
+
const { unmount } = render(
|
|
856
|
+
<>
|
|
857
|
+
<Text>
|
|
858
|
+
No project selected. Would you like to create one or use an
|
|
859
|
+
existing project?
|
|
860
|
+
</Text>
|
|
861
|
+
<SelectInput
|
|
862
|
+
items={[
|
|
863
|
+
{
|
|
864
|
+
key: "new",
|
|
865
|
+
label: "Create a new project",
|
|
866
|
+
value: "new",
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
key: "existing",
|
|
870
|
+
label: "Use an existing project",
|
|
871
|
+
value: "existing",
|
|
872
|
+
},
|
|
873
|
+
]}
|
|
874
|
+
onSelect={async (selected) => {
|
|
875
|
+
resolve(selected.value as "new" | "existing");
|
|
876
|
+
unmount();
|
|
877
|
+
}}
|
|
878
|
+
/>
|
|
879
|
+
</>
|
|
880
|
+
);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
switch (existingOrNew) {
|
|
884
|
+
case "existing": {
|
|
885
|
+
const projects = (await listProjects({ accountId })).filter(
|
|
886
|
+
(project) => !project.source
|
|
887
|
+
);
|
|
888
|
+
projectName = await new Promise((resolve) => {
|
|
889
|
+
const { unmount } = render(
|
|
890
|
+
<>
|
|
891
|
+
<Text>Select a project:</Text>
|
|
892
|
+
<SelectInput
|
|
893
|
+
items={projects.map((project) => ({
|
|
894
|
+
key: project.name,
|
|
895
|
+
label: project.name,
|
|
896
|
+
value: project,
|
|
897
|
+
}))}
|
|
898
|
+
onSelect={async (selected) => {
|
|
899
|
+
resolve(selected.value.name);
|
|
900
|
+
unmount();
|
|
901
|
+
}}
|
|
902
|
+
/>
|
|
903
|
+
</>
|
|
904
|
+
);
|
|
905
|
+
});
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
case "new": {
|
|
909
|
+
projectName = await prompt("Enter the name of your new project:");
|
|
910
|
+
|
|
911
|
+
if (!projectName) {
|
|
912
|
+
throw new FatalError("Must specify a project name.", 1);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
let isGitDir = true;
|
|
916
|
+
try {
|
|
917
|
+
execSync(`git rev-parse --is-inside-work-tree`, {
|
|
918
|
+
stdio: "ignore",
|
|
919
|
+
});
|
|
920
|
+
} catch (err) {
|
|
921
|
+
isGitDir = false;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const productionBranch = await prompt(
|
|
925
|
+
"Enter the production branch name:",
|
|
926
|
+
"text",
|
|
927
|
+
isGitDir
|
|
928
|
+
? execSync(`git rev-parse --abbrev-ref HEAD`).toString().trim()
|
|
929
|
+
: "production"
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
if (!productionBranch) {
|
|
933
|
+
throw new FatalError("Must specify a production branch.", 1);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
await fetchResult<Project>(`/accounts/${accountId}/pages/projects`, {
|
|
937
|
+
method: "POST",
|
|
938
|
+
body: JSON.stringify({
|
|
939
|
+
name: projectName,
|
|
940
|
+
production_branch: productionBranch,
|
|
941
|
+
}),
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
|
|
945
|
+
account_id: accountId,
|
|
946
|
+
project_name: projectName,
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
logger.log(`✨ Successfully created the '${projectName}' project.`);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!projectName) {
|
|
956
|
+
throw new FatalError("Must specify a project name.", 1);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// We infer git info by default is not passed in
|
|
960
|
+
|
|
961
|
+
let isGitDir = true;
|
|
962
|
+
try {
|
|
963
|
+
execSync(`git rev-parse --is-inside-work-tree`, {
|
|
964
|
+
stdio: "ignore",
|
|
965
|
+
});
|
|
966
|
+
} catch (err) {
|
|
967
|
+
isGitDir = false;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
let isGitDirty = false;
|
|
971
|
+
|
|
972
|
+
if (isGitDir) {
|
|
973
|
+
try {
|
|
974
|
+
isGitDirty = Boolean(
|
|
975
|
+
execSync(`git status --porcelain`).toString().length
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
if (!branch) {
|
|
979
|
+
branch = execSync(`git rev-parse --abbrev-ref HEAD`)
|
|
980
|
+
.toString()
|
|
981
|
+
.trim();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (!commitHash) {
|
|
985
|
+
commitHash = execSync(`git rev-parse HEAD`).toString().trim();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (!commitMessage) {
|
|
989
|
+
commitMessage = execSync(`git show -s --format=%B ${commitHash}`)
|
|
990
|
+
.toString()
|
|
991
|
+
.trim();
|
|
992
|
+
}
|
|
993
|
+
} catch (err) {}
|
|
994
|
+
|
|
995
|
+
if (isGitDirty && !commitDirty) {
|
|
996
|
+
logger.warn(
|
|
997
|
+
`Warning: Your working directory is a git repo and has uncommitted changes\nTo silense this warning, pass in --commit-dirty=true`
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (commitDirty === undefined) {
|
|
1002
|
+
commitDirty = isGitDirty;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
let builtFunctions: string | undefined = undefined;
|
|
1007
|
+
const functionsDirectory = join(cwd(), "functions");
|
|
1008
|
+
if (existsSync(functionsDirectory)) {
|
|
1009
|
+
const outfile = join(tmpdir(), "./functionsWorker.js");
|
|
1010
|
+
|
|
1011
|
+
await new Promise((resolve) =>
|
|
1012
|
+
buildFunctions({
|
|
1013
|
+
outfile,
|
|
1014
|
+
functionsDirectory,
|
|
1015
|
+
onEnd: () => resolve(null),
|
|
1016
|
+
buildOutputDirectory: dirname(outfile),
|
|
1017
|
+
})
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
builtFunctions = readFileSync(outfile, "utf-8");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
type File = {
|
|
1024
|
+
content: Buffer;
|
|
1025
|
+
metadata: Metadata;
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
type Metadata = {
|
|
1029
|
+
sizeInBytes: number;
|
|
1030
|
+
hash: string;
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const IGNORE_LIST = [
|
|
1034
|
+
"_worker.js",
|
|
1035
|
+
"_redirects",
|
|
1036
|
+
"_headers",
|
|
1037
|
+
".DS_Store",
|
|
1038
|
+
"node_modules",
|
|
1039
|
+
];
|
|
1040
|
+
|
|
1041
|
+
const walk = async (
|
|
1042
|
+
dir: string,
|
|
1043
|
+
fileMap: Map<string, File> = new Map(),
|
|
1044
|
+
depth = 0
|
|
1045
|
+
) => {
|
|
1046
|
+
const files = await readdir(dir);
|
|
1047
|
+
|
|
1048
|
+
await Promise.all(
|
|
1049
|
+
files.map(async (file) => {
|
|
1050
|
+
const filepath = join(dir, file);
|
|
1051
|
+
const filestat = await stat(filepath);
|
|
1052
|
+
|
|
1053
|
+
if (IGNORE_LIST.includes(file)) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (filestat.isSymbolicLink()) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (filestat.isDirectory()) {
|
|
1062
|
+
fileMap = await walk(filepath, fileMap, depth + 1);
|
|
1063
|
+
} else {
|
|
1064
|
+
let name;
|
|
1065
|
+
if (depth) {
|
|
1066
|
+
name = filepath.split(sep).slice(1).join("/");
|
|
1067
|
+
} else {
|
|
1068
|
+
name = file;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// TODO: Move this to later so we don't hold as much in memory
|
|
1072
|
+
const fileContent = await readFile(filepath);
|
|
1073
|
+
|
|
1074
|
+
const base64Content = fileContent.toString("base64");
|
|
1075
|
+
const extension =
|
|
1076
|
+
name.split(".").length > 1 ? name.split(".").at(-1) || "" : "";
|
|
1077
|
+
|
|
1078
|
+
const content = base64Content + extension;
|
|
1079
|
+
|
|
1080
|
+
if (filestat.size > 25 * 1024 * 1024) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
`Error: Pages only supports files up to ${prettyBytes(
|
|
1083
|
+
25 * 1024 * 1024
|
|
1084
|
+
)} in size\n${name} is ${prettyBytes(filestat.size)} in size`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
fileMap.set(name, {
|
|
1089
|
+
content: fileContent,
|
|
1090
|
+
metadata: {
|
|
1091
|
+
sizeInBytes: filestat.size,
|
|
1092
|
+
hash: hash(content).toString("hex").slice(0, 32),
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
})
|
|
1097
|
+
);
|
|
1098
|
+
|
|
1099
|
+
return fileMap;
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
const fileMap = await walk(directory);
|
|
1103
|
+
|
|
1104
|
+
const start = Date.now();
|
|
1105
|
+
|
|
1106
|
+
const files: Array<Promise<void>> = [];
|
|
1107
|
+
|
|
1108
|
+
if (fileMap.size > 1000) {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
`Error: Pages only supports up to 1,000 files in a deployment at the moment.\nTry a smaller project perhaps?`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
let counter = 0;
|
|
1115
|
+
|
|
1116
|
+
const { rerender, unmount } = render(
|
|
1117
|
+
<Progress done={counter} total={fileMap.size} />
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
fileMap.forEach((file: File, name: string) => {
|
|
1121
|
+
const form = new FormData();
|
|
1122
|
+
form.append(
|
|
1123
|
+
"file",
|
|
1124
|
+
new File([new Uint8Array(file.content.buffer)], name)
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
// TODO: Consider a retry
|
|
1128
|
+
|
|
1129
|
+
const promise = fetchResult<{ id: string }>(
|
|
1130
|
+
`/accounts/${accountId}/pages/projects/${projectName}/file`,
|
|
1131
|
+
{
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
body: form,
|
|
1134
|
+
}
|
|
1135
|
+
).then((response) => {
|
|
1136
|
+
counter++;
|
|
1137
|
+
rerender(<Progress done={counter} total={fileMap.size} />);
|
|
1138
|
+
if (response.id != file.metadata.hash) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
`Looks like there was an issue uploading '${name}'. Try again perhaps?`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
files.push(promise);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
await Promise.all(files);
|
|
1149
|
+
|
|
1150
|
+
unmount();
|
|
1151
|
+
|
|
1152
|
+
const uploadMs = Date.now() - start;
|
|
1153
|
+
|
|
1154
|
+
logger.log(
|
|
1155
|
+
`✨ Success! Uploaded ${fileMap.size} files ${formatTime(uploadMs)}\n`
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
const formData = new FormData();
|
|
1159
|
+
|
|
1160
|
+
formData.append(
|
|
1161
|
+
"manifest",
|
|
1162
|
+
JSON.stringify(
|
|
1163
|
+
Object.fromEntries(
|
|
1164
|
+
[...fileMap.entries()].map(([fileName, file]) => [
|
|
1165
|
+
`/${fileName}`,
|
|
1166
|
+
file.metadata.hash,
|
|
1167
|
+
])
|
|
1168
|
+
)
|
|
1169
|
+
)
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
if (branch) {
|
|
1173
|
+
formData.append("branch", branch);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (commitMessage) {
|
|
1177
|
+
formData.append("commit_message", commitMessage);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (commitHash) {
|
|
1181
|
+
formData.append("commit_hash", commitHash);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (commitDirty !== undefined) {
|
|
1185
|
+
formData.append("commit_dirty", commitDirty);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
let _headers: string | undefined,
|
|
1189
|
+
_redirects: string | undefined,
|
|
1190
|
+
_workerJS: string | undefined;
|
|
1191
|
+
|
|
1192
|
+
try {
|
|
1193
|
+
_headers = readFileSync(join(directory, "_headers"), "utf-8");
|
|
1194
|
+
} catch {}
|
|
1195
|
+
|
|
1196
|
+
try {
|
|
1197
|
+
_redirects = readFileSync(join(directory, "_redirects"), "utf-8");
|
|
1198
|
+
} catch {}
|
|
1199
|
+
|
|
1200
|
+
try {
|
|
1201
|
+
_workerJS = readFileSync(join(directory, "_worker.js"), "utf-8");
|
|
1202
|
+
} catch {}
|
|
1203
|
+
|
|
1204
|
+
if (_headers) {
|
|
1205
|
+
formData.append("_headers", new File([_headers], "_headers"));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (_redirects) {
|
|
1209
|
+
formData.append("_redirects", new File([_redirects], "_redirects"));
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (builtFunctions) {
|
|
1213
|
+
formData.append("_worker.js", new File([builtFunctions], "_worker.js"));
|
|
1214
|
+
} else if (_workerJS) {
|
|
1215
|
+
formData.append("_worker.js", new File([_workerJS], "_worker.js"));
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const deploymentResponse = await fetchResult<Deployment>(
|
|
1219
|
+
`/accounts/${accountId}/pages/projects/${projectName}/deployments`,
|
|
1220
|
+
{
|
|
1221
|
+
method: "POST",
|
|
1222
|
+
body: formData,
|
|
1223
|
+
}
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
|
|
1227
|
+
account_id: accountId,
|
|
1228
|
+
project_name: projectName,
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
logger.log(
|
|
1232
|
+
`✨ Deployment complete! Take a peek over at ${deploymentResponse.url}`
|
|
1233
|
+
);
|
|
1234
|
+
},
|
|
1235
|
+
};
|
|
1236
|
+
|
|
706
1237
|
export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
707
1238
|
return yargs
|
|
708
1239
|
.command(
|
|
@@ -779,10 +1310,10 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
779
1310
|
_: [_pages, _dev, ...remaining],
|
|
780
1311
|
}) => {
|
|
781
1312
|
// Beta message for `wrangler pages <commands>` usage
|
|
782
|
-
|
|
1313
|
+
logger.log(pagesBetaWarning);
|
|
783
1314
|
|
|
784
1315
|
if (!local) {
|
|
785
|
-
|
|
1316
|
+
logger.error("Only local mode is supported at the moment.");
|
|
786
1317
|
return;
|
|
787
1318
|
}
|
|
788
1319
|
|
|
@@ -809,17 +1340,18 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
809
1340
|
);
|
|
810
1341
|
|
|
811
1342
|
if (usingFunctions) {
|
|
812
|
-
const
|
|
1343
|
+
const outfile = join(tmpdir(), "./functionsWorker.js");
|
|
813
1344
|
|
|
814
|
-
|
|
1345
|
+
logger.log(`Compiling worker to "${outfile}"...`);
|
|
815
1346
|
|
|
816
1347
|
try {
|
|
817
1348
|
await buildFunctions({
|
|
818
|
-
|
|
1349
|
+
outfile,
|
|
819
1350
|
functionsDirectory,
|
|
820
1351
|
sourcemap: true,
|
|
821
1352
|
watch: true,
|
|
822
1353
|
onEnd: () => scriptReadyResolve(),
|
|
1354
|
+
buildOutputDirectory: directory,
|
|
823
1355
|
});
|
|
824
1356
|
} catch {}
|
|
825
1357
|
|
|
@@ -828,16 +1360,17 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
828
1360
|
ignoreInitial: true,
|
|
829
1361
|
}).on("all", async () => {
|
|
830
1362
|
await buildFunctions({
|
|
831
|
-
|
|
1363
|
+
outfile,
|
|
832
1364
|
functionsDirectory,
|
|
833
1365
|
sourcemap: true,
|
|
834
1366
|
watch: true,
|
|
835
1367
|
onEnd: () => scriptReadyResolve(),
|
|
1368
|
+
buildOutputDirectory: directory,
|
|
836
1369
|
});
|
|
837
1370
|
});
|
|
838
1371
|
|
|
839
1372
|
miniflareArgs = {
|
|
840
|
-
scriptPath,
|
|
1373
|
+
scriptPath: outfile,
|
|
841
1374
|
};
|
|
842
1375
|
} else {
|
|
843
1376
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -853,7 +1386,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
853
1386
|
scriptPath,
|
|
854
1387
|
};
|
|
855
1388
|
} else {
|
|
856
|
-
|
|
1389
|
+
logger.log("No functions. Shimming...");
|
|
857
1390
|
miniflareArgs = {
|
|
858
1391
|
// TODO: The fact that these request/response hacks are necessary is ridiculous.
|
|
859
1392
|
// We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
|
|
@@ -885,6 +1418,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
885
1418
|
? await generateAssetsFetch(directory)
|
|
886
1419
|
: invalidAssetsFetch;
|
|
887
1420
|
|
|
1421
|
+
const requestContextCheckOptions =
|
|
1422
|
+
await getRequestContextCheckOptions();
|
|
1423
|
+
|
|
888
1424
|
const miniflare = new Miniflare({
|
|
889
1425
|
port,
|
|
890
1426
|
watch: true,
|
|
@@ -920,7 +1456,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
920
1456
|
url.host = `localhost:${proxyPort}`;
|
|
921
1457
|
return await fetch(url, request);
|
|
922
1458
|
} catch (thrown) {
|
|
923
|
-
|
|
1459
|
+
logger.error(`Could not proxy request: ${thrown}`);
|
|
924
1460
|
|
|
925
1461
|
// TODO: Pretty error page
|
|
926
1462
|
return new Response(
|
|
@@ -932,7 +1468,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
932
1468
|
try {
|
|
933
1469
|
return await assetsFetch(request);
|
|
934
1470
|
} catch (thrown) {
|
|
935
|
-
|
|
1471
|
+
logger.error(`Could not serve static asset: ${thrown}`);
|
|
936
1472
|
|
|
937
1473
|
// TODO: Pretty error page
|
|
938
1474
|
return new Response(
|
|
@@ -949,13 +1485,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
949
1485
|
cachePersist: true,
|
|
950
1486
|
liveReload,
|
|
951
1487
|
|
|
1488
|
+
...requestContextCheckOptions,
|
|
952
1489
|
...miniflareArgs,
|
|
953
1490
|
});
|
|
954
1491
|
|
|
955
1492
|
try {
|
|
956
1493
|
// `startServer` might throw if user code contains errors
|
|
957
1494
|
const server = await miniflare.startServer();
|
|
958
|
-
|
|
1495
|
+
logger.log(`Serving at http://localhost:${port}/`);
|
|
959
1496
|
|
|
960
1497
|
if (process.env.BROWSER !== "none") {
|
|
961
1498
|
await openInBrowser(`http://localhost:${port}/`);
|
|
@@ -970,13 +1507,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
970
1507
|
});
|
|
971
1508
|
}
|
|
972
1509
|
|
|
973
|
-
|
|
1510
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
974
1511
|
server.close();
|
|
975
1512
|
miniflare.dispose().catch((err) => miniflare.log.error(err));
|
|
976
1513
|
});
|
|
977
1514
|
} catch (e) {
|
|
978
1515
|
miniflare.log.error(e as Error);
|
|
979
|
-
|
|
1516
|
+
CLEANUP();
|
|
1517
|
+
throw new FatalError("Could not start Miniflare.", 1);
|
|
980
1518
|
}
|
|
981
1519
|
}
|
|
982
1520
|
)
|
|
@@ -994,7 +1532,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
994
1532
|
description: "The directory of Pages Functions",
|
|
995
1533
|
})
|
|
996
1534
|
.options({
|
|
997
|
-
|
|
1535
|
+
outfile: {
|
|
998
1536
|
type: "string",
|
|
999
1537
|
default: "_worker.js",
|
|
1000
1538
|
description: "The location of the output Worker script",
|
|
@@ -1026,32 +1564,258 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
1026
1564
|
description:
|
|
1027
1565
|
"Watch for changes to the functions and automatically rebuild the Worker script",
|
|
1028
1566
|
},
|
|
1567
|
+
plugin: {
|
|
1568
|
+
type: "boolean",
|
|
1569
|
+
default: false,
|
|
1570
|
+
description: "Build a plugin rather than a Worker script",
|
|
1571
|
+
},
|
|
1029
1572
|
})
|
|
1030
1573
|
.epilogue(pagesBetaWarning),
|
|
1031
1574
|
async ({
|
|
1032
1575
|
directory,
|
|
1033
|
-
|
|
1576
|
+
outfile,
|
|
1034
1577
|
"output-config-path": outputConfigPath,
|
|
1035
1578
|
minify,
|
|
1036
1579
|
sourcemap,
|
|
1037
1580
|
fallbackService,
|
|
1038
1581
|
watch,
|
|
1582
|
+
plugin,
|
|
1039
1583
|
}) => {
|
|
1040
1584
|
// Beta message for `wrangler pages <commands>` usage
|
|
1041
|
-
|
|
1585
|
+
logger.log(pagesBetaWarning);
|
|
1042
1586
|
|
|
1043
1587
|
await buildFunctions({
|
|
1044
|
-
|
|
1588
|
+
outfile,
|
|
1045
1589
|
outputConfigPath,
|
|
1046
1590
|
functionsDirectory: directory,
|
|
1047
1591
|
minify,
|
|
1048
1592
|
sourcemap,
|
|
1049
1593
|
fallbackService,
|
|
1050
1594
|
watch,
|
|
1595
|
+
plugin,
|
|
1596
|
+
buildOutputDirectory: dirname(outfile),
|
|
1051
1597
|
});
|
|
1052
1598
|
}
|
|
1053
1599
|
)
|
|
1054
|
-
)
|
|
1600
|
+
)
|
|
1601
|
+
.command("project", "⚡️ Interact with your Pages projects", (yargs) =>
|
|
1602
|
+
yargs
|
|
1603
|
+
.command(
|
|
1604
|
+
"list",
|
|
1605
|
+
"List your Cloudflare Pages projects",
|
|
1606
|
+
(yargs) => yargs.epilogue(pagesBetaWarning),
|
|
1607
|
+
async () => {
|
|
1608
|
+
const config = getConfigCache<PagesConfigCache>(
|
|
1609
|
+
PAGES_CONFIG_CACHE_FILENAME
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
const accountId = await requireAuth(config);
|
|
1613
|
+
|
|
1614
|
+
const projects: Array<Project> = await listProjects({ accountId });
|
|
1615
|
+
|
|
1616
|
+
const data = projects.map((project) => {
|
|
1617
|
+
return {
|
|
1618
|
+
"Project Name": project.name,
|
|
1619
|
+
"Project Domains": `${project.domains.join(", ")}`,
|
|
1620
|
+
"Git Provider": project.source ? "Yes" : "No",
|
|
1621
|
+
"Last Modified": project.latest_deployment
|
|
1622
|
+
? timeagoFormat(project.latest_deployment.modified_on)
|
|
1623
|
+
: timeagoFormat(project.created_on),
|
|
1624
|
+
};
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
|
|
1628
|
+
account_id: accountId,
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
render(<Table data={data}></Table>);
|
|
1632
|
+
}
|
|
1633
|
+
)
|
|
1634
|
+
.command(
|
|
1635
|
+
"create [project-name]",
|
|
1636
|
+
"Create a new Cloudflare Pages project",
|
|
1637
|
+
(yargs) =>
|
|
1638
|
+
yargs
|
|
1639
|
+
.positional("project-name", {
|
|
1640
|
+
type: "string",
|
|
1641
|
+
demandOption: true,
|
|
1642
|
+
description: "The name of your Pages project",
|
|
1643
|
+
})
|
|
1644
|
+
.options({
|
|
1645
|
+
"production-branch": {
|
|
1646
|
+
type: "string",
|
|
1647
|
+
description:
|
|
1648
|
+
"The name of the production branch of your project",
|
|
1649
|
+
},
|
|
1650
|
+
})
|
|
1651
|
+
.epilogue(pagesBetaWarning),
|
|
1652
|
+
async ({ productionBranch, projectName }) => {
|
|
1653
|
+
const config = getConfigCache<PagesConfigCache>(
|
|
1654
|
+
PAGES_CONFIG_CACHE_FILENAME
|
|
1655
|
+
);
|
|
1656
|
+
const accountId = await requireAuth(config);
|
|
1657
|
+
|
|
1658
|
+
const isInteractive = process.stdin.isTTY;
|
|
1659
|
+
if (!projectName && isInteractive) {
|
|
1660
|
+
projectName = await prompt("Enter the name of your new project:");
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (!projectName) {
|
|
1664
|
+
throw new FatalError("Must specify a project name.", 1);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (!productionBranch && isInteractive) {
|
|
1668
|
+
let isGitDir = true;
|
|
1669
|
+
try {
|
|
1670
|
+
execSync(`git rev-parse --is-inside-work-tree`, {
|
|
1671
|
+
stdio: "ignore",
|
|
1672
|
+
});
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
isGitDir = false;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
productionBranch = await prompt(
|
|
1678
|
+
"Enter the production branch name:",
|
|
1679
|
+
"text",
|
|
1680
|
+
isGitDir
|
|
1681
|
+
? execSync(`git rev-parse --abbrev-ref HEAD`)
|
|
1682
|
+
.toString()
|
|
1683
|
+
.trim()
|
|
1684
|
+
: "production"
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (!productionBranch) {
|
|
1689
|
+
throw new FatalError("Must specify a production branch.", 1);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const { subdomain } = await fetchResult<Project>(
|
|
1693
|
+
`/accounts/${accountId}/pages/projects`,
|
|
1694
|
+
{
|
|
1695
|
+
method: "POST",
|
|
1696
|
+
body: JSON.stringify({
|
|
1697
|
+
name: projectName,
|
|
1698
|
+
production_branch: productionBranch,
|
|
1699
|
+
}),
|
|
1700
|
+
}
|
|
1701
|
+
);
|
|
1702
|
+
|
|
1703
|
+
saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
|
|
1704
|
+
account_id: accountId,
|
|
1705
|
+
project_name: projectName,
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
logger.log(
|
|
1709
|
+
`✨ Successfully created the '${projectName}' project. It will be available at https://${subdomain}/ once you create your first deployment.`
|
|
1710
|
+
);
|
|
1711
|
+
logger.log(
|
|
1712
|
+
`To deploy a folder of assets, run 'wrangler pages publish [directory]'.`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
)
|
|
1716
|
+
.epilogue(pagesBetaWarning)
|
|
1717
|
+
)
|
|
1718
|
+
.command(
|
|
1719
|
+
"deployment",
|
|
1720
|
+
"🚀 Interact with the deployments of a project",
|
|
1721
|
+
(yargs) =>
|
|
1722
|
+
yargs
|
|
1723
|
+
.command(
|
|
1724
|
+
"list",
|
|
1725
|
+
"List deployments in your Cloudflare Pages project",
|
|
1726
|
+
(yargs) =>
|
|
1727
|
+
yargs
|
|
1728
|
+
.options({
|
|
1729
|
+
"project-name": {
|
|
1730
|
+
type: "string",
|
|
1731
|
+
description:
|
|
1732
|
+
"The name of the project you would like to list deployments for",
|
|
1733
|
+
},
|
|
1734
|
+
})
|
|
1735
|
+
.epilogue(pagesBetaWarning),
|
|
1736
|
+
async ({ projectName }) => {
|
|
1737
|
+
const config = getConfigCache<PagesConfigCache>(
|
|
1738
|
+
PAGES_CONFIG_CACHE_FILENAME
|
|
1739
|
+
);
|
|
1740
|
+
const accountId = await requireAuth(config);
|
|
1741
|
+
|
|
1742
|
+
projectName ??= config.project_name;
|
|
1743
|
+
|
|
1744
|
+
const isInteractive = process.stdin.isTTY;
|
|
1745
|
+
if (!projectName && isInteractive) {
|
|
1746
|
+
const projects = await listProjects({ accountId });
|
|
1747
|
+
projectName = await new Promise((resolve) => {
|
|
1748
|
+
const { unmount } = render(
|
|
1749
|
+
<>
|
|
1750
|
+
<Text>Select a project:</Text>
|
|
1751
|
+
<SelectInput
|
|
1752
|
+
items={projects.map((project) => ({
|
|
1753
|
+
key: project.name,
|
|
1754
|
+
label: project.name,
|
|
1755
|
+
value: project,
|
|
1756
|
+
}))}
|
|
1757
|
+
onSelect={async (selected) => {
|
|
1758
|
+
resolve(selected.value.name);
|
|
1759
|
+
unmount();
|
|
1760
|
+
}}
|
|
1761
|
+
/>
|
|
1762
|
+
</>
|
|
1763
|
+
);
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (!projectName) {
|
|
1768
|
+
throw new FatalError("Must specify a project name.", 1);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const deployments: Array<Deployment> = await fetchResult(
|
|
1772
|
+
`/accounts/${accountId}/pages/projects/${projectName}/deployments`
|
|
1773
|
+
);
|
|
1774
|
+
|
|
1775
|
+
const titleCase = (word: string) =>
|
|
1776
|
+
word.charAt(0).toUpperCase() + word.slice(1);
|
|
1777
|
+
|
|
1778
|
+
const shortSha = (sha: string) => sha.slice(0, 7);
|
|
1779
|
+
|
|
1780
|
+
const getStatus = (deployment: Deployment) => {
|
|
1781
|
+
// Return a pretty time since timestamp if successful otherwise the status
|
|
1782
|
+
if (deployment.latest_stage.status === `success`) {
|
|
1783
|
+
return timeagoFormat(deployment.latest_stage.ended_on);
|
|
1784
|
+
}
|
|
1785
|
+
return titleCase(deployment.latest_stage.status);
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
const data = deployments.map((deployment) => {
|
|
1789
|
+
return {
|
|
1790
|
+
Environment: titleCase(deployment.environment),
|
|
1791
|
+
Branch: deployment.deployment_trigger.metadata.branch,
|
|
1792
|
+
Source: shortSha(
|
|
1793
|
+
deployment.deployment_trigger.metadata.commit_hash
|
|
1794
|
+
),
|
|
1795
|
+
Deployment: deployment.url,
|
|
1796
|
+
Status: getStatus(deployment),
|
|
1797
|
+
// TODO: Use a url shortener
|
|
1798
|
+
Build: `https://dash.cloudflare.com/${accountId}/pages/view/${deployment.project_name}/${deployment.id}`,
|
|
1799
|
+
};
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
|
|
1803
|
+
account_id: accountId,
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
render(<Table data={data}></Table>);
|
|
1807
|
+
}
|
|
1808
|
+
)
|
|
1809
|
+
.command({
|
|
1810
|
+
command: "create [directory]",
|
|
1811
|
+
...createDeployment,
|
|
1812
|
+
} as CommandModule)
|
|
1813
|
+
.epilogue(pagesBetaWarning)
|
|
1814
|
+
)
|
|
1815
|
+
.command({
|
|
1816
|
+
command: "publish [directory]",
|
|
1817
|
+
...createDeployment,
|
|
1818
|
+
} as CommandModule);
|
|
1055
1819
|
};
|
|
1056
1820
|
|
|
1057
1821
|
const invalidAssetsFetch: typeof fetch = () => {
|
|
@@ -1059,3 +1823,44 @@ const invalidAssetsFetch: typeof fetch = () => {
|
|
|
1059
1823
|
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
|
|
1060
1824
|
);
|
|
1061
1825
|
};
|
|
1826
|
+
|
|
1827
|
+
const listProjects = async ({
|
|
1828
|
+
accountId,
|
|
1829
|
+
}: {
|
|
1830
|
+
accountId: string;
|
|
1831
|
+
}): Promise<Array<Project>> => {
|
|
1832
|
+
const pageSize = 10;
|
|
1833
|
+
let page = 1;
|
|
1834
|
+
const results = [];
|
|
1835
|
+
while (results.length % pageSize === 0) {
|
|
1836
|
+
const json: Array<Project> = await fetchResult(
|
|
1837
|
+
`/accounts/${accountId}/pages/projects`,
|
|
1838
|
+
{},
|
|
1839
|
+
new URLSearchParams({
|
|
1840
|
+
per_page: pageSize.toString(),
|
|
1841
|
+
page: page.toString(),
|
|
1842
|
+
})
|
|
1843
|
+
);
|
|
1844
|
+
page++;
|
|
1845
|
+
results.push(...json);
|
|
1846
|
+
if (json.length < pageSize) {
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return results;
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
function formatTime(duration: number) {
|
|
1854
|
+
return `(${(duration / 1000).toFixed(2)} sec)`;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function Progress({ done, total }: { done: number; total: number }) {
|
|
1858
|
+
return (
|
|
1859
|
+
<>
|
|
1860
|
+
<Text>
|
|
1861
|
+
<Spinner type="earth" />
|
|
1862
|
+
{` Uploading... (${done}/${total})\n`}
|
|
1863
|
+
</Text>
|
|
1864
|
+
</>
|
|
1865
|
+
);
|
|
1866
|
+
}
|