Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/static-assets-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"wrangler": minor
---

Improve error messaging when no entry point is found by detecting potential static asset directories

When `wrangler deploy` fails due to missing entry points, Wrangler now automatically detects common static asset directories (like `dist`, `build`, `public`, etc.) and framework-specific output directories (like Astro, Vite, Next.js, Eleventy). The error message now includes helpful suggestions about which directories might contain static assets and provides the exact command to deploy them with `--assets`.

This feature helps new users better understand what they need to do when deploying static sites to Cloudflare without requiring them to understand Wrangler configuration files.
6 changes: 6 additions & 0 deletions packages/wrangler/.changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
21 changes: 21 additions & 0 deletions packages/wrangler/.changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "cloudflare/workers-sdk" }
],
"commit": false,
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"bumpVersionsWithWorkspaceProtocolOnly": true,
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
},
"ignore": [],
"privatePackages": {
"tag": true,
"version": true
}
}
6 changes: 6 additions & 0 deletions packages/wrangler/.changeset_readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
100 changes: 100 additions & 0 deletions packages/wrangler/src/__tests__/get-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,104 @@ describe("getEntry()", () => {
moduleRoot: "/tmp/dir/other-worker/src",
});
});

describe("error messages with asset directory suggestions", () => {
it("should suggest single detected asset directory", async () => {
await seed({
"dist/index.html": "<html></html>",
"dist/style.css": "body { color: red; }",
});

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/We noticed that there is a directory called `\.\/dist` in your project \(common build output directory\)\. If you are trying to deploy the contents of that directory to Cloudflare, please run:/
);
});

it("should suggest multiple detected asset directories", async () => {
await seed({
"dist/index.html": "<html></html>",
"build/app.js": "console.log('hello');",
"public/favicon.ico": "fake-ico-content",
});

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/We noticed several directories that might contain static assets:/
);

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/- `\.\/build` \(common build output directory\)/
);

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/- `\.\/dist` \(common build output directory\)/
);

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/- `\.\/public` \(common static assets directory\)/
);
});

it("should suggest framework-specific directories", async () => {
await seed({
"package.json": JSON.stringify({
name: "my-astro-project",
dependencies: {
astro: "^4.0.0",
},
}),
"astro.config.mjs": "export default {};",
"dist/index.html": "<html></html>",
});

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/We noticed that there is a directory called `\.\/dist` in your project \(detected astro\.config project\)/
);
});

it("should not suggest asset directories when none exist", async () => {
await seed({
"src/some-file.ts": "export default {};",
});

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/Missing entry-point to Worker script or to assets directory/
);

// Should not contain asset directory suggestions
await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.not.toThrow(/We noticed/);
});

it("should not suggest empty directories", async () => {
await seed({
"dist/": "", // Creates empty directory
});

await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.toThrow(
/Missing entry-point to Worker script or to assets directory/
);

// Should not contain asset directory suggestions
await expect(
getEntry({}, defaultWranglerConfig, "deploy")
).rejects.not.toThrow(/We noticed/);
});
});
});
185 changes: 185 additions & 0 deletions packages/wrangler/src/__tests__/static-assets-detector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { detectStaticAssetDirectories } from "../deployment-bundle/static-assets-detector";
import { mockConsoleMethods } from "./helpers/mock-console";
import { runInTempDir } from "./helpers/run-in-tmp";
import { seed } from "./helpers/seed";

