Skip to content

Commit 2984c23

Browse files
committed
add interactive flow for wonky asset deploy commands
1 parent 3851388 commit 2984c23

File tree

6 files changed

+293
-10
lines changed

6 files changed

+293
-10
lines changed

.changeset/deep-onions-move.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Interactively handle `wrangler deploy`s that are probably assets-only, where there is no config file and flags are incorrect or missing.
6+
7+
For example:
8+
9+
`npx wrangler deploy ./public` will now ask if you meant to deploy an folder of assets only, ask for a name, set the compat date and then ask to write your choices out to `wrangler.json` for subsequent deployments.
10+
11+
`npx wrangler deploy --assets=./public` will now ask for a name, set the compat date and then ask to write your choices out to `wrangler.json` for subsequent deployments.
12+
13+
In non-interactive contexts, we will error as we currently do.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "workers-with-assets-only",
3+
"compatibility_date": "2025-07-18",
4+
"assets": {
5+
"directory": "public"
6+
}
7+
}

fixtures/workers-with-assets-only/wrangler.jsonc

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/wrangler/src/__tests__/deploy.test.ts

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as esbuild from "esbuild";
1010
import { http, HttpResponse } from "msw";
1111
import dedent from "ts-dedent";
1212
import { vi } from "vitest";
13+
import { findWranglerConfig } from "../config/config-helpers";
1314
import {
1415
printBundleSize,
1516
printOffendingDependencies,
@@ -20,7 +21,7 @@ import { writeAuthConfigFile } from "../user";
2021
import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
2122
import { mockAuthDomain } from "./helpers/mock-auth-domain";
2223
import { mockConsoleMethods } from "./helpers/mock-console";
23-
import { clearDialogs, mockConfirm } from "./helpers/mock-dialogs";
24+
import { clearDialogs, mockConfirm, mockPrompt } from "./helpers/mock-dialogs";
2425
import { mockGetZoneFromHostRequest } from "./helpers/mock-get-zone-from-host";
2526
import { useMockIsTTY } from "./helpers/mock-istty";
2627
import { mockCollectKnownRoutesRequest } from "./helpers/mock-known-routes";
@@ -2720,6 +2721,157 @@ addEventListener('fetch', event => {});`
27202721
});
27212722
});
27222723
});
2724+
2725+
describe("should interactively handle misconfigured asset-only deployments", () => {
2726+
beforeEach(() => {
2727+
setIsTTY(true);
2728+
2729+
// Mock the date to ensure consistent compatibility_date
2730+
vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
2731+
2732+
fs.mkdirSync("my-site");
2733+
process.chdir("my-site");
2734+
const assets = [
2735+
{ filePath: "index.html", content: "<html>test</html>" },
2736+
];
2737+
writeAssets(assets);
2738+
expect(findWranglerConfig().configPath).toBe(undefined);
2739+
mockSubDomainRequest();
2740+
mockUploadWorkerRequest({
2741+
expectedAssets: {
2742+
jwt: "<<aus-completion-token>>",
2743+
config: {},
2744+
},
2745+
expectedType: "none",
2746+
});
2747+
});
2748+
afterEach(() => {
2749+
setIsTTY(false);
2750+
});
2751+
2752+
it("should handle `wrangler deploy <directory>`", async () => {
2753+
mockConfirm({
2754+
text: "It looks like you are trying to deploy a directory of static assets only. Is this correct?",
2755+
result: true,
2756+
});
2757+
mockPrompt({
2758+
text: "What do you want to name your project?",
2759+
options: { defaultValue: "my-site" },
2760+
result: "test-name",
2761+
});
2762+
mockConfirm({
2763+
text: "Do you want Wrangler to write a wrangler.json config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.",
2764+
result: true,
2765+
});
2766+
2767+
const bodies: AssetManifest[] = [];
2768+
await mockAUSRequest(bodies);
2769+
2770+
await runWrangler("deploy ./assets");
2771+
expect(bodies.length).toBe(1);
2772+
expect(bodies[0]).toEqual({
2773+
manifest: {
2774+
"/index.html": {
2775+
hash: "8308ce789f3d08668ce87176838d59d0",
2776+
size: 17,
2777+
},
2778+
},
2779+
});
2780+
expect(fs.readFileSync("wrangler.json", "utf-8"))
2781+
.toMatchInlineSnapshot(`
2782+
"{
2783+
\\"name\\": \\"test-name\\",
2784+
\\"compatibility_date\\": \\"2024-01-01\\",
2785+
\\"assets\\": {
2786+
\\"directory\\": \\"./assets\\"
2787+
}
2788+
}"
2789+
`);
2790+
expect(std.out).toMatchInlineSnapshot(`
2791+
"
2792+
2793+
2794+
No compatibility date found Defaulting to today: 2024-01-01
2795+
2796+
Wrote
2797+
{
2798+
\\"name\\": \\"test-name\\",
2799+
\\"compatibility_date\\": \\"2024-01-01\\",
2800+
\\"assets\\": {
2801+
\\"directory\\": \\"./assets\\"
2802+
}
2803+
}
2804+
to <cwd>/wrangler.json.
2805+
Please run wrangler deploy instead of wrangler deploy ./assets next time. Wrangler will automatically use the configuration saved to wrangler.json.
2806+
2807+
Total Upload: xx KiB / gzip: xx KiB
2808+
Worker Startup Time: 100 ms
2809+
Uploaded test-name (TIMINGS)
2810+
Deployed test-name triggers (TIMINGS)
2811+
https://test-name.test-sub-domain.workers.dev
2812+
Current Version ID: Galaxy-Class"
2813+
`);
2814+
});
2815+
it("should handle `wrangler deploy --assets` without name or compat date", async () => {
2816+
// if the user has used --assets flag and args.script is not set, we just need to prompt for the name and add compat date
2817+
mockPrompt({
2818+
text: "What do you want to name your project?",
2819+
options: { defaultValue: "my-site" },
2820+
result: "test-name",
2821+
});
2822+
mockConfirm({
2823+
text: "Do you want Wrangler to write a wrangler.json config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.",
2824+
result: true,
2825+
});
2826+
2827+
const bodies: AssetManifest[] = [];
2828+
await mockAUSRequest(bodies);
2829+
2830+
await runWrangler("deploy --assets ./assets");
2831+
expect(bodies.length).toBe(1);
2832+
expect(bodies[0]).toEqual({
2833+
manifest: {
2834+
"/index.html": {
2835+
hash: "8308ce789f3d08668ce87176838d59d0",
2836+
size: 17,
2837+
},
2838+
},
2839+
});
2840+
expect(fs.readFileSync("wrangler.json", "utf-8"))
2841+
.toMatchInlineSnapshot(`
2842+
"{
2843+
\\"name\\": \\"test-name\\",
2844+
\\"compatibility_date\\": \\"2024-01-01\\",
2845+
\\"assets\\": {
2846+
\\"directory\\": \\"./assets\\"
2847+
}
2848+
}"
2849+
`);
2850+
expect(std.out).toMatchInlineSnapshot(`
2851+
"
2852+
2853+
No compatibility date found Defaulting to today: 2024-01-01
2854+
2855+
Wrote
2856+
{
2857+
\\"name\\": \\"test-name\\",
2858+
\\"compatibility_date\\": \\"2024-01-01\\",
2859+
\\"assets\\": {
2860+
\\"directory\\": \\"./assets\\"
2861+
}
2862+
}
2863+
to <cwd>/wrangler.json.
2864+
Please run wrangler deploy instead of wrangler deploy ./assets next time. Wrangler will automatically use the configuration saved to wrangler.json.
2865+
2866+
Total Upload: xx KiB / gzip: xx KiB
2867+
Worker Startup Time: 100 ms
2868+
Uploaded test-name (TIMINGS)
2869+
Deployed test-name triggers (TIMINGS)
2870+
https://test-name.test-sub-domain.workers.dev
2871+
Current Version ID: Galaxy-Class"
2872+
`);
2873+
});
2874+
});
27232875
});
27242876

27252877
describe("(legacy) asset upload", () => {
@@ -4346,7 +4498,8 @@ addEventListener('fetch', event => {});`
43464498
);
43474499
});
43484500

