wrangler 0.0.0-e88a54e → 0.0.0-e95e5a0
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 +25 -14
- package/bin/wrangler.js +13 -1
- package/miniflare-dist/index.mjs +4828 -21
- package/package.json +29 -24
- 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 +530 -201
- package/src/__tests__/dev.test.tsx +284 -18
- package/src/__tests__/guess-worker-format.test.ts +28 -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 +26 -1
- 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 +27 -769
- package/src/__tests__/init.test.ts +1516 -0
- package/src/__tests__/jest.setup.ts +4 -4
- package/src/__tests__/kv.test.ts +133 -61
- package/src/__tests__/logger.test.ts +115 -0
- package/src/__tests__/pages.test.ts +311 -26
- package/src/__tests__/publish.test.ts +2789 -618
- package/src/__tests__/r2.test.ts +26 -10
- package/src/__tests__/route.test.ts +4 -4
- package/src/__tests__/secret.test.ts +55 -34
- package/src/__tests__/tail.test.ts +146 -20
- package/src/__tests__/user.test.ts +131 -0
- package/src/__tests__/whoami.test.tsx +14 -9
- package/src/bundle.ts +30 -3
- 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 +32 -4
- package/src/config/index.ts +4 -3
- package/src/config/validation-helpers.ts +96 -7
- package/src/config/validation.ts +388 -54
- 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 +132 -78
- package/src/dev/local.tsx +101 -21
- package/src/dev/remote.tsx +23 -13
- package/src/dev/use-esbuild.ts +23 -5
- package/src/dialogs.tsx +16 -6
- package/src/entry.ts +83 -24
- package/src/environment-variables.ts +3 -1
- package/src/errors.ts +11 -0
- package/src/https-options.ts +3 -2
- package/src/index.tsx +689 -394
- package/src/inspect.ts +17 -11
- package/src/intl-polyfill.d.ts +139 -0
- 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 +875 -56
- package/src/parse.ts +1 -1
- package/src/proxy.ts +28 -19
- package/src/publish.ts +263 -141
- package/src/sites.tsx +27 -14
- package/src/tail/index.ts +2 -2
- package/src/tail/printing.ts +13 -8
- package/src/update-check.ts +19 -0
- package/src/user.tsx +116 -41
- 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 +21162 -50227
- package/miniflare-dist/index.mjs.map +0 -7
- package/src/__tests__/dev2.test.tsx +0 -154
- 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,36 +2,100 @@
|
|
|
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
|
|
23
80
|
// don't want to do this if pages commands aren't being called.
|
|
24
81
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
export const pagesBetaWarning =
|
|
83
|
+
"🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose";
|
|
84
|
+
|
|
85
|
+
const CLEANUP_CALLBACKS: (() => void)[] = [];
|
|
86
|
+
const CLEANUP = () => {
|
|
87
|
+
CLEANUP_CALLBACKS.forEach((callback) => callback());
|
|
88
|
+
RUNNING_BUILDERS.forEach((builder) => builder.stop?.());
|
|
31
89
|
};
|
|
32
90
|
|
|
33
|
-
process.on("SIGINT", () =>
|
|
34
|
-
|
|
91
|
+
process.on("SIGINT", () => {
|
|
92
|
+
CLEANUP();
|
|
93
|
+
process.exit();
|
|
94
|
+
});
|
|
95
|
+
process.on("SIGTERM", () => {
|
|
96
|
+
CLEANUP();
|
|
97
|
+
process.exit();
|
|
98
|
+
});
|
|
35
99
|
|
|
36
100
|
function isWindows() {
|
|
37
101
|
return process.platform === "win32";
|
|
@@ -91,7 +155,7 @@ function getPort(pid: number) {
|
|
|
91
155
|
const match = matches[0];
|
|
92
156
|
if (match) return parseInt(match[1]);
|
|
93
157
|
} catch (thrown) {
|
|
94
|
-
|
|
158
|
+
logger.error(
|
|
95
159
|
`Error scanning for ports of process with PID ${pid}: ${thrown}`
|
|
96
160
|
);
|
|
97
161
|
}
|
|
@@ -104,13 +168,15 @@ async function spawnProxyProcess({
|
|
|
104
168
|
port?: number;
|
|
105
169
|
command: (string | number)[];
|
|
106
170
|
}): Promise<void | number> {
|
|
107
|
-
if (command.length === 0)
|
|
108
|
-
|
|
171
|
+
if (command.length === 0) {
|
|
172
|
+
CLEANUP();
|
|
173
|
+
throw new FatalError(
|
|
109
174
|
"Must specify a directory of static assets to serve or a command to run.",
|
|
110
175
|
1
|
|
111
176
|
);
|
|
177
|
+
}
|
|
112
178
|
|
|
113
|
-
|
|
179
|
+
logger.log(`Running ${command.join(" ")}...`);
|
|
114
180
|
const proxy = spawn(
|
|
115
181
|
command[0].toString(),
|
|
116
182
|
command.slice(1).map((value) => value.toString()),
|
|
@@ -122,30 +188,30 @@ async function spawnProxyProcess({
|
|
|
122
188
|
},
|
|
123
189
|
}
|
|
124
190
|
);
|
|
125
|
-
|
|
191
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
126
192
|
proxy.kill();
|
|
127
193
|
});
|
|
128
194
|
|
|
129
195
|
proxy.stdout.on("data", (data) => {
|
|
130
|
-
|
|
196
|
+
logger.log(`[proxy]: ${data}`);
|
|
131
197
|
});
|
|
132
198
|
|
|
133
199
|
proxy.stderr.on("data", (data) => {
|
|
134
|
-
|
|
200
|
+
logger.error(`[proxy]: ${data}`);
|
|
135
201
|
});
|
|
136
202
|
|
|
137
203
|
proxy.on("close", (code) => {
|
|
138
|
-
|
|
204
|
+
logger.error(`Proxy exited with status ${code}.`);
|
|
139
205
|
});
|
|
140
206
|
|
|
141
207
|
// Wait for proxy process to start...
|
|
142
208
|
while (!proxy.pid) {}
|
|
143
209
|
|
|
144
210
|
if (port === undefined) {
|
|
145
|
-
|
|
211
|
+
logger.log(
|
|
146
212
|
`Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
|
|
147
213
|
);
|
|
148
|
-
|
|
214
|
+
logger.log("To skip, specify the proxy port with --proxy.");
|
|
149
215
|
await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
|
|
150
216
|
|
|
151
217
|
port = getPids(proxy.pid)
|
|
@@ -153,12 +219,13 @@ async function spawnProxyProcess({
|
|
|
153
219
|
.filter((port) => port !== undefined)[0];
|
|
154
220
|
|
|
155
221
|
if (port === undefined) {
|
|
156
|
-
|
|
222
|
+
CLEANUP();
|
|
223
|
+
throw new FatalError(
|
|
157
224
|
"Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
|
|
158
225
|
1
|
|
159
226
|
);
|
|
160
227
|
} else {
|
|
161
|
-
|
|
228
|
+
logger.log(`Automatically determined the proxy port to be ${port}.`);
|
|
162
229
|
}
|
|
163
230
|
}
|
|
164
231
|
|
|
@@ -425,12 +492,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
425
492
|
}).on("change", (path) => {
|
|
426
493
|
switch (path) {
|
|
427
494
|
case headersFile: {
|
|
428
|
-
|
|
495
|
+
logger.log("_headers modified. Re-evaluating...");
|
|
429
496
|
headersMatcher = generateHeadersMatcher(headersFile);
|
|
430
497
|
break;
|
|
431
498
|
}
|
|
432
499
|
case redirectsFile: {
|
|
433
|
-
|
|
500
|
+
logger.log("_redirects modified. Re-evaluating...");
|
|
434
501
|
redirectsMatcher = generateRedirectsMatcher(redirectsFile);
|
|
435
502
|
break;
|
|
436
503
|
}
|
|
@@ -643,7 +710,7 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
643
710
|
const RUNNING_BUILDERS: BuildResult[] = [];
|
|
644
711
|
|
|
645
712
|
async function buildFunctions({
|
|
646
|
-
|
|
713
|
+
outfile,
|
|
647
714
|
outputConfigPath,
|
|
648
715
|
functionsDirectory,
|
|
649
716
|
minify = false,
|
|
@@ -651,8 +718,10 @@ async function buildFunctions({
|
|
|
651
718
|
fallbackService = "ASSETS",
|
|
652
719
|
watch = false,
|
|
653
720
|
onEnd,
|
|
721
|
+
plugin = false,
|
|
722
|
+
buildOutputDirectory,
|
|
654
723
|
}: {
|
|
655
|
-
|
|
724
|
+
outfile: string;
|
|
656
725
|
outputConfigPath?: string;
|
|
657
726
|
functionsDirectory: string;
|
|
658
727
|
minify?: boolean;
|
|
@@ -660,6 +729,8 @@ async function buildFunctions({
|
|
|
660
729
|
fallbackService?: string;
|
|
661
730
|
watch?: boolean;
|
|
662
731
|
onEnd?: () => void;
|
|
732
|
+
plugin?: boolean;
|
|
733
|
+
buildOutputDirectory?: string;
|
|
663
734
|
}) {
|
|
664
735
|
RUNNING_BUILDERS.forEach(
|
|
665
736
|
(runningBuilder) => runningBuilder.stop && runningBuilder.stop()
|
|
@@ -686,19 +757,483 @@ async function buildFunctions({
|
|
|
686
757
|
outfile: routesModule,
|
|
687
758
|
});
|
|
688
759
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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;
|
|
700
794
|
}
|
|
701
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
|
+
|
|
702
1237
|
export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
703
1238
|
return yargs
|
|
704
1239
|
.command(
|
|
@@ -759,7 +1294,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
759
1294
|
description: "Auto reload HTML pages when change is detected",
|
|
760
1295
|
},
|
|
761
1296
|
// TODO: Miniflare user options
|
|
762
|
-
})
|
|
1297
|
+
})
|
|
1298
|
+
.epilogue(pagesBetaWarning);
|
|
763
1299
|
},
|
|
764
1300
|
async ({
|
|
765
1301
|
local,
|
|
@@ -773,8 +1309,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
773
1309
|
"live-reload": liveReload,
|
|
774
1310
|
_: [_pages, _dev, ...remaining],
|
|
775
1311
|
}) => {
|
|
1312
|
+
// Beta message for `wrangler pages <commands>` usage
|
|
1313
|
+
logger.log(pagesBetaWarning);
|
|
1314
|
+
|
|
776
1315
|
if (!local) {
|
|
777
|
-
|
|
1316
|
+
logger.error("Only local mode is supported at the moment.");
|
|
778
1317
|
return;
|
|
779
1318
|
}
|
|
780
1319
|
|
|
@@ -801,17 +1340,18 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
801
1340
|
);
|
|
802
1341
|
|
|
803
1342
|
if (usingFunctions) {
|
|
804
|
-
const
|
|
1343
|
+
const outfile = join(tmpdir(), "./functionsWorker.js");
|
|
805
1344
|
|
|
806
|
-
|
|
1345
|
+
logger.log(`Compiling worker to "${outfile}"...`);
|
|
807
1346
|
|
|
808
1347
|
try {
|
|
809
1348
|
await buildFunctions({
|
|
810
|
-
|
|
1349
|
+
outfile,
|
|
811
1350
|
functionsDirectory,
|
|
812
1351
|
sourcemap: true,
|
|
813
1352
|
watch: true,
|
|
814
1353
|
onEnd: () => scriptReadyResolve(),
|
|
1354
|
+
buildOutputDirectory: directory,
|
|
815
1355
|
});
|
|
816
1356
|
} catch {}
|
|
817
1357
|
|
|
@@ -820,16 +1360,17 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
820
1360
|
ignoreInitial: true,
|
|
821
1361
|
}).on("all", async () => {
|
|
822
1362
|
await buildFunctions({
|
|
823
|
-
|
|
1363
|
+
outfile,
|
|
824
1364
|
functionsDirectory,
|
|
825
1365
|
sourcemap: true,
|
|
826
1366
|
watch: true,
|
|
827
1367
|
onEnd: () => scriptReadyResolve(),
|
|
1368
|
+
buildOutputDirectory: directory,
|
|
828
1369
|
});
|
|
829
1370
|
});
|
|
830
1371
|
|
|
831
1372
|
miniflareArgs = {
|
|
832
|
-
scriptPath,
|
|
1373
|
+
scriptPath: outfile,
|
|
833
1374
|
};
|
|
834
1375
|
} else {
|
|
835
1376
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -845,7 +1386,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
845
1386
|
scriptPath,
|
|
846
1387
|
};
|
|
847
1388
|
} else {
|
|
848
|
-
|
|
1389
|
+
logger.log("No functions. Shimming...");
|
|
849
1390
|
miniflareArgs = {
|
|
850
1391
|
// TODO: The fact that these request/response hacks are necessary is ridiculous.
|
|
851
1392
|
// We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
|
|
@@ -877,6 +1418,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
877
1418
|
? await generateAssetsFetch(directory)
|
|
878
1419
|
: invalidAssetsFetch;
|
|
879
1420
|
|
|
1421
|
+
const requestContextCheckOptions =
|
|
1422
|
+
await getRequestContextCheckOptions();
|
|
1423
|
+
|
|
880
1424
|
const miniflare = new Miniflare({
|
|
881
1425
|
port,
|
|
882
1426
|
watch: true,
|
|
@@ -897,7 +1441,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
897
1441
|
// User bindings
|
|
898
1442
|
bindings: {
|
|
899
1443
|
...Object.fromEntries(
|
|
900
|
-
bindings
|
|
1444
|
+
bindings
|
|
1445
|
+
.map((binding) => binding.toString().split("="))
|
|
1446
|
+
.map(([key, ...values]) => [key, values.join("=")])
|
|
901
1447
|
),
|
|
902
1448
|
},
|
|
903
1449
|
|
|
@@ -910,7 +1456,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
910
1456
|
url.host = `localhost:${proxyPort}`;
|
|
911
1457
|
return await fetch(url, request);
|
|
912
1458
|
} catch (thrown) {
|
|
913
|
-
|
|
1459
|
+
logger.error(`Could not proxy request: ${thrown}`);
|
|
914
1460
|
|
|
915
1461
|
// TODO: Pretty error page
|
|
916
1462
|
return new Response(
|
|
@@ -922,7 +1468,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
922
1468
|
try {
|
|
923
1469
|
return await assetsFetch(request);
|
|
924
1470
|
} catch (thrown) {
|
|
925
|
-
|
|
1471
|
+
logger.error(`Could not serve static asset: ${thrown}`);
|
|
926
1472
|
|
|
927
1473
|
// TODO: Pretty error page
|
|
928
1474
|
return new Response(
|
|
@@ -939,13 +1485,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
939
1485
|
cachePersist: true,
|
|
940
1486
|
liveReload,
|
|
941
1487
|
|
|
1488
|
+
...requestContextCheckOptions,
|
|
942
1489
|
...miniflareArgs,
|
|
943
1490
|
});
|
|
944
1491
|
|
|
945
1492
|
try {
|
|
946
1493
|
// `startServer` might throw if user code contains errors
|
|
947
1494
|
const server = await miniflare.startServer();
|
|
948
|
-
|
|
1495
|
+
logger.log(`Serving at http://localhost:${port}/`);
|
|
949
1496
|
|
|
950
1497
|
if (process.env.BROWSER !== "none") {
|
|
951
1498
|
await openInBrowser(`http://localhost:${port}/`);
|
|
@@ -960,13 +1507,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
960
1507
|
});
|
|
961
1508
|
}
|
|
962
1509
|
|
|
963
|
-
|
|
1510
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
964
1511
|
server.close();
|
|
965
1512
|
miniflare.dispose().catch((err) => miniflare.log.error(err));
|
|
966
1513
|
});
|
|
967
1514
|
} catch (e) {
|
|
968
1515
|
miniflare.log.error(e as Error);
|
|
969
|
-
|
|
1516
|
+
CLEANUP();
|
|
1517
|
+
throw new FatalError("Could not start Miniflare.", 1);
|
|
970
1518
|
}
|
|
971
1519
|
}
|
|
972
1520
|
)
|
|
@@ -984,7 +1532,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
984
1532
|
description: "The directory of Pages Functions",
|
|
985
1533
|
})
|
|
986
1534
|
.options({
|
|
987
|
-
|
|
1535
|
+
outfile: {
|
|
988
1536
|
type: "string",
|
|
989
1537
|
default: "_worker.js",
|
|
990
1538
|
description: "The location of the output Worker script",
|
|
@@ -1016,28 +1564,258 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
1016
1564
|
description:
|
|
1017
1565
|
"Watch for changes to the functions and automatically rebuild the Worker script",
|
|
1018
1566
|
},
|
|
1019
|
-
|
|
1567
|
+
plugin: {
|
|
1568
|
+
type: "boolean",
|
|
1569
|
+
default: false,
|
|
1570
|
+
description: "Build a plugin rather than a Worker script",
|
|
1571
|
+
},
|
|
1572
|
+
})
|
|
1573
|
+
.epilogue(pagesBetaWarning),
|
|
1020
1574
|
async ({
|
|
1021
1575
|
directory,
|
|
1022
|
-
|
|
1576
|
+
outfile,
|
|
1023
1577
|
"output-config-path": outputConfigPath,
|
|
1024
1578
|
minify,
|
|
1025
1579
|
sourcemap,
|
|
1026
1580
|
fallbackService,
|
|
1027
1581
|
watch,
|
|
1582
|
+
plugin,
|
|
1028
1583
|
}) => {
|
|
1584
|
+
// Beta message for `wrangler pages <commands>` usage
|
|
1585
|
+
logger.log(pagesBetaWarning);
|
|
1586
|
+
|
|
1029
1587
|
await buildFunctions({
|
|
1030
|
-
|
|
1588
|
+
outfile,
|
|
1031
1589
|
outputConfigPath,
|
|
1032
1590
|
functionsDirectory: directory,
|
|
1033
1591
|
minify,
|
|
1034
1592
|
sourcemap,
|
|
1035
1593
|
fallbackService,
|
|
1036
1594
|
watch,
|
|
1595
|
+
plugin,
|
|
1596
|
+
buildOutputDirectory: dirname(outfile),
|
|
1037
1597
|
});
|
|
1038
1598
|
}
|
|
1039
1599
|
)
|
|
1040
|
-
)
|
|
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);
|
|
1041
1819
|
};
|
|
1042
1820
|
|
|
1043
1821
|
const invalidAssetsFetch: typeof fetch = () => {
|
|
@@ -1045,3 +1823,44 @@ const invalidAssetsFetch: typeof fetch = () => {
|
|
|
1045
1823
|
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
|
|
1046
1824
|
);
|
|
1047
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
|
+
}
|