diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java index 430798aef3..bda4443b04 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java @@ -29,6 +29,7 @@ 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 RouterFunction endpoint() { 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 fetchJsBundle(ServerRequest request) { + Optional 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 fetchCssBundle(ServerRequest request) { + Optional 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 upgradeFromUri(ServerRequest request) { var name = request.pathVariable("name"); var content = request.bodyToMono(UpgradeFromUriRequest.class) diff --git a/application/src/main/java/run/halo/app/core/extension/service/PluginService.java b/application/src/main/java/run/halo/app/core/extension/service/PluginService.java index 2b2175290e..bcf404dbec 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/PluginService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/PluginService.java @@ -40,4 +40,26 @@ public interface PluginService { * @see run.halo.app.plugin.HaloPluginManager#reloadPlugin(String) */ Mono reload(String name); + + /** + * Uglify js bundle from all enabled plugins to a single js bundle string. + * + * @return uglified js bundle + */ + Mono uglifyJsBundle(); + + /** + * Uglify css bundle from all enabled plugins to a single css bundle string. + * + * @return uglified css bundle + */ + Mono uglifyCssBundle(); + + /** + *

Generate js bundle version for cache control.

+ * 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 generateJsBundleVersion(); } diff --git a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java index 5f7d9a834d..f404520ef0 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java +++ b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java @@ -3,12 +3,18 @@ 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.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 Mono reload(String name) { return updateReloadAnno(name, pluginWrapper.getPluginPath()); } + @Override + public Mono uglifyJsBundle() { + return Mono.fromSupplier(() -> { + StringBuilder jsBundle = new StringBuilder(); + List 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 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 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 findPluginManifest(Path path) { return Mono.fromSupplier( () -> { diff --git a/application/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java b/application/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java index d81d589e80..5f14e6a3e7 100644 --- a/application/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java +++ b/application/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java @@ -19,8 +19,8 @@ */ 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. diff --git a/application/src/main/resources/extensions/role-template-authenticated.yaml b/application/src/main/resources/extensions/role-template-authenticated.yaml index e487f2b975..95df303e32 100644 --- a/application/src/main/resources/extensions/role-template-authenticated.yaml +++ b/application/src/main/resources/extensions/role-template-authenticated.yaml @@ -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" diff --git a/console/docs/extension-points/editor.md b/console/docs/extension-points/editor.md index 21dd63df8c..5e19f6a300 100644 --- a/console/docs/extension-points/editor.md +++ b/console/docs/extension-points/editor.md @@ -12,6 +12,7 @@ export default definePlugin({ { name: "markdown-editor", displayName: "Markdown", + logo: "logo.png" component: markRaw(MarkdownEditor), rawType: "markdown", }, diff --git a/console/packages/shared/src/states/editor.ts b/console/packages/shared/src/states/editor.ts index 56aa3d123f..870eef71f7 100644 --- a/console/packages/shared/src/states/editor.ts +++ b/console/packages/shared/src/states/editor.ts @@ -3,6 +3,7 @@ import type { Component } from "vue"; export interface EditorProvider { name: string; displayName: string; + logo?: string; component: Component; rawType: string; } diff --git a/console/src/components/dropdown-selector/EditorProviderSelector.vue b/console/src/components/dropdown-selector/EditorProviderSelector.vue index 209b1bf8f2..12b6dbc00f 100644 --- a/console/src/components/dropdown-selector/EditorProviderSelector.vue +++ b/console/src/components/dropdown-selector/EditorProviderSelector.vue @@ -1,8 +1,6 @@