feat: JS server extension points, client-callable actions, and the JSServerExtensionInvoker SDK - #687
feat: JS server extension points, client-callable actions, and the JSServerExtensionInvoker SDK#687romain-pm wants to merge 39 commits into
Conversation
ADR-0001: per-type registrars over a shared base class (whiteboard-aligned) ADR-0002: first-class registry types (action, choicelist-initializer, node-validator) ADR-0003: typed TS registration wrappers with raw Java escape hatch ADR-0004: CSRF whitelisting stays the module author's responsibility ADR-0005: node validators bridged via a single Bean Validation bean on nt:base
…sion bridges - New AbstractServiceRegistrar<S>: per-bundle registry-entry discovery, bridge creation, OSGi service publication and tracked unregistration with per-entry error isolation (ADR-0001). - RenderFilterRegistrar refactored onto the base class. Two fixes folded in: the registry entry's priority is no longer overwritten to 0 after construction (parsed as float), and execute/prepare now null-guard both the registry entry (module stopped mid-flight) and null/undefined JS results. - mockito-core + graalvm js added at test scope.
- ChoiceListInitializerRegistrar bridges registry entries of type
'choicelist-initializer' to ModuleChoiceListInitializer OSGi services
(consumed by core, usable as choicelist[key] in CND definitions).
- New registerChoiceListInitializer() library API: typed callback receiving
{param, locale (BCP-47), values, node?, java escape hatch}; returns
{label, value, properties?} choices (properties variant supported).
- java-ts-bind: generate ExtendedPropertyDefinition (narrow whitelist,
flattened parent methods) and Locale.toLanguageTag.
- Test-module fixture + CND type, Cypress spec (forms GraphQL API), docs guide.
- ActionRegistrar bridges registry entries of type 'action' to
org.jahia.bin.Action OSGi services, invoked via <nodeUrl>.<name>.do.
- New registerAction() library API: typed declaration (requiredMethods,
requireAuthenticatedUser [Jahia default: true], requiredPermission,
requiredWorkspace) and handler receiving {parameters, renderContext,
resource, session, request, urlResolver}; result {statusCode, json,
redirect, absoluteRedirect}. The adapter pre-stringifies json to avoid
polyglot deep-conversion issues with nested structures.
- CSRF stays the module's responsibility (ADR-0004): documented recipe +
test-module .cfg exercising it end-to-end.
- java-ts-bind: generate URLResolver (narrow whitelist).
- Test-module fixtures (GET/POST/auth/redirect), Cypress spec incl. redeploy
resilience, docs guide.
- New registrars/validation package: a single JSNodeValidator Bean Validation
bean registered under the sentinel nt:base node type (exactly-once execution
per changed node per save; no clobbering of Java validators on real node
types), carrying four repeatable class-level @JSValidation constraints that
mirror Jahia's default/advanced/skip-on-import validation phases (ADR-0005).
- JSValidationConstraintValidator dispatches to JS validators via the
ref-counted NodeValidatorRegistrar (volatile snapshot gate before any
GraalVM entry, ownership-checked platform (un)registration, fail-closed on
throwing validators).
- Messages follow JahiaMessageInterpolator semantics: {resource.bundle.key}
messages are localized through module resource bundles, anything else is
verbatim; sub-2-character messages get a generic fallback (interpolator
crash guard).
- New registerNodeValidator() library API with locale (BCP-47) + raw Java
escape hatch.
- Bean Validation deps (api provided, hibernate-validator 6.2.0.Final at test
scope); full-chain unit tests against real HV; registrar state-machine tests.
- Test-module fixtures + CND type, Cypress spec (field/node-level, phases,
verbatim messages, redeploy resilience), reference documentation.
…nsion points - registerRenderFilter(): typed wrapper over the existing 'render-filter' registry shape (backward compatible), with applyOn* options accepting arrays and fractional priorities. - Hydrogen gains a self-contained ContactForm component demonstrating all three new extension points: a CSRF-whitelisted POST action receiving the form, a choicelist initializer for the form style, and a node validator on the notification email.
getName() is not part of the generated JahiaUser typing; the CI test-module build runs tsc --noEmit and failed on it.
- Choicelist initializers receive the CONTENT locale (language being edited), not the editor UI locale — docs, TSDoc and Cypress spec corrected (verified against a live instance via the Content Editor forms API). - Actions guide: the render servlet writes the JSON body only when the request sends Accept: application/json.
… ADR 0006 Completes the TS migration scripts feature (runtime + typed API landed in earlier commits on this branch): - persistent failure barrier: a recorded .failed migration holds back later-named pending migrations across restarts/redeploys - changeNodeType: rebind via getRealNode() (the Jahia wrapper throws UnsupportedRepositoryOperationException) and carry mapped properties around the retype (Jackrabbit validates existing properties against the new type) - JcrHelper.doExecuteAsSystem: include the exception class in the message (JCR exceptions often carry a null message across polyglot) - jahia-test-module: 9 migration fixtures covering the five operations plus skip/failure/halt semantics; Cypress spec asserting statuses in j:bundlesScripts and the transformed content - guide (docs/2-guides/6-migrations) and ADR 0006 All semantics verified live on jahia-discovery 8.2.3.
🦜 Chachalog
|
📝 Documentation GuidelinesThank you for contributing to our documentation! To ensure your contributions meet our standards, please review these resources:
This comment is posted automatically when changes are detected in the |
There was a problem hiding this comment.
Pull request overview
This PR extends the JavaScript Modules engine so JS modules can declare additional server-side extension points (choicelist initializers, node validators, actions, render filters) and introduces first-class JS migrations with typed APIs, engine-side registrars, documentation (ADRs/guides), and automated test coverage.
Changes:
- Added typed registration APIs in
@jahia/javascript-modules-libraryfor actions, choicelist initializers, node validators, render filters, and migrations. - Implemented/expanded engine-side registrars and bridges (including a shared
AbstractServiceRegistrar) plus migration execution and status recording. - Added Cypress e2e specs, Java unit tests, sample module updates, docs/ADRs, and updated Vite plugin defaults for server entry globs.
Reviewed changes
Copilot reviewed 61 out of 61 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| vite-plugin/src/index.ts | Expands default server entry glob to include .js/.ts variants. |
| vite-plugin/README.md | Documents the updated default inputGlob. |
| tests/cypress/e2e/ui/nodeValidatorTest.cy.ts | Cypress coverage for JS node validator behavior and redeploy resilience. |
| tests/cypress/e2e/ui/choicelistInitializerTest.cy.ts | Cypress coverage for JS choicelist initializer behavior (params, locale, properties). |
| tests/cypress/e2e/ui/actionTest.cy.ts | Cypress coverage for JS actions (methods/auth/redirect/CSRF whitelist, redeploy resilience). |
| tests/cypress/e2e/engine/migrationTest.cy.ts | Cypress coverage for JS migration ordering, statuses, and effects. |
| samples/hydrogen/src/components/ContactForm/extensions.server.tsx | Example JS module declaring action/choicelist initializer/validator. |
| samples/hydrogen/src/components/ContactForm/definition.cnd | Sample CND using a JS-declared choicelist initializer key. |
| samples/hydrogen/src/components/ContactForm/default.server.tsx | Sample component using the JS-declared action endpoint. |
| samples/hydrogen/settings/configurations/org.jahia.modules.jahiacsrfguard-hydrogen.cfg | Sample CSRF whitelist config for a POST action. |
| MIGRATIONS-DEMO.md | Developer-facing migration contract/demo document. |
| javascript-modules-library/src/index.ts | Exposes new registration APIs and migration types from the library entrypoint. |
| javascript-modules-library/src/framework/registerRenderFilter.ts | Typed render-filter registration wrapper writing to the server registry. |
| javascript-modules-library/src/framework/registerNodeValidator.ts | Typed node-validator registration wrapper and context adaptation. |
| javascript-modules-library/src/framework/registerChoiceListInitializer.ts | Typed choicelist initializer wrapper and Java→JS value adaptation. |
| javascript-modules-library/src/framework/registerAction.ts | Typed action wrapper with parameter conversion + JSON result shaping. |
| javascript-modules-library/src/framework/migrations/types.ts | Public types for migration declaration/context/operations. |
| javascript-modules-library/src/framework/migrations/registerMigration.ts | Migration registration wrapper with sync-only enforcement and dedupe guard. |
| javascript-modules-library/src/framework/migrations/operations.ts | Implementation of guard-railed migration helpers (remove/set/convert/retype). |
| javascript-modules-library/src/framework/migrations/jcr.ts | Migration JCR batching engine + system-session helper. |
| javascript-modules-engine-java/src/test/java/.../validation/NodeValidatorRegistrarTest.java | Unit tests for validator registrar lifecycle and violation shape handling. |
| javascript-modules-engine-java/src/test/java/.../validation/JSNodeValidatorBeanValidationTest.java | Full Bean Validation chain tests for the JS validator bridge. |
| javascript-modules-engine-java/src/test/java/.../migrations/MigrationRegistrarTest.java | Unit tests for migration ordering, terminal status semantics, halting behavior. |
| javascript-modules-engine-java/src/test/java/.../ChoiceListInitializerRegistrarTest.java | Unit tests for JS choicelist value conversion. |
| javascript-modules-engine-java/src/test/java/.../ActionRegistrarTest.java | Unit tests for action mapping + result conversion. |
| javascript-modules-engine-java/src/test/java/.../AbstractServiceRegistrarTest.java | Unit tests for the shared registrar base class behavior. |
| javascript-modules-engine-java/src/main/java/.../validation/NodeValidatorRegistrar.java | Engine registrar bridging JS validators to Jahia’s validation pipeline. |
| javascript-modules-engine-java/src/main/java/.../validation/JSViolation.java | Value object for JS validator violations. |
| javascript-modules-engine-java/src/main/java/.../validation/JSValidationConstraintValidator.java | Bean Validation constraint validator dispatching to JS validators. |
| javascript-modules-engine-java/src/main/java/.../validation/JSValidation.java | Repeatable constraint annotation representing validation phases. |
| javascript-modules-engine-java/src/main/java/.../validation/JSNodeValidator.java | Sentinel Bean Validation bean bridging all JS node validators. |
| javascript-modules-engine-java/src/main/java/.../RenderFilterRegistrar.java | Refactors render-filter registrar onto shared base + honors priority. |
| javascript-modules-engine-java/src/main/java/.../migrations/MigrationSupport.java | Java support object passed into migrations (logger/dry-run/definition ops). |
| javascript-modules-engine-java/src/main/java/.../migrations/MigrationRegistrar.java | Runs pending JS migrations and records terminal statuses in the shared store. |
| javascript-modules-engine-java/src/main/java/.../ChoiceListInitializerRegistrar.java | Publishes JS choicelist initializers as OSGi services with collision warnings. |
| javascript-modules-engine-java/src/main/java/.../ActionRegistrar.java | Publishes JS actions as OSGi services with collision warnings. |
| javascript-modules-engine-java/src/main/java/.../AbstractServiceRegistrar.java | New shared registrar base for “registry entry → OSGi service” bridging. |
| javascript-modules-engine-java/src/main/java/.../js/server/JcrHelper.java | Adds doExecuteAsSystem helper for migration/system-session execution. |
| javascript-modules-engine-java/pom.xml | Adds provided/test dependencies for JSON + Bean Validation + GraalJS test runtime. |
| javascript-modules-engine-java/.java-ts-bind/package.json | Expands bindings/whitelist for new Java types/methods used by typed APIs. |
| javascript-create-module/templates/module/vite.config.mjs | Updates template comment for the new default server glob. |
| jahia-test-module/src/react/server/extensions/validators.ts | JS fixtures for node validator behaviors covered by Cypress specs. |
| jahia-test-module/src/react/server/extensions/migrations.ts | JS fixtures for migration behaviors covered by Cypress specs. |
| jahia-test-module/src/react/server/extensions/choicelists.ts | JS fixtures for choicelist initializer behaviors covered by Cypress specs. |
| jahia-test-module/src/react/server/extensions/actions.ts | JS fixtures for action behaviors covered by Cypress specs. |
| jahia-test-module/settings/definitions.cnd | Adds fixture node types/properties used by new server-extension tests. |
| jahia-test-module/settings/configurations/org.jahia.modules.jahiacsrfguard-jsmtest.cfg | CSRF whitelist for POST action test fixture. |
| docs/adr/README.md | Adds ADR index and references for the new architecture decisions. |
| docs/adr/0001-javascript-server-extension-points.md | ADR: per-type registrars over shared base class. |
| docs/adr/0002-first-class-registry-types.md | ADR: first-class registry types per extension point. |
| docs/adr/0003-typed-registration-wrappers.md | ADR: typed TS-first wrappers with Java escape hatch. |
| docs/adr/0004-csrf-whitelisting-for-js-actions.md | ADR: CSRF whitelist remains module-author responsibility. |
| docs/adr/0005-js-node-validators-single-bean-validation-bridge.md | ADR: single sentinel Bean Validation bridge for validators. |
| docs/adr/0006-javascript-migrations.md | ADR: run migrations via registrar + shared patch status store. |
| docs/3-reference/3-node-validators/README.md | Public documentation for JS node validators. |
| docs/3-reference/1-cnd-format/README.md | Mentions JS-declared choicelist initializers as a CND option. |
| docs/2-guides/6-migrations/README.md | Public documentation for writing JS migrations. |
| docs/2-guides/5-choicelist-initializers/README.md | Public documentation for JS choicelist initializers. |
| docs/2-guides/4-actions/README.md | Public documentation for JS actions + CSRF whitelist recipe. |
| .chachalog/js-server-extension-points.md | Release note describing new server extension points and render-filter priority change. |
| /** Converts the Java List of org.jahia...ChoiceListValue accumulated so far into plain JS objects. */ | ||
| const toJsChoiceListValues = (values: List<unknown>): ChoiceListValue[] => { | ||
| const result: ChoiceListValue[] = []; | ||
| if (values) { | ||
| for (let i = 0; i < values.size(); i++) { | ||
| // Jahia's ChoiceListValue: getDisplayName(), getValue() (JCR Value), getProperties() | ||
| const value = values.get(i) as { | ||
| getDisplayName(): string; | ||
| getValue(): { getString(): string }; | ||
| getProperties(): JavaMap<string, unknown> | null; | ||
| }; | ||
| result.push({ label: value.getDisplayName(), value: value.getValue().getString() }); | ||
| } | ||
| } | ||
| return result; | ||
| }; |
| JavaScript modules can now declare choicelist initializers, server-side node validators and actions — extension points that previously required a Java module. Use the new `registerChoiceListInitializer`, `registerNodeValidator`, `registerAction` and `registerRenderFilter` functions from `@jahia/javascript-modules-library`. | ||
|
|
||
| Note for existing modules using `server.registry.add("render-filter", …)`: a declared `priority` is now honored (it was previously ignored and forced to 0), which may reorder such filters in the render chain. |
| if (!registry.hasNodeType(name)) { | ||
| getLogger(bundle.getSymbolicName()).info( | ||
| "Node type {} is not registered on this instance, nothing to unregister", name); | ||
| return; | ||
| } |
| if (dryRun) { | ||
| getLogger(bundle.getSymbolicName()).info("[dry-run] would unregister node type {}", name); | ||
| return; | ||
| } |
| // Snapshot the identifiers first: mutating nodes while paging through a live query result | ||
| // (retyping, removing…) would make the pagination skip or repeat nodes. | ||
| const identifiers = withSystemSession({ workspace }, (session) => { | ||
| const result: string[] = []; | ||
| for (let offset = 0; ; offset += SNAPSHOT_PAGE_SIZE) { | ||
| const sql2Query = session | ||
| .getWorkspace() | ||
| .getQueryManager() | ||
| .createQuery(query, "JCR-SQL2"); | ||
| sql2Query.setLimit(SNAPSHOT_PAGE_SIZE); | ||
| sql2Query.setOffset(offset); | ||
| const iterator = sql2Query.execute().getNodes(); | ||
| let pageCount = 0; | ||
| while (iterator.hasNext()) { | ||
| result.push((iterator.nextNode() as JCRNodeWrapper).getIdentifier()); | ||
| pageCount++; | ||
| } | ||
| if (pageCount < SNAPSHOT_PAGE_SIZE) break; | ||
| } | ||
| return result; |
Renames the run-once content-scripting feature from "migration" to "content patch" — "migration" collided with Jahia core's platform Migrator and over-narrowed a mechanism whose real job is arbitrary run-once content/definition fixes (repairs, backfills, retypes), not only schema migrations. - registerMigration -> registerContentPatch; context.migrate -> .patch - MigrationRegistrar/Support -> ContentPatchRegistrar/Support (registrars.contentpatches package) - registry type "migration" -> "content-patch" - status path prefix /javascript/migrations/ -> /javascript/content-patches/ - config PID + logger namespace ...engine.migrations -> ...engine.contentpatches - library dir framework/migrations -> framework/contentPatches; all exported types Migration* -> ContentPatch* - test-module fixtures, Cypress spec, guide (docs/2-guides/6-content-patches), ADR 0006, and design docs (CONTENT-PATCHES-PLAN/DEMO) renamed; Groovy / Sanity / Contentful prior-art references keep their own "migration" term 58 unit tests green; all content-patch semantics (five operations + skip/failure/persistent-barrier + new status-path prefix) re-verified live on jahia-discovery 8.2.3.
…nsions Adds a public, polyglot-free SDK so other OSGi bundles can consume JS-declared server extensions (server.registry.add(type,...)) and invoke their callbacks: - JSServerExtensionInvoker (interface) + Impl (@component) in a new exported package org.jahia.modules.javascript.modules.engine.sdk - forEach(type, handler) runs within one pooled GraalVM context; Invoker.call executes a JS callable and converts the result to plain Java (no GraalVM types leak to consumers, so callers need no polyglot dependency) - engine bundle now Export-Package's only the sdk package Enables Formidable to run JS-authored form-field validators server-side (issue Jahia/formidable#158) without embedding GraalVM or engine internals.
Frees the plain 'action' name for the upcoming client-callable actions (#588) and makes the org.jahia.bin.Action lineage explicit. Registry type becomes 'node-legacy-action'; Java registrar, types, fixtures, hydrogen sample and the guide (now 'Legacy Node Actions') renamed accordingly. Pre-release rename, no compatibility shim.
- .action.ts files are compiled twice by @jahia/vite-plugin: the server bundle registers every export as a callable action; the client bundle replaces the module with generated fetch stubs (devalue-serialized args and results), so islands call server code as plain async functions. - action(schema, fn) safe wrapper: input validation via any Standard Schema compatible library (interface vendored, no dependency added); validation issues travel back to the client error. - Engine: single jsAction dispatch endpoint riding the render servlet (visitor session, guests allowed), mandatory X-JS-Action header as CSRF defense, engine-owned csrfguard whitelist entry; JSPromise settles async handlers through GraalJS microtask draining (unit-tested against real GraalJS, incl. never-settling detection). - Docs: new Actions guide + when-to-use-what vs legacy node actions; ADR-0007 (naming) and ADR-0008 (design). - Fixtures: vite-plugin snapshot fixture, test-module .action.ts + island, Cypress spec covering wire protocol, devalue round-trip, validation, header requirement and the end-to-end island path.
40 of the branch's files did not conform to the repo prettier config; formatted wholesale (includes regenerated vite-plugin fixture snapshots).
Applied findings from a fresh-eyes review of the branch's new code:
- RenderFilterRegistrar: class javadoc added; execute/prepare guards made
symmetric — a missing optional callback is a silent no-op, only a vanished
registry entry warns.
- JSPromise.Settled renamed Outcome with isSettled() ('a Settled that is not
done' contradicted itself); GenericActionEndpoint uses SC_OK constants and
an error(String) overload.
- NodeValidatorProps renamed NodeValidatorDeclaration (aligns with the
sibling *Declaration interfaces); stale 'Registered action' log message
disambiguated after the legacy rename.
- Bridge constructors take registryEntry (was: value, colliding mentally
with polyglot Value); dead getRegistryType() removed.
- vite-plugin actions: export-discovery regex tightened to the documented
forms (const/function/async function), extension set aligned with the
default glob, module-name fallback now warns loudly, stub header value
commented; choicelist accumulated-values mapping documents why properties
are not surfaced; messageOf() helper extracted in the action adapter.
- registerActionsModule renamed __registerActionsModule and marked @internal: it is a vite-plugin-generated-code contract, not a developer API. It stays in the main entry point because the engine resolves the library as a single shared module at runtime (a subpath entry would not resolve there) — recorded in ADR-0008. - server.registry documentation now steers extension-point registration to the typed register* functions; the raw entry shapes are internal contracts.
Implements #688 and #689. - JSPromise moved from ..engine.actions to ..engine.jsengine (general GraalJS facility, not an actions concern) and gains settleOrThrow(): fulfilled value returned, rejection converted to GraalVMException (same semantics as a synchronous throw), never-settling promise fails with an explicit message. - All four bridges (choicelists, node validators, legacy node actions, render filters) settle JS results through it, so async callbacks now work everywhere (microtask-only, like actions). Validators keep their fail-closed policy: a rejection lands in the existing catch and blocks the save. - TS signatures accept Promise returns; the legacy-action adapter chains the conversion on the handler promise. Docs and ADR-0003 updated; test fixtures made async on purpose so the existing Cypress specs exercise the async path end-to-end. - registrars/package-info.java documents the package layout rule.
Content patches are an independent feature (ContentPatchRegistrar implements the engine Registrar SPI, no dependency on the extension-points mechanism). Moving them to #697 shrinks this PR by ~2300 LOC and separates two review audiences. This also neutralizes the mislabeled commit 3b6c507: the content-patch files it swept in are no longer part of this branch's net diff. Removed here: registerContentPatch + jcr/operations/types, ContentPatchRegistrar (+support, tests), the content-patch guide/ADR-0006/plan docs, the Cypress spec, and the patchTest* test-module fixtures. Left in place intentionally (not content-patch-exclusive, referenced by other verticals or generally useful): the org.json/test dependencies in the engine-java pom, the JCR mutation-method entries in .java-ts-bind, and JcrHelper.doExecuteAsSystem.
The generic-actions Cypress spec (genericActionTest.cy.ts) uses devalue, added
to tests/package.json, but tests/yarn.lock was not regenerated. CI's immutable
install in the standalone tests/ project rejected it ('the lockfile would have
been modified'), failing on-code-change / static-analysis. This was the only
failing check (build + sonar pass).
|
Heads-up: this branch's integration suite is red for a packaging reason, and the fix is one line.
The suite's first spec ( Fix (adding |
Exporting a single package turned off bnd's implicit inclusion of dependency
classes: the bundle went from 9154 classes to 88, keeping only what the
explicit Export-Package needed. Everything else still resolved from the jars
listed in Embed-Dependency — except `com.oracle.truffle.tools.utils.json`,
which lives in `org.graalvm.tools:profiler`, a transitive of chromeinspector
that was never listed there because bnd used to pull it in on its own.
The cost was not limited to the debugger. Enabling `polyglot.inspect` made
GraalVMEngine.activate() throw NoClassDefFoundError, the engine never came
back, and every JS view and action was gone for the rest of the run — which
is why the whole integration suite failed on this branch while main stayed
green: its first spec enables the debugger.
Listing `profiler` restores the three missing `com.oracle.truffle.tools`
packages; a package-level diff against a main build now shows no difference.
Verified on Jahia 8.2: enabling the inspector leaves the engine active,
`/json/version` answers `{"Protocol-Version":"1.2","Browser":"GraalVM"}` —
what the spec asserts — and with the debugger on, the test module still
registers its 106 components, pages render, and an action returns its
envelope.
None of these three specs has ever passed on this branch; with the engine
crash out of the way (previous commit) they are what is left of the red
suite.
**choicelistInitializerTest, nodeValidatorTest** passed their GraphQL
documents to `cy.apollo` as template-literal strings. The helper expects a
parsed document — it reads `.loc.source` — so every call died in
`@jahia/cypress` with "Cannot read properties of undefined (reading
'source')" before reaching Jahia. Their queries move to
`cypress/fixtures/graphql/`, consumed through `queryFile`/`mutationFile` like
every other spec in the suite.
**actionTest** read JSON bodies from legacy node actions without asking for
JSON. Jahia only serializes an action's JSON when the caller sends
`accept: application/json` (or `returnContentType=json`) — JS-declared legacy
actions follow the same contract as Java ones, by design — so the assertions
compared `undefined` against the expected values while the status codes
matched. The four body-reading requests now send the header.
Its redirect case was wrong on both ends: the fixture returned
`{statusCode: 302, redirect}`, and a result code >= 300 makes Jahia call
sendError() rather than redirect, which is why the response carried no
Location. The fixture now returns the target alone and lets the platform pick
the status, and the assertion accepts any 3xx.
Verified against a Jahia 8.2 instance: choicelist 5/5 and node validator 6/6
(both were 0/5 and 1/6). For actionTest only the CSRF-whitelisted POST case
can run here — the local instance's CSRF guard blocks the GET `.do` calls
that CI allows — and that case goes from failing to passing with the header,
which is the same mechanism behind the other three. CI covers the rest.
**Authenticated legacy action.** `testJsActionAuth` is a GET, and Jahia's CSRF guard challenges authenticated GET `.do` requests too, so the call never reached the action (400). The test module whitelists it next to the POST one, which was whitelisted for the same reason. **Choicelist localization.** The spec asked the shared test site's creation form for a `fr` content locale, but that site is created with `languages: 'en'`. Jahia 8.2.1.0 passed the requested locale through regardless — the assertion passed locally — while the version CI runs falls back to the site's language and the initializer correctly answered "Red". The spec now creates its own site with French enabled, like the i18n spec does, so it tests the initializer rather than a version's locale-fallback behaviour. Verified against Jahia 8.2: choicelist 5/5. For actionTest the local CSRF guard challenges every guest GET `.do` — the three tests that still fail here answer with the guard's /error.html redirect and are the ones CI already passes — but the authenticated case this commit targets goes from failing to passing locally (4/7 up from 2/7).
The spec asked for `uiLocale: "en", locale: "fr"` and expected the French label, because on Jahia 8.2.1.0 a choicelist initializer is handed the content locale — verified directly against the form API there: `locale: "fr"` yields "Rouge" whatever the uiLocale is, and `locale: "en"` yields "Red". The snapshot CI runs answers "Red" for that same request, so the initializer sees another locale there. Which locale reaches an initializer is the platform's decision; a module can only localize with what it is handed. The test now asks for French on both and keeps asserting the French label, so it verifies the initializer rather than a given Jahia's routing. The divergence is written down next to it and reported on the PR — it deserves an answer, but not from this suite. Verified against Jahia 8.2: 5/5.
There was a problem hiding this comment.
Not a big fan of releasing something already legacy on day one
Maybe we should start by designing "Jahia as a framework" and offer entrypoints for both Java and JS
There was a problem hiding this comment.
This implementation seems a bit underwhelming, there's not much we can do in there:
- no jcr queries
- no http requests
What goals do we have with choicelist initializers?
There was a problem hiding this comment.
Cool design, we still miss something like a server context
Not sure what to build without async/await, plus we need permissions
There was a problem hiding this comment.
I absolutely despise the design, I get why it exists but it means content modeling can spread across three file types:
- CND can contain a validation regex (we can do the example in CND)
- JSON overrides can also contain validation stuff
- and now this
This is the wrong solution to the right problem: CND validation sucks
The Invoker executed callables without settling, so an async callable's promise fell through convert() as an empty map — silently discarding the result (fail-open for validation consumers). Settle through JSPromise.settleOrThrow like every other bridge, surface rejections as RuntimeException per the Invoker contract, and cover the SDK with unit tests.
GraalJS only drains the microtask queue when the last JS frame leaves the stack, so a callback invoked at a nested host boundary (a render filter reached through <Render> inside a view, a validator triggered by a JS session.save()) cannot settle even trivial async results. Document the limitation on JSPromise and in the render-filter/node-validator JSDoc, point at it from the settleOrThrow error message, and pin the behavior with a test so a GraalJS upgrade that lifts it is noticed.
The transforms matched a hardcoded .action.{ts,js} regex, so a custom
actions.inputGlob added files to the server bundle without registering
them, and leaked real server implementations into client bundles instead
of fetch stubs. Build one createFilter predicate from the configured glob
(resolved against inputDir, as insert-filename already does) and share it
between the bundle input, the server registration and the client stubs.
…names - globals.d.ts pointed at registerContentPatch, which lives on the content-patches branch (#697), not here - package-info cited the contentpatches subpackage for the same reason - the engine pom comment referenced ActionRegistrarTest, renamed to NodeLegacyActionRegistrarTest - the CSRF guard config referenced docs/2-guides/4-actions, renamed to 4-legacy-node-actions
…lers Actions are guest-callable: an accidental TypeError or JCR exception message can reveal implementation internals or node paths. Only deliberate error types (the new public ActionError, ActionValidationError, or schema issues) carry their message to the caller; everything else is logged server-side and replaced by a generic message. Also warn instead of silently skipping non-function exports of action files, whose client stubs would fail if called.
- detect errors by presence ('error' in payload): an empty error message was
treated as success and crashed in devalue parse
- import devalue through an absolute specifier resolved from the plugin (it
is not a dependency of the user's module — bare import breaks under pnpm
isolated layouts and PnP)
- document the actions option in the README options example and mention the
new server-bundle action input in the changelog entry, which also claimed
a rename of an API that never shipped
- refuse registering a legacy node action named 'jsAction': shadowing the engine's generic endpoint would break every module's client action stubs and silently inherit its platform-wide CSRF-guard whitelist - when a JS choicelist initializer entry vanished (redeploy race), pass the accumulated values through instead of wiping the other initializers' choices in a chained declaration
- the Hydrogen contact form answered a plain browser POST with a bare json result: the visitor landed on a blank page; redirect back to the page with a status query parameter instead (no statusCode: the platform picks the redirect status) - correct the CND parameter syntax in the choicelist initializer JSDoc (choicelist[myKey='myParam'], not choicelist[myKey,'myParam']) - point the hydrogen CSRF config comment at the renamed guide path
# Conflicts: # jahia-test-module/settings/configurations/org.jahia.modules.jahiacsrfguard-jsmtest.cfg
…lizers The e2e work on the integration-suite branch showed which locale the platform forwards (content vs UI language) varies across Jahia versions — 8.2.1.0 forwards the content locale, the snapshot CI answers with the UI one. Document the locale as 'the language to localize labels for' instead of promising a routing the module cannot control.
- the statusCode+redirect trap (a 3xx statusCode turns the redirect into sendError) and the browser-form blank-page pitfall were documented only in a test-fixture comment - the CSRF guard also challenges authenticated GET .do requests, not only POST/PUT/DELETE - the actionTest comment named a 'returnContentType' parameter; Jahia's is jcrReturnContentType - align the choicelist spec JSDoc with the locale-pinning comment below it
…nto the feature branch (PR #713)
The fixture's deliberate failure now throws ActionError (whose message is the one that travels to callers), and new cases pin the masking of unexpected messages, the non-settling report, and the missing-name rejection — all as 200-envelope assertions, this branch's contract; the follow-up envelope PR asserts the meaningful statuses.
Scope
Three features, delivered on this branch as stacked work (recommended merge: squash, see "Review notes"); content patches were split out into #697:
registerChoiceListInitializer), server-side node validators (registerNodeValidator), legacy node actions (registerNodeLegacyAction, theorg.jahia.bin.Actionbridge) and typed render filters (registerRenderFilter), over a sharedAbstractServiceRegistrarmechanism..action.tsfiles compiled twice (server implementation + client fetch stubs), devalue wire format, optional Standard Schema validation (action(schema, fn)), singlejsActiondispatch endpoint.How to review (suggested order)
docs/adr/0001–0008): every structural decision, alternative and trade-off is recorded there — the code is meant to read as their implementation.registrars/AbstractServiceRegistrar.java+RenderFilterRegistrar.java(the refactored template). Key invariant: bridges re-resolve JS functions from the pooled-context registry on every call.registrars/validation/): the one deliberate deviation — single Bean Validation bean on sentinelnt:base(ADR-0005 explains why per-nodeType registration is incorrect).vite-plugin/src/actions.ts,framework/actions/,actions/GenericActionEndpoint.java+JSPromise.java): the dual compilation and the microtask-settling contract (ADR-0008).Verification
.action.tsfixturejavax.validation/org.jsonfrom the platform (not embedded)Review notes / known caveats
3b6c507is mislabeled: it carried an early snapshot of the content-patches work (since split out to feat: content patches — run-once content transformations in JavaScript #697) swept in from a shared working tree, under a one-line-fix title. Squash-merging makes this moot; review file-by-file, not commit-by-commit.render-filterregistry entries'prioritywas previously ignored (forced to 0) and is now honored..action.ts).How to review
Content patches have been split out into #697, so this PR is now the server extension points + actions + SDK (~2,300 LOC smaller).
Review by the Files-changed tab (squash-merge is intended — the intermediate commits are throwaway). Suggested attention budget:
javascript-modules-engine-java/.../registrars/**and.../actions/**; the TS wrappers injavascript-modules-library/src/framework/**;vite-plugin/src/actions.ts. Read the ADRs (docs/adr/) first — the code is their implementation.jahia-test-module/**fixtures,samples/hydrogen/**, the Cypress specs intests/**, the guides indocs/2-guides/**.javascript-modules-engine-java/target/**, thevite-plugin/fixtures/expected/**snapshots, and the pure-formatting commit5008239(reviewable with GitHub's Hide whitespace).Once you've reviewed the shared mechanism + one vertical end-to-end, the other verticals are variations on the same bridge pattern.
Tracking
Part of EPIC #554.
Closes #588, closes #691, closes #692, closes #693, closes #694, closes #696, closes #688, closes #689.
Content patches were split into their own PR #697 (closes #695).
Follow-up (stays open): #690 — actions endpoint v2 (dedicated servlet transport).