Skip to content
Open
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/v1-assetsignore-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/vite-plugin": minor
---

Validate asset ignore files and update local Flagship support

Upgrade Miniflare to 5.20261023.2-beta and Wrangler to 4.155.0. Builds and asset manifest generation now reject `.assetsignore` files with invalid UTF-8 or null characters instead of silently ignoring their patterns. The Local Explorer also supports Flagship management by app ID, and local flag evaluations retain metadata.

Miniflare also waits for temporary file cleanup before disposal completes, so closing or restarting a Vite dev server finishes cleanup before returning.
2 changes: 1 addition & 1 deletion fixtures/dev-registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"typescript": "catalog:default",
"vite": "catalog:default",
"vitest": "catalog:default",
"wrangler": "4.154.0"
"wrangler": "4.155.0"
}
}
4 changes: 2 additions & 2 deletions fixtures/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"description": "Shared fixtures for testing",
"devDependencies": {
"@cloudflare/workers-utils": "0.49.0",
"wrangler": "4.154.0"
"@cloudflare/workers-utils": "0.50.0",
"wrangler": "4.155.0"
}
}
2 changes: 1 addition & 1 deletion packages/mock-npm-registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"test:ci": "vitest run"
},
"devDependencies": {
"@cloudflare/workers-utils": "0.49.0",
"@cloudflare/workers-utils": "0.50.0",
"@types/node": "catalog:default",
"@verdaccio/types": "^10.8.0",
"get-port": "^7.0.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/remote-bindings/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
"test:watch": "vitest"
},
"dependencies": {
"@cloudflare/cli-shared-helpers": "0.2.7",
"@cloudflare/deploy-helpers": "0.24.1",
"@cloudflare/cli-shared-helpers": "0.2.10",
"@cloudflare/deploy-helpers": "0.24.2",
"@cloudflare/workers-auth": "0.15.1",
"@cloudflare/workers-utils": "0.49.0",
"@cloudflare/workers-utils": "0.50.0",
"chalk": "catalog:default",
"miniflare": "5.20261023.1-beta",
"miniflare": "5.20261023.2-beta",
"undici": "catalog:default",
"ws": "catalog:default"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@cloudflare/vite-plugin": "*",
"@cloudflare/workers-types": "^5.20260706.1",
"globals": "^15.14.0",
"miniflare": "5.20261023.1-beta",
"miniflare": "5.20261023.2-beta",
"typescript": "~5.7.2",
"vite": "^8.0.0",
"wrangler": "*"
Expand Down
10 changes: 5 additions & 5 deletions packages/vite-plugin-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@
},
"dependencies": {
"@cloudflare/unenv-preset": "2.16.2",
"miniflare": "5.20261023.1-beta",
"miniflare": "5.20261023.2-beta",
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
"unenv": "2.0.0-rc.24",
"wrangler": "4.154.0",
"wrangler": "4.155.0",
"ws": "8.22.0"
},
"devDependencies": {
"@cloudflare/containers-shared": "0.23.3",
"@cloudflare/containers-shared": "0.23.4",
"@cloudflare/mock-npm-registry": "workspace:*",
"@cloudflare/remote-bindings": "workspace:*",
"@cloudflare/workers-shared": "workspace:*",
"@cloudflare/workers-tsconfig": "workspace:*",
"@cloudflare/workers-types": "5.20261023.1",
"@cloudflare/workers-utils": "0.49.0",
"@cloudflare/workers-utils": "0.50.0",
"@remix-run/node-fetch-server": "1.0.0",
"@types/node": "22.15.17",
"@types/semver": "7.8.0",
Expand All @@ -82,7 +82,7 @@
},
"peerDependencies": {
"vite": "^6.1.0 || ^7.0.0 || ^8.0.0",
"wrangler": "^4.154.0"
"wrangler": "^4.155.0"
},
"workers-sdk": {
"prerelease": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@cloudflare/vite-plugin": "workspace:*",
"@cloudflare/workers-tsconfig": "workspace:*",
"@cloudflare/workers-types": "catalog:default",
"@cloudflare/workers-utils": "0.49.0",
"@cloudflare/workers-utils": "0.50.0",
"@prisma/adapter-d1": "^7.0.0",
"@prisma/client": "^7.0.0",
"prisma": "^7.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { removeDirSync } from "@cloudflare/workers-utils";
import * as vite from "vite";
import { describe, onTestFinished, test } from "vitest";
import { cloudflare } from "../index";
import { getOutputConfig } from "../plugins/output-config";
import type { ResolvedWorkerConfig } from "../plugin-config";
import type * as vite from "vite";

describe("getWorkerOutputConfig", () => {
function createRoot() {
Expand All @@ -16,6 +17,42 @@ describe("getWorkerOutputConfig", () => {
return tempRoot;
}

test.for([
["invalid UTF-8", Buffer.from([0xff, 0xfe])],
["null characters", Buffer.from("ignored\0.txt")],
] as const)(
"rejects source .assetsignore with %s before emitting it",
async ([, contents], { expect }) => {
const root = createRoot();
fs.mkdirSync(path.join(root, "public"));
fs.writeFileSync(path.join(root, "public/index.html"), "<h1>Assets</h1>");
fs.writeFileSync(path.join(root, "public/.assetsignore"), contents);
fs.writeFileSync(
path.join(root, "worker.js"),
'export default { fetch() { return new Response("ready"); } };'
);
fs.writeFileSync(
path.join(root, "wrangler.json"),
JSON.stringify({
name: "ignore-validation",
main: "worker.js",
compatibility_date: "2026-10-23",
assets: { binding: "ASSETS" },
})
);
const builder = await vite.createBuilder({
root,
configFile: false,
logLevel: "silent",
plugins: [cloudflare({ inspectorPort: false, persistState: false })],
});

await expect(builder.buildApp()).rejects.toThrow(
/must be UTF-8 encoded and must not contain null characters/
);
}
);

function resolvedViteConfig(root: string): vite.ResolvedConfig {
return {
root,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert";
import * as fs from "node:fs";
import * as path from "node:path";
import { readAssetsIgnoreFile as readAssetsIgnorePatterns } from "@cloudflare/workers-shared/utils/helpers";
import * as vite from "vite";
import { MAIN_ENTRY_NAME } from "../cloudflare-environment";
import { assertIsNotPreview } from "../context";
Expand Down Expand Up @@ -263,9 +263,7 @@ function rewriteMigrationsPattern(
}

function readAssetsIgnoreFile(assetsIgnorePath: string): string {
const content = fs.existsSync(assetsIgnorePath)
? fs.readFileSync(assetsIgnorePath, "utf-8")
: "";
const content = readAssetsIgnorePatterns(assetsIgnorePath) ?? "";
if (content.length === 0) {
return "";
}
Expand Down
41 changes: 40 additions & 1 deletion packages/workers-shared/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import { isAbsolute, resolve, sep } from "node:path";
import { TextDecoder } from "node:util";
import ignore from "ignore";
import mime from "mime";
import {
Expand Down Expand Up @@ -62,6 +63,44 @@ export function maybeGetFile(filePath: PathOrFileDescriptor) {
}
}

/** An ignore file whose encoding cannot be used to match asset paths. */
export class InvalidAssetsIgnoreError extends Error {
constructor(filePath: string) {
super(
`The .assetsignore file at "${filePath}" must be UTF-8 encoded and must not contain null characters. Save the file as UTF-8 and try again.`
);
this.name = "InvalidAssetsIgnoreError";
}
}

/**
* Read ignore patterns without silently replacing undecodable characters.
* @param filePath Path to the asset ignore file.
* @returns Valid UTF-8 patterns, or undefined when the file does not exist.
*/
export function readAssetsIgnoreFile(filePath: string): string | undefined {
let contents: Buffer;
try {
contents = readFileSync(filePath);
} catch (error) {
if (thrownIsDoesNotExistError(error)) {
return;
}
throw error;
}

try {
const text = new TextDecoder("utf-8", { fatal: true }).decode(contents);
// BOM-less UTF-16 can be valid UTF-8 bytes but cannot form usable patterns.
if (text.includes("\0")) {
throw new Error("Ignore patterns contain a null character.");
}
return text;
} catch {
throw new InvalidAssetsIgnoreError(filePath);
}
}

/**
* Create a function for filtering out ignored assets.
*
Expand All @@ -80,7 +119,7 @@ export async function createAssetsIgnoreFunction(dir: string) {
];

let assetsIgnoreFilePresent = false;
const assetsIgnore = maybeGetFile(cfAssetIgnorePath);
const assetsIgnore = readAssetsIgnoreFile(cfAssetIgnorePath);
if (assetsIgnore !== undefined) {
assetsIgnoreFilePresent = true;
ignorePatterns.push(...assetsIgnore.split("\n"));
Expand Down
58 changes: 56 additions & 2 deletions packages/workers-shared/utils/tests/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ import { writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "vitest";
import { createAssetsIgnoreFunction, getContentType } from "../helpers";
import {
createAssetsIgnoreFunction,
getContentType,
InvalidAssetsIgnoreError,
} from "../helpers";

describe("assets", () => {
const tmpDir = mkdtempSync(join(tmpdir(), "wrangler-tests"));

describe(".assetsignore", () => {
it("should ignore metafiles by default", async ({ expect }) => {
const { assetsIgnoreFunction } = await createAssetsIgnoreFunction(tmpDir);
const { assetsIgnoreFunction, assetsIgnoreFilePresent } =
await createAssetsIgnoreFunction(tmpDir);

expect(assetsIgnoreFilePresent).toBe(false);

expect(assetsIgnoreFunction(".assetsignore")).toBeTruthy();
expect(assetsIgnoreFunction("_redirects")).toBeTruthy();
Expand Down Expand Up @@ -52,6 +59,53 @@ describe("assets", () => {
).toBeTruthy();
expect(assetsIgnoreFunction(join("child", "nope.svg"))).toBeFalsy();
});

it.for(["", "\ufeff"])(
"reads UTF-8 patterns with an optional BOM (%j)",
async (bom, { expect }) => {
await writeFile(
join(tmpDir, ".assetsignore"),
`${bom}café.txt\r\nchild/*.txt\r\n!child/keep.txt\r\n`
);
const { assetsIgnoreFunction, assetsIgnoreFilePresent } =
await createAssetsIgnoreFunction(tmpDir);

expect(assetsIgnoreFilePresent).toBe(true);
expect(assetsIgnoreFunction("café.txt")).toBe(true);
expect(assetsIgnoreFunction("child/ignore.txt")).toBe(true);
expect(assetsIgnoreFunction("child/keep.txt")).toBe(false);
}
);

it.for([
["UTF-16 LE", Buffer.from("\ufeffignored.txt", "utf16le")],
["UTF-16 BE", Buffer.from("\ufeffignored.txt", "utf16le").swap16()],
["UTF-16 without a BOM", Buffer.from("ignored.txt", "utf16le")],
["invalid UTF-8", Buffer.from([0xff])],
["null characters", Buffer.from("ignored\0.txt")],
] as const)(
"rejects %s instead of ignoring its patterns",
async ([, contents], { expect }) => {
await writeFile(join(tmpDir, ".assetsignore"), contents);

await expect(createAssetsIgnoreFunction(tmpDir)).rejects.toBeInstanceOf(
InvalidAssetsIgnoreError
);
await expect(createAssetsIgnoreFunction(tmpDir)).rejects.toThrow(
"Save the file as UTF-8 and try again."
);
}
);

it("preserves an intentionally empty ignore file", async ({ expect }) => {
await writeFile(join(tmpDir, ".assetsignore"), "");
const { assetsIgnoreFunction, assetsIgnoreFilePresent } =
await createAssetsIgnoreFunction(tmpDir);

expect(assetsIgnoreFilePresent).toBe(true);
expect(assetsIgnoreFunction("keep.txt")).toBe(false);
expect(assetsIgnoreFunction(".assetsignore")).toBe(true);
});
});
});

Expand Down
Loading
Loading