4349-
it("should error if directory specified by flag --assets does not exist", async () => {
4501+
it("should error if directory specified by flag --assets does not exist in non-interactive mode", async () => {
4502+
setIsTTY(false);
43504503
await expect(runWrangler("deploy --assets abc")).rejects.toThrow(
43514504
new RegExp(
43524505
'^The directory specified by the "--assets" command line argument does not exist:[Ss]*'

packages/wrangler/src/deploy/index.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import assert from "node:assert";
2+
import { statSync, writeFileSync } from "node:fs";
23
import path from "node:path";
4+
import chalk from "chalk";
35
import { getAssetsOptions, validateAssetsArgsAndConfig } from "../assets";
46
import { configFileName } from "../config";
57
import { createCommand } from "../core/create-command";
68
import { getEntry } from "../deployment-bundle/entry";
9+
import { confirm, prompt } from "../dialogs";
710
import { getCIOverrideName } from "../environment-variables/misc-variables";
811
import { UserError } from "../errors";
12+
import { isNonInteractiveOrCI } from "../is-interactive";
913
import { logger } from "../logger";
1014
import { verifyWorkerMatchesCITag } from "../match-tag";
1115
import * as metrics from "../metrics";
@@ -244,8 +248,29 @@ export const deployCommand = createCommand({
244248
const projectRoot =
245249
config.userConfigPath && path.dirname(config.userConfigPath);
246250

247-
const entry = await getEntry(args, config, "deploy");
251+
if (!config.configPath) {
252+
// Attempt to interactively handle `wrangler deploy <directory>`
253+
if (args.script) {
254+
try {
255+
const stats = statSync(args.script);
256+
if (stats.isDirectory()) {
257+
args = await handleMaybeAssetsDeployment(args);
258+
}
259+
} catch (error) {
260+
// If this is our UserError, re-throw it
261+
if (error instanceof UserError) {
262+
throw error;
263+
}
264+
// If stat fails, let the original flow handle the error
265+
}
266+
}
267+
// atttempt to interactively handle `wrangler deploy --assets <directory>` missing compat date or name
268+
else if (args.assets && (!args.compatibilityDate || !args.name)) {
269+
args = await handleMaybeAssetsDeployment(args);
270+
}
271+
}
248272

273+
const entry = await getEntry(args, config, "deploy");
249274
validateAssetsArgsAndConfig(args, config);
250275

251276
const assetsOptions = getAssetsOptions(args, config);
@@ -362,3 +387,91 @@ export const deployCommand = createCommand({
362387
});
363388

364389
export type DeployArgs = (typeof deployCommand)["args"];
390+
391+
/**
392+
* Handles the case where a user provides a directory as a positional argument,
393+
* probably intending to deploy static assets. e.g. wrangler deploy ./public
394+
* We then interactively take the user through deployment (missing name, compatibility date, etc.)
395+
* and try and output this as a wrangler.json for future deployments.
396+
*/
397+
export async function handleMaybeAssetsDeployment(
398+
args: DeployArgs
399+
): Promise<DeployArgs> {
400+
if (isNonInteractiveOrCI()) {
401+
return args;
402+
}
403+
404+
// Ask if user intended to deploy assets only
405+
logger.log("");
406+
if (!args.assets) {
407+
const deployAssets = await confirm(
408+
"It looks like you are trying to deploy a directory of static assets only. Is this correct?",
409+
{ defaultValue: true }
410+
);
411+
logger.log("");
412+
if (deployAssets) {
413+
args.assets = args.script;
414+
args.script = undefined;
415+
} else {
416+
// let the usual error handling path kick in
417+
return args;
418+
}
419+
}
420+
421+
// Check if name is provided, if not ask for it
422+
if (!args.name) {
423+
const projectName = await prompt("What do you want to name your project?", {
424+
defaultValue: process.cwd().split(path.sep).pop(),
425+
});
426+
args.name = projectName;
427+
logger.log("");
428+
}
429+
430+
// Set compatibility date if not provided
431+
if (!args.compatibilityDate) {
432+
const today = new Date();
433+
const compatibilityDate = [
434+
today.getFullYear(),
435+
(today.getMonth() + 1).toString().padStart(2, "0"),
436+
today.getDate().toString().padStart(2, "0"),
437+
].join("-");
438+
args.compatibilityDate = compatibilityDate;
439+
logger.log(
440+
`${chalk.bold("No compatibility date found")} Defaulting to today:`,
441+
compatibilityDate
442+
);
443+
logger.log("");
444+
}
445+
446+
// Ask if user wants to write config file
447+
const writeConfig = await confirm(
448+
`Do you want Wrangler to write a wrangler.json config file to store this configuration?\n${chalk.dim("This will allow you to simply run `wrangler deploy` on future deployments.")}`
449+
);
450+
451+
if (writeConfig) {
452+
const configPath = path.join(process.cwd(), "wrangler.json");
453+
const jsonString = JSON.stringify(
454+
{
455+
name: args.name,
456+
compatibility_date: args.compatibilityDate,
457+
assets: { directory: args.assets },
458+
},
459+
null,
460+
2
461+
);
462+
writeFileSync(configPath, jsonString);
463+
logger.log(`Wrote \n${jsonString}\n to ${chalk.bold(configPath)}.`);
464+
logger.log(
465+
`Please run ${chalk.bold("wrangler deploy")} instead of ${chalk.bold(`wrangler deploy ${args.assets}`)} next time. Wrangler will automatically use the configuration saved to wrangler.json.`
466+
);
467+
} else {
468+
logger.log("Proceeding with deployment...");
469+
logger.log(
470+
`You should run ${chalk.bold(
471+
`wrangler deploy --name ${args.name} --compatibility-date ${args.compatibilityDate} --assets ${args.assets}`
472+
)} next time to deploy this Worker without going through this flow again.`
473+
);
474+
}
475+
logger.log("");
476+
return args;
477+
}

packages/wrangler/src/deployment-bundle/run-custom-build.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ function getMissingEntryPointMessage(
136136
"```\n" +
137137
possiblePaths.map((filePath) => `main = "./${filePath}"\n`).join("") +
138138
"```";
139+
} else {
140+
message +=
141+
`\n If you want to deploy a directory of static assets, you can do so by using the \`--assets\` flag. For example:\n\n` +
142+
`wrangler deploy --assets=./${relativeEntryPointPath}\n`;
139143
}
140144
}
141145
return message;

0 commit comments

Comments
 (0)