describe("detectStaticAssetDirectories()", () => {
runInTempDir();
mockConsoleMethods();

it("should detect dist directory", async () => {
await seed({
"dist/index.html": "<html></html>",
"dist/style.css": "body { color: red; }",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toEqual([
{
directory: "./dist",
reason: "common build output directory",
},
]);
});

it("should detect multiple asset directories", async () => {
await seed({
"dist/index.html": "<html></html>",
"build/app.js": "console.log('hello');",
"public/favicon.ico": "fake-ico-content",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toEqual([
{
directory: "./build",
reason: "common build output directory",
},
{
directory: "./dist",
reason: "common build output directory",
},
{
directory: "./public",
reason: "common static assets directory",
},
]);
});

it("should not detect empty directories", async () => {
await seed({
"dist/": "",
"build/": "",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toEqual([]);
});

it("should detect Astro project with dist directory", async () => {
await seed({
"package.json": JSON.stringify({
name: "my-astro-project",
dependencies: {
astro: "^4.0.0",
},
}),
"astro.config.mjs": "export default {};",
"dist/index.html": "<html></html>",
"dist/style.css": "body { color: red; }",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toContainEqual({
directory: "./dist",
reason: "detected astro.config project",
});
});

it("should detect Next.js project with out directory", async () => {
await seed({
"package.json": JSON.stringify({
name: "my-next-project",
dependencies: {
next: "^14.0.0",
},
}),
"next.config.js": "module.exports = {};",
"out/index.html": "<html></html>",
"out/favicon.ico": "fake-ico-content",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toContainEqual({
directory: "./out",
reason: "detected next.config project",
});
});

it("should detect Vite project from package.json", async () => {
await seed({
"package.json": JSON.stringify({
name: "my-vite-project",
devDependencies: {
vite: "^5.0.0",
},
}),
"dist/index.html": "<html></html>",
"dist/assets/main.js": "console.log('hello');",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toContainEqual({
directory: "./dist",
reason: "detected package.json project",
});
});

it("should detect Eleventy project", async () => {
await seed({
"package.json": JSON.stringify({
name: "my-eleventy-project",
devDependencies: {
"@11ty/eleventy": "^2.0.0",
},
}),
".eleventy.js": "module.exports = {};",
"_site/index.html": "<html></html>",
"_site/style.css": "body { color: red; }",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toContainEqual({
directory: "./_site",
reason: "detected .eleventy project",
});
});

it("should not duplicate suggestions", async () => {
await seed({
"package.json": JSON.stringify({
name: "my-astro-project",
dependencies: {
astro: "^4.0.0",
},
}),
"astro.config.mjs": "export default {};",
"dist/index.html": "<html></html>",
});

const suggestions = detectStaticAssetDirectories();
// Should only have one suggestion for dist, not duplicates
const distSuggestions = suggestions.filter(s => s.directory === "./dist");
expect(distSuggestions).toHaveLength(1);
});

it("should handle non-existent directories gracefully", async () => {
await seed({
"package.json": JSON.stringify({
name: "my-project",
dependencies: {
astro: "^4.0.0",
},
}),
// No dist directory created
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toEqual([]);
});

it("should handle invalid package.json gracefully", async () => {
await seed({
"package.json": "invalid json content",
"dist/index.html": "<html></html>",
});

const suggestions = detectStaticAssetDirectories();
expect(suggestions).toEqual([
{
directory: "./dist",
reason: "common build output directory",
},
]);
});
});
31 changes: 26 additions & 5 deletions packages/wrangler/src/deployment-bundle/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
resolveEntryWithScript,
} from "./resolve-entry";
import { runCustomBuild } from "./run-custom-build";
import { detectStaticAssetDirectories } from "./static-assets-detector";
import type { Config, RawConfig } from "../config";
import type { DurableObjectBindings } from "../config/environment";
import type { CfScriptFormat } from "./worker";
Expand Down Expand Up @@ -101,8 +102,11 @@ export async function getEntry(
`;

const fullCommand = `${getNpxEquivalent()} wrangler ${command}`;
throw new UserError(
dedent`

// Detect potential static asset directories
const assetSuggestions = detectStaticAssetDirectories(config.configPath ? path.dirname(config.configPath) : process.cwd());

let errorMessage = dedent`
Missing entry-point to Worker script or to assets directory

If there is code to deploy, you can either:
Expand All @@ -111,9 +115,26 @@ export async function getEntry(

If are uploading a directory of assets, you can either:
- Specify the path to the directory of assets via the command line: (ex: \`${fullCommand} --assets=./dist\`)
- Or ${updateConfigMessage({ assets: { directory: "./dist" } })}`,
{ telemetryMessage: "missing worker entrypoint or assets directory" }
);
- Or ${updateConfigMessage({ assets: { directory: "./dist" } })}`;

// Add asset directory suggestions if any were found
if (assetSuggestions.length > 0) {
errorMessage += "\n\n";
if (assetSuggestions.length === 1) {
const suggestion = assetSuggestions[0];
errorMessage += dedent`
We noticed that there is a directory called \`${suggestion.directory}\` in your project (${suggestion.reason}). If you are trying to deploy the contents of that directory to Cloudflare, please run:
\`${fullCommand} --assets ${suggestion.directory}\``;
} else {
errorMessage += "We noticed several directories that might contain static assets:\n";
for (const suggestion of assetSuggestions) {
errorMessage += `- \`${suggestion.directory}\` (${suggestion.reason})\n`;
}
errorMessage += `\nIf you want to deploy one of these directories, run: \`${fullCommand} --assets <directory>\``;
}
}

throw new UserError(errorMessage, { telemetryMessage: "missing worker entrypoint or assets directory" });
}
await runCustomBuild(
paths.absolutePath,
Expand Down
Loading
Loading