Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 14 additions & 18 deletions src/main/java/com/celements/css/CssScriptService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import java.util.List;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.annotation.Requirement;
import org.xwiki.context.Execution;
import org.xwiki.script.service.ScriptService;

import com.celements.execution.XWikiExecutionProp;
import com.celements.web.css.CSS;
import com.celements.web.plugin.cmd.CssCommand;
import com.xpn.xwiki.XWikiContext;
Expand All @@ -17,20 +19,21 @@
@Component("css")
public class CssScriptService implements ScriptService {

public static final String CELEMENTS_CSSCOMMAND = "com.celements.web.CssCommand";

public static final Logger LOGGER = LoggerFactory.getLogger(CssScriptService.class);

@Requirement
@Inject
private Execution execution;

@Inject
private CssCommand cssCommand;

private XWikiContext getContext() {
return (XWikiContext) execution.getContext().getProperty("xwikicontext");
return execution.getContext().get(XWikiExecutionProp.XWIKI_CONTEXT).orElseThrow();
}

public List<CSS> getAllCSS() {
try {
return getCssCmd().getAllCSS(getContext());
return cssCommand.getAllCSS(getContext());
} catch (XWikiException e) {
LOGGER.error("Call to CssComman.getAllCss failed.", e);
return List.of();
Expand All @@ -39,7 +42,7 @@ public List<CSS> getAllCSS() {

public String displayAllCSS() {
try {
return getCssCmd().displayAllCSS(getContext());
return cssCommand.displayAllCSS(getContext());
} catch (XWikiException e) {
LOGGER.error("Call to CssCommand.displayAllCss failed.", e);
return "";
Expand All @@ -48,20 +51,20 @@ public String displayAllCSS() {

public List<CSS> getRTEContentCSS() {
try {
return getCssCmd().getRTEContentCSS(getContext());
return cssCommand.getRTEContentCSS(getContext());
} catch (XWikiException e) {
LOGGER.error("Call to CssCommand.getRTEContentCSS failed.", e);
return List.of();
}
}

public void includeCSSPage(String css) {
getCssCmd().includeCSSPage(css, getContext());
cssCommand.includeCSSPage(css, getContext());
}

public void includeCSSAfterPreferences(String css) {
try {
getCssCmd().includeCSSAfterPreferences(css, getContext());
cssCommand.includeCSSAfterPreferences(css, getContext());
} catch (XWikiException e) {
LOGGER.error("Call to CssCommand.includeCSSAfterPreferences failed.", e);
}
Expand All @@ -75,13 +78,6 @@ public void includeCSSAfterPreferences(String css) {
*/
@Deprecated(since = "6.7", forRemoval = true)
public void includeCSSAfterSkin(String css) {
getCssCmd().includeCSSAfterSkin(css, getContext());
}

private CssCommand getCssCmd() {
if (!getContext().containsKey(CELEMENTS_CSSCOMMAND)) {
getContext().put(CELEMENTS_CSSCOMMAND, new CssCommand());
}
return (CssCommand) getContext().get(CELEMENTS_CSSCOMMAND);
cssCommand.includeCSSAfterSkin(css, getContext());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.celements.javascript;

import javax.inject.Inject;

import org.springframework.stereotype.Component;

import com.celements.web.plugin.cmd.CssCommand;
import com.celements.web.plugin.cmd.ExternalJavaScriptFilesCommand;
import com.celements.web.plugin.cmd.IExtJSFilesListener;

@Component
public class FrontendResourceIncludeCssListener implements IExtJSFilesListener {

private final CssCommand cssCommand;
private final FrontendResourceResolver resolver;

@Inject
public FrontendResourceIncludeCssListener(
CssCommand cssCommand,
FrontendResourceResolver resolver) {
this.cssCommand = cssCommand;
this.resolver = resolver;
}

@Override
public void beforeAllExtFinish(ExternalJavaScriptFilesCommand jsCommand) {
// Include frontend entry sources. CSSEngine treats :frontend/... values as manifest keys and
// expands them to the CSS assets emitted for that entrypoint.
jsCommand.streamExtJsFiles()
.filter(resolver::isFrontendSource)
.forEach(cssCommand::includeCSSPage);
}
}
132 changes: 107 additions & 25 deletions src/main/java/com/celements/javascript/FrontendResourceResolver.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.celements.javascript;

import static com.google.common.base.Predicates.*;
import static java.util.Objects.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
Expand All @@ -21,10 +25,11 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Suppliers;

import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;

/**
* Resolves frontend resource paths based on the vite manifest file.
* Resolves frontend JavaScript & CSS resources based on the vite manifest file.
*
* The manifest file is generated by vite during the build process and contains mappings from source
* files to their corresponding production files with hashed names.
Expand All @@ -44,8 +49,9 @@ public class FrontendResourceResolver {
private final ResourcePatternResolver resourceLoader;
private final ObjectMapper objectMapper;

// ":frontend/file.ts" -> "dist/file.a8b3.mjs"
private final Supplier<Map<String, String>> manifest = Suppliers.memoize(this::readManifestFiles);
// ":frontend/file.ts" -> ("dist/file.a8b3.mjs", ["dist/assets/file.a8b3.css"])
private final Supplier<Map<String, FrontendResource>> manifest = Suppliers.memoize(
this::readManifestFiles);

@Inject
public FrontendResourceResolver(ResourcePatternResolver resourceLoader) {
Expand All @@ -55,20 +61,31 @@ public FrontendResourceResolver(ResourcePatternResolver resourceLoader) {

@PostConstruct
public void init() {
manifest.get();
getManifest();
}

public Map<String, FrontendResource> getManifest() {
return manifest.get();
}

/**
* Resolves a source path to a production dist path.
* @param sourcePath e.g. ":frontend/file.ts"
* Resolves a frontend source path to its emitted dist files.
*
* @return The resolved path e.g. "dist/file.a8b3.mjs"
* @param sourcePath
* e.g. ":frontend/file.ts"
*
* @return The resolved frontend resource with JS and CSS dist paths, e.g.
* "dist/file.a8b3.mjs" and ["dist/assets/file.a8b3.css"]
*/
public Optional<String> resolve(String sourcePath) {
return Optional.ofNullable(manifest.get().get(sourcePath));
public Optional<FrontendResource> get(String sourcePath) {
return Optional.ofNullable(getManifest().get(sourcePath));
}

private Map<String, String> readManifestFiles() {
public boolean isFrontendSource(String path) {
return requireNonNullElse(path, "").trim().startsWith(SOURCE_PREFIX);
}

private Map<String, FrontendResource> readManifestFiles() {
Resource[] resources;
try {
resources = resourceLoader.getResources(MANIFEST_PATH);
Expand All @@ -78,10 +95,7 @@ private Map<String, String> readManifestFiles() {
var map = StreamEx.of(resources)
.filter(Resource::exists)
.flatMap(this::readJson)
.flatMap(rootNode -> StreamEx.of(rootNode.fields()))
.mapToEntry(Entry::getKey, Entry::getValue)
.flatMapKeys(this::prefixKey)
.flatMapValues(this::extractFilePath)
.flatMapToEntry(json -> new ViteManifest(json).frontendResources())
.toImmutableMap(); // ISE on duplicate keys, we expect unique source files
LOGGER.info("readManifest - {}", map);
return map;
Expand All @@ -96,17 +110,85 @@ private Stream<JsonNode> readJson(Resource resource) {
}
}

private Stream<String> prefixKey(String key) {
return Stream.ofNullable(key)
.filter(k -> k.startsWith(MANIFEST_KEY_PREFIX))
.map(k -> k.replaceFirst(MANIFEST_KEY_PREFIX, SOURCE_PREFIX));
public record FrontendResource(String jsPath, List<String> cssPaths) {}

static final class ViteManifest {

private final JsonNode manifest;

ViteManifest(JsonNode manifest) {
this.manifest = manifest;
}

Map<String, FrontendResource> frontendResources() {
return EntryStream.of(manifest.fields())
.mapToValue(ViteManifestEntry::new)
.mapToKeyPartial((key, entry) -> entry.sourcePath())
.mapToValuePartial((sourcePath, entry) -> entry.jsPath()
.map(jsPath -> toFrontendResource(jsPath, entry)))
.toImmutableMap();
}

private FrontendResource toFrontendResource(String jsPath, ViteManifestEntry entry) {
var cssPaths = collectCssPaths(entry).distinct().toList();
return new FrontendResource(jsPath, cssPaths);
}

private StreamEx<String> collectCssPaths(ViteManifestEntry entry) {
return StreamEx.of(entry.imports()
.flatMap(key -> entry(key).stream())
.flatMap(this::collectCssPaths))
.append(entry.cssPaths());
}

private Optional<ViteManifestEntry> entry(String key) {
return Optional.ofNullable(manifest.get(key))
.map(json -> new ViteManifestEntry(key, json));
}
}

private Stream<String> extractFilePath(JsonNode node) {
return Stream.ofNullable(node)
.map(n -> n.get("file"))
.map(f -> f.asText())
.filter(f -> !f.isEmpty())
.map(f -> TARGET_DIR + f);
record ViteManifestEntry(String key, JsonNode json) {

private static final String FIELD_FILE = "file";
private static final String FIELD_CSS = "css";
private static final String FIELD_IMPORTS = "imports";

Optional<String> sourcePath() {
return Optional.ofNullable(key)
.filter(k -> k.startsWith(MANIFEST_KEY_PREFIX))
.map(k -> k.replaceFirst(MANIFEST_KEY_PREFIX, SOURCE_PREFIX));
}

Optional<String> jsPath() {
return getNode(FIELD_FILE).map(this::toTargetPath);
}

Stream<String> cssPaths() {
return getArray(FIELD_CSS).map(this::toTargetPath).filter(Objects::nonNull);
}

public Stream<String> imports() {
return getArray(FIELD_IMPORTS).map(JsonNode::asText).filter(not(String::isEmpty));
}

private Optional<JsonNode> getNode(String name) {
return Optional.ofNullable(json.get(name));
}

private Stream<JsonNode> getArray(String fieldName) {
return getNode(fieldName)
.filter(JsonNode::isArray)
.stream()
.map(JsonNode::elements)
.flatMap(StreamEx::of);
}

private String toTargetPath(JsonNode node) {
return Optional.ofNullable(node)
.map(JsonNode::asText)
.filter(not(String::isEmpty))
.map(file -> TARGET_DIR + file)
.orElse(null);
}
}
}
5 changes: 4 additions & 1 deletion src/main/java/com/celements/javascript/JSScriptService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.celements.javascript;

import static com.celements.spring.context.SpringContextProvider.*;

import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -166,7 +168,8 @@ public String addLazyExtJSfile(@Nullable String jsFile, @Nullable String action,

private ExternalJavaScriptFilesCommand getExtJavaScriptFileCmd() {
if (getContext().get(JAVA_SCRIPT_FILES_COMMAND_KEY) == null) {
getContext().put(JAVA_SCRIPT_FILES_COMMAND_KEY, new ExternalJavaScriptFilesCommand());
getContext().put(JAVA_SCRIPT_FILES_COMMAND_KEY, getSpringContext()
.getBean(ExternalJavaScriptFilesCommand.class));
}
return (ExternalJavaScriptFilesCommand) getContext().get(JAVA_SCRIPT_FILES_COMMAND_KEY);
}
Expand Down
Loading