mirror of https://github.com/halo-dev/halo
feat: add API to obtain the bundled js file for all enabled plugins (#3444)
#### What type of PR is this? /kind feature /milestone 2.3.x /area core #### What this PR does / why we need it: 提供 `/apis/api.console.halo.run/v1alpha1/plugins/bundle.js` 来获取已启用插件的捆绑后的 main.js 和 style.css 文件 #### Which issue(s) this PR fixes: Fixes #3442 #### Does this PR introduce a user-facing change? ```release-note 优化已启用插件 jsbundle 文件的加载方式 ```pull/4486/head
parent
ec0187d8aa
commit
5c115563e0
|
@ -29,6 +29,7 @@ import java.util.Comparator;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
@ -217,10 +218,61 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
builder -> builder.operationId("ListPluginPresets")
|
||||
.description("List all plugin presets in the system.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder().implementationArray(Plugin.class)))
|
||||
.response(responseBuilder().implementationArray(Plugin.class))
|
||||
)
|
||||
.GET("plugins/-/bundle.js", this::fetchJsBundle,
|
||||
builder -> builder.operationId("fetchJsBundle")
|
||||
.description("Merge all JS bundles of enabled plugins into one.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder().implementation(String.class))
|
||||
)
|
||||
.GET("plugins/-/bundle.css", this::fetchCssBundle,
|
||||
builder -> builder.operationId("fetchCssBundle")
|
||||
.description("Merge all CSS bundles of enabled plugins into one.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder().implementation(String.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fetchJsBundle(ServerRequest request) {
|
||||
Optional<String> versionOption = request.queryParam("v");
|
||||
if (versionOption.isEmpty()) {
|
||||
return pluginService.generateJsBundleVersion()
|
||||
.flatMap(v -> ServerResponse
|
||||
.temporaryRedirect(buildJsBundleUri("js", v))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
return pluginService.uglifyJsBundle()
|
||||
.defaultIfEmpty("")
|
||||
.flatMap(bundle -> ServerResponse.ok()
|
||||
.contentType(MediaType.valueOf("text/javascript"))
|
||||
.bodyValue(bundle)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fetchCssBundle(ServerRequest request) {
|
||||
Optional<String> versionOption = request.queryParam("v");
|
||||
if (versionOption.isEmpty()) {
|
||||
return pluginService.generateJsBundleVersion()
|
||||
.flatMap(v -> ServerResponse
|
||||
.temporaryRedirect(buildJsBundleUri("css", v))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
return pluginService.uglifyCssBundle()
|
||||
.flatMap(bundle -> ServerResponse.ok()
|
||||
.contentType(MediaType.valueOf("text/css"))
|
||||
.bodyValue(bundle)
|
||||
);
|
||||
}
|
||||
|
||||
URI buildJsBundleUri(String type, String version) {
|
||||
return URI.create(
|
||||
"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle." + type + "?v=" + version);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> upgradeFromUri(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
var content = request.bodyToMono(UpgradeFromUriRequest.class)
|
||||
|
|
|
@ -40,4 +40,26 @@ public interface PluginService {
|
|||
* @see run.halo.app.plugin.HaloPluginManager#reloadPlugin(String)
|
||||
*/
|
||||
Mono<Plugin> reload(String name);
|
||||
|
||||
/**
|
||||
* Uglify js bundle from all enabled plugins to a single js bundle string.
|
||||
*
|
||||
* @return uglified js bundle
|
||||
*/
|
||||
Mono<String> uglifyJsBundle();
|
||||
|
||||
/**
|
||||
* Uglify css bundle from all enabled plugins to a single css bundle string.
|
||||
*
|
||||
* @return uglified css bundle
|
||||
*/
|
||||
Mono<String> uglifyCssBundle();
|
||||
|
||||
/**
|
||||
* <p>Generate js bundle version for cache control.</p>
|
||||
* This method will list all enabled plugins version and sign it to a string.
|
||||
*
|
||||
* @return signed js bundle version by all enabled plugins version.
|
||||
*/
|
||||
Mono<String> generateJsBundleVersion();
|
||||
}
|
||||
|
|
|
@ -3,12 +3,18 @@ package run.halo.app.core.extension.service.impl;
|
|||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import com.google.common.hash.Hashing;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
@ -37,6 +43,7 @@ import run.halo.app.plugin.PluginConst;
|
|||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.PluginUtils;
|
||||
import run.halo.app.plugin.YamlPluginFinder;
|
||||
import run.halo.app.plugin.resources.BundleResourceUtils;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
|
@ -124,6 +131,70 @@ public class PluginServiceImpl implements PluginService {
|
|||
return updateReloadAnno(name, pluginWrapper.getPluginPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> uglifyJsBundle() {
|
||||
return Mono.fromSupplier(() -> {
|
||||
StringBuilder jsBundle = new StringBuilder();
|
||||
List<String> pluginNames = new ArrayList<>();
|
||||
for (PluginWrapper pluginWrapper : pluginManager.getStartedPlugins()) {
|
||||
String pluginName = pluginWrapper.getPluginId();
|
||||
pluginNames.add(pluginName);
|
||||
Resource jsBundleResource =
|
||||
BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
|
||||
BundleResourceUtils.JS_BUNDLE);
|
||||
if (jsBundleResource != null) {
|
||||
try {
|
||||
jsBundle.append(
|
||||
jsBundleResource.getContentAsString(StandardCharsets.UTF_8));
|
||||
jsBundle.append("\n");
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read js bundle of plugin [{}]", pluginName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String plugins = """
|
||||
this.enabledPluginNames = [%s];
|
||||
""".formatted(pluginNames.stream()
|
||||
.collect(Collectors.joining("','", "'", "'")));
|
||||
return jsBundle + plugins;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> uglifyCssBundle() {
|
||||
return Mono.fromSupplier(() -> {
|
||||
StringBuilder cssBundle = new StringBuilder();
|
||||
for (PluginWrapper pluginWrapper : pluginManager.getStartedPlugins()) {
|
||||
String pluginName = pluginWrapper.getPluginId();
|
||||
Resource cssBundleResource =
|
||||
BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
|
||||
BundleResourceUtils.CSS_BUNDLE);
|
||||
if (cssBundleResource != null) {
|
||||
try {
|
||||
cssBundle.append(
|
||||
cssBundleResource.getContentAsString(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read css bundle of plugin [{}]", pluginName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cssBundle.toString();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> generateJsBundleVersion() {
|
||||
return Mono.fromSupplier(() -> {
|
||||
var compactVersion = pluginManager.getStartedPlugins()
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(PluginWrapper::getPluginId))
|
||||
.map(pluginWrapper -> pluginWrapper.getDescriptor().getVersion())
|
||||
.collect(Collectors.joining());
|
||||
return Hashing.sha256().hashUnencodedChars(compactVersion).toString();
|
||||
});
|
||||
}
|
||||
|
||||
Mono<Plugin> findPluginManifest(Path path) {
|
||||
return Mono.fromSupplier(
|
||||
() -> {
|
||||
|
|
|
@ -19,8 +19,8 @@ import run.halo.app.plugin.PluginConst;
|
|||
*/
|
||||
public abstract class BundleResourceUtils {
|
||||
private static final String CONSOLE_BUNDLE_LOCATION = "console";
|
||||
private static final String JS_BUNDLE = "main.js";
|
||||
private static final String CSS_BUNDLE = "style.css";
|
||||
public static final String JS_BUNDLE = "main.js";
|
||||
public static final String CSS_BUNDLE = "style.css";
|
||||
|
||||
/**
|
||||
* Gets plugin css bundle resource path relative to the plugin classpath if exists.
|
||||
|
|
|
@ -22,6 +22,10 @@ rules:
|
|||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "auth-providers" ]
|
||||
verbs: [ "list" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "plugins/bundle.js", "plugins/bundle.css" ]
|
||||
resourceNames: [ "-" ]
|
||||
verbs: [ "get" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
|
|
|
@ -12,6 +12,7 @@ export default definePlugin({
|
|||
{
|
||||
name: "markdown-editor",
|
||||
displayName: "Markdown",
|
||||
logo: "logo.png"
|
||||
component: markRaw(MarkdownEditor),
|
||||
rawType: "markdown",
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { Component } from "vue";
|
|||
export interface EditorProvider {
|
||||
name: string;
|
||||
displayName: string;
|
||||
logo?: string;
|
||||
component: Component;
|
||||
rawType: string;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
useEditorExtensionPoints,
|
||||
type EditorProvider,
|
||||
} from "@/composables/use-editor-extension-points";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import {
|
||||
VAvatar,
|
||||
IconExchange,
|
||||
|
@ -51,7 +49,7 @@ const { editorProviders } = useEditorExtensionPoints();
|
|||
"
|
||||
@click="emit('select', editorProvider)"
|
||||
>
|
||||
<template #prefix-icon>
|
||||
<template v-if="editorProvider.logo" #prefix-icon>
|
||||
<VAvatar :src="editorProvider.logo" size="xs"></VAvatar>
|
||||
</template>
|
||||
{{ editorProvider.displayName }}
|
||||
|
|
|
@ -77,7 +77,8 @@ import { useFetchAttachmentPolicy } from "@/modules/contents/attachments/composa
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { i18n } from "@/locales";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
import { usePluginModuleStore, type PluginModule } from "@/stores/plugin";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { PluginModule } from "@halo-dev/console-shared";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { EditorProvider as EditorProviderRaw } from "@halo-dev/console-shared";
|
||||
import type { PluginModule } from "@/stores/plugin";
|
||||
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
||||
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
||||
import { VLoading } from "@halo-dev/components";
|
||||
import Logo from "@/assets/logo.png";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export interface EditorProvider extends EditorProviderRaw {
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
interface useEditorExtensionPointsReturn {
|
||||
editorProviders: Ref<EditorProvider[]>;
|
||||
}
|
||||
|
@ -42,14 +37,7 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
|||
|
||||
const providers = extensionPoints["editor:create"]() as EditorProvider[];
|
||||
|
||||
if (providers) {
|
||||
providers.forEach((provider) => {
|
||||
editorProviders.value.push({
|
||||
...provider,
|
||||
logo: pluginModule.extension.status?.logo,
|
||||
});
|
||||
});
|
||||
}
|
||||
editorProviders.value.push(...providers);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -786,8 +786,8 @@ core:
|
|||
last_starttime: Last Start Time
|
||||
loader:
|
||||
toast:
|
||||
entry_load_failed: "{name}: Failed to load plugin entry file"
|
||||
style_load_failed: "{name}: Failed to load plugin stylesheet file"
|
||||
entry_load_failed: "Failed to load plugins entry file"
|
||||
style_load_failed: "Failed to load plugins stylesheet file"
|
||||
extension_points:
|
||||
editor:
|
||||
providers:
|
||||
|
|
|
@ -786,8 +786,8 @@ core:
|
|||
last_starttime: 最近一次启动
|
||||
loader:
|
||||
toast:
|
||||
entry_load_failed: "{name}:加载插件入口文件失败"
|
||||
style_load_failed: "{name}:加载插件样式文件失败"
|
||||
entry_load_failed: "加载插件入口文件失败"
|
||||
style_load_failed: "加载插件样式文件失败"
|
||||
extension_points:
|
||||
editor:
|
||||
providers:
|
||||
|
|
|
@ -786,8 +786,8 @@ core:
|
|||
last_starttime: 最近一次啟動
|
||||
loader:
|
||||
toast:
|
||||
entry_load_failed: "{name}:讀取插件入口文件失敗"
|
||||
style_load_failed: "{name}:讀取插件樣式文件失敗"
|
||||
entry_load_failed: "讀取插件入口文件失敗"
|
||||
style_load_failed: "讀取插件樣式文件失敗"
|
||||
extension_points:
|
||||
editor:
|
||||
providers:
|
||||
|
|
|
@ -3,25 +3,20 @@ import type { DirectiveBinding } from "vue";
|
|||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import type { PluginModule, RouteRecordAppend } from "@halo-dev/console-shared";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
// setup
|
||||
import "./setup/setupStyles";
|
||||
import { setupComponents } from "./setup/setupComponents";
|
||||
import { setupI18n, i18n, getBrowserLanguage } from "./locales";
|
||||
// core modules
|
||||
import { coreModules } from "./modules";
|
||||
import { useScriptTag } from "@vueuse/core";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { useThemeStore } from "./stores/theme";
|
||||
import { useUserStore } from "./stores/user";
|
||||
import { useSystemConfigMapStore } from "./stores/system-configmap";
|
||||
import { setupVueQuery } from "./setup/setupVueQuery";
|
||||
import { useGlobalInfoStore } from "./stores/global-info";
|
||||
import { setupCoreModules, setupPluginModules } from "./setup/setupModules";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
|
@ -31,167 +26,6 @@ setupVueQuery(app);
|
|||
|
||||
app.use(createPinia());
|
||||
|
||||
function registerModule(pluginModule: PluginModule, core: boolean) {
|
||||
if (pluginModule.components) {
|
||||
Object.keys(pluginModule.components).forEach((key) => {
|
||||
const component = pluginModule.components?.[key];
|
||||
if (component) {
|
||||
app.component(key, component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pluginModule.routes) {
|
||||
if (!Array.isArray(pluginModule.routes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetRouteMeta(pluginModule.routes);
|
||||
|
||||
for (const route of pluginModule.routes) {
|
||||
if ("parentName" in route) {
|
||||
router.addRoute(route.parentName, route.route);
|
||||
} else {
|
||||
router.addRoute(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetRouteMeta(routes: RouteRecordRaw[] | RouteRecordAppend[]) {
|
||||
for (const route of routes) {
|
||||
if ("parentName" in route) {
|
||||
if (route.route.meta?.menu) {
|
||||
route.route.meta = {
|
||||
...route.route.meta,
|
||||
core,
|
||||
};
|
||||
}
|
||||
if (route.route.children) {
|
||||
resetRouteMeta(route.route.children);
|
||||
}
|
||||
} else {
|
||||
if (route.meta?.menu) {
|
||||
route.meta = {
|
||||
...route.meta,
|
||||
core,
|
||||
};
|
||||
}
|
||||
if (route.children) {
|
||||
resetRouteMeta(route.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadCoreModules() {
|
||||
coreModules.forEach((module) => {
|
||||
registerModule(module, true);
|
||||
});
|
||||
}
|
||||
|
||||
const pluginModuleStore = usePluginModuleStore();
|
||||
|
||||
function loadStyle(href: string) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let shouldAppend = false;
|
||||
let el: HTMLLinkElement | null = document.querySelector(
|
||||
'script[src="' + href + '"]'
|
||||
);
|
||||
if (!el) {
|
||||
el = document.createElement("link");
|
||||
el.rel = "stylesheet";
|
||||
el.type = "text/css";
|
||||
el.href = href;
|
||||
shouldAppend = true;
|
||||
} else if (el.hasAttribute("data-loaded")) {
|
||||
resolve(el);
|
||||
return;
|
||||
}
|
||||
|
||||
el.addEventListener("error", reject);
|
||||
el.addEventListener("abort", reject);
|
||||
el.addEventListener("load", function loadStyleHandler() {
|
||||
el?.setAttribute("data-loaded", "true");
|
||||
resolve(el);
|
||||
});
|
||||
|
||||
if (shouldAppend) document.head.prepend(el);
|
||||
});
|
||||
}
|
||||
|
||||
const pluginErrorMessages: Array<string> = [];
|
||||
|
||||
async function loadPluginModules() {
|
||||
const { data } = await apiClient.plugin.listPlugins(
|
||||
{
|
||||
enabled: true,
|
||||
page: 0,
|
||||
size: 0,
|
||||
},
|
||||
{ mute: true }
|
||||
);
|
||||
|
||||
// Get all started plugins
|
||||
const plugins = data.items.filter((plugin) => {
|
||||
const { entry, stylesheet } = plugin.status || {};
|
||||
return plugin.status?.phase === "STARTED" && (!!entry || !!stylesheet);
|
||||
});
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const { entry, stylesheet } = plugin.status || {
|
||||
entry: "",
|
||||
stylesheet: "",
|
||||
};
|
||||
|
||||
if (entry) {
|
||||
try {
|
||||
const { load } = useScriptTag(
|
||||
`${import.meta.env.VITE_API_URL}${plugin.status?.entry}`
|
||||
);
|
||||
|
||||
await load();
|
||||
|
||||
const pluginModule = window[plugin.metadata.name];
|
||||
|
||||
if (pluginModule) {
|
||||
registerModule(pluginModule, false);
|
||||
pluginModuleStore.registerPluginModule({
|
||||
...pluginModule,
|
||||
extension: plugin,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const message = i18n.global.t(
|
||||
"core.plugin.loader.toast.entry_load_failed",
|
||||
{ name: plugin.spec.displayName }
|
||||
);
|
||||
console.error(message, e);
|
||||
pluginErrorMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (stylesheet) {
|
||||
try {
|
||||
await loadStyle(`${import.meta.env.VITE_API_URL}${stylesheet}`);
|
||||
} catch (e) {
|
||||
const message = i18n.global.t(
|
||||
"core.plugin.loader.toast.style_load_failed",
|
||||
{ name: plugin.spec.displayName }
|
||||
);
|
||||
console.error(message, e);
|
||||
pluginErrorMessages.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginErrorMessages.length > 0) {
|
||||
pluginErrorMessages.forEach((message) => {
|
||||
Toast.error(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserPermissions() {
|
||||
const { data: currentPermissions } = await apiClient.user.getPermissions({
|
||||
name: "-",
|
||||
|
@ -239,7 +73,7 @@ async function initApp() {
|
|||
}
|
||||
|
||||
try {
|
||||
loadCoreModules();
|
||||
setupCoreModules(app);
|
||||
|
||||
const userStore = useUserStore();
|
||||
await userStore.fetchCurrentUser();
|
||||
|
@ -258,7 +92,7 @@ async function initApp() {
|
|||
await loadUserPermissions();
|
||||
|
||||
try {
|
||||
await loadPluginModules();
|
||||
await setupPluginModules(app);
|
||||
} catch (e) {
|
||||
console.error("Failed to load plugins", e);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,8 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { usePermission } from "@/utils/permission";
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { usePluginModuleStore, type PluginModule } from "@/stores/plugin";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { PluginModule } from "@halo-dev/console-shared";
|
||||
import type {
|
||||
CommentSubjectRefProvider,
|
||||
CommentSubjectRefResult,
|
||||
|
|
|
@ -28,10 +28,8 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { useRouter } from "vue-router";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { useContentCache } from "@/composables/use-content-cache";
|
||||
import {
|
||||
useEditorExtensionPoints,
|
||||
type EditorProvider,
|
||||
} from "@/composables/use-editor-extension-points";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
|
|
@ -28,10 +28,8 @@ import { useRouteQuery } from "@vueuse/router";
|
|||
import { useRouter } from "vue-router";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { useContentCache } from "@/composables/use-content-cache";
|
||||
import {
|
||||
useEditorExtensionPoints,
|
||||
type EditorProvider,
|
||||
} from "@/composables/use-editor-extension-points";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
|
|
@ -17,11 +17,11 @@ import { usePermission } from "@/utils/permission";
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { PluginTab } from "@halo-dev/console-shared";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import { markRaw } from "vue";
|
||||
import DetailTab from "./tabs/Detail.vue";
|
||||
import SettingTab from "./tabs/Setting.vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
@ -89,10 +89,9 @@ const { data: setting } = useQuery({
|
|||
provide<Ref<Setting | undefined>>("setting", setting);
|
||||
|
||||
onMounted(() => {
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
const currentPluginModule = pluginModules.find(
|
||||
(item) => item.extension.metadata.name === route.params.name
|
||||
);
|
||||
const { pluginModuleMap } = usePluginModuleStore();
|
||||
|
||||
const currentPluginModule = pluginModuleMap[route.params.name as string];
|
||||
|
||||
if (!currentPluginModule) {
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import { i18n } from "@/locales";
|
||||
import { coreModules } from "@/modules";
|
||||
import router from "@/router";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { PluginModule, RouteRecordAppend } from "@halo-dev/console-shared";
|
||||
import { useScriptTag } from "@vueuse/core";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import type { App } from "vue";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { loadStyle } from "@/utils/load-style";
|
||||
|
||||
export function setupCoreModules(app: App) {
|
||||
coreModules.forEach((module) => {
|
||||
registerModule(app, module, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function setupPluginModules(app: App) {
|
||||
const pluginModuleStore = usePluginModuleStore();
|
||||
try {
|
||||
const { load } = useScriptTag(
|
||||
`${
|
||||
import.meta.env.VITE_API_URL
|
||||
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js`
|
||||
);
|
||||
|
||||
await load();
|
||||
|
||||
const enabledPluginNames = window["enabledPluginNames"] as string[];
|
||||
|
||||
enabledPluginNames.forEach((name) => {
|
||||
const module = window[name];
|
||||
|
||||
if (module) {
|
||||
registerModule(app, module, false);
|
||||
pluginModuleStore.registerPluginModule(name, module);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
const message = i18n.global.t("core.plugin.loader.toast.entry_load_failed");
|
||||
console.error(message, e);
|
||||
Toast.error(message);
|
||||
}
|
||||
|
||||
try {
|
||||
await loadStyle(
|
||||
`${
|
||||
import.meta.env.VITE_API_URL
|
||||
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css`
|
||||
);
|
||||
} catch (e) {
|
||||
const message = i18n.global.t("core.plugin.loader.toast.style_load_failed");
|
||||
console.error(message, e);
|
||||
Toast.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function registerModule(app: App, pluginModule: PluginModule, core: boolean) {
|
||||
if (pluginModule.components) {
|
||||
Object.keys(pluginModule.components).forEach((key) => {
|
||||
const component = pluginModule.components?.[key];
|
||||
if (component) {
|
||||
app.component(key, component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pluginModule.routes) {
|
||||
if (!Array.isArray(pluginModule.routes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetRouteMeta(pluginModule.routes);
|
||||
|
||||
for (const route of pluginModule.routes) {
|
||||
if ("parentName" in route) {
|
||||
router.addRoute(route.parentName, route.route);
|
||||
} else {
|
||||
router.addRoute(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetRouteMeta(routes: RouteRecordRaw[] | RouteRecordAppend[]) {
|
||||
for (const route of routes) {
|
||||
if ("parentName" in route) {
|
||||
if (route.route.meta?.menu) {
|
||||
route.route.meta = {
|
||||
...route.route.meta,
|
||||
core,
|
||||
};
|
||||
}
|
||||
if (route.route.children) {
|
||||
resetRouteMeta(route.route.children);
|
||||
}
|
||||
} else {
|
||||
if (route.meta?.menu) {
|
||||
route.meta = {
|
||||
...route.meta,
|
||||
core,
|
||||
};
|
||||
}
|
||||
if (route.children) {
|
||||
resetRouteMeta(route.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,17 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type { PluginModule as PluginModuleRaw } from "@halo-dev/console-shared";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
import type { PluginModule } from "@halo-dev/console-shared";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
export interface PluginModule extends PluginModuleRaw {
|
||||
extension: Plugin;
|
||||
}
|
||||
export const usePluginModuleStore = defineStore("plugin", () => {
|
||||
const pluginModuleMap = ref<Record<string, PluginModule>>({});
|
||||
|
||||
interface PluginStoreState {
|
||||
pluginModules: PluginModule[];
|
||||
}
|
||||
function registerPluginModule(name: string, pluginModule: PluginModule) {
|
||||
pluginModuleMap.value[name] = pluginModule;
|
||||
}
|
||||
|
||||
export const usePluginModuleStore = defineStore("plugin", {
|
||||
state: (): PluginStoreState => ({
|
||||
pluginModules: [],
|
||||
}),
|
||||
actions: {
|
||||
registerPluginModule(pluginModule: PluginModule) {
|
||||
this.pluginModules.push(pluginModule);
|
||||
},
|
||||
},
|
||||
const pluginModules = computed(() => {
|
||||
return Object.values(pluginModuleMap.value);
|
||||
});
|
||||
|
||||
return { pluginModuleMap, pluginModules, registerPluginModule };
|
||||
});
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
export function loadStyle(href: string) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let shouldAppend = false;
|
||||
let el: HTMLLinkElement | null = document.querySelector(
|
||||
'script[src="' + href + '"]'
|
||||
);
|
||||
if (!el) {
|
||||
el = document.createElement("link");
|
||||
el.rel = "stylesheet";
|
||||
el.type = "text/css";
|
||||
el.href = href;
|
||||
shouldAppend = true;
|
||||
} else if (el.hasAttribute("data-loaded")) {
|
||||
resolve(el);
|
||||
return;
|
||||
}
|
||||
|
||||
el.addEventListener("error", reject);
|
||||
el.addEventListener("abort", reject);
|
||||
el.addEventListener("load", function loadStyleHandler() {
|
||||
el?.setAttribute("data-loaded", "true");
|
||||
resolve(el);
|
||||
});
|
||||
|
||||
if (shouldAppend) document.head.prepend(el);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue