feat: bundle browser dependencies locally

This commit is contained in:
2026-06-13 08:40:15 +02:00
parent 0ea9e4310f
commit 5e10be463e
8 changed files with 95 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import exifr from "https://cdn.jsdelivr.net/npm/exifr@7.1.3/+esm";
import exifr from "/vendor/exifr/full.esm.mjs";
import { createPhotoMarkerIcon } from "./map-view.js";
import { parseListing, resolveDavBaseUrl } from "./nextcloud-import.js";
import {

View File

@@ -2,25 +2,28 @@ import { readFile } from "node:fs/promises";
import type { ServerResponse } from "node:http";
const CLIENT_ASSET_PREFIX = "/client/";
const VENDOR_ASSET_PREFIX = "/vendor/";
export function isClientAssetPath(pathname: string): boolean {
return pathname.startsWith(CLIENT_ASSET_PREFIX);
return pathname.startsWith(CLIENT_ASSET_PREFIX) || pathname.startsWith(VENDOR_ASSET_PREFIX);
}
export async function handleClientAssetRequest(url: URL, res: ServerResponse): Promise<void> {
const assetName = url.pathname.slice(CLIENT_ASSET_PREFIX.length);
const isVendorAsset = url.pathname.startsWith(VENDOR_ASSET_PREFIX);
const assetPrefix = isVendorAsset ? VENDOR_ASSET_PREFIX : CLIENT_ASSET_PREFIX;
const assetName = url.pathname.slice(assetPrefix.length);
if (!/^[a-z0-9-]+\.js$/i.test(assetName)) {
if (!isAllowedAssetPath(assetName, isVendorAsset)) {
res.statusCode = 404;
res.end("Not found");
return;
}
try {
const assetUrl = new URL(`../client/${assetName}`, import.meta.url);
const source = await readFile(assetUrl, "utf8");
const assetUrl = new URL(`../${isVendorAsset ? "vendor" : "client"}/${assetName}`, import.meta.url);
const source = await readFile(assetUrl);
res.statusCode = 200;
res.setHeader("content-type", "text/javascript; charset=utf-8");
res.setHeader("content-type", getContentType(assetName));
res.setHeader("cache-control", "no-cache");
res.end(source);
} catch {
@@ -28,3 +31,27 @@ export async function handleClientAssetRequest(url: URL, res: ServerResponse): P
res.end("Not found");
}
}
function isAllowedAssetPath(assetName: string, isVendorAsset: boolean): boolean {
if (assetName.split("/").includes("..")) {
return false;
}
if (isVendorAsset) {
return /^[a-z0-9/_.-]+\.(css|js|mjs|png)$/i.test(assetName);
}
return /^[a-z0-9-]+\.js$/i.test(assetName);
}
function getContentType(assetName: string): string {
if (assetName.endsWith(".css")) {
return "text/css; charset=utf-8";
}
if (assetName.endsWith(".png")) {
return "image/png";
}
return "text/javascript; charset=utf-8";
}

View File

@@ -7,12 +7,7 @@ export function htmlPage(): string {
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mapix</title>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<link rel="stylesheet" href="/vendor/leaflet/leaflet.css" />
<style>
${APP_STYLES}
</style>
@@ -153,7 +148,7 @@ ${APP_STYLES}
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="/vendor/leaflet/leaflet.js"></script>
<script type="module" src="/client/app.js"></script>
</body>
</html>`;

View File

@@ -0,0 +1,34 @@
import { cp, mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
const projectRoot = process.cwd();
const vendorRoot = join(projectRoot, "dist", "vendor");
async function copyFile(source: string, target: string): Promise<void> {
await mkdir(join(target, ".."), { recursive: true });
await cp(source, target);
}
async function copyVendorAssets(): Promise<void> {
await rm(vendorRoot, { recursive: true, force: true });
await copyFile(
join(projectRoot, "node_modules", "leaflet", "dist", "leaflet.css"),
join(vendorRoot, "leaflet", "leaflet.css")
);
await copyFile(
join(projectRoot, "node_modules", "leaflet", "dist", "leaflet.js"),
join(vendorRoot, "leaflet", "leaflet.js")
);
await cp(
join(projectRoot, "node_modules", "leaflet", "dist", "images"),
join(vendorRoot, "leaflet", "images"),
{ recursive: true }
);
await copyFile(
join(projectRoot, "node_modules", "exifr", "dist", "full.esm.mjs"),
join(vendorRoot, "exifr", "full.esm.mjs")
);
}
await copyVendorAssets();