Skip to content

Commit 53b0fce

Browse files
authored
Detect non-framework static sites (#11180)
* Detect non-framework static sites * Create heavy-squids-care.md * address comments
1 parent 5d7c4c2 commit 53b0fce

File tree

6 files changed

+243
-31
lines changed

6 files changed

+243
-31
lines changed

.changeset/heavy-squids-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Detect non-framework static sites

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

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { existsSync } from "node:fs";
12
import { writeFile } from "node:fs/promises";
3+
import { join } from "node:path";
24
import { FatalError, readFileSync } from "@cloudflare/workers-utils";
35
import { vi } from "vitest";
46
import * as c3 from "../autoconfig/c3-vendor/packages";
@@ -13,6 +15,7 @@ import { clearDialogs, mockConfirm } from "./helpers/mock-dialogs";
1315
import { useMockIsTTY } from "./helpers/mock-istty";
1416
import { runInTempDir } from "./helpers/run-in-tmp";
1517
import { runWrangler } from "./helpers/run-wrangler";
18+
import { seed } from "./helpers/seed";
1619
import { writeWorkerSource } from "./helpers/write-worker-source";
1720
import { writeWranglerConfig } from "./helpers/write-wrangler-config";
1821
import type { Framework } from "../autoconfig/frameworks";
@@ -87,7 +90,9 @@ describe("autoconfig (deploy)", () => {
8790
it("should run autoconfig if project is not configured", async () => {
8891
const getDetailsSpy = vi
8992
.spyOn(details, "getDetailsForAutoConfig")
90-
.mockImplementationOnce(() => Promise.resolve({ configured: false }));
93+
.mockImplementationOnce(() =>
94+
Promise.resolve({ configured: false, projectPath: process.cwd() })
95+
);
9196
const runSpy = vi.spyOn(run, "runAutoConfig");
9297

9398
await runDeploy("--x-autoconfig");
@@ -99,7 +104,9 @@ describe("autoconfig (deploy)", () => {
99104
it("should not run autoconfig if project is already configured", async () => {
100105
const getDetailsSpy = vi
101106
.spyOn(details, "getDetailsForAutoConfig")
102-
.mockImplementationOnce(() => Promise.resolve({ configured: true }));
107+
.mockImplementationOnce(() =>
108+
Promise.resolve({ configured: true, projectPath: process.cwd() })
109+
);
103110
const runSpy = vi.spyOn(run, "runAutoConfig");
104111

105112
await runDeploy("--x-autoconfig");
@@ -129,22 +136,16 @@ describe("autoconfig (deploy)", () => {
129136
})
130137
);
131138

132-
await expect(details.getDetailsForAutoConfig()).resolves
133-
.toMatchInlineSnapshot(`
134-
Object {
135-
"buildCommand": "astro build",
136-
"configured": false,
137-
"framework": Astro {
138-
"name": "astro",
139-
},
140-
"outputDir": "dist",
141-
"packageJson": Object {
142-
"dependencies": Object {
143-
"astro": "5",
144-
},
145-
},
146-
}
147-
`);
139+
await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({
140+
buildCommand: "astro build",
141+
configured: false,
142+
outputDir: "dist",
143+
packageJson: {
144+
dependencies: {
145+
astro: "5",
146+
},
147+
},
148+
});
148149
});
149150

150151
it("should bail when multiple frameworks are detected", async () => {
@@ -182,6 +183,42 @@ describe("autoconfig (deploy)", () => {
182183
buildCommand: "npm run build",
183184
});
184185
});
186+
187+
it("outputDir should be empty if nothing can be detected", async () => {
188+
await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({
189+
outputDir: undefined,
190+
});
191+
});
192+
193+
it("outputDir should be set to cwd if an index.html file exists", async () => {
194+
await writeFile("index.html", `<h1>Hello World</h1>`);
195+
196+
await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({
197+
outputDir: process.cwd(),
198+
});
199+
});
200+
201+
it("outputDir should find first child directory with an index.html file", async () => {
202+
await seed({
203+
"public/index.html": `<h1>Hello World</h1>`,
204+
"random/index.html": `<h1>Hello World</h1>`,
205+
});
206+
207+
await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({
208+
outputDir: join(process.cwd(), "public"),
209+
});
210+
});
211+
212+
it("outputDir should prioritize the project directory over its child directories", async () => {
213+
await seed({
214+
"index.html": `<h1>Hello World</h1>`,
215+
"public/index.html": `<h1>Hello World</h1>`,
216+
});
217+
218+
await expect(details.getDetailsForAutoConfig()).resolves.toMatchObject({
219+
outputDir: process.cwd(),
220+
});
221+
});
185222
});
186223

187224
describe("runAutoConfig()", () => {
@@ -212,6 +249,7 @@ describe("autoconfig (deploy)", () => {
212249
assets: { directory: outputDir },
213250
}));
214251
await run.runAutoConfig({
252+
projectPath: process.cwd(),
215253
buildCommand: "echo 'built' > build.txt",
216254
configured: false,
217255
framework: {
@@ -268,6 +306,34 @@ describe("autoconfig (deploy)", () => {
268306

269307
// The framework's build command should have been run
270308
expect(readFileSync("build.txt")).toContain("built");
309+
310+
// outputDir !== projectPath, so there's no need for an assets ignore file
311+
expect(existsSync(".assetsignore")).toBeFalsy();
312+
});
313+
314+
it(".assetsignore should contain Wrangler files if outputDir === projectPath", async () => {
315+
mockConfirm({
316+
text: "Do you want to deploy using these settings?",
317+
result: true,
318+
});
319+
320+
await run.runAutoConfig({
321+
projectPath: process.cwd(),
322+
configured: false,
323+
outputDir: process.cwd(),
324+
});
325+
326+
expect(readFileSync(".assetsignore")).toMatchInlineSnapshot(`
327+
"
328+
329+
# wrangler files
330+
.wrangler
331+
.dev.vars*
332+
!.dev.vars.example
333+
.env*
334+
!.env.example
335+
"
336+
`);
271337
});
272338
});
273339
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { appendFileSync, existsSync, writeFileSync } from "node:fs";
2+
import { brandColor, dim } from "@cloudflare/cli/colors";
3+
import { spinner } from "@cloudflare/cli/interactive";
4+
import { readFileSync } from "@cloudflare/workers-utils";
5+
6+
export const addWranglerToAssetsIgnore = (projectPath: string) => {
7+
const assetsIgnorePath = `${projectPath}/.assetsignore`;
8+
const assetsIgnorePreExisted = existsSync(assetsIgnorePath);
9+
10+
if (!assetsIgnorePreExisted) {
11+
writeFileSync(assetsIgnorePath, "");
12+
}
13+
14+
const existingAssetsIgnoreContent = readFileSync(assetsIgnorePath);
15+
const wranglerAssetsIgnoreFilesToAdd: string[] = [];
16+
17+
const hasDotWrangler = existingAssetsIgnoreContent.match(
18+
/^\/?\.wrangler(\/|\s|$)/m
19+
);
20+
if (!hasDotWrangler) {
21+
wranglerAssetsIgnoreFilesToAdd.push(".wrangler");
22+
}
23+
24+
const hasDotDevDotVars = existingAssetsIgnoreContent.match(
25+
/^\/?\.dev\.vars\*(\s|$)/m
26+
);
27+
if (!hasDotDevDotVars) {
28+
wranglerAssetsIgnoreFilesToAdd.push(".dev.vars*");
29+
}
30+
31+
const hasDotDevVarsExample = existingAssetsIgnoreContent.match(
32+
/^!\/?\.dev\.vars\.example(\s|$)/m
33+
);
34+
if (!hasDotDevVarsExample) {
35+
wranglerAssetsIgnoreFilesToAdd.push("!.dev.vars.example");
36+
}
37+
38+
/**
39+
* We check for the following type of occurrences:
40+
*
41+
* ```
42+
* .env
43+
* .env*
44+
* .env.<local|production|staging|...>
45+
* .env*.<local|production|staging|...>
46+
* ```
47+
*
48+
* Any of these may alone on a line or be followed by a space and a trailing comment:
49+
*
50+
* ```
51+
* .env.<local|production|staging> # some trailing comment
52+
* ```
53+
*/
54+
const hasDotEnv = existingAssetsIgnoreContent.match(
55+
/^\/?\.env\*?(\..*?)?(\s|$)/m
56+
);
57+
if (!hasDotEnv) {
58+
wranglerAssetsIgnoreFilesToAdd.push(".env*");
59+
}
60+
61+
const hasDotEnvExample = existingAssetsIgnoreContent.match(
62+
/^!\/?\.env\.example(\s|$)/m
63+
);
64+
if (!hasDotEnvExample) {
65+
wranglerAssetsIgnoreFilesToAdd.push("!.env.example");
66+
}
67+
68+
if (wranglerAssetsIgnoreFilesToAdd.length === 0) {
69+
return;
70+
}
71+
72+
const s = spinner();
73+
s.start("Adding Wrangler files to the .assetsignore file");
74+
75+
const linesToAppend = [
76+
"",
77+
...(!existingAssetsIgnoreContent.match(/\n\s*$/) ? [""] : []),
78+
];
79+
80+
if (!hasDotWrangler && wranglerAssetsIgnoreFilesToAdd.length > 1) {
81+
linesToAppend.push("# wrangler files");
82+
}
83+
84+
wranglerAssetsIgnoreFilesToAdd.forEach((line) => linesToAppend.push(line));
85+
86+
linesToAppend.push("");
87+
88+
appendFileSync(assetsIgnorePath, linesToAppend.join("\n"));
89+
90+
s.stop(
91+
`${brandColor(assetsIgnorePreExisted ? "updated" : "created")} ${dim(
92+
".assetsignore file"
93+
)}`
94+
);
95+
};

packages/wrangler/src/autoconfig/get-details.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { resolve } from "node:path";
1+
import { readdir, stat } from "node:fs/promises";
2+
import { join, resolve } from "node:path";
23
import {
34
FatalError,
45
parsePackageJSON,
@@ -11,7 +12,7 @@ import { logger } from "../logger";
1112
import { getPackageManager } from "../package-manager";
1213
import { getFramework } from "./frameworks/get-framework";
1314
import type { AutoConfigDetails } from "./types";
14-
import type { Config } from "@cloudflare/workers-utils";
15+
import type { Config, PackageJSON } from "@cloudflare/workers-utils";
1516
import type { Settings } from "@netlify/build-info";
1617

1718
class MultipleFrameworksError extends FatalError {
@@ -24,6 +25,37 @@ class MultipleFrameworksError extends FatalError {
2425
}
2526
}
2627

28+
async function hasIndexHtml(dir: string): Promise<boolean> {
29+
const children = await readdir(dir);
30+
for (const child of children) {
31+
const stats = await stat(join(dir, child));
32+
if (stats.isFile() && child === "index.html") {
33+
return true;
34+
}
35+
}
36+
return false;
37+
}
38+
39+
/**
40+
* If we haven't detected a framework being used, we need to "guess" what output dir the user is intending to use.
41+
* This is best-effort, and so will not be accurate all the time. The heuristic we use is the first child directory
42+
* with an `index.html` file present.
43+
*/
44+
async function findAssetsDir(from: string): Promise<string | undefined> {
45+
if (await hasIndexHtml(from)) {
46+
return from;
47+
}
48+
const children = await readdir(from);
49+
for (const child of children) {
50+
const path = join(from, child);
51+
const stats = await stat(path);
52+
if (stats.isDirectory() && (await hasIndexHtml(path))) {
53+
return path;
54+
}
55+
}
56+
return undefined;
57+
}
58+
2759
export async function getDetailsForAutoConfig({
2860
projectPath = process.cwd(),
2961
wranglerConfig,
@@ -35,7 +67,7 @@ export async function getDetailsForAutoConfig({
3567

3668
// If a real Wrangler config has been found & used, don't run autoconfig
3769
if (wranglerConfig?.configPath) {
38-
return { configured: true };
70+
return { configured: true, projectPath };
3971
}
4072
const fs = new NodeFS();
4173

@@ -60,22 +92,30 @@ export async function getDetailsForAutoConfig({
6092
detectedFramework?.framework.id
6193
);
6294
const packageJsonPath = resolve("package.json");
63-
const packageJson = parsePackageJSON(
64-
readFileSync(packageJsonPath),
65-
packageJsonPath
66-
);
95+
96+
let packageJson: PackageJSON | undefined;
97+
98+
try {
99+
packageJson = parsePackageJSON(
100+
readFileSync(packageJsonPath),
101+
packageJsonPath
102+
);
103+
} catch {
104+
logger.debug("No package.json found when running autoconfig");
105+
}
67106

68107
const { type } = await getPackageManager();
69108

70-
const packageJsonBuild = packageJson.scripts?.["build"]
109+
const packageJsonBuild = packageJson?.scripts?.["build"]
71110
? `${type} run build`
72111
: undefined;
73112

74113
return {
114+
projectPath: projectPath,
75115
configured: framework?.configured ?? false,
76116
framework,
77117
packageJson,
78118
buildCommand: detectedFramework?.buildCommand ?? packageJsonBuild,
79-
outputDir: detectedFramework?.dist,
119+
outputDir: detectedFramework?.dist ?? (await findAssetsDir(projectPath)),
80120
};
81121
}

packages/wrangler/src/autoconfig/run.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { confirm } from "../dialogs";
88
import { getCIOverrideName } from "../environment-variables/misc-variables";
99
import { logger } from "../logger";
1010
import { getDevCompatibilityDate } from "../utils/compatibility-date";
11+
import { addWranglerToAssetsIgnore } from "./add-wrangler-assetsignore";
1112
import { addWranglerToGitIgnore } from "./c3-vendor/add-wrangler-gitignore";
1213
import { installWrangler } from "./c3-vendor/packages";
1314
import type { AutoConfigDetails } from "./types";
@@ -54,7 +55,7 @@ export async function runAutoConfig(
5455
name:
5556
getCIOverrideName() ??
5657
autoConfigDetails.packageJson?.name ??
57-
dirname(autoConfigDetails.projectPath ?? process.cwd()),
58+
dirname(autoConfigDetails.projectPath),
5859
compatibility_date: getDevCompatibilityDate(undefined),
5960
observability: {
6061
enabled: true,
@@ -66,14 +67,19 @@ export async function runAutoConfig(
6667
)
6768
);
6869

69-
await addWranglerToGitIgnore(autoConfigDetails.projectPath ?? process.cwd());
70+
await addWranglerToGitIgnore(autoConfigDetails.projectPath);
71+
72+
// If we're uploading the project path as the output directory, make sure we don't accidentally upload any sensitive Wrangler files
73+
if (autoConfigDetails.outputDir === autoConfigDetails.projectPath) {
74+
await addWranglerToAssetsIgnore(autoConfigDetails.projectPath);
75+
}
7076

7177
endSection(`Application configured`);
7278

7379
if (autoConfigDetails.buildCommand) {
7480
await runCommand(
7581
autoConfigDetails.buildCommand,
76-
autoConfigDetails.projectPath ?? process.cwd(),
82+
autoConfigDetails.projectPath,
7783
"[build]"
7884
);
7985
}

0 commit comments

Comments
 (0)