[Update] Change download and load plugins structure.(change the plugins' build method vite to webpack. load esm.js )
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { usePlugins } from "@logics_configs";
|
||||
|
||||
// ホスト側でReactやjotaiをグローバル変数として提供
|
||||
import ReactModule from "react";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
if (typeof window !== "undefined") {
|
||||
window.React = ReactModule;
|
||||
window.React = React;
|
||||
}
|
||||
|
||||
export const PluginsController = () => {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import semver from "semver";
|
||||
import { usePlugins } from "@logics_configs";
|
||||
import styles from "./Plugins.module.scss";
|
||||
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
||||
|
||||
const MAIN_VRCT_VERSION = "3.0.5";
|
||||
// PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL
|
||||
const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json";
|
||||
|
||||
export const Plugins = () => {
|
||||
return (
|
||||
@@ -13,26 +19,45 @@ export const Plugins = () => {
|
||||
const PluginDownloadContainer = () => {
|
||||
const [plugin_list, set_plugin_list] = useState([]);
|
||||
const [download_progress, set_download_progress] = useState({});
|
||||
|
||||
const { downloadAndExtractPlugin } = usePlugins();
|
||||
|
||||
useEffect(() => {
|
||||
// GitHub上のJSONファイルからプラグインリストを取得
|
||||
const fetchPluginList = async () => {
|
||||
async function asyncFetchPluginInfoList() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json"
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch plugin list");
|
||||
// tauriFetch を使用して vrct_plugins_list.json を取得(CORS 対策)
|
||||
const response = await tauriFetch(PLUGIN_LIST_URL, {
|
||||
method: "GET",
|
||||
responseType: ResponseType.Json,
|
||||
headers: { "Cache-Control": "no-cache" }
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to fetch plugin list, status: " + response.status);
|
||||
}
|
||||
const data = await response.json();
|
||||
set_plugin_list(data);
|
||||
// 取得される plugin_list.json は各プラグインの plugin_info.json への raw URL の配列とする
|
||||
const plugins_data = response.data;
|
||||
const updated_list = await Promise.all(
|
||||
plugins_data.map(async (plugin_data) => {
|
||||
try {
|
||||
const plugin_manifest = await asyncFetchPluginManifest(plugin_data.url);
|
||||
return { ...plugin_manifest };
|
||||
} catch (error) {
|
||||
console.error("Error fetching manifest for URL:", plugin_data.url, error);
|
||||
// エラー発生時は、plugin_data.title とエラーメッセージを返す
|
||||
return {
|
||||
title: plugin_data.title,
|
||||
plugin_id: plugin_data.plugin_id || plugin_data.title,
|
||||
error: error.message,
|
||||
url: plugin_data.url
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
set_plugin_list(updated_list);
|
||||
} catch (error) {
|
||||
console.error("Error fetching plugin list:", error);
|
||||
console.error("Error fetching plugin info list:", error);
|
||||
}
|
||||
};
|
||||
fetchPluginList();
|
||||
}
|
||||
asyncFetchPluginInfoList();
|
||||
}, []);
|
||||
|
||||
const handleDownload = async (plugin) => {
|
||||
@@ -44,14 +69,24 @@ const PluginDownloadContainer = () => {
|
||||
{plugin_list.map((plugin) => (
|
||||
<div key={plugin.plugin_id}>
|
||||
<h3>{plugin.title}</h3>
|
||||
<h3>{plugin.plugin_id}</h3>
|
||||
<button onClick={() => handleDownload(plugin)}>
|
||||
Download and Load Plugin
|
||||
</button>
|
||||
{download_progress[plugin.plugin_id] !== undefined && (
|
||||
<div>
|
||||
Download Progress: {download_progress[plugin.plugin_id].toFixed(0)}%
|
||||
</div>
|
||||
<h4>{plugin.plugin_id}</h4>
|
||||
{plugin.error ? (
|
||||
<p style={{ color: "red" }}>Error: {plugin.error}</p>
|
||||
) : (
|
||||
<>
|
||||
<p>Version: {plugin.plugin_version}</p>
|
||||
<p>
|
||||
Compatible: {plugin.min_compatible_version} ~ {plugin.max_compatible_version}
|
||||
</p>
|
||||
<button onClick={() => handleDownload(plugin)}>
|
||||
Download and Load Plugin
|
||||
</button>
|
||||
{download_progress[plugin.plugin_id] !== undefined && (
|
||||
<div>
|
||||
Download Progress: {download_progress[plugin.plugin_id].toFixed(0)}%
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -59,3 +94,69 @@ const PluginDownloadContainer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// GitHub Releases の latest 情報から plugin_info.json を取得する(tauriFetch を使用)
|
||||
async function asyncFetchPluginManifest(manifest_url) {
|
||||
// リリース情報を取得
|
||||
const release_response = await tauriFetch(manifest_url, {
|
||||
method: "GET",
|
||||
responseType: ResponseType.Json,
|
||||
headers: {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": "VRCTPluginApp"
|
||||
}
|
||||
});
|
||||
if (release_response.status !== 200) {
|
||||
throw new Error(`Failed to fetch release info from ${manifest_url}`);
|
||||
}
|
||||
const release_data = release_response.data;
|
||||
// assets 内に plugin_info.json があるかチェック
|
||||
const manifest_asset = release_data.assets.find(asset => asset.name === "plugin_info.json");
|
||||
if (!manifest_asset) {
|
||||
throw new Error("plugin_info.json not found in release assets");
|
||||
}
|
||||
// plugin_info.json の内容を取得
|
||||
const manifest_response = await tauriFetch(manifest_asset.browser_download_url, {
|
||||
method: "GET",
|
||||
responseType: ResponseType.Json,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "VRCTPluginApp",
|
||||
"Cache-Control": "no-cache"
|
||||
}
|
||||
});
|
||||
if (manifest_response.status !== 200) {
|
||||
throw new Error(`Failed to fetch plugin_info.json from ${manifest_asset.browser_download_url}`);
|
||||
}
|
||||
const plugin_manifest = manifest_response.data;
|
||||
return {
|
||||
title: plugin_manifest.title,
|
||||
plugin_id: plugin_manifest.plugin_id,
|
||||
plugin_version: plugin_manifest.plugin_version,
|
||||
min_compatible_version: plugin_manifest.min_compatible_version,
|
||||
max_compatible_version: plugin_manifest.max_compatible_version,
|
||||
asset_name: plugin_manifest.asset_name,
|
||||
url: manifest_url
|
||||
};
|
||||
}
|
||||
|
||||
export { PluginDownloadContainer };
|
||||
|
||||
|
||||
|
||||
// // プラグインのマニフェスト(plugin.json から取得した情報の例)
|
||||
// const plugin_manifest = {
|
||||
// compatible_lower_version: "3.0.4",
|
||||
// compatible_upper_version: "3.0.6",
|
||||
// // 他の情報...
|
||||
// };
|
||||
|
||||
// const isPluginCompatible = (main_version, lower_version, upper_version) => {
|
||||
// // lower_version 以上かつ upper_version 以下なら互換性ありと判定
|
||||
// return semver.gte(main_version, lower_version) && semver.lte(main_version, upper_version);
|
||||
// };
|
||||
|
||||
// if (isPluginCompatible(currentSoftwareVersion.data, plugin_manifest.compatible_lower_version, plugin_manifest.compatible_upper_version)) {
|
||||
// console.log("プラグインは互換性があります。");
|
||||
// } else {
|
||||
// console.error("プラグインは現在の VRCT バージョンと互換性がありません。");
|
||||
// }
|
||||
@@ -1,50 +1,41 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { createAtomWithHook, useStore_LoadedPluginsList } from "@store";
|
||||
import { useSoftwareVersion } from "@logics_configs";
|
||||
import { transform } from "@babel/standalone";
|
||||
import { writeFile, createDir, exists, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs";
|
||||
import { writeFile, createDir, exists, removeDir, 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";
|
||||
|
||||
import React from "react";
|
||||
// import ReactDOM from "react-dom/client";
|
||||
|
||||
// グローバルに公開
|
||||
window.React = React;
|
||||
// window.ReactDOM = ReactDOM;
|
||||
window.React$1 = React;
|
||||
window.we = React;
|
||||
|
||||
export const usePlugins = () => {
|
||||
const { updateLoadedPluginsList } = useStore_LoadedPluginsList();
|
||||
const { currentSoftwareVersion } = useSoftwareVersion();
|
||||
|
||||
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 }];
|
||||
});
|
||||
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;
|
||||
|
||||
try {
|
||||
const plugin_code = await readTextFile(plugin_relative_path, { dir: BaseDirectory.Resource, recursive: true });
|
||||
const cleanedCode = removeImportStatements(plugin_code);
|
||||
const transpiled_code = transform(cleanedCode, {
|
||||
const cleaned_code = removeImportStatements(plugin_code);
|
||||
const transpiled_code = transform(cleaned_code, {
|
||||
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);
|
||||
@@ -60,7 +51,7 @@ export const usePlugins = () => {
|
||||
|
||||
const loadAllPlugins = async () => {
|
||||
if (import.meta.env.DEV) {
|
||||
// ホットリロード対応 src-tauri以下にあるpluginsディレクトリから直接読み込み(開発用)
|
||||
// 開発時: ホットリロード対応、src-tauri以下のpluginsから直接読み込み
|
||||
Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => {
|
||||
if (plugin_module && plugin_module.init) {
|
||||
plugin_module.init(plugin_context);
|
||||
@@ -70,7 +61,7 @@ export const usePlugins = () => {
|
||||
try {
|
||||
const plugin_files = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true });
|
||||
for (const target_dir of plugin_files) {
|
||||
const target_path = target_dir.name + "/index.es.js";
|
||||
const target_path = target_dir.name + "/index.esm.js";
|
||||
await asyncLoadPlugin(target_path, plugin_context);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -82,68 +73,63 @@ export const usePlugins = () => {
|
||||
const downloadAndExtractPlugin = async (plugin) => {
|
||||
try {
|
||||
const plugin_zip_url = await fetchLatestPluginZipUrl(plugin);
|
||||
// Rust コマンド経由で zip をダウンロード
|
||||
const base64Zip = await invoke("download_zip_asset", { url: plugin_zip_url });
|
||||
// base64Zip は文字列なので、デコードして Uint8Array に変換
|
||||
const binaryString = atob(base64Zip);
|
||||
const len = binaryString.length;
|
||||
// Rust コマンド経由で ZIP をダウンロード
|
||||
const base64_zip = await invoke("download_zip_asset", { url: plugin_zip_url });
|
||||
// base64_zip をデコードして Uint8Array に変換
|
||||
const binary_string = atob(base64_zip);
|
||||
const len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
|
||||
// JSZip で zip を解凍
|
||||
// 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) {
|
||||
// }
|
||||
// 展開先ディレクトリのパス(例:"plugins/<plugin_id>" とする)
|
||||
const target_plugin_path = "plugins/" + plugin.asset_name.replace(".zip", "");
|
||||
// 既に存在する場合は削除してから新規作成
|
||||
if (await exists(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true })) {
|
||||
await removeDir(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true });
|
||||
}
|
||||
await createDir(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true });
|
||||
|
||||
|
||||
const filePromises = [];
|
||||
zip.forEach((relativePath, zipEntry) => {
|
||||
const file_promises = [];
|
||||
zip.forEach((relative_path, zip_entry) => {
|
||||
// .git 以下のファイルはスキップ
|
||||
if (relativePath.startsWith(".git") || relativePath.includes("/.git/")) {
|
||||
if (relative_path.startsWith(".git") || relative_path.includes("/.git/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = target_plugin_path + "/" + relativePath;
|
||||
|
||||
if (zipEntry.dir) {
|
||||
// フォルダの場合、ディレクトリを作成
|
||||
filePromises.push(
|
||||
createDir(filePath, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => {
|
||||
const file_path = target_plugin_path + "/" + relative_path;
|
||||
if (zip_entry.dir) {
|
||||
file_promises.push(
|
||||
createDir(file_path, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => {
|
||||
if (!err.message?.includes("already exists")) {
|
||||
console.error("Failed to create directory:", filePath, err);
|
||||
console.error("Failed to create directory:", file_path, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// ファイルの場合、ディレクトリを作成してから書き込む
|
||||
const dirPath = filePath.substring(0, filePath.lastIndexOf("/")); // 親ディレクトリのパス
|
||||
|
||||
const promise = createDir(dirPath, { dir: BaseDirectory.Resource, recursive: true })
|
||||
const dir_path = file_path.substring(0, file_path.lastIndexOf("/"));
|
||||
const promise = createDir(dir_path, { dir: BaseDirectory.Resource, recursive: true })
|
||||
.catch((err) => {
|
||||
if (!err.message?.includes("already exists")) {
|
||||
console.error("Failed to create parent directory:", dirPath, err);
|
||||
console.error("Failed to create parent directory:", dir_path, err);
|
||||
}
|
||||
})
|
||||
.then(() => zipEntry.async("text"))
|
||||
.then(async (fileData) => {
|
||||
await writeFile(filePath, fileData, { dir: BaseDirectory.Resource, recursive: true });
|
||||
.then(() => zip_entry.async("text"))
|
||||
.then(async (file_data) => {
|
||||
await writeFile(file_path, file_data, { dir: BaseDirectory.Resource, recursive: true });
|
||||
});
|
||||
|
||||
filePromises.push(promise);
|
||||
file_promises.push(promise);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(filePromises);
|
||||
await Promise.all(file_promises);
|
||||
console.log("Plugin downloaded successfully.");
|
||||
|
||||
const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/" + "index.es.js"
|
||||
await asyncLoadPlugin(index_file_relative_path);
|
||||
const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/index.esm.js";
|
||||
await asyncLoadPlugin(index_file_relative_path, plugin_context);
|
||||
|
||||
console.log("Plugin loaded successfully.");
|
||||
} catch (error) {
|
||||
@@ -151,8 +137,7 @@ export const usePlugins = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// JSON内のURLから GitHub API を使って最新リリース情報を取得し、
|
||||
// assets 配列から plugin.asset_name に一致するアセットの browser_download_url を返す
|
||||
// GitHub API を使用して、最新リリース情報から asset_name に一致するアセットのブラウザダウンロード URL を返す
|
||||
const fetchLatestPluginZipUrl = async (plugin) => {
|
||||
const api_url = plugin.url;
|
||||
const response = await tauriFetch(api_url, {
|
||||
|
||||
Reference in New Issue
Block a user