This is an automated email from the ASF dual-hosted git repository.
raducotescu pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-scripting-sightly-js-provider.git
The following commit(s) were added to refs/heads/master by this push:
new 80bcdb2 SLING-13324 - Relative JS Use dependencies are not resolved
via sling:resourceSuperType when the component is overlaid under a
higher-priority search path (e.g. /apps over /libs) (#12)
80bcdb2 is described below
commit 80bcdb2f1762cad6cc5e552676d9ed4c2beefd11
Author: pnvwrk <[email protected]>
AuthorDate: Tue Sep 1 14:39:05 2026 +0200
SLING-13324 - Relative JS Use dependencies are not resolved via
sling:resourceSuperType when the component is overlaid under a higher-priority
search path (e.g. /apps over /libs) (#12)
* Added fallback logic in DependencyResolver to climb the resource
hierarchy for script resolution when sling:resourceSuperType is not present.
* Introduced new test cases to verify the correct resolution of
dependencies, including scenarios for apps overlays and local caller precedence.
---------
Co-authored-by: Mikalai Puzyryn <[email protected]>
---
.../sightly/js/impl/use/DependencyResolver.java | 24 ++
.../js/impl/use/DependencyResolverTest.java | 309 +++++++++++++++++++++
2 files changed, 333 insertions(+)
diff --git
a/src/main/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolver.java
b/src/main/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolver.java
index 11297cf..efe01e3 100644
---
a/src/main/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolver.java
+++
b/src/main/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolver.java
@@ -97,6 +97,30 @@ public class DependencyResolver {
} else {
scriptResource = callerType.getChild(dependency);
}
+ // Fallback: climb from the caller. Search-path overlays
under /apps often omit
+ // sling:resourceSuperType (it lives on /libs), so the
driver-anchored walk above
+ // can miss inherited scripts that resolve from the
executing /libs caller.
+ if (scriptResource == null) {
+ Resource hierarchyResource = callerType;
+ while (hierarchyResource != null && scriptResource ==
null) {
+ String nextType =
hierarchyResource.getResourceSuperType();
+ if (nextType == null) {
+ break;
+ }
+ hierarchyResource =
scriptingResourceResolver.getResource(nextType);
+ if (hierarchyResource != null) {
+ if (dependency.startsWith("..")) {
+ String absolutePath =
+
ResourceUtil.normalize(hierarchyResource.getPath() + "/" + dependency);
+ if (StringUtils.isNotEmpty(absolutePath)) {
+ scriptResource =
scriptingResourceResolver.getResource(absolutePath);
+ }
+ } else {
+ scriptResource =
hierarchyResource.getChild(dependency);
+ }
+ }
+ }
+ }
}
}
}
diff --git
a/src/test/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolverTest.java
b/src/test/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolverTest.java
index 7210eff..d7a5c9c 100644
---
a/src/test/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolverTest.java
+++
b/src/test/java/org/apache/sling/scripting/sightly/js/impl/use/DependencyResolverTest.java
@@ -21,8 +21,10 @@ package org.apache.sling.scripting.sightly.js.impl.use;
import javax.script.Bindings;
import javax.script.ScriptEngine;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.sling.api.SlingHttpServletRequest;
@@ -40,6 +42,7 @@ import org.mockito.quality.Strictness;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
@@ -54,6 +57,16 @@ class DependencyResolverTest {
private static final String CALLER_PATH = "/libs/caller/caller.html";
private static final String SCRIPT_PATH = "/libs/caller/caller.js";
+ private static final String[] SEARCH_PATH = {"/apps/", "/libs/"};
+
+ private static final String XF_TYPE =
"cq/experience-fragments/components/xfpage";
+ private static final String APPS_XF = "/apps/" + XF_TYPE;
+ private static final String LIBS_XF = "/libs/" + XF_TYPE;
+ private static final String FOUNDATION_TYPE =
"wcm/foundation/components/page";
+ private static final String LIBS_FOUNDATION = "/libs/" + FOUNDATION_TYPE;
+ private static final String CALLER_HTL = LIBS_XF +
"/head.nocloudconfigs.html";
+ private static final String HEAD_JS = LIBS_FOUNDATION + "/head.js";
+
@Mock
private ResourceResolver scriptingResourceResolver;
@@ -81,6 +94,7 @@ class DependencyResolverTest {
when(caller.getParent()).thenReturn(callerParent);
when(scriptingResourceResolver.getResource(CALLER_PATH)).thenReturn(caller);
when(scriptingResourceResolver.getResource(SCRIPT_PATH)).thenReturn(dependency);
+
when(scriptingResourceResolver.getSearchPath()).thenReturn(SEARCH_PATH);
dependencyResolver = new DependencyResolver(scriptingResourceResolver);
bindings = new SlingBindings();
bindings.put(ScriptEngine.FILENAME, CALLER_PATH);
@@ -131,4 +145,299 @@ class DependencyResolverTest {
verify(stream, never()).read();
verify(stream, never()).close();
}
+
+ /**
+ * Regression: /apps overlay of xfpage without sling:resourceSuperType
(and without head.js)
+ * must still resolve inherited head.js from the foundation page via the
/libs caller.
+ */
+ @Test
+ void resolveInheritedDependency_appsOverlayWithoutSuperType() {
+ Resource libsXf = mockResource(LIBS_XF, FOUNDATION_TYPE);
+ Resource appsXf = mockResource(APPS_XF, null);
+ Resource foundation = mockResource(LIBS_FOUNDATION, null);
+ Resource headJs = mockJsResource(HEAD_JS);
+ Resource callerHtl = mockResource(CALLER_HTL, "nt:file");
+ Resource contentResource =
mockContent("/content/xf/master/jcr:content", XF_TYPE);
+
+ when(callerHtl.getParent()).thenReturn(libsXf);
+ when(libsXf.getChild("head.js")).thenReturn(null);
+ when(appsXf.getChild("head.js")).thenReturn(null);
+ when(foundation.getChild("head.js")).thenReturn(headJs);
+
+
when(scriptingResourceResolver.getResource(CALLER_HTL)).thenReturn(callerHtl);
+
when(scriptingResourceResolver.getResource(any())).thenAnswer(invocation -> {
+ String path = invocation.getArgument(0);
+ if (CALLER_HTL.equals(path)) {
+ return callerHtl;
+ }
+ if ("head.js".equals(path)) {
+ return null;
+ }
+ if (XF_TYPE.equals(path) || APPS_XF.equals(path)) {
+ return appsXf;
+ }
+ if (LIBS_XF.equals(path)) {
+ return libsXf;
+ }
+ if (FOUNDATION_TYPE.equals(path) || LIBS_FOUNDATION.equals(path)) {
+ return foundation;
+ }
+ if (HEAD_JS.equals(path)) {
+ return headJs;
+ }
+ return null;
+ });
+
+ when(request.getResource()).thenReturn(contentResource);
+
+ bindings.put(ScriptEngine.FILENAME, CALLER_HTL);
+
+ ScriptNameAwareReader reader = dependencyResolver.resolve(bindings,
"head.js");
+ assertNotNull(reader);
+ assertEquals(HEAD_JS, reader.getScriptName());
+ }
+
+ /**
+ * Overlay under /apps that provides head.js must win when resolving
relative to the /libs caller.
+ */
+ @Test
+ void resolveDependency_prefersAppsOverlayScript() {
+ String appsHeadJs = APPS_XF + "/head.js";
+ Resource libsXf = mockResource(LIBS_XF, FOUNDATION_TYPE);
+ Resource appsXf = mockResource(APPS_XF, FOUNDATION_TYPE);
+ Resource appsHead = mockJsResource(appsHeadJs);
+ Resource callerHtl = mockResource(CALLER_HTL, "nt:file");
+ Resource contentResource =
mockContent("/content/xf/master/jcr:content", XF_TYPE);
+
+ when(callerHtl.getParent()).thenReturn(libsXf);
+ when(libsXf.getChild("head.js")).thenReturn(null);
+ when(appsXf.getChild("head.js")).thenReturn(appsHead);
+
+
when(scriptingResourceResolver.getResource(any())).thenAnswer(invocation -> {
+ String path = invocation.getArgument(0);
+ if (CALLER_HTL.equals(path)) {
+ return callerHtl;
+ }
+ if ("head.js".equals(path)) {
+ return null;
+ }
+ if (XF_TYPE.equals(path) || APPS_XF.equals(path)) {
+ return appsXf;
+ }
+ if (LIBS_XF.equals(path)) {
+ return libsXf;
+ }
+ if (appsHeadJs.equals(path)) {
+ return appsHead;
+ }
+ return null;
+ });
+ when(request.getResource()).thenReturn(contentResource);
+
+ bindings.put(ScriptEngine.FILENAME, CALLER_HTL);
+
+ ScriptNameAwareReader reader = dependencyResolver.resolve(bindings,
"head.js");
+ assertNotNull(reader);
+ assertEquals(appsHeadJs, reader.getScriptName());
+ }
+
+ /**
+ * SLING-9657: a Use script next to the caller (partials/head.js) must win
over a same-named
+ * script on a resourceSuperType.
+ */
+ @Test
+ void resolveDependency_localCallerWinsOverSuperType() {
+ String projectPage = "/apps/project/page";
+ String partials = projectPage + "/partials";
+ String localHead = partials + "/head.js";
+ String superHead = "/apps/page/head.js";
+ String callerHtl = partials + "/head.html";
+
+ Resource partialsResource = mockResource(partials, null);
+ Resource projectPageResource = mockResource(projectPage, "page");
+ Resource pageResource = mockResource("/apps/page", null);
+ Resource localHeadResource = mockJsResource(localHead);
+ Resource superHeadResource = mockJsResource(superHead);
+ Resource callerHtlResource = mockResource(callerHtl, "nt:file");
+ Resource contentResource = mockContent("/content/page",
"project/page");
+
+ when(callerHtlResource.getParent()).thenReturn(partialsResource);
+
when(partialsResource.getChild("head.js")).thenReturn(localHeadResource);
+ when(projectPageResource.getChild("head.js")).thenReturn(null);
+ when(pageResource.getChild("head.js")).thenReturn(superHeadResource);
+
+
when(scriptingResourceResolver.getResource(any())).thenAnswer(invocation -> {
+ String path = invocation.getArgument(0);
+ if (callerHtl.equals(path)) {
+ return callerHtlResource;
+ }
+ if ("head.js".equals(path)) {
+ return null;
+ }
+ if ("project/page".equals(path) || projectPage.equals(path)) {
+ return projectPageResource;
+ }
+ if ("page".equals(path) || "/apps/page".equals(path)) {
+ return pageResource;
+ }
+ if (localHead.equals(path)) {
+ return localHeadResource;
+ }
+ if (superHead.equals(path)) {
+ return superHeadResource;
+ }
+ return null;
+ });
+ when(request.getResource()).thenReturn(contentResource);
+
+ bindings.put(ScriptEngine.FILENAME, callerHtl);
+
+ ScriptNameAwareReader reader = dependencyResolver.resolve(bindings,
"head.js");
+ assertNotNull(reader);
+ assertEquals(localHead, reader.getScriptName());
+ }
+
+ @Test
+ void resolveDependency_missingThrows() {
+ Resource libsXf = mockResource(LIBS_XF, null);
+ Resource appsXf = mockResource(APPS_XF, null);
+ Resource callerHtl = mockResource(CALLER_HTL, "nt:file");
+ Resource contentResource =
mockContent("/content/xf/master/jcr:content", XF_TYPE);
+
+ when(callerHtl.getParent()).thenReturn(libsXf);
+ when(libsXf.getChild("head.js")).thenReturn(null);
+ when(appsXf.getChild("head.js")).thenReturn(null);
+
+
when(scriptingResourceResolver.getResource(any())).thenAnswer(invocation -> {
+ String path = invocation.getArgument(0);
+ if (CALLER_HTL.equals(path)) {
+ return callerHtl;
+ }
+ if (XF_TYPE.equals(path) || APPS_XF.equals(path)) {
+ return appsXf;
+ }
+ if (LIBS_XF.equals(path)) {
+ return libsXf;
+ }
+ return null;
+ });
+ when(request.getResource()).thenReturn(contentResource);
+
+ bindings.put(ScriptEngine.FILENAME, CALLER_HTL);
+
+ assertThrows(
+ org.apache.sling.scripting.sightly.SightlyException.class,
+ () -> dependencyResolver.resolve(bindings, "head.js"));
+ }
+
+ /**
+ * Fallback climb must also resolve {@code ../}-relative dependencies from
a super type.
+ */
+ @Test
+ void resolveInheritedDependency_relativeParentPathViaCallerFallback() {
+ String relativeDependency = "../shared.js";
+ String sharedJsPath = "/libs/wcm/foundation/components/shared.js";
+
+ Resource libsXf = mockResource(LIBS_XF, FOUNDATION_TYPE);
+ Resource appsXf = mockResource(APPS_XF, null);
+ Resource foundation = mockResource(LIBS_FOUNDATION, null);
+ Resource sharedJs = mockJsResource(sharedJsPath);
+ Resource callerHtl = mockResource(CALLER_HTL, "nt:file");
+ Resource contentResource =
mockContent("/content/xf/master/jcr:content", XF_TYPE);
+
+ when(callerHtl.getParent()).thenReturn(libsXf);
+
when(scriptingResourceResolver.getResource(any())).thenAnswer(invocation -> {
+ String path = invocation.getArgument(0);
+ if (CALLER_HTL.equals(path)) {
+ return callerHtl;
+ }
+ if (relativeDependency.equals(path)) {
+ return null;
+ }
+ if (XF_TYPE.equals(path) || APPS_XF.equals(path)) {
+ return appsXf;
+ }
+ if (LIBS_XF.equals(path)) {
+ return libsXf;
+ }
+ if (FOUNDATION_TYPE.equals(path) || LIBS_FOUNDATION.equals(path)) {
+ return foundation;
+ }
+ // caller-local normalize before climb: .../xfpage/../shared.js
+ if
("/libs/cq/experience-fragments/components/shared.js".equals(path)
+ ||
"/apps/cq/experience-fragments/components/shared.js".equals(path)) {
+ return null;
+ }
+ // climb normalize: .../page/../shared.js
+ if (sharedJsPath.equals(path)) {
+ return sharedJs;
+ }
+ return null;
+ });
+ when(request.getResource()).thenReturn(contentResource);
+
+ bindings.put(ScriptEngine.FILENAME, CALLER_HTL);
+
+ ScriptNameAwareReader reader = dependencyResolver.resolve(bindings,
relativeDependency);
+ assertNotNull(reader);
+ assertEquals(sharedJsPath, reader.getScriptName());
+ }
+
+ /**
+ * Fallback climb stops cleanly when a resourceSuperType cannot be
resolved.
+ */
+ @Test
+ void resolveInheritedDependency_unresolvableSuperTypeFallsThrough() {
+ Resource libsXf = mockResource(LIBS_XF, "missing/super/type");
+ Resource appsXf = mockResource(APPS_XF, null);
+ Resource callerHtl = mockResource(CALLER_HTL, "nt:file");
+ Resource contentResource =
mockContent("/content/xf/master/jcr:content", XF_TYPE);
+
+ when(callerHtl.getParent()).thenReturn(libsXf);
+ when(libsXf.getChild("head.js")).thenReturn(null);
+ when(appsXf.getChild("head.js")).thenReturn(null);
+
+
when(scriptingResourceResolver.getResource(any())).thenAnswer(invocation -> {
+ String path = invocation.getArgument(0);
+ if (CALLER_HTL.equals(path)) {
+ return callerHtl;
+ }
+ if (XF_TYPE.equals(path) || APPS_XF.equals(path)) {
+ return appsXf;
+ }
+ if (LIBS_XF.equals(path)) {
+ return libsXf;
+ }
+ // super type path does not resolve
+ return null;
+ });
+ when(request.getResource()).thenReturn(contentResource);
+
+ bindings.put(ScriptEngine.FILENAME, CALLER_HTL);
+
+ assertThrows(
+ org.apache.sling.scripting.sightly.SightlyException.class,
+ () -> dependencyResolver.resolve(bindings, "head.js"));
+ }
+
+ private static Resource mockResource(String path, String
resourceSuperType) {
+ Resource resource = mock(Resource.class);
+ when(resource.getPath()).thenReturn(path);
+ when(resource.getResourceSuperType()).thenReturn(resourceSuperType);
+ return resource;
+ }
+
+ private static Resource mockContent(String path, String resourceType) {
+ Resource resource = mock(Resource.class);
+ when(resource.getPath()).thenReturn(path);
+ when(resource.getResourceType()).thenReturn(resourceType);
+ return resource;
+ }
+
+ private static Resource mockJsResource(String path) {
+ Resource resource = mockResource(path, null);
+ when(resource.adaptTo(InputStream.class))
+ .thenReturn(new
ByteArrayInputStream("use(function(){});".getBytes(StandardCharsets.UTF_8)));
+ return resource;
+ }
}