Skip to content

Commit

Permalink
✨ プラグインを読み込みための基本実装
Browse files Browse the repository at this point in the history
  • Loading branch information
ci7lus committed Aug 7, 2021
1 parent bdf2ee5 commit 84fb2c1
Show file tree
Hide file tree
Showing 38 changed files with 1,548 additions and 512 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ jobs:
with:
name: mac-build-image
path: build/*.dmg
- name: Build dts for plugin
run: yarn build:dts-plugin
- name: Upload plugin.d.ts
uses: actions/upload-artifact@v2
with:
name: plugin-dts
path: dist/plugin.d.ts
build-windows:
runs-on: windows-latest

Expand Down
4 changes: 3 additions & 1 deletion electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ files:
- "node_modules/lru-cache"
- "node_modules/yallist"
- "node_modules/type-fest"
- "node_modules/object-hash"
- "node_modules/esm"
- from: node_modules/webchimera.js
to: node_modules/webchimera.js
publish: null
Expand All @@ -205,7 +207,7 @@ mac:
entitlements: "entitlements.plist"
entitlementsInherit: "entitlements.plist"
extendInfo:
NSHumanReadableCopyright: "Copyright © 2021 ci7lus\n\nこのアプリは libVLC を同梱しています。このアプリイメージには LGPLv2.1 または GPLv2 が適用されます。"
NSHumanReadableCopyright: "Copyright © 2021 ci7lus\nこのアプリは libVLC を同梱しています。このアプリイメージには LGPLv2.1 または GPLv2 が適用されます。"
win:
icon: "assets/miraktest.ico"
target:
Expand Down
17 changes: 12 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"lint:prettier": "prettier --check './src/**/*.{js,ts,tsx}'",
"format:prettier": "prettier --write './src/**/*.{js,ts,tsx}'",
"lint:eslint": "eslint --max-warnings 0 --cache './src/**/*.{js,ts,tsx}'",
"format:eslint": "eslint './src/**/*.{js,ts,tsx}' --cache --fix"
"format:eslint": "eslint './src/**/*.{js,ts,tsx}' --cache --fix",
"build:dts-plugin": "yarn dts-bundle-generator --export-referenced-types=false --external-inlines=electron -o dist/plugin.d.ts --no-check src/types/plugin.ts && ts-node setLicenseInDts.ts"
},
"lint-staged": {
"*.{js,ts,tsx}": "eslint --max-warnings 0 --cache --fix",
Expand All @@ -40,7 +41,9 @@
"@tailwindcss/custom-forms": "^0.2.1",
"@types/discord-rpc": "^3.0.5",
"@types/dplayer": "^1.25.1",
"@types/esm": "^3.2.0",
"@types/mini-css-extract-plugin": "^1.4.3",
"@types/object-hash": "^2.1.1",
"@types/react": "^17.0.13",
"@types/react-dom": "^17.0.8",
"@types/yarnpkg__lockfile": "^1.1.5",
Expand All @@ -51,7 +54,8 @@
"babel-loader": "^8.2.2",
"cross-env": "^7.0.3",
"css-loader": "^5.2.6",
"electron": "^12.0.13",
"dts-bundle-generator": "^5.9.0",
"electron": "^12.0.15",
"electron-builder": "^22.10.5",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
Expand Down Expand Up @@ -82,18 +86,21 @@
"discord-rpc": "^4.0.1",
"electron-reload": "^1.5.0",
"electron-store": "^8.0.0",
"esm": "^3.2.25",
"object-hash": "^2.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-feather": "^2.0.9",
"react-toastify": "^7.0.4",
"react-use": "^17.2.4",
"recoil": "^0.3.1",
"reconnecting-websocket": "^4.4.0",
"webchimera.js": "^0.5.2"
"webchimera.js": "^0.5.2",
"zod": "^3.5.1"
},
"cmake-js": {
"runtime": "electron",
"runtimeVersion": "12.0.13"
"runtimeVersion": "12.0.15"
},
"browserslist": "electron 12.0.13"
"browserslist": "electron 12.0.15"
}
7 changes: 6 additions & 1 deletion pickRequiredDeps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ type Package = {
dependencies?: { [key: string]: string }
}

