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;
+    }
 }

Reply via email to