183 lines
5.5 KiB
JavaScript
183 lines
5.5 KiB
JavaScript
|
|
// src/index.ts
|
||
|
|
import { dirname, extname, relative, resolve } from "node:path";
|
||
|
|
import { slash } from "@antfu/utils";
|
||
|
|
import axios from "axios";
|
||
|
|
import md5 from "blueimp-md5";
|
||
|
|
import Debug from "debug";
|
||
|
|
import fs from "fs-extra";
|
||
|
|
import MagicString from "magic-string";
|
||
|
|
import { DevEnvironment } from "vite";
|
||
|
|
var DefaultRules = [
|
||
|
|
{
|
||
|
|
match: /\b(https?:\/\/[\w#&?./-]*?\.(?:png|jpe?g|svg|ico))(?=[`'")\]])/gi
|
||
|
|
}
|
||
|
|
];
|
||
|
|
function isValidHttpUrl(str) {
|
||
|
|
let url;
|
||
|
|
try {
|
||
|
|
url = new URL(str);
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return url.protocol === "http:" || url.protocol === "https:";
|
||
|
|
}
|
||
|
|
function sleep(seconds) {
|
||
|
|
return new Promise((resolve2) => setTimeout(resolve2, seconds * 1e3));
|
||
|
|
}
|
||
|
|
var debug = Debug("vite-plugin-remote-assets");
|
||
|
|
function VitePluginRemoteAssets(options = {}) {
|
||
|
|
const {
|
||
|
|
assetsDir = "node_modules/.remote-assets",
|
||
|
|
rules = DefaultRules,
|
||
|
|
resolveMode = "relative",
|
||
|
|
awaitDownload = true,
|
||
|
|
retryTooManyRequests = false
|
||
|
|
} = options;
|
||
|
|
let dir = void 0;
|
||
|
|
let config;
|
||
|
|
const envs = [];
|
||
|
|
async function downloadTo(url, filepath, { retryTooManyRequests: retryTooManyRequests2 }) {
|
||
|
|
const writer = fs.createWriteStream(filepath);
|
||
|
|
const response = await axios({
|
||
|
|
url,
|
||
|
|
method: "GET",
|
||
|
|
validateStatus: (status) => {
|
||
|
|
if (status >= 200 && status < 300)
|
||
|
|
return true;
|
||
|
|
else if (retryTooManyRequests2 && status === 429)
|
||
|
|
return true;
|
||
|
|
else
|
||
|
|
return false;
|
||
|
|
},
|
||
|
|
responseType: "stream"
|
||
|
|
});
|
||
|
|
if (response.status === 429) {
|
||
|
|
const retryAfter = response.headers["retry-after"];
|
||
|
|
if (!retryAfter) {
|
||
|
|
throw new Error(`${url}: 429 without retry-after header`);
|
||
|
|
} else {
|
||
|
|
debug(`${url}: 429, retry after ${retryAfter} seconds`);
|
||
|
|
await sleep(retryAfter);
|
||
|
|
return await downloadTo(url, filepath, { retryTooManyRequests: retryTooManyRequests2 });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
response.data.pipe(writer);
|
||
|
|
return new Promise((resolve2, reject) => {
|
||
|
|
writer.on("finish", resolve2);
|
||
|
|
writer.on("error", reject);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const tasksMap = {};
|
||
|
|
async function transform(code, id) {
|
||
|
|
const tasks = [];
|
||
|
|
const s = new MagicString(code);
|
||
|
|
let hasReplaced = false;
|
||
|
|
let match;
|
||
|
|
for (const rule of rules) {
|
||
|
|
rule.match.lastIndex = 0;
|
||
|
|
while (match = rule.match.exec(code)) {
|
||
|
|
const start = match.index;
|
||
|
|
const end = start + match[0].length;
|
||
|
|
const url = match[0];
|
||
|
|
if (!url || !isValidHttpUrl(url))
|
||
|
|
continue;
|
||
|
|
const hash = md5(url) + (rule.ext || extname(url));
|
||
|
|
const filepath = slash(resolve(dir, hash));
|
||
|
|
debug("detected", url, hash);
|
||
|
|
if (!fs.existsSync(filepath) || tasksMap[filepath]) {
|
||
|
|
if (!tasksMap[filepath]) {
|
||
|
|
tasksMap[filepath] = (async () => {
|
||
|
|
try {
|
||
|
|
debug("downloading", url);
|
||
|
|
await downloadTo(url, filepath, { retryTooManyRequests });
|
||
|
|
debug("downloaded", url);
|
||
|
|
} catch (e) {
|
||
|
|
if (fs.existsSync(filepath))
|
||
|
|
await fs.unlink(filepath);
|
||
|
|
throw e;
|
||
|
|
} finally {
|
||
|
|
delete tasksMap[filepath];
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
}
|
||
|
|
tasks.push(tasksMap[filepath]);
|
||
|
|
if (!awaitDownload)
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
hasReplaced = true;
|
||
|
|
const mode = typeof resolveMode === "function" ? resolveMode(id, url) : resolveMode;
|
||
|
|
let newUrl;
|
||
|
|
if (mode === "relative") {
|
||
|
|
newUrl = slash(relative(dirname(id), `${dir}/${hash}`));
|
||
|
|
if (newUrl[0] !== ".")
|
||
|
|
newUrl = `./${newUrl}`;
|
||
|
|
} else {
|
||
|
|
let path = `${dir}/${hash}`;
|
||
|
|
if (!path.startsWith("/"))
|
||
|
|
path = `/${path}`;
|
||
|
|
newUrl = `/@fs${path}`;
|
||
|
|
}
|
||
|
|
s.overwrite(start, end, newUrl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (tasks.length) {
|
||
|
|
if (awaitDownload) {
|
||
|
|
await Promise.all(tasks);
|
||
|
|
} else {
|
||
|
|
Promise.all(tasks).then(() => {
|
||
|
|
envs.forEach((env) => {
|
||
|
|
if (env instanceof DevEnvironment) {
|
||
|
|
const module2 = env.moduleGraph.getModuleById(id);
|
||
|
|
if (module2)
|
||
|
|
env.moduleGraph.invalidateModule(module2);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const module = env.moduleGraph.getModuleById(id);
|
||
|
|
if (module)
|
||
|
|
env.moduleGraph.invalidateModule(module);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!hasReplaced)
|
||
|
|
return null;
|
||
|
|
return {
|
||
|
|
code: s.toString(),
|
||
|
|
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
name: "vite-plugin-remote-assets",
|
||
|
|
enforce: "pre",
|
||
|
|
async configResolved(_config) {
|
||
|
|
config = _config;
|
||
|
|
dir = slash(resolve(config.root, assetsDir));
|
||
|
|
if ("force" in config.server && config.server.force || config.optimizeDeps.force)
|
||
|
|
await fs.emptyDir(dir);
|
||
|
|
await fs.ensureDir(dir);
|
||
|
|
},
|
||
|
|
configureServer(server) {
|
||
|
|
if ("environments" in server && typeof server.environments === "object" && server.environments != null) {
|
||
|
|
Object.values(server.environments).forEach((env) => envs.push(env));
|
||
|
|
} else {
|
||
|
|
envs.push(server);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
async transform(code, id) {
|
||
|
|
return await transform(code, id);
|
||
|
|
},
|
||
|
|
transformIndexHtml: {
|
||
|
|
order: "pre",
|
||
|
|
async handler(code, ctx) {
|
||
|
|
return (await transform(code, ctx.filename))?.code;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
var src_default = VitePluginRemoteAssets;
|
||
|
|
export {
|
||
|
|
DefaultRules,
|
||
|
|
VitePluginRemoteAssets,
|
||
|
|
src_default as default
|
||
|
|
};
|