wrangler 0.0.0-e95bea2 → 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 +4886 -0
- package/package.json +33 -25
- package/pages/functions/buildPlugin.ts +92 -0
- package/pages/functions/buildWorker.ts +80 -4
- package/pages/functions/filepath-routing.test.ts +136 -25
- package/pages/functions/filepath-routing.ts +14 -3
- 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 +1179 -271
- package/src/__tests__/dev.test.tsx +382 -25
- package/src/__tests__/guess-worker-format.test.ts +28 -1
- package/src/__tests__/helpers/mock-account-id.ts +4 -0
- 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-kv.ts +9 -16
- package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
- package/src/__tests__/helpers/run-in-tmp.ts +10 -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 +172 -126
- package/src/__tests__/logger.test.ts +115 -0
- package/src/__tests__/pages.test.ts +311 -26
- package/src/__tests__/publish.test.ts +2946 -524
- 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 +26 -7
- package/src/bundle.ts +30 -3
- package/src/cfetch/internal.ts +9 -7
- package/src/cli.ts +5 -11
- package/src/config/README.md +7 -7
- package/src/config/config.ts +33 -78
- package/src/config/environment.ts +93 -4
- package/src/config/index.ts +15 -4
- package/src/config/validation-helpers.ts +202 -15
- package/src/config/validation.ts +530 -133
- 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 +136 -75
- package/src/dev/local.tsx +196 -125
- package/src/dev/remote.tsx +25 -13
- package/src/dev/use-esbuild.ts +23 -5
- package/src/dialogs.tsx +16 -6
- package/src/entry.ts +119 -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 +739 -400
- package/src/inspect.ts +17 -11
- package/src/intl-polyfill.d.ts +139 -0
- package/src/{kv/index.tsx → kv.ts} +8 -28
- package/src/logger.ts +66 -0
- package/src/miniflare-cli/README.md +30 -0
- package/src/miniflare-cli/enum-keys.ts +17 -0
- package/src/miniflare-cli/index.ts +41 -0
- package/src/miniflare-cli/request-context.ts +40 -0
- package/src/module-collection.ts +63 -58
- package/src/open-in-browser.ts +5 -4
- package/src/package-manager.ts +9 -8
- package/src/pages.tsx +883 -62
- package/src/parse.ts +1 -1
- package/src/proxy.ts +32 -21
- package/src/publish.ts +276 -161
- package/src/sites.tsx +88 -33
- 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 +231 -103
- 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 +22087 -49340
- package/src/__tests__/dev2.test.tsx +0 -152
- package/src/__tests__/helpers/mock-user.ts +0 -27
- package/src/__tests__/logout.test.ts +0 -50
- 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,33 +1340,37 @@ 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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1347
|
+
try {
|
|
1348
|
+
await buildFunctions({
|
|
1349
|
+
outfile,
|
|
1350
|
+
functionsDirectory,
|
|
1351
|
+
sourcemap: true,
|
|
1352
|
+
watch: true,
|
|
1353
|
+
onEnd: () => scriptReadyResolve(),
|
|
1354
|
+
buildOutputDirectory: directory,
|
|
1355
|
+
});
|
|
1356
|
+
} catch {}
|
|
815
1357
|
|
|
816
1358
|
watch([functionsDirectory], {
|
|
817
1359
|
persistent: true,
|
|
818
1360
|
ignoreInitial: true,
|
|
819
1361
|
}).on("all", async () => {
|
|
820
1362
|
await buildFunctions({
|
|
821
|
-
|
|
1363
|
+
outfile,
|
|
822
1364
|
functionsDirectory,
|
|
823
1365
|
sourcemap: true,
|
|
824
1366
|
watch: true,
|
|
825
1367
|
onEnd: () => scriptReadyResolve(),
|
|
1368
|
+
buildOutputDirectory: directory,
|
|
826
1369
|
});
|
|
827
1370
|
});
|
|
828
1371
|
|
|
829
1372
|
miniflareArgs = {
|
|
830
|
-
scriptPath,
|
|
1373
|
+
scriptPath: outfile,
|
|
831
1374
|
};
|
|
832
1375
|
} else {
|
|
833
1376
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -843,7 +1386,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
843
1386
|
scriptPath,
|
|
844
1387
|
};
|
|
845
1388
|
} else {
|
|
846
|
-
|
|
1389
|
+
logger.log("No functions. Shimming...");
|
|
847
1390
|
miniflareArgs = {
|
|
848
1391
|
// TODO: The fact that these request/response hacks are necessary is ridiculous.
|
|
849
1392
|
// We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
|
|
@@ -875,6 +1418,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
875
1418
|
? await generateAssetsFetch(directory)
|
|
876
1419
|
: invalidAssetsFetch;
|
|
877
1420
|
|
|
1421
|
+
const requestContextCheckOptions =
|
|
1422
|
+
await getRequestContextCheckOptions();
|
|
1423
|
+
|
|
878
1424
|
const miniflare = new Miniflare({
|
|
879
1425
|
port,
|
|
880
1426
|
watch: true,
|
|
@@ -895,7 +1441,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
895
1441
|
// User bindings
|
|
896
1442
|
bindings: {
|
|
897
1443
|
...Object.fromEntries(
|
|
898
|
-
bindings
|
|
1444
|
+
bindings
|
|
1445
|
+
.map((binding) => binding.toString().split("="))
|
|
1446
|
+
.map(([key, ...values]) => [key, values.join("=")])
|
|
899
1447
|
),
|
|
900
1448
|
},
|
|
901
1449
|
|
|
@@ -908,7 +1456,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
908
1456
|
url.host = `localhost:${proxyPort}`;
|
|
909
1457
|
return await fetch(url, request);
|
|
910
1458
|
} catch (thrown) {
|
|
911
|
-
|
|
1459
|
+
logger.error(`Could not proxy request: ${thrown}`);
|
|
912
1460
|
|
|
913
1461
|
// TODO: Pretty error page
|
|
914
1462
|
return new Response(
|
|
@@ -920,7 +1468,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
920
1468
|
try {
|
|
921
1469
|
return await assetsFetch(request);
|
|
922
1470
|
} catch (thrown) {
|
|
923
|
-
|
|
1471
|
+
logger.error(`Could not serve static asset: ${thrown}`);
|
|
924
1472
|
|
|
925
1473
|
// TODO: Pretty error page
|
|
926
1474
|
return new Response(
|
|
@@ -937,13 +1485,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
937
1485
|
cachePersist: true,
|
|
938
1486
|
liveReload,
|
|
939
1487
|
|
|
1488
|
+
...requestContextCheckOptions,
|
|
940
1489
|
...miniflareArgs,
|
|
941
1490
|
});
|
|
942
1491
|
|
|
943
1492
|
try {
|
|
944
1493
|
// `startServer` might throw if user code contains errors
|
|
945
1494
|
const server = await miniflare.startServer();
|
|
946
|
-
|
|
1495
|
+
logger.log(`Serving at http://localhost:${port}/`);
|
|
947
1496
|
|
|
948
1497
|
if (process.env.BROWSER !== "none") {
|
|
949
1498
|
await openInBrowser(`http://localhost:${port}/`);
|
|
@@ -958,13 +1507,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
958
1507
|
});
|
|
959
1508
|
}
|
|
960
1509
|
|
|
961
|
-
|
|
1510
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
962
1511
|
server.close();
|
|
963
1512
|
miniflare.dispose().catch((err) => miniflare.log.error(err));
|
|
964
1513
|
});
|
|
965
1514
|
} catch (e) {
|
|
966
1515
|
miniflare.log.error(e as Error);
|
|
967
|
-
|
|
1516
|
+
CLEANUP();
|
|
1517
|
+
throw new FatalError("Could not start Miniflare.", 1);
|
|
968
1518
|
}
|
|
969
1519
|
}
|
|
970
1520
|
)
|
|
@@ -982,7 +1532,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
982
1532
|
description: "The directory of Pages Functions",
|
|
983
1533
|
})
|
|
984
1534
|
.options({
|
|
985
|
-
|
|
1535
|
+
outfile: {
|
|
986
1536
|
type: "string",
|
|
987
1537
|
default: "_worker.js",
|
|
988
1538
|
description: "The location of the output Worker script",
|
|
@@ -1014,28 +1564,258 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
1014
1564
|
description:
|
|
1015
1565
|
"Watch for changes to the functions and automatically rebuild the Worker script",
|
|
1016
1566
|
},
|
|
1017
|
-
|
|
1567
|
+
plugin: {
|
|
1568
|
+
type: "boolean",
|
|
1569
|
+
default: false,
|
|
1570
|
+
description: "Build a plugin rather than a Worker script",
|
|
1571
|
+
},
|
|
1572
|
+
})
|
|
1573
|
+
.epilogue(pagesBetaWarning),
|
|
1018
1574
|
async ({
|
|
1019
1575
|
directory,
|
|
1020
|
-
|
|
1576
|
+
outfile,
|
|
1021
1577
|
"output-config-path": outputConfigPath,
|
|
1022
1578
|
minify,
|
|
1023
1579
|
sourcemap,
|
|
1024
1580
|
fallbackService,
|
|
1025
1581
|
watch,
|
|
1582
|
+
plugin,
|
|
1026
1583
|
}) => {
|
|
1584
|
+
// Beta message for `wrangler pages <commands>` usage
|
|
1585
|
+
logger.log(pagesBetaWarning);
|
|
1586
|
+
|
|
1027
1587
|
await buildFunctions({
|
|
1028
|
-
|
|
1588
|
+
outfile,
|
|
1029
1589
|
outputConfigPath,
|
|
1030
1590
|
functionsDirectory: directory,
|
|
1031
1591
|
minify,
|
|
1032
1592
|
sourcemap,
|
|
1033
1593
|
fallbackService,
|
|
1034
1594
|
watch,
|
|
1595
|
+
plugin,
|
|
1596
|
+
buildOutputDirectory: dirname(outfile),
|
|
1035
1597
|
});
|
|
1036
1598
|
}
|
|
1037
1599
|
)
|
|
1038
|
-
)
|
|
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);
|
|
1039
1819
|
};
|
|
1040
1820
|
|
|
1041
1821
|
const invalidAssetsFetch: typeof fetch = () => {
|
|
@@ -1043,3 +1823,44 @@ const invalidAssetsFetch: typeof fetch = () => {
|
|
|
1043
1823
|
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
|
|
1044
1824
|
);
|
|
1045
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
|
+
}
|