Files
VRCT/src-ui/logics/configs/plugins/usePlugins.js
2025-03-05 23:22:22 +09:00

191 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { invoke } from '@tauri-apps/api/tauri';
import { createAtomWithHook, useStore_LoadedPluginsList } from "@store";
import { transform } from "@babel/standalone";
import { writeFile, createDir, exists, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs";
const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true });
import JSZip from "jszip";
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
export const usePlugins = () => {
const { updateLoadedPluginsList } = useStore_LoadedPluginsList();
const plugin_context = {
registerComponent: ({ plugin_id, location, component }) => {
if (!plugin_id || !location || !component) {
return console.error("An invalid plugin was detected.", plugin_id, location, component);
}
updateLoadedPluginsList((prev) => {
const filtered = prev.data.filter(item => item.plugin_id !== plugin_id);
return [...filtered, { plugin_id, location, component }];
});
},
createAtomWithHook: (...args) => createAtomWithHook(...args)
};
const asyncLoadPlugin = async (plugin_relative_path) => {
plugin_relative_path = "plugins/" + plugin_relative_path;
console.log("plugin_relative_path",plugin_relative_path);
try {
const plugin_code = await readTextFile(plugin_relative_path, { dir: BaseDirectory.Resource, recursive: true });
const cleanedCode = removeImportStatements(plugin_code);
const transpiled_code = transform(cleanedCode, {
presets: [
["env", { modules: false }],
"react"
],
sourceType: "module"
}).code;
const blob = new Blob([transpiled_code], { type: "text/javascript" });
const blob_url = URL.createObjectURL(blob);
const plugin_module = await import(/* @vite-ignore */ blob_url);
URL.revokeObjectURL(blob_url);
if (plugin_module && plugin_module.init) {
plugin_module.init(plugin_context);
}
} catch (error) {
console.error("Failed to load plugin from", plugin_relative_path, error);
}
};
const loadAllPlugins = async () => {
if (import.meta.env.DEV) {
// ホットリロード対応 src-tauri以下にあるpluginsディレクトリから直接読み込み開発用
Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => {
console.log(plugin_module);
if (plugin_module && plugin_module.init) {
plugin_module.init(plugin_context);
}
});
} else {
try {
const plugin_files = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true });
for (const target_dir of plugin_files) {
console.log(target_dir);
const target_path = target_dir.name + "\\index.jsx";
await asyncLoadPlugin(target_path, plugin_context);
}
} catch (error) {
console.error("Error loading plugins:", error);
}
}
};
const downloadAndExtractPlugin = async (plugin) => {
try {
const plugin_zip_url = await fetchLatestPluginZipUrl(plugin);
console.log("Latest plugin zip URL:", plugin_zip_url);
// Rust コマンド経由で zip をダウンロード
const base64Zip = await invoke("download_zip_asset", { url: plugin_zip_url });
// base64Zip は文字列なので、デコードして Uint8Array に変換
const binaryString = atob(base64Zip);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// JSZip で zip を解凍
const zip = await JSZip.loadAsync(bytes);
// const plugin_dir_exists = await exists("plugins", { dir: BaseDirectory.Resource, recursive: true });
await createDir("plugins/" + plugin.asset_name.replace(".zip", ""), { dir: BaseDirectory.Resource, recursive: true });
// if (!plugin_dir_exists) {
// }
const target_plugin_path = "plugins/" + plugin.asset_name.replace(".zip", "");
const filePromises = [];
zip.forEach((relativePath, zipEntry) => {
// .git 以下のファイルはスキップ
if (relativePath.startsWith(".git") || relativePath.includes("/.git/")) {
// console.log("Skipping .git file: " + relativePath);
return;
}
const filePath = target_plugin_path + "/" + relativePath;
if (zipEntry.dir) {
// フォルダの場合、ディレクトリを作成
filePromises.push(
createDir(filePath, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => {
console.log(err);
if (!err.message?.includes("already exists")) {
console.error("Failed to create directory:", filePath, err);
}
})
);
} else {
// ファイルの場合、ディレクトリを作成してから書き込む
const dirPath = filePath.substring(0, filePath.lastIndexOf("/")); // 親ディレクトリのパス
const promise = createDir(dirPath, { dir: BaseDirectory.Resource, recursive: true })
.catch((err) => {
if (!err.message?.includes("already exists")) {
console.error("Failed to create parent directory:", dirPath, err);
}
})
.then(() => zipEntry.async("text"))
.then(async (fileData) => {
await writeFile(filePath, fileData, { dir: BaseDirectory.Resource, recursive: true });
});
filePromises.push(promise);
}
});
await Promise.all(filePromises);
console.log("Plugin downloaded successfully.");
const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/" + "index.jsx"
console.log("index_file_relative_path", index_file_relative_path);
await asyncLoadPlugin(index_file_relative_path);
console.log("Plugin loaded successfully.");
} catch (error) {
console.error("Error downloading and extracting plugin:", error);
}
};
// JSON内のURLから GitHub API を使って最新リリース情報を取得し、
// assets 配列から plugin.asset_name に一致するアセットの browser_download_url を返す
const fetchLatestPluginZipUrl = async (plugin) => {
const api_url = plugin.url;
const response = await tauriFetch(api_url, {
method: "GET",
responseType: ResponseType.Json,
headers: {
"Accept": "application/vnd.github+json",
"User-Agent": "VRCTPluginApp"
}
});
if (response.status !== 200) {
throw new Error("Failed to fetch latest release info, status: " + response.status);
}
const release_info = response.data;
const asset = release_info.assets.find((a) => a.name === plugin.asset_name);
if (!asset) {
throw new Error(`Asset ${plugin.asset_name} not found in the latest release`);
}
return asset.browser_download_url;
};
return {
loadAllPlugins,
downloadAndExtractPlugin,
};
};
const removeImportStatements = (code) => {
return code
.split("\n")
.filter(line => !line.match(/^import\s+.*['"]react['"]/))
.join("\n");
};