Imported from Jahia/store-template (
AGENTS.md). Install upstream withnpx skills add Jahia/store-template. Copyright stays with the author.
AGENTS.md - jahia-store-template
Guidance for AI agents (and humans) working in this repo. Read this before editing.
What this is
jahia-store-template is the Jahia 8.2 JavaScript module that renders the Private
App Store website (storefront + module detail/authoring + JAR upload). It was
migrated from a legacy JSP/Bootstrap-3 template set to a server-side React
module running on the javascript-modules-engine (GraalVM SSR + Vite).
See docs/JS-MODULE-MIGRATION.md for the full migration history and rationale.
Store administration (Forge settings / Categories / Roles) lives in the Jahia site administration (jContent), provided by the
jahia-storemodule's React app - it is NOT in-site here (an earlier in-site admin was removed). The chrome reads site branding (logo + footer) server-side via thejahia-storeForgeSettingsServiceOSGi bridge (seesrc/components/forge/forgeBranding.ts), NOT fromjmix:forgeSettingsnode properties.
- Packaging: a Jahia JS module shipped as a
.tgz(not a JAR).mvn packageshells out to npm; the build artifact isdist/package.tgz. - Runtime data (modules, categories, versions) lives in the JCR and is mostly
produced by the sibling
jahia-storeJava module. This repo is the presentation + authoring UI;jahia-storeis the backend/contract. - Release dates come from
uploadDate, notjcr:lastModified.jahia-store'screateEntryFromJarstampsuploadDateonce, when a version node is created, and never rewrites it - so a changelog edit or a publish toggle no longer moves a release date. Read it throughreleaseStamp/releaseDay(nodeProps.ts), which fall back tojcr:lastModifiedfor versions created before the property existed or outside the upload action. Never usejcr:created: it is the migration run date for content copied from the legacy store. The module-level date (detail Information rail) is the MAX over published versions -latestReleaseDate- so it matches the storefront grid order and is the same for an owner and a visitor.
Architecture
- Two Vite build environments (
@jahia/vite-plugin):- client - files matching
**/*.client.{jsx,tsx}are hydration islands, emitted todist/client/…. They are SSR-rendered first, then hydrated. - ssr -
**/*.server.*plus everything they import is bundled intodist/server/index.js.ssr.noExternal: true, so every imported dep is inlined into the SSR bundle.
- client - files matching
- Islands: wrap a
.client.tsxcomponent in<Island component={X} props={…} />from a server component. Props must be JSON-serializable (they cross the SSR→hydration boundary). Use adata-*-readyattribute set in a mount effect as a deterministic "hydrated" signal for tests. - i18n: site chrome uses the engine's
settings/locales. - Site branding: the header logo + footer (copyright + privacy/terms/cookies
- facebook/linkedin/twitter/youtube) are read server-side by the chrome from
jahia-store's per-site OSGi configuration via theForgeSettingsServiceOSGi bridge (src/components/forge/forgeBranding.tscallsserver.osgi.getService(...)in-process — no GraphQL round-trip, no permission gate, reads only public branding getters). The logo is a plain JCR path string. Configured in jContent's Store administration → Settings. Each falls back to a Jahia default when unset. (Storage was moved off the cross-modulejmix:forgeSettingsnode type, which is now legacy.)
- facebook/linkedin/twitter/youtube) are read server-side by the chrome from
- Page templates are React components with
componentType: "template".settings/import.xmlseeds a working store on every jahia-store-template site.
Hard engine constraints (do not relearn these the hard way)
- No Moonstone / heavy React UI libs. The engine runs React 19; Moonstone pulls React 18 → conflict. UI here is plain HTML + CSS Modules.
- No Apollo Client. Its SSR build imports
node:module, which GraalVM rejects. Usefetch- seesrc/lib/graphql.ts(gqlRequest). - GraalVM rejects optional-call
?.()on Java host objects. Call methods onJCRNodeWrapperetc. directly; guard withif/hasPropertyinstead. - Keep heavy browser-only deps out of the SSR bundle. DOMPurify is loaded
with a dynamic
await import("dompurify")so it becomes its own lazy chunk (dist/server/purify.es-*.js) and never entersindex.js. If you add a browser-only lib used only in a handler/effect, import it the same way. manualPureFunctions: ["useEffect"]strips effects from the SSR render - do not rely on an effect running server-side.- Every fragment is cached, and the cache key knows permissions but NOT
identity. Jahia keys a rendered fragment on the resource plus an ACL-derived
component (which also separates anonymous from authenticated). It does not
include the user. So two people holding the same role share one cached
fragment, and anything you render that varies between them leaks from one to
the other. Anything reaching
Layout- the wholeHeader/Footerchrome - is shared this way on every page. A template or view whose output varies per user (not merely per permission) MUST declare it:properties: { "cache.perUser": "true" }. Worked examples:templates/Page/default.server.tsx(the header renders the viewer's username) andcomponents/ForgeMyModulesList/default.server.tsx(rows selected onjcr:createdBy). Not knowing this cost SEC-375 / GHSA-g6wp-ghxm-mx76, where one logged-in Store user was served another's username. The victims are ordinary accounts: Jahia does not cache a fragment for a user who can edit the resource, so a role carryingjcr:write_live(store-developer does) incidentally immunises its holder - which makes the defect easy to test wrongly, since a two-user check built from privileged accounts passes against fully vulnerable code. Note thatcache.expiration: "0"on a nested view does not help the enclosing page fragment - each fragment carries its own policy. Guarded by../privateappstore/tests/cypress/e2e/25-fragmentCacheIdentity.cy.ts; read its header before changing it, because the obvious ways to "improve" that spec all make a broken build pass.
The GraphQL permission wall (critical for authoring features)
The Jahia /modules/graphql endpoint is permission-gated: ordinary
authenticated users get GqlAccessDeniedException. Only roles such as
store-developer/admin can use it.
- Owner-only authoring (edit own module metadata, manage screenshots) uses
generic JCR GraphQL mutations via
gqlRequest; JCR ACLs enforce permissions. - Write actions that need an in-workspace Java side (e.g. uploading a module
JAR, which runs a Maven deploy then creates the nodes) CANNOT use GraphQL. They
go to a Jahia Action in
jahia-store(createEntryFromJar), invoked at…/modules-repository.createEntryFromJar.do. - CSRF: Jahia's OWASP CSRFGuard injects
/modules/CsrfServlet, which patches XMLHttpRequest only - notfetch, and not plain full-page<form>posts. Action POSTs (the.doURLs) must useXMLHttpRequest, or they are rejected ("Required Token is missing" / "Request Token does not match Page Token"). SeeFileUpload/FileUpload.client.tsx#postMultipart. (PlaingqlRequest/fetchto/modules/graphqlis fine - that endpoint is not CSRF-gated.)
Directory map
src/
components/
forge/ module/package cards, detail, screenshots (Lightbox),
CKEditor 5 richtext (loadCKEditor/CKEditorField), icon upload,
versions, store filter, publish + changelog editors
chrome/ site header (logo, nav, search, login island), footer
(branding via forgeBranding.ts → ForgeSettingsService), layout
ForgeModulesList/, ForgeMyModulesList/, FileUpload/ server views
lib/ graphql.ts (gqlRequest + gqlUpload multipart), helpers
settings/ import.xml (seed structure) + locales
docs/ JS-MODULE-MIGRATION.md, SECURITY-CSP.md
dist/ build output (client islands, server bundle, package.tgz)
Build / deploy / test
- Build:
npm run build→vite build+npm pack→dist/package.tgz. (mvn packagedoes the same for CI parity; the pom ispackaging=pomand just drives npm.) - Deploy to a running Jahia (dev loop): copy the tgz into the container and
install via the provisioning API; a same-version tgz must be uninstalled
first (it clashes otherwise):
docker cp dist/package.tgz jahia:/tmp/jahia-store-template.tgz # POST /modules/api/provisioning (basic auth): # [{"uninstallBundle":"org.jahia.modules.javascript/jahia-store-template/5.0.0.SNAPSHOT"}] # [{"installBundle":["js:file:/tmp/jahia-store-template.tgz"],"autoStart":true}] - E2E lives in
../privateappstore/tests(Cypress,baseUrllocalhost:8080).npx cypress run. The suite must stay green. Selectors that code changes must preserve are listed below under "Preserved E2E selectors" - that section is the single canonical list (CLAUDE.md just points here; do not fork a second copy).
Preserved E2E selectors
Refactoring markup must not break any of these, exercised by the Cypress suite
in ../privateappstore/tests:
[data-editor-ready], [data-filter-ready], [data-upload-ready],
[data-forge-card], [role="tab"]/[role="tabpanel"] (admin + module editor),
[data-ckeditor-state], [data-ckeditor5-styles],
[data-icon-input]/[data-icon-status], [data-changelog-ready],
[data-tag-list], #edit-status/#edit-tags,
[data-dependency-lists]/[data-dependency-column]/[data-dependency],
[data-forge-version] (one version card, incl. its "Released"/"Requires Jahia"
footer), [data-version-delete-scope]/[data-version-delete-ready], [data-add-version]
(owner upload-new-version form in the detail Versions tab), the shared global
.store-btn button classes (incl. .store-btn--danger),
[data-detail-tabs-ready], [role="tab"]/[data-detail-panel],
#forge-url/#forge-id/#forge-user, and [data-account-name] (the header
account-name span - spec 25 asserts which username a cached page carries, so
[data-account-toggle] alone is not enough).
SonarQube
- Project key:
org.jahia.modules.javascript:jahia-store-template. - Scan needs JDK 17 - the bare pom resolves the latest
sonar-maven-plugin(Java-17 bytecode); the host default is Java 11, which fails withUnsupportedClassVersionError. Run withJAVA_HOMEpointing at a JDK 17:JAVA_HOME=/usr/lib/jvm/graalvm-jdk-17.* mvn -B clean install sonar:sonar \ -Dsonar.sources=src -Dsonar.exclusions='**/node_modules/**,dist/**' - The quality gate uses the overall reliability rating (worst-issue-wins), so
even one reliability-impact issue (
S2871array sort,S7781replace→replaceAll,S6847/S1082/S6842non-interactive elements with handlers) fails the gate. Maintainability smells (S6759,S7764,S6582,S6819) do not move the gate but should be kept clean. - Richtext editing uses CKEditor 5 from the deployed
richtext-ckeditor5module, loaded at runtime (loadCKEditor.ts→CKEditorField.tsx), NOT bundled. That module exposes CKEditor as a webpack Module Federation remote;loadCKEditorstubswindow.appShell.remotes, injects/modules/richtext-ckeditor5/javascript/apps/remoteEntry.js, thencontainer.init({})+get(".")to reachClassicEditor(verified to work on the live delivery page, where jContent's app-shell is absent). Keeping CKEditor out of jahia-store-template's bundle is deliberate - inlining a real editor into the SSR bundle is the failure mode that ruled out Apollo. Output is still DOMPurify-sanitized on save (defense-in-depth). If the remote can't load,CKEditorFielddegrades to a textarea. (The old dependency-freeexecCommandRichTextEditor/S1874exception is gone.)
Accessibility invariants (target: WCAG 2.2 Level AAA)
This module targets WCAG 2.2 Level AAA for everything it controls (structure, color, focus, keyboard). The invariants below are what keep it there - don't regress them. Verified against an axe-core / EqualWeb audit.
Layoutowns the one and only<main>landmark. Page templates (templates/Page/*.server.tsx) supply content asLayoutchildren - they must NOT render their own<main>, or you get a nested/duplicatemainlandmark (axe flags three best-practice violations at once).- Muted/secondary text must use
var(--color-text-muted), never a hardcoded gray. The token is tuned to WCAG AAA enhanced contrast (≥7:1) on every surface; hardcoding a lighter gray silently drops below 7:1 and re-introduces the contrast violation. --ringmust stay opaque and two-tone (inner--color-bgband, outer--color-accentband). The outer band measures ~7.85:1 against white; the old single translucent ring measured ~1.61:1 on white and ~1.00:1 (invisible) on.store-btn--primary- a module-wide WCAG 2.2 SC 1.4.11 Non-text Contrast (3:1) failure. Collapsing it back to one translucent value silently reintroduces that failure; no unit test catches it.
Conventions
- CSS Modules per component; design tokens as CSS custom properties.
- Components PascalCase; hooks
use*; client islands end in.client.tsx; server views in*.server.tsx. - Keep files focused (< ~400 lines). Immutable updates. Handle errors explicitly
(no swallowed catches; no
console.*in production code). - Do not break the
moduleList.jsoncontract consumed byjahia-store.