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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
@ -217,10 +218,61 @@ public class PluginEndpoint implements CustomEndpoint {
|
||||||
builder -> builder.operationId("ListPluginPresets")
|
builder -> builder.operationId("ListPluginPresets")
|
||||||
.description("List all plugin presets in the system.")
|
.description("List all plugin presets in the system.")
|
||||||
.tag(tag)
|
.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();
|
.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) {
|
private Mono<ServerResponse> upgradeFromUri(ServerRequest request) {
|
||||||
var name = request.pathVariable("name");
|
var name = request.pathVariable("name");
|
||||||
var content = request.bodyToMono(UpgradeFromUriRequest.class)
|
var content = request.bodyToMono(UpgradeFromUriRequest.class)
|
||||||
|
|
|
@ -40,4 +40,26 @@ public interface PluginService {
|
||||||
* @see run.halo.app.plugin.HaloPluginManager#reloadPlugin(String)
|
* @see run.halo.app.plugin.HaloPluginManager#reloadPlugin(String)
|
||||||
*/
|
*/
|
||||||
Mono<Plugin> reload(String name);
|
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 static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||||
|
|
||||||
import com.github.zafarkhaja.semver.Version;
|
import com.github.zafarkhaja.semver.Version;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.Validate;
|
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.PluginProperties;
|
||||||
import run.halo.app.plugin.PluginUtils;
|
import run.halo.app.plugin.PluginUtils;
|
||||||
import run.halo.app.plugin.YamlPluginFinder;
|
import run.halo.app.plugin.YamlPluginFinder;
|
||||||
|
import run.halo.app.plugin.resources.BundleResourceUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@ -124,6 +131,70 @@ public class PluginServiceImpl implements PluginService {
|
||||||
return updateReloadAnno(name, pluginWrapper.getPluginPath());
|
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) {
|
Mono<Plugin> findPluginManifest(Path path) {
|
||||||
return Mono.fromSupplier(
|
return Mono.fromSupplier(
|
||||||
() -> {
|
() -> {
|
||||||
|
|
|
@ -19,8 +19,8 @@ import run.halo.app.plugin.PluginConst;
|
||||||
*/
|
*/
|
||||||
public abstract class BundleResourceUtils {
|
public abstract class BundleResourceUtils {
|
||||||
private static final String CONSOLE_BUNDLE_LOCATION = "console";
|
private static final String CONSOLE_BUNDLE_LOCATION = "console";
|
||||||
private static final String JS_BUNDLE = "main.js";
|
public static final String JS_BUNDLE = "main.js";
|
||||||
private static final String CSS_BUNDLE = "style.css";
|
public static final String CSS_BUNDLE = "style.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets plugin css bundle resource path relative to the plugin classpath if exists.
|
* Gets plugin css bundle resource path relative to the plugin classpath if exists.
|
||||||
|
|
|
@ -22,6 +22,10 @@ rules:
|
||||||
- apiGroups: [ "api.console.halo.run" ]
|
- apiGroups: [ "api.console.halo.run" ]
|
||||||
resources: [ "auth-providers" ]
|
resources: [ "auth-providers" ]
|
||||||
verbs: [ "list" ]
|
verbs: [ "list" ]
|
||||||
|
- apiGroups: [ "api.console.halo.run" ]
|
||||||
|
resources: [ "plugins/bundle.js", "plugins/bundle.css" ]
|
||||||
|
resourceNames: [ "-" ]
|
||||||
|
verbs: [ "get" ]
|
||||||
---
|
---
|
||||||
apiVersion: v1alpha1
|
apiVersion: v1alpha1
|
||||||
kind: "Role"
|
kind: "Role"
|
||||||
|
|
|
@ -12,6 +12,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
name: "markdown-editor",
|
name: "markdown-editor",
|
||||||
displayName: "Markdown",
|
displayName: "Markdown",
|
||||||
|
logo: "logo.png"
|
||||||
component: markRaw(MarkdownEditor),
|
component: markRaw(MarkdownEditor),
|
||||||
rawType: "markdown",
|
rawType: "markdown",
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { Component } from "vue";
|
||||||
export interface EditorProvider {
|
export interface EditorProvider {
|
||||||
name: string;
|
name: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
logo?: string;
|
||||||
component: Component;
|
component: Component;
|
||||||
rawType: string;
|
rawType: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||||
useEditorExtensionPoints,
|
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||||
type EditorProvider,
|
|
||||||
} from "@/composables/use-editor-extension-points";
|
|
||||||
import {
|
import {
|
||||||
VAvatar,
|
VAvatar,
|
||||||
IconExchange,
|
IconExchange,
|
||||||
|
@ -51,7 +49,7 @@ const { editorProviders } = useEditorExtensionPoints();
|
||||||
"
|
"
|
||||||
@click="emit('select', editorProvider)"
|
@click="emit('select', editorProvider)"
|
||||||
>
|
>
|
||||||
<template #prefix-icon>
|
<template v-if="editorProvider.logo" #prefix-icon>
|
||||||
<VAvatar :src="editorProvider.logo" size="xs"></VAvatar>
|
<VAvatar :src="editorProvider.logo" size="xs"></VAvatar>
|
||||||
</template>
|
</template>
|
||||||
{{ editorProvider.displayName }}
|
{{ editorProvider.displayName }}
|
||||||
|
|
|
@ -77,7 +77,8 @@ import { useFetchAttachmentPolicy } from "@/modules/contents/attachments/composa
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { i18n } from "@/locales";
|
import { i18n } from "@/locales";
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
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";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
import type { EditorProvider as EditorProviderRaw } from "@halo-dev/console-shared";
|
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
||||||
import type { PluginModule } from "@/stores/plugin";
|
|
||||||
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
||||||
import { VLoading } from "@halo-dev/components";
|
import { VLoading } from "@halo-dev/components";
|
||||||
import Logo from "@/assets/logo.png";
|
import Logo from "@/assets/logo.png";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
export interface EditorProvider extends EditorProviderRaw {
|
|
||||||
logo?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface useEditorExtensionPointsReturn {
|
interface useEditorExtensionPointsReturn {
|
||||||
editorProviders: Ref<EditorProvider[]>;
|
editorProviders: Ref<EditorProvider[]>;
|
||||||
}
|
}
|
||||||
|
@ -42,14 +37,7 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
||||||
|
|
||||||
const providers = extensionPoints["editor:create"]() as EditorProvider[];
|
const providers = extensionPoints["editor:create"]() as EditorProvider[];
|
||||||
|
|
||||||
if (providers) {
|
editorProviders.value.push(...providers);
|
||||||
providers.forEach((provider) => {
|
|
||||||
editorProviders.value.push({
|
|
||||||
...provider,
|
|
||||||
logo: pluginModule.extension.status?.logo,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -786,8 +786,8 @@ core:
|
||||||
last_starttime: Last Start Time
|
last_starttime: Last Start Time
|
||||||
loader:
|
loader:
|
||||||
toast:
|
toast:
|
||||||
entry_load_failed: "{name}: Failed to load plugin entry file"
|
entry_load_failed: "Failed to load plugins entry file"
|
||||||
style_load_failed: "{name}: Failed to load plugin stylesheet file"
|
style_load_failed: "Failed to load plugins stylesheet file"
|
||||||
extension_points:
|
extension_points:
|
||||||
editor:
|
editor:
|
||||||
providers:
|
providers:
|
||||||
|
|
|
@ -786,8 +786,8 @@ core:
|
||||||
last_starttime: 最近一次启动
|
last_starttime: 最近一次启动
|
||||||
loader:
|
loader:
|
||||||
toast:
|
toast:
|
||||||
entry_load_failed: "{name}:加载插件入口文件失败"
|
entry_load_failed: "加载插件入口文件失败"
|
||||||
style_load_failed: "{name}:加载插件样式文件失败"
|
style_load_failed: "加载插件样式文件失败"
|
||||||
extension_points:
|
extension_points:
|
||||||
editor:
|
editor:
|
||||||
providers:
|
providers:
|
||||||
|
|
|
@ -786,8 +786,8 @@ core:
|
||||||
last_starttime: 最近一次啟動
|
last_starttime: 最近一次啟動
|
||||||
loader:
|
loader:
|
||||||
toast:
|
toast:
|
||||||
entry_load_failed: "{name}:讀取插件入口文件失敗"
|
entry_load_failed: "讀取插件入口文件失敗"
|
||||||
style_load_failed: "{name}:讀取插件樣式文件失敗"
|
style_load_failed: "讀取插件樣式文件失敗"
|
||||||
extension_points:
|
extension_points:
|
||||||
editor:
|
editor:
|
||||||
providers:
|
providers:
|
||||||
|
|
|
@ -3,25 +3,20 @@ import type { DirectiveBinding } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
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";
|
import { apiClient } from "@/utils/api-client";
|
||||||
// setup
|
// setup
|
||||||
import "./setup/setupStyles";
|
import "./setup/setupStyles";
|
||||||
import { setupComponents } from "./setup/setupComponents";
|
import { setupComponents } from "./setup/setupComponents";
|
||||||
import { setupI18n, i18n, getBrowserLanguage } from "./locales";
|
import { setupI18n, i18n, getBrowserLanguage } from "./locales";
|
||||||
// core modules
|
// core modules
|
||||||
import { coreModules } from "./modules";
|
|
||||||
import { useScriptTag } from "@vueuse/core";
|
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
|
||||||
import { hasPermission } from "@/utils/permission";
|
import { hasPermission } from "@/utils/permission";
|
||||||
import { useRoleStore } from "@/stores/role";
|
import { useRoleStore } from "@/stores/role";
|
||||||
import type { RouteRecordRaw } from "vue-router";
|
|
||||||
import { useThemeStore } from "./stores/theme";
|
import { useThemeStore } from "./stores/theme";
|
||||||
import { useUserStore } from "./stores/user";
|
import { useUserStore } from "./stores/user";
|
||||||
import { useSystemConfigMapStore } from "./stores/system-configmap";
|
import { useSystemConfigMapStore } from "./stores/system-configmap";
|
||||||
import { setupVueQuery } from "./setup/setupVueQuery";
|
import { setupVueQuery } from "./setup/setupVueQuery";
|
||||||
import { useGlobalInfoStore } from "./stores/global-info";
|
import { useGlobalInfoStore } from "./stores/global-info";
|
||||||
|
import { setupCoreModules, setupPluginModules } from "./setup/setupModules";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
|
@ -31,167 +26,6 @@ setupVueQuery(app);
|
||||||
|
|
||||||
app.use(createPinia());
|
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() {
|
async function loadUserPermissions() {
|
||||||
const { data: currentPermissions } = await apiClient.user.getPermissions({
|
const { data: currentPermissions } = await apiClient.user.getPermissions({
|
||||||
name: "-",
|
name: "-",
|
||||||
|
@ -239,7 +73,7 @@ async function initApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loadCoreModules();
|
setupCoreModules(app);
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
await userStore.fetchCurrentUser();
|
await userStore.fetchCurrentUser();
|
||||||
|
@ -258,7 +92,7 @@ async function initApp() {
|
||||||
await loadUserPermissions();
|
await loadUserPermissions();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadPluginModules();
|
await setupPluginModules(app);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load plugins", e);
|
console.error("Failed to load plugins", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,8 @@ import cloneDeep from "lodash.clonedeep";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
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 {
|
import type {
|
||||||
CommentSubjectRefProvider,
|
CommentSubjectRefProvider,
|
||||||
CommentSubjectRefResult,
|
CommentSubjectRefResult,
|
||||||
|
|
|
@ -28,10 +28,8 @@ import cloneDeep from "lodash.clonedeep";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { useContentCache } from "@/composables/use-content-cache";
|
import { useContentCache } from "@/composables/use-content-cache";
|
||||||
import {
|
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||||
useEditorExtensionPoints,
|
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||||
type EditorProvider,
|
|
||||||
} from "@/composables/use-editor-extension-points";
|
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
|
@ -28,10 +28,8 @@ import { useRouteQuery } from "@vueuse/router";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { useContentCache } from "@/composables/use-content-cache";
|
import { useContentCache } from "@/composables/use-content-cache";
|
||||||
import {
|
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||||
useEditorExtensionPoints,
|
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||||
type EditorProvider,
|
|
||||||
} from "@/composables/use-editor-extension-points";
|
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
|
@ -17,11 +17,11 @@ import { usePermission } from "@/utils/permission";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import type { PluginTab } from "@halo-dev/console-shared";
|
import type { PluginTab } from "@halo-dev/console-shared";
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
import DetailTab from "./tabs/Detail.vue";
|
import DetailTab from "./tabs/Detail.vue";
|
||||||
import SettingTab from "./tabs/Setting.vue";
|
import SettingTab from "./tabs/Setting.vue";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -89,10 +89,9 @@ const { data: setting } = useQuery({
|
||||||
provide<Ref<Setting | undefined>>("setting", setting);
|
provide<Ref<Setting | undefined>>("setting", setting);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const { pluginModules } = usePluginModuleStore();
|
const { pluginModuleMap } = usePluginModuleStore();
|
||||||
const currentPluginModule = pluginModules.find(
|
|
||||||
(item) => item.extension.metadata.name === route.params.name
|
const currentPluginModule = pluginModuleMap[route.params.name as string];
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentPluginModule) {
|
if (!currentPluginModule) {
|
||||||
return;
|
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 { defineStore } from "pinia";
|
||||||
import type { PluginModule as PluginModuleRaw } from "@halo-dev/console-shared";
|
import type { PluginModule } from "@halo-dev/console-shared";
|
||||||
import type { Plugin } from "@halo-dev/api-client";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
export interface PluginModule extends PluginModuleRaw {
|
export const usePluginModuleStore = defineStore("plugin", () => {
|
||||||
extension: Plugin;
|
const pluginModuleMap = ref<Record<string, PluginModule>>({});
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginStoreState {
|
function registerPluginModule(name: string, pluginModule: PluginModule) {
|
||||||
pluginModules: PluginModule[];
|
pluginModuleMap.value[name] = pluginModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePluginModuleStore = defineStore("plugin", {
|
const pluginModules = computed(() => {
|
||||||
state: (): PluginStoreState => ({
|
return Object.values(pluginModuleMap.value);
|
||||||
pluginModules: [],
|
});
|
||||||
}),
|
|
||||||
actions: {
|
return { pluginModuleMap, pluginModules, registerPluginModule };
|
||||||
registerPluginModule(pluginModule: PluginModule) {
|
|
||||||
this.pluginModules.push(pluginModule);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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