const targets = ["webchimera.js", "electron-store"] as const
const targets = [
"webchimera.js",
"electron-store",
"object-hash",
"esm",
] as const
const dependencies = { ...pkg.dependencies, ...pkg.devDependencies }
const targetWithVersion = targets.map(
(depName) => `${depName}@${dependencies[depName]}`
Expand Down
20 changes: 20 additions & 0 deletions setLicenseInDts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fs from "fs"

const main = async () => {
const dts = await fs.promises.readFile("./dist/plugin.d.ts", "utf8")
const license = await fs.promises.readFile(`./LICENSE`, "utf8")
const licenses = ["MirakTest: " + license]
const libs = ["electron"]
for (const lib of libs) {
const license = await fs.promises.readFile(
`./node_modules/${lib}/LICENSE`,
"utf8"
)
licenses.push(`${lib}: ${license}`)
}
const rewrited = `/** plugin.d.ts - Type definitions for creating plug-ins for MirakTest.\n---\n${licenses.join(
"\n---\n"
)}\n*/\n${dts}`
await fs.promises.writeFile("./dist/plugin.d.ts", rewrited)
}
main()
10 changes: 10 additions & 0 deletions src/@types/dom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RecoilState } from "recoil"
import { PluginDefineInRenderer } from "../types/plugin"

export declare global {
interface Window {
atoms?: RecoilState<unknown>[]
plugins?: PluginDefineInRenderer[]
contextMenus?: { [key: string]: Electron.MenuItemConstructorOptions }
}
}
43 changes: 0 additions & 43 deletions src/App.tsx

This file was deleted.

145 changes: 145 additions & 0 deletions src/Plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import Axios from "axios"
import { ipcRenderer, remote } from "electron"
import React, { useEffect, useState } from "react"
import Recoil, { RecoilState } from "recoil"
import pkg from "../package.json"
import { StateRoot } from "./State"
import { Splash } from "./components/global/Splash"
import { REUQEST_OPEN_WINDOW } from "./constants/ipc"
import {
RECOIL_SHARED_ATOM_KEYS,
RECOIL_STORED_ATOM_KEYS,
} from "./constants/recoil"
import { OpenWindowArg } from "./types/ipc"
import {
InitPlugin,
PluginDefineInRenderer,
PluginInRendererArgs,
} from "./types/plugin"
import { ObjectLiteral } from "./types/struct"
import { nativeImport } from "./utils/nativeImport"
import { pluginValidator } from "./utils/plugin"

export const PluginLoader: React.VFC<{
states: ObjectLiteral
pluginPaths: string[]
}> = ({ states, pluginPaths }) => {
const [isLoading, setIsLoading] = useState(true)
const [sharedAtoms, setSharedAtoms] = useState(RECOIL_SHARED_ATOM_KEYS)
const [storedAtoms, setStoredAtoms] = useState(RECOIL_STORED_ATOM_KEYS)
useEffect(() => {
window.plugins = window.atoms = []
window.contextMenus = {}
const contextMenus: { [key: string]: Electron.MenuItemConstructorOptions } =
{}
const openWindow = async (args: OpenWindowArg) => {
return await ipcRenderer.invoke(REUQEST_OPEN_WINDOW, args)
}
const args: PluginInRendererArgs = {
packages: {
React,
Recoil,
Axios,
Electron: remote,
IpcRenderer: ipcRenderer,
},
appInfo: { version: pkg.version },
functions: {
openWindow,
},
}
;(async () => {
const atoms: RecoilState<unknown>[] = []
const plugins: PluginDefineInRenderer[] = []
console.info("pluginPaths:", pluginPaths)
const openedPlugins: PluginDefineInRenderer[] = []
for (const filePath of pluginPaths) {
try {
console.info("[Plugin] 取り込み中:", filePath)
const module: { default: InitPlugin } | InitPlugin =
await nativeImport(filePath)
const plugin =
"default" in module
? module.default.renderer(args)
: module.renderer(args)
pluginValidator.parse(plugin)
console.info(
`[Plugin] 読込中: ${plugin.name} (${plugin.id}, ${plugin.version})`
)
if (
![
...plugin.storedAtoms,
...plugin.sharedAtoms,
...plugin.exposedAtoms,
].every((atom) => atom.key.startsWith("plugins."))
) {
throw new Error(
`すべての露出した atom のキーは \`plugins.\` から開始する必要があります: ${plugin.id}`
)
}
openedPlugins.push(plugin)
} catch (error) {
console.error("[Plugin] 読み込みエラー:", error)
}
}
for (const plugin of openedPlugins) {
try {
await plugin.setup({ plugins: openedPlugins })
if (plugin.contextMenu) {
contextMenus[plugin.id] = plugin.contextMenu
}
plugin.sharedAtoms.forEach((atom) => {
const mached = atoms.find((_atom) => _atom.key === atom.key)
if (!mached) {
atoms.push(atom)
}
setSharedAtoms((atoms) => [...atoms, atom.key])
})
plugin.storedAtoms.forEach((atom) => {
const mached = atoms.find((_atom) => _atom.key === atom.key)
if (!mached) {
atoms.push(atom)
}
setStoredAtoms((atoms) => [...atoms, atom.key])
})
plugin.exposedAtoms.forEach((atom) => {
const mached = atoms.find((_atom) => _atom.key === atom.key)
if (!mached) {
atoms.push(atom)
}
})
plugins.push(plugin)
} catch (error) {
console.error(
"[Plugin] setup 中にエラーが発生しました:",
plugin.id,
error
)
try {
await plugin.destroy()
} catch (error) {
console.error(
"[Plugin] destroy 中にエラーが発生しました:",
plugin.id,
error
)
}
}
}
window.plugins = plugins
window.atoms = atoms
window.contextMenus = contextMenus
setIsLoading(false)
})()
}, [])
if (isLoading) {
return <Splash />
}
return (
<StateRoot
states={states}
sharedAtoms={sharedAtoms}
storedAtoms={storedAtoms}
/>
)
}
44 changes: 44 additions & 0 deletions src/Router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useEffect, useState } from "react"
import { useRecoilBridgeAcrossReactRoots_UNSTABLE } from "recoil"
import { ComponentShadowWrapper } from "./components/common/ComponentShadowWrapper"
import { Splash } from "./components/global/Splash"
import { Routes } from "./types/struct"
import { CoiledContentPlayer } from "./windows/ContentPlayer"
import { Settings } from "./windows/Settings"

export const Router: React.VFC<{}> = () => {
const RecoilBridge = useRecoilBridgeAcrossReactRoots_UNSTABLE()
const [hash, setHash] = useState<Routes>("")
useEffect(() => {
const onHashChange = () => {
const hash = window.location.hash.replace("#", "")
setHash(hash)
}
window.addEventListener("hashchange", onHashChange)
onHashChange()
return () => window.removeEventListener("hashchange", onHashChange)
}, [])
if (hash === "ContentPlayer") {
return <CoiledContentPlayer />
} else if (hash === "Settings") {
return <Settings />
} else if (hash === "ProgramTable") {
return <Settings />
} else {
const Component = window.plugins?.find((plugin) => plugin.windows?.[hash])
?.windows[hash]
if (Component) {
return (
<ComponentShadowWrapper
_id={hash}
Component={() => (
<RecoilBridge>
<Component />
</RecoilBridge>
)}
/>
)
}
return <Splash>Error: 想定していない表示です({hash}</Splash>
}
}
23 changes: 23 additions & 0 deletions src/State.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react"
import { RecoilRoot } from "recoil"
import { Router } from "./Router"
import { RecoilApplier } from "./components/global/RecoilApplier"
import { RecoilObserver } from "./components/global/RecoilObserver"
import { ObjectLiteral } from "./types/struct"
import { initializeState } from "./utils/recoil"

export const StateRoot: React.VFC<{
states: ObjectLiteral
sharedAtoms: string[]
storedAtoms: string[]
}> = ({ states, sharedAtoms, storedAtoms }) => {
return (
<RecoilRoot
initializeState={initializeState({ states, sharedAtoms, storedAtoms })}
>
<RecoilObserver />
<RecoilApplier />
<Router />
</RecoilRoot>
)
}
Loading

0 comments on commit 84fb2c1

Please sign in to comment.