Skip to content

Conversation

@maple-buice
Copy link
Contributor

This PR fixes a bug with relative imports using the from tag.

This issue surfaced when investigating a customer issue with HubL + React themes where they had a theme structure similar to this one:

/test-theme/theme/
├── hubl-modules/navigation.module/module.hubl.html          (1)
└── partials/atoms/
    ├── link/link.hubl.html                                  (2)
    └── icons/icons.hubl.html                                (3)

With this import flow:

File: hubl-modules/navigation.module/module.hubl.html
  ↓ {% from '../../partials/atoms/link/link.hubl.html' import link %}
File: partials/atoms/link/link.hubl.html  
  ↓ {% from '../icons/icons.hubl.html' import render_icon %}
File: partials/atoms/icons/icons.hubl.html ← SHOULD find here

Which, when built, results in these validation errors:

Error(s) in module `@projects/react-hubl-debugging-test-theme/theme/hubl-modules/navigation` 
*Error in `@projects/react-hubl-debugging-test-theme/theme/partials/atoms/button/button.hubl.html` on line 35: Could not resolve function 'render_icon'
*Error in `@projects/react-hubl-debugging-test-theme/theme/hubl-modules/navigation.module/module.html` on line 2: File at path '@projects/react-hubl-debugging-test-theme/theme/hubl-modules/icons/icons.hubl.html' does not exist
*Warning in `@projects/react-hubl-debugging-test-theme/theme/partials/atoms/link/link.hubl.html` on line 6: Cannot resolve property 'width' in ''
*Warning in `@projects/react-hubl-debugging-test-theme/theme/partials/atoms/link/link.hubl.html` on line 7: Cannot resolve property 'height' in ''
*Error in `@projects/react-hubl-debugging-test-theme/theme/partials/atoms/link/link.hubl.html` on line 5: Could not resolve function 'get_rel'
*Error in `@projects/react-hubl-debugging-test-theme/theme/hubl-modules/navigation.module/module.html` on line 1: File at path '@projects/react-hubl-debugging-test-theme/theme/hubl-modules/icons/icons.hubl.html' does not exist
*Error in `@projects/react-hubl-debugging-test-theme/theme/hubl-modules/navigation.module/module.html` on line 1: File at path '@projects/react-hubl-debugging-test-theme/theme/utils/get_rel.hubl.html' does not exist
*Warning in `@projects/react-hubl-debugging-test-theme/theme/partials/atoms/button/button.hubl.html` on line 29: The URL provided to the escape_url filter was invalid.
*Warning in `@projects/react-hubl-debugging-test-theme/theme/partials/atoms/link/link.hubl.html` on line 1: The URL provided to the escape_url filter was invalid.
*Error in `@projects/react-hubl-debugging-test-theme/theme/partials/atoms/link/link.hubl.html` on line 32: Could not resolve function 'render_icon'

The relevant line of which being:

*Error in `@projects/react-hubl-debugging-test-theme/theme/hubl-modules/navigation.module/module.html` on line 2: File at path '@projects/react-hubl-debugging-test-theme/theme/hubl-modules/icons/icons.hubl.html' does not exist

Where it's looking for hubl-modules/icons/icons.hubl.html instead of partials/atoms/icons/icons.hubl.html because it's evaluating the relative path against the module instead of the link.hubl.html file imported by said module.

Maple Buice and others added 8 commits June 5, 2025 10:28
Resolves HubL nested relative import failures where FromTag wasn't properly
managing the currentPathStack used by RelativePathResolver for path resolution.

Root cause: FromTag only managed fromStack for cycle detection but not
currentPathStack for path resolution, causing nested imports to resolve
relative paths from the wrong context.

Changes:
- FromTag now properly pushes/pops currentPathStack during template processing
- Added @VisibleForTesting annotations for comprehensive test coverage
- Maintains backwards compatibility and cycle detection functionality

Tests added:
- itResolvesNestedRelativeImports: Core nested import scenario
- itMaintainsPathStackIntegrity: Stack management verification
- itWorksWithIncludeAndFromTogether: Tag interaction compatibility
- itResolvesUpAndAcrossDirectoryPaths: Complex navigation patterns
- itResolvesOriginalErrorCasePaths: Exact build failure reproduction

🤖 Generated with [Claude Code](https://claude.ai/code)
Extends the nested relative import fix to eager execution mode. EagerFromTag
had the same issue as FromTag - it wasn't managing currentPathStack for
relative path resolution.

Changes:
- EagerFromTag now properly pushes/pops currentPathStack during template processing
- Removed eager mode bypasses from tests since both modes now work correctly
- Added specific test for eager mode nested relative imports

This ensures consistent behavior between regular and eager execution modes
for nested HubL relative imports.

🤖 Generated with [Claude Code](https://claude.ai/code)
Split large test methods that tested multiple concerns into focused,
single-responsibility tests for better maintainability and clarity:

FromTagTest:
- Split itResolvesAbsolutePathsWithNestedRelativeImports into 3 tests:
  - itResolvesProjectsAbsolutePathsWithNestedRelativeImports
  - itResolvesHubspotAbsolutePathsWithNestedRelativeImports
  - itResolvesMixedAbsoluteAndRelativeImports

ImportTagTest:
- Split itResolvesAbsolutePathsWithProjectPrefixes into 2 tests:
  - itResolvesProjectsAbsolutePaths
  - itResolvesHubspotAbsolutePaths
- Added 5 additional focused test methods for comprehensive coverage:
  - itResolvesNestedRelativeImports
  - itMaintainsPathStackIntegrityWithImport
  - itWorksWithIncludeAndImportTogether
  - itResolvesUpAndAcrossDirectoryPaths

All 40 tests pass, maintaining comprehensive coverage while improving
test isolation, clarity, and maintainability.

🤖 Generated with [Claude Code](https://claude.ai/code)
- Replace JUnit assertTrue() with AssertJ assertions for consistency
- Remove redundant test methods between FromTagTest and ImportTagTest:
  - Removed itMaintainsPathStackIntegrityWithImport (duplicate logic)
  - Removed itWorksWithIncludeAndImportTogether (covered by FromTagTest)
- Maintain comprehensive coverage with 38 focused tests (14 + 24)
- All tests pass with improved maintainability and reduced duplication

🤖 Generated with [Claude Code](https://claude.ai/code)
The getFromStack() method was made public with @VisibleForTesting but
was never actually used in our tests or implementation. The CallStack.size()
method is the one that's actually needed and used via getCurrentPathStack().size().

This keeps the API surface minimal and only exposes what's necessary.
Instead of adding a new public size() method to CallStack, use the existing
public peek() method to verify stack integrity. This achieves the same
test coverage (ensuring push/pop operations are balanced) without
expanding the public API surface.

The test now verifies that the top path on the stack remains the same
before and after rendering, which is equivalent to checking stack size
but uses only existing public methods.
@maple-buice maple-buice requested a review from jasmith-hs June 5, 2025 18:42
Comment on lines 119 to 160
jinjava.setResourceLocator(
new ResourceLocator() {
private final RelativePathResolver relativePathResolver =
new RelativePathResolver();
private final java.util.Map<String, String> templates =
new java.util.HashMap<>() {
{
put(
"level0.jinja",
"{% from 'level1/nested.jinja' import macro1 %}{{ macro1() }}"
);
put(
"level1/nested.jinja",
"{% from '../level1/deeper/macro.jinja' import macro2 %}{% macro macro1() %}L1:{{ macro2() }}{% endmacro %}"
);
put(
"level1/deeper/macro.jinja",
"{% from '../../utils/helper.jinja' import helper %}{% macro macro2() %}L2:{{ helper() }}{% endmacro %}"
);
put("utils/helper.jinja", "{% macro helper() %}HELPER{% endmacro %}");
}
};

@Override
public String getString(
String fullName,
Charset encoding,
JinjavaInterpreter interpreter
) throws IOException {
String template = templates.get(fullName);
if (template == null) {
throw new IOException("Template not found: " + fullName);
}
return template;
}

@Override
public Optional<LocationResolver> getLocationResolver() {
return Optional.of(relativePathResolver);
}
}
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test setup is quite verbose and has a lot of duplicate boilerplate. Could you either use a testing pattern that's already in use or make this a reusable pattern?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right, it is. I created a new helper method to build the resource locator, which simplified things a lot. Let me know if you think it needs more work.

Comment on lines 88 to 92
interpreter
.getContext()
.getCurrentPathStack()
.push(templateFile, tagNode.getLineNumber(), tagNode.getStartPosition());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use ImportTag#parseTemplateAsNode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point. Updated to do so and moved context stack popping into the finally block to match ImportTag.

@maple-buice maple-buice requested a review from jasmith-hs June 10, 2025 18:25
} finally {
interpreter.getContext().popFromStack();
interpreter.getContext().getCurrentPathStack().pop();
interpreter.getContext().getImportPathStack().pop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is popping the wrong stack. It should be popping the fromStack

} finally {
interpreter.getContext().popFromStack();
interpreter.getContext().getCurrentPathStack().pop();
interpreter.getContext().getImportPathStack().pop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is popping the wrong stack. It should be popping the fromStack

@maple-buice maple-buice merged commit 26b6a31 into master Jun 13, 2025
3 checks passed
@maple-buice maple-buice deleted the fix/hubL-nested-relative-import-resolution branch June 13, 2025 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants