From f9c0fed5e31064f65187c90f101bbf423d78b451 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Sun, 12 Jul 2026 23:07:16 +0200 Subject: [PATCH] fix: preserve extension-only plugin asset MIME types --- src/server/piWebPluginService.test.ts | 16 ++++++++++++++++ src/server/piWebPluginService.ts | 17 ++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/server/piWebPluginService.test.ts b/src/server/piWebPluginService.test.ts index 7507aa7..6b00d34 100644 --- a/src/server/piWebPluginService.test.ts +++ b/src/server/piWebPluginService.test.ts @@ -44,6 +44,22 @@ describe("PiWebPluginService", () => { expect(asset?.content.toString("utf8")).toContain("export default"); }); + it("preserves content types for extension-only asset names", async () => { + const pluginDir = join(tempDir, "plugins", "extension-only"); + await writePlugin(pluginDir, { + packageJson: { piWeb: { plugins: [{ id: "extension-only", module: ".js" }] } }, + files: { + ".js": "export default {};", + ".svg": '', + }, + }); + + const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); + + await expect(service.readAsset("extension-only", ".js")).resolves.toMatchObject({ contentType: "application/javascript; charset=utf-8" }); + await expect(service.readAsset("extension-only", ".svg")).resolves.toMatchObject({ contentType: "image/svg+xml" }); + }); + it("serves nested SVG assets with a browser-compatible content type", async () => { const pluginDir = join(tempDir, "plugins", "icons"); const svg = ''; diff --git a/src/server/piWebPluginService.ts b/src/server/piWebPluginService.ts index 0566153..45938ea 100644 --- a/src/server/piWebPluginService.ts +++ b/src/server/piWebPluginService.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import { readdir, readFile, realpath, stat } from "node:fs/promises"; -import { dirname, extname, join, relative, resolve, sep } from "node:path"; +import { dirname, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent"; import { loadPiWebConfig, piWebDataDir, type PiWebConfig } from "../config.js"; @@ -335,14 +335,13 @@ function isWithin(root: string, candidate: string): boolean { } function contentTypeFor(path: string): string { - switch (extname(path).toLowerCase()) { - case ".js": return "application/javascript; charset=utf-8"; - case ".json": return "application/json; charset=utf-8"; - case ".css": return "text/css; charset=utf-8"; - case ".html": return "text/html; charset=utf-8"; - case ".svg": return "image/svg+xml"; - default: return "application/octet-stream"; - } + const lowerPath = path.toLowerCase(); + if (lowerPath.endsWith(".js")) return "application/javascript; charset=utf-8"; + if (lowerPath.endsWith(".json")) return "application/json; charset=utf-8"; + if (lowerPath.endsWith(".css")) return "text/css; charset=utf-8"; + if (lowerPath.endsWith(".html")) return "text/html; charset=utf-8"; + if (lowerPath.endsWith(".svg")) return "image/svg+xml"; + return "application/octet-stream"; } function isRecord(value: unknown): value is Record {