94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
import axios from "axios";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
export interface AssetMap {
|
|
[originalUrl: string]: string;
|
|
}
|
|
|
|
export class AssetManager {
|
|
private userAgent: string;
|
|
|
|
constructor(userAgent: string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") {
|
|
this.userAgent = userAgent;
|
|
}
|
|
|
|
public sanitizePath(rawPath: string): string {
|
|
return rawPath
|
|
.split("/")
|
|
.map((p) => p.replace(/[^a-z0-9._-]/gi, "_"))
|
|
.join("/");
|
|
}
|
|
|
|
public async downloadFile(url: string, assetsDir: string): Promise<string | null> {
|
|
if (url.startsWith("//")) url = `https:${url}`;
|
|
if (!url.startsWith("http")) return null;
|
|
|
|
try {
|
|
const u = new URL(url);
|
|
const relPath = this.sanitizePath(u.hostname + u.pathname);
|
|
const dest = path.join(assetsDir, relPath);
|
|
|
|
if (fs.existsSync(dest)) return `./assets/${relPath}`;
|
|
|
|
const res = await axios.get(url, {
|
|
responseType: "arraybuffer",
|
|
headers: { "User-Agent": this.userAgent },
|
|
timeout: 15000,
|
|
validateStatus: () => true,
|
|
});
|
|
|
|
if (res.status !== 200) return null;
|
|
|
|
if (!fs.existsSync(path.dirname(dest)))
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
fs.writeFileSync(dest, Buffer.from(res.data));
|
|
return `./assets/${relPath}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async processCssRecursively(
|
|
cssContent: string,
|
|
cssUrl: string,
|
|
assetsDir: string,
|
|
urlMap: AssetMap,
|
|
depth = 0,
|
|
): Promise<string> {
|
|
if (depth > 5) return cssContent;
|
|
|
|
const urlRegex = /(?:url\(["']?|@import\s+["'])([^"'\)]+)["']?\)?/gi;
|
|
let match;
|
|
let newContent = cssContent;
|
|
|
|
while ((match = urlRegex.exec(cssContent)) !== null) {
|
|
const originalUrl = match[1];
|
|
if (originalUrl.startsWith("data:") || originalUrl.startsWith("blob:"))
|
|
continue;
|
|
|
|
try {
|
|
const absUrl = new URL(originalUrl, cssUrl).href;
|
|
const local = await this.downloadFile(absUrl, assetsDir);
|
|
|
|
if (local) {
|
|
const u = new URL(cssUrl);
|
|
const cssPath = u.hostname + u.pathname;
|
|
const assetPath = new URL(absUrl).hostname + new URL(absUrl).pathname;
|
|
|
|
const rel = path.relative(
|
|
path.dirname(this.sanitizePath(cssPath)),
|
|
this.sanitizePath(assetPath),
|
|
);
|
|
|
|
newContent = newContent.split(originalUrl).join(rel);
|
|
urlMap[absUrl] = local;
|
|
}
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
return newContent;
|
|
}
|
|
}
|