diff --git a/.github/renovate.json b/.github/renovate.json
index ed525891b..09c2a5983 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -12,7 +12,15 @@
"packageRules": [
{
"matchPackagePatterns": ["actions.*"],
- "dependencyDashboardApproval": true
+ "dependencyDashboardApproval": true,
+ "matchUpdateTypes": ["patch"],
+ "matchCurrentVersion": "!/^0/",
+ "automerge": true
+ },
+ {
+ "matchUpdateTypes": ["patch"],
+ "matchCurrentVersion": "!/^0/",
+ "automerge": true
}
]
}
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index cc034b1e3..26ae97640 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -45,14 +45,14 @@ jobs:
fetch-depth: 0
- name: "🔧 Setup GraalVM CE"
- uses: graalvm/setup-graalvm@v1.2.1
+ uses: graalvm/setup-graalvm@v1.2.5
with:
distribution: 'graalvm'
java-version: ${{ matrix.java }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: "🔧 Setup Gradle"
- uses: gradle/gradle-build-action@v3.3.0
+ uses: gradle/gradle-build-action@v3.5.0
- name: "âť“ Optional setup step"
run: |
@@ -70,7 +70,7 @@ jobs:
- name: "đź“Š Publish Test Report"
if: always()
- uses: mikepenz/action-junit-report@v4
+ uses: mikepenz/action-junit-report@v5
with:
check_name: Java CI / Test Report (${{ matrix.java }})
report_paths: '**/build/test-results/test/TEST-*.xml'
@@ -78,7 +78,7 @@ jobs:
- name: "đź“ś Upload binary compatibility check results"
if: matrix.java == '17'
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+ uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: binary-compatibility-reports
path: "**/build/reports/binary-compatibility-*.html"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 065ee5620..2dda05848 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -66,13 +66,13 @@ jobs:
# Store the hash in a file, which is uploaded as a workflow artifact.
sha256sum $ARTIFACTS | base64 -w0 > artifacts-sha256
- name: Upload build artifacts
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+ uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: gradle-build-outputs
path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/*
retention-days: 5
- name: Upload artifacts-sha256
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+ uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: artifacts-sha256
path: artifacts-sha256
@@ -115,7 +115,7 @@ jobs:
artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }}
steps:
- name: Download artifacts-sha256
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: artifacts-sha256
# The SLSA provenance generator expects the hash digest of artifacts to be passed as a job
@@ -134,7 +134,7 @@ jobs:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: "${{ needs.provenance-subject.outputs.artifacts-sha256 }}"
upload-assets: true # Upload to a new release.
@@ -146,11 +146,9 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout repository
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download artifacts
- # Important: update actions/download-artifact to v4 only when generator_generic_slsa3.yml is also compatible.
- # See https://github.com/slsa-framework/slsa-github-generator/issues/3068
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: gradle-build-outputs
path: build/repo
@@ -162,6 +160,6 @@ jobs:
- name: Upload assets
# Upload the artifacts to the existing release. Note that the SLSA provenance will
# attest to each artifact file and not the aggregated ZIP file.
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
+ uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
with:
files: artifacts.zip
diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.views-fieldset-tck.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.views-fieldset-tck.gradle
index 1c94d0349..1cd3f3657 100644
--- a/buildSrc/src/main/groovy/io.micronaut.build.internal.views-fieldset-tck.gradle
+++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.views-fieldset-tck.gradle
@@ -13,9 +13,9 @@ dependencies {
testImplementation(projects.micronautViewsFieldset)
testImplementation(projects.micronautViewsFieldsetTck)
- testImplementation(libs.junit.jupiter.api)
+ testImplementation(mnTest.junit.jupiter.api)
testImplementation(mnTest.micronaut.test.junit5)
- testImplementation(libs.junit.jupiter.engine)
+ testImplementation(mnTest.junit.jupiter.engine)
testImplementation(libs.junit.platform.engine)
}
test {
diff --git a/gradle.properties b/gradle.properties
index 06595a4b3..f7beb87c4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-projectVersion=5.3.0-SNAPSHOT
+projectVersion=5.6.0-SNAPSHOT
projectGroup=io.micronaut.views
title=Micronaut Views
@@ -18,6 +18,8 @@ testsviewsRocker=views-rocker/src/test
testsviewsPebble=views-pebble/src/test
testsviewsJte=views-jte/src/test
testsviewsJstachio=views-jstachio/src/test
+srcjsReact=views-react/src/test/js
+srcjsReactRender=views-react/src/main/resources/io/micronaut/views/react
org.gradle.caching=true
org.gradle.jvmargs=-Xmx1g
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index aa4f88f89..9f09738d5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,29 +1,31 @@
[versions]
-micronaut = "4.4.4"
-micronaut-platform = "4.3.1"
+micronaut = "4.7.2"
+micronaut-platform = "4.6.3"
micronaut-docs = '2.0.0'
-micronaut-test = "4.0.1"
-micronaut-data = "4.7.0"
-micronaut-sql = "5.5.2"
-micronaut-security = "4.6.9"
-micronaut-serde = "2.8.2"
-micronaut-validation = "4.4.4"
-micronaut-gradle-plugin = "4.3.6"
-managed-freemarker = "2.3.32"
+micronaut-test = "4.5.0"
+micronaut-data = "4.9.6"
+micronaut-sql = "5.8.2"
+micronaut-security = "4.11.0"
+micronaut-serde = "2.12.0"
+micronaut-validation = "4.8.0"
+micronaut-gradle-plugin = "4.4.4"
+managed-freemarker = "2.3.33"
managed-handlebars = "4.3.1"
-managed-jstachio = "1.3.5"
-managed-jte = "3.1.10"
-managed-rocker = "1.4.0"
+managed-jstachio = "1.3.6"
+managed-jte = "3.1.13"
+managed-rocker = "2.0.1"
managed-soy = "2023-09-13"
+org-json = "20240303"
managed-thymeleaf = "3.1.2.RELEASE"
-managed-velocity = "2.3"
+managed-velocity = "2.4.1"
+graal = "24.1.1"
pebble = "3.2.2"
thymeleaf-extra-java8time = "3.0.4.RELEASE"
-kotlin = "1.9.23"
-kotlinx-coroutines = "1.8.0"
+kotlin = "1.9.25"
+kotlinx-coroutines = "1.9.0"
-micronaut-logging = "1.2.3"
+micronaut-logging = "1.5.0"
[libraries]
# Core
@@ -42,14 +44,14 @@ managed-handlebars = { module = "com.github.jknack:handlebars", version.ref = "m
managed-jstachio = { module = "io.jstach:jstachio", version.ref = "managed-jstachio" }
managed-jstachio-apt = { module = "io.jstach:jstachio-apt", version.ref = "managed-jstachio" }
managed-jte = { module = "gg.jte:jte", version.ref = "managed-jte" }
+managed-jte-native-resources = { module = "gg.jte:jte-native-resources", version.ref = "managed-jte" }
managed-jte-kotlin = { module = "gg.jte:jte-kotlin", version.ref = "managed-jte" }
managed-rocker-runtime = { module = "com.fizzed:rocker-runtime", version.ref = "managed-rocker" }
managed-soy = { module = "com.google.template:soy", version.ref = "managed-soy" }
+org-json = { module = "org.json:json", version.ref = "org-json" }
managed-thymeleaf = { module = "org.thymeleaf:thymeleaf", version.ref = "managed-thymeleaf" }
managed-velocity-engine-core = { module = "org.apache.velocity:velocity-engine-core", version.ref = "managed-velocity" }
-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" }
-junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" }
junit-platform-engine = { module = "org.junit.platform:junit-platform-suite-engine" }
pebble = { module = "io.pebbletemplates:pebble", version.ref = "pebble" }
thymeleaf-extras-java8time = { module = "org.thymeleaf.extras:thymeleaf-extras-java8time", version.ref = "thymeleaf-extra-java8time" }
@@ -57,7 +59,13 @@ kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", versi
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
groovy-json = { module = "org.apache.groovy:groovy-json" }
+graal-polyglot = { module = "org.graalvm.polyglot:polyglot", version.ref = "graal" }
+graal-js = { module = "org.graalvm.polyglot:js", version.ref = "graal" }
+
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
jte = { id = "gg.jte.gradle", version.ref = "managed-jte" }
+buildtools-native = { id = "org.graalvm.buildtools.native"}
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e6441136f..a4b76b953 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23a4..df97d72b8 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 1aa94a426..f5feea6d6 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
diff --git a/gradlew.bat b/gradlew.bat
index 25da30dbd..9d21a2183 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
diff --git a/settings.gradle b/settings.gradle
index c5f6ed3af..781c28015 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,7 +6,7 @@ pluginManagement {
}
plugins {
- id("io.micronaut.build.shared.settings") version "6.7.0"
+ id("io.micronaut.build.shared.settings") version "7.2.3"
}
enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS'
@@ -23,16 +23,21 @@ include 'views-handlebars'
include 'views-thymeleaf'
include 'views-htmx'
include 'views-velocity'
+include 'views-react'
include 'views-rocker'
include 'views-pebble'
include 'views-jte'
include 'views-jstachio'
include "test-suite"
+include "test-suite-csrf"
include "test-suite-http"
include "test-suite-groovy"
include "test-suite-kotlin"
include "test-suite-thymeleaf-fieldset"
+if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) {
+include "test-suite-jte-fieldset"
+}
include "test-suite-freemarker-fieldset"
include "test-suite-graal:test-suite-graal-common"
include "test-suite-graal:test-suite-graal-freemarker"
diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml
index 278ff5eea..c77a9f644 100644
--- a/src/main/docs/guide/toc.yml
+++ b/src/main/docs/guide/toc.yml
@@ -22,6 +22,15 @@ views:
title: JStachio
jstachioInstallation: JStachio Installation
jstachioExample: JStachio Example
+ react:
+ title: React SSR
+ reactpreparingjs: Preparing your Javascript
+ reactsettingproperties: Setting serving properties
+ preact: Integrating with Preact
+ reactrenderscripts:
+ title: Render scripts
+ reactheadmanagers: Using head managers
+ reacttodo: Known limitations
model:
title: Working with Models
custom: Dynamically Enriching Models
@@ -30,6 +39,7 @@ views:
fieldsetExample: Form Generation Example
fieldsetAnnotations: Fieldset Annotations
fieldsetFetcher: Radio, Checkbox and Option Fetcher
+ csrfHidden: CSRF Token Hidden Field
customFormElement: Custom Form Elements
htmx:
title: HTMX
diff --git a/src/main/docs/guide/views/fieldset/csrfHidden.adoc b/src/main/docs/guide/views/fieldset/csrfHidden.adoc
new file mode 100644
index 000000000..e0d8df61a
--- /dev/null
+++ b/src/main/docs/guide/views/fieldset/csrfHidden.adoc
@@ -0,0 +1 @@
+If you use the https://micronaut-projects.github.io/micronaut-security/latest/guide/#csrf[Micronaut Security CSRF module], and a CSRF token is resolved, the generated form automatically contains a hidden input with the CSRF token as the value.
\ No newline at end of file
diff --git a/src/main/docs/guide/views/htmx/outOfBandSwaps.adoc b/src/main/docs/guide/views/htmx/outOfBandSwaps.adoc
index 1e2a12843..30775c3ff 100644
--- a/src/main/docs/guide/views/htmx/outOfBandSwaps.adoc
+++ b/src/main/docs/guide/views/htmx/outOfBandSwaps.adoc
@@ -1,3 +1,3 @@
-You can return an API:views.htmx.HtmxResponse[] in a controller method to render multiple views in a single HTMX response—for example, to do https://htmx.org/docs/#oob_swaps[Out Of Band Swaps].
+You can return an api:views.htmx.http.HtmxResponse[] in a controller method to render multiple views in a single HTMX response—for example, to do https://htmx.org/docs/#oob_swaps[Out Of Band Swaps].
-snippet::io.micronaut.views.docs.htmx.HtmxTest[tags="outOfBandSwaps",indent=0]
\ No newline at end of file
+snippet::io.micronaut.views.docs.htmx.HtmxTest[tags="outOfBandSwaps",indent=0]
diff --git a/src/main/docs/guide/views/security/csp.adoc b/src/main/docs/guide/views/security/csp.adoc
index 6b92ee019..029f0eb4f 100644
--- a/src/main/docs/guide/views/security/csp.adoc
+++ b/src/main/docs/guide/views/security/csp.adoc
@@ -42,6 +42,14 @@ That's it! After applying the above configuration, HTTP responses might include
Content-Security-Policy: default-src https: self:; script-src 'nonce-4ze2IRazk4Yu/j5K6SEzjA';
```
+The nonce value can be accessed on the server as a request attribute named `cspNonce`. This is the value to use
+in the `nonce` attribute on `script` and related tags. For example (adapt as appropriate for your template language):
+
+[source,html]
+----
+
+----
+
Inline scripts which aren't otherwise whitelisted will be declined for execution, unless CSP is operating in report-only
mode. Inline scripts can be whitelisted with the syntax:
diff --git a/src/main/docs/guide/views/security/security-model-enhancement.adoc b/src/main/docs/guide/views/security/security-model-enhancement.adoc
index 99693d276..afcd1b646 100644
--- a/src/main/docs/guide/views/security/security-model-enhancement.adoc
+++ b/src/main/docs/guide/views/security/security-model-enhancement.adoc
@@ -25,3 +25,14 @@ include::{testsSuite}/resources/views/securitydecorator.vm[tag=html]
----
You can access information about the current user with the `security` map.
+
+=== CSRF Token View Model Processor
+
+If you use the https://micronaut-projects.github.io/micronaut-security/latest/guide/#csrf[Micronaut Security CSRF module], there is also a view model processor for a model of type `Map`.
+If a CSRF Token can be resolved, `CSRFViewModelProcessor` adds it to the model.
+
+The following properties allow you to customize the injection:
+
+include::{includedir}configurationProperties/io.micronaut.views.model.security.CsrfViewModelProcessorConfigurationProperties.adoc[]
+
+
diff --git a/src/main/docs/guide/views/templates/jstachio/jstachioInstallation.adoc b/src/main/docs/guide/views/templates/jstachio/jstachioInstallation.adoc
index c66888d8c..3314d1fba 100644
--- a/src/main/docs/guide/views/templates/jstachio/jstachioInstallation.adoc
+++ b/src/main/docs/guide/views/templates/jstachio/jstachioInstallation.adoc
@@ -6,4 +6,6 @@ Additionally, you need to add JStachio's Java annotation processor.
dependency:jstachio-apt[groupId="io.jstach",scope="annotationProcessor"]
+NOTE: For Kotlin, add the `jstachio-apt` dependency in https://docs.micronaut.io/4.4.3/guide/#kaptOrKsp[kapt or ksp scope], and for Groovy add `jstachio-apt` in compileOnly scope.
+
Read the https://jstach.io/doc/jstachio/1.2.1/apidocs/#installation[Jstachio's user guide] to learn more.
diff --git a/src/main/docs/guide/views/templates/jte.adoc b/src/main/docs/guide/views/templates/jte.adoc
index 909202333..2f24e4b5d 100644
--- a/src/main/docs/guide/views/templates/jte.adoc
+++ b/src/main/docs/guide/views/templates/jte.adoc
@@ -5,6 +5,10 @@ Add the `micronaut-views-jte` dependency to your classpath.
dependency:micronaut-views-jte[groupId="io.micronaut.views"]
+If you want to write your views in Kotlin, you can include an additional dependency:
+
+dependency:jte-kotlin[groupId="gg.jte"]
+
The example shown in the <> section, could be rendered with the following Jte template:
[source,html]
diff --git a/src/main/docs/guide/views/templates/react.adoc b/src/main/docs/guide/views/templates/react.adoc
new file mode 100644
index 000000000..430648285
--- /dev/null
+++ b/src/main/docs/guide/views/templates/react.adoc
@@ -0,0 +1,70 @@
+React server-side rendering (SSR) allows you to pre-render React components to HTML before the page is sent to the user.
+This improves performance by ensuring the page appears before any Javascript has loaded (albeit in a non-responsive
+state) and makes it easier for search engines to index your pages.
+
+NOTE: This module is experimental and subject to change.
+
+Micronaut's support for React SSR has the following useful features:
+
+* Javascript runs using https://www.graalvm.org/[GraalJS], a high performance Javascript engine native to the JVM. Make sure to run your app on GraalVM or by compiling it to a native image to get full Javascript performance.
+* Compatible out of the box with both React and https://www.preactjs.com/[Preact], an alternative lighter weight implementation of the React concept.
+* Customize the Javascript used to invoke SSR to add features like head managers, or use the prepackaged default scripts to get going straight away.
+* The Javascript can be sandboxed, ensuring that your server environment is protected from possible supply chain attacks.
+* You can pass any `@Introspectable` Java objects to use as _props_ for your page components. This is convenient for passing in things like the user profile info.
+* Logging from Javascript is sent to the Micronaut logs. `console.log` and related will go to the `INFO` level of the logger named `js`, `console.error` and Javascript exceptions will go to the `ERROR` level of the same.
+
+To use React SSR you need to add two dependencies.
+
+1. Add the `micronaut-views-react` dependency.
+2. Add a dependency on `org.graalvm.polyglot:js` or `org.graalvm.polyglot:js-community`. The difference is to do with licensing and performance, with the `js` version being faster and free to use but not open source. https://www.graalvm.org/latest/docs/introduction/#licensing-and-support[Learn more about choosing an edition.]
+
+dependency:micronaut-views-react[groupId="io.micronaut.views"]
+
+== Scaffold a Micronaut frontend/backend project
+
+You can easily create a new ReactJS-based project from scratch using https://micronaut.io/launch[Micronaut Launch], the built-in IntelliJ Micronaut wizard, or the https://micronaut.io/download/[`mn` CLI tool]:
+
+[source,shell]
+----
+$ mn create-app --features=views-react my-cool-project
+----
+
+This will create a complete project with `webpack` based bundling for client and server side rendering, a pre-configured Micronaut Views React app, and a sample component/page handler. Javascript package installation and bundling is handled by Gradle or Maven, depending on the value of the `--build` flag you pass to `mn` (we recommend Gradle as it will be much faster). The build scripts will even download NodeJS for you, so you don't need anything other than a JDK to get started.
+
+== Configuration properties
+
+The properties used can be customized by overriding the values of:
+
+include::{includedir}configurationProperties/io.micronaut.views.react.ReactViewsRendererConfiguration.adoc[]
+
+== How it fits together
+
+Props can be supplied in the form of an introspectable bean or a `Map`. Both forms will be serialized to JSON and sent to the client for hydration, as well as used to render the root component. The URL of the current page will be taken from the request and added to the props under the `url` key, which is useful when working with libraries like https://github.com/preactjs/preact-router[`preact-router`]. If you use `Map` as your model type and use Micronaut Security, authenticated usernames and other security info will be added to your props automatically.
+
+On the server side prop objects are exposed to Javascript directly, without being serialized to JSON first. Micronaut's compile time reflection is used and this avoids some overhead as well as simplifying access to your props (see below). If your props bean returns a non-introspectable object from a property, it will be mapped in the normal way for GraalJS (meaning it will use runtime reflection and may require `@HostAccess.Export` on methods you wish to call).
+
+By default, you will need React components that return the entire page, including the `` tag. You'll also need to prepare your Javascript (see below). Then just name your required page component in the `@View` annotation on a controller, for example `@View("App")` will render the `` component with your page props.
+
+If your page components don't render the whole page or you need better control over how the framework is invoked you can use _render scripts_ (see below).
+
+== Accessing Java from Javascript
+
+The https://www.graalvm.org/latest/reference-manual/embed-languages/#access-java-from-guest-languages[usual GraalJS rules for accessing Java apply] with a few differences:
+
+1. Your root prop object and any introspectable object reachable from it can be accessed using normal Javascript property syntax, for instance if you have an `@Introspectable` bean with a `String getFoo()` method then you can just access that property by writing `props.foo` instead of `props.getFoo()`, as would normally be required when accessing Java objects.
+2. Methods annotated with `@Executable` can be invoked from Javascript. Arguments and return values are mapped to/from Java in a https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#target-type-mapping-heading[natural manner].
+3. Your code can use `Java.type("com.foo.bar.BazClass")` style calls to get access to Java classes and then instantiate them or call static methods on them.
+
+Note that props are read only. Attempting to set the value of a Java property on a props object will fail.
+
+== Sandbox
+
+By default, Javascript executing server side runs with the same privilege level as the server itself. This is similar to the Node security model but exposes you to supply chain attacks. If a third party React component you depend on turns out to be malicious or simply buggy, it could allow an attacker to run code server side instead of only inside the browser sandbox.
+
+Normally with React SSR you can't do much about this, but with Micronaut Views React you can enable a server-side sandbox if you use GraalVM 24.1 or higher. This prevents Javascript from accessing any Java host objects that haven't been specifically exposed into the sandbox. To use this set `micronaut.views.react.sandbox` to true in your `application.properties`.
+
+In this mode:
+
+- The `Java` top level object that lets code access any class will be gone.
+- Methods in `@Introspectable` objects reachable from your prop objects that are marked as `@Executable` will be exposed into the sandbox _regardless_ of sandbox settings. So be careful what methods you add to your props.
+- Any objects exposed via your root props that are *not* marked `@Introspectable` will be exposed via runtime reflection instead. In that case what's available inside the sandbox will depend on the https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/HostAccess.html[`HostAccess`] policy, which can be customized by using the factory replacement mechanism (see the docs for Micronaut Core for details). By default anything not annotated with `@HostAccess.Exposed` will be invisible and uninvokable. Normally this is sufficient, but customizing the `HostAccess` can be useful if you want to expose third party code you don't control into the sandbox.
diff --git a/src/main/docs/guide/views/templates/react/preact.adoc b/src/main/docs/guide/views/templates/react/preact.adoc
new file mode 100644
index 000000000..eb9fb346c
--- /dev/null
+++ b/src/main/docs/guide/views/templates/react/preact.adoc
@@ -0,0 +1,38 @@
+The https://www.preactjs.com/[Preact] library is a smaller and lighter weight implementation of React, with a few nice enhancements as well. Like React it also supports server side rendering and can be used with Micronaut React SSR. It requires some small changes to how you prepare your Javascript. Please read and understand how to prepare your JS for regular React first, as this section only covers the differences.
+
+Your `server.js` should look like this:
+
+[source,javascript]
+.src/main/js/server.js
+----
+include::{srcjsReact}/server.preact.js[]
+----
+
+Notice the differences: we're re-exporting the `h` symbol from Preact (which it uses instead of `React.createComponent`) and `renderToString` from the separate `preact-render-to-string` module. Otherwise the script is the same: we have to export each page component.
+
+Your `client.js` should look like this:
+
+[source,javascript]
+.src/main/js/client.js
+----
+include::{srcjsReact}/client.preact.js[]
+----
+
+Finally, you need to tell Micronaut Views React to use a different render script (see below). Set the `micronaut.views.react.render-script` application property to be `classpath:/io/micronaut/views/react/preact.js`.
+
+That's it. If you want to use existing React components then you will also need to set up aliases in your `webpack.{client,server}.js` files like this:
+
+[source,javascript]
+----
+module.exports = {
+ // ... existing values
+ resolve: {
+ alias: {
+ "react": "preact/compat",
+ "react-dom/test-utils": "preact/test-utils",
+ "react-dom": "preact/compat", // Must be below test-utils
+ "react/jsx-runtime": "preact/jsx-runtime"
+ },
+ }
+}
+----
diff --git a/src/main/docs/guide/views/templates/react/reactheadmanagers.adoc b/src/main/docs/guide/views/templates/react/reactheadmanagers.adoc
new file mode 100644
index 000000000..e17142f4a
--- /dev/null
+++ b/src/main/docs/guide/views/templates/react/reactheadmanagers.adoc
@@ -0,0 +1,40 @@
+Head managers are libraries that let you build up the contents of your `` block as your `` renders. One use of custom render scripts is to integrate a head manager with your code. Here's an example of a simple render script that usees the https://github.com/nfl/react-helmet[React Helmet] library in this way. Remember to export `Helmet` from your server-side bundle.
+
+[source,javascript]
+----
+export async function ssr(component, props, callback, config) {
+ // Create the vdom.
+ const element = React.createElement(component, props, null);
+ // Render the given component, expecting it to fill a in the tag.
+ const body = ReactDOMServer.renderToString(element)
+ // Get the data that should populate the from the Helmet library.
+ const helmet = Helmet.renderStatic();
+ // Data to be passed to the browser after the main HTML has finished loading.
+ const boot = {
+ rootProps: props,
+ rootComponent: component.name,
+ };
+
+ // Assemble the HTML.
+ const html = `
+
+
+
+ ${helmet.title.toString()}
+ ${helmet.meta.toString()}
+ ${helmet.link.toString()}
+
+
+
+ ${body}
+
+
+
+
+
+ `;
+
+ // Send it back.
+ callback.write(html);
+}
+----
diff --git a/src/main/docs/guide/views/templates/react/reactpreparingjs.adoc b/src/main/docs/guide/views/templates/react/reactpreparingjs.adoc
new file mode 100644
index 000000000..75f948b3e
--- /dev/null
+++ b/src/main/docs/guide/views/templates/react/reactpreparingjs.adoc
@@ -0,0 +1,53 @@
+An app that uses SSR needs the React components to be bundled twice, once for the client and once for the server. For the server you need to make a Javascript module bundle that imports and then re-exports the (page) components you will render, along with `React` and `ReactServerDOM`. The bundle must be compatible with GraalJS, which is not NodeJS and thus doesn't support the same set of APIs. You will also need to create a client-side bundle as per usual, and change how you start up React.
+
+TIP: This tutorial doesn't take you through how to create a ReactJS project from scratch - please refer to the React documentation for that.
+
+To start we will need a `server.js` file. It should be a part of your frontend project and can be named and placed wherever you like, as the server will only need the final compiled bundle. Your `server.js` should look like this:
+
+[source,javascript]
+.src/main/js/server.js
+----
+include::{srcjsReact}/server.js[]
+----
+
+Add your page components as imports, and then also to the export line. We will now set up Webpack to turn this file into a bundle.
+
+1. Run `npm i webpack node-polyfill-webpack-plugin text-encoding` to install some extra packages that are needed.
+2. Create a config file called e.g. `webpack.server.js` like the following:
+
+[source,javascript]
+.src/main/js/webpack.server.js
+----
+include::{srcjsReact}/webpack.server.js[]
+----
+
+This Webpack config does several things:
+
+* It polyfills APIs that lack a native implementation in the GraalJS engine.
+* It ensures the output is a native Javascript module.
+* It names the result `ssr-components.mjs`. You can use any name, but it's looked for under this name in the `views` resource directory by default. All components must be in one server side bundle.
+* It makes the `SERVER` variable be statically true when the Javascript is being bundled for server-side rendering. This allows you to include/exclude code blocks at bundle optimization time.
+
+You can use such a config by running `npx webpack --mode production --config webpack.server.js`. Add the `--watch` flag if you want the bundle to be recreated whenever an input file changes. Micronaut React SSR will notice if the bundle file has changed on disk and reload it (see <>).
+
+Now create `client.js`. This will contain the Javascript that runs once the page is loaded, and which will "hydrate" the React app (reconnect the event handlers to the pre-existing DOM). It should look like this:
+
+[source,javascript]
+.src/main/js/client.js
+----
+include::{srcjsReact}/client.js[]
+----
+
+Depending on how you configure minification, you may also need to import your page components here. This small snippet of code reads the `Micronaut` object which is generated by the Micronaut React SSR renderer just before your `client.js` code is loaded. It contains the component named in your `@View("MyPageComponent")` annotation, which is then loaded assuming it is in a Javascript module of the same name. The props that will be passed to that page component as generated from the object you return from your controller method. If you wish you can wrap `` here with any contexts you need.
+
+And now for the `webpack.client.js` config:
+
+[source,javascript]
+.src/main/js/webpack.client.js
+----
+include::{srcjsReact}/webpack.client.js[]
+----
+
+It tells Webpack to generate a series of JS files that are then placed in the `src/main/resources/static` directory.
+
+Run Webpack to generate the needed Javascript files for both client and server.
diff --git a/src/main/docs/guide/views/templates/react/reactrenderscripts.adoc b/src/main/docs/guide/views/templates/react/reactrenderscripts.adoc
new file mode 100644
index 000000000..0fa03f12a
--- /dev/null
+++ b/src/main/docs/guide/views/templates/react/reactrenderscripts.adoc
@@ -0,0 +1,40 @@
+The code that kicks off the SSR process using your React libraries API is called a render script. Micronaut Views React ships with two pre-packaged render scripts, one for ReactJS and one for Preact, but you are also able to supply your own. This lets you take complete control over the server-side Javascript. To use a custom script, place it somewhere on your classpath or file system and then set the `micronaut.views.react.render-script` property to its path, prefixed with either `classpath:` or `file:` depending on where it should be found.
+
+A render script should be an ESM module that exports a single function called `ssr` that takes four arguments:
+
+1. A function object for the page component to render.
+2. An object containing the root props.
+3. A callback object that contains APIs used to communicate with Micronaut.
+4. A string that receives the URL of the bundle that the browser should load. This is specified by the `micronaut.views.react.clientBundleURL` application property.
+
+The default render script looks like this:
+
+[source,javascript]
+.classpath:/io/micronaut/views/react/react.js
+----
+include::{srcjsReactRender}/react.js[]
+----
+
+The default render script for Preact looks like this:
+
+[source,javascript]
+.classpath:/io/micronaut/views/react/preact.js
+----
+include::{srcjsReactRender}/preact.js[]
+----
+
+A more sophisticated render script might support the use of head managers (see below), do multiple renders, expose other APIs and so on.
+
+A render script is evaluated _after_ your server side bundle, and has access to any symbols your server script exported. If you wish to access a JS module you should therefore include it in your `server.js` that gets fed to Webpack or similar bundler, and then re-export it like this:
+
+[source,javascript]
+----
+import * as mymod from 'mymod';
+export { mymod };
+----
+
+The callback object has a few different APIs you can use:
+
+1. `write(string)`: Writes the given string to the network response.
+2. `write(bytes)`: Writes the given array of bytes to the network response.
+3. `url()`: Returns either null or a string containing the URL of the page being served. Useful for sending to page routers.
diff --git a/src/main/docs/guide/views/templates/react/reactsettingproperties.adoc b/src/main/docs/guide/views/templates/react/reactsettingproperties.adoc
new file mode 100644
index 000000000..39676a2b1
--- /dev/null
+++ b/src/main/docs/guide/views/templates/react/reactsettingproperties.adoc
@@ -0,0 +1,64 @@
+React SSR needs some Micronaut application properties to be set.
+
+[configuration]
+----
+micronaut:
+ # Point to the server-side JS. This value is the default.
+ views:
+ react:
+ server-bundle-path: "classpath:views/ssr-components.mjs"
+
+ router:
+ static-resources:
+ js:
+ mapping: "/static/**"
+ paths: "classpath:static"
+
+ # A temporary workaround for a GraalJS limitation.
+ executors:
+ blocking:
+ virtual: false
+----
+
+This sets up static file serving so your client JS will be served by your Micronaut app. This isn't mandatory: you can serve your client JS from anywhere, but you would need to set `micronaut.views.react.client-bundle-url` in that case to where the client root bundle can be found.
+
+IMPORTANT: Watch out for the last property that disables virtual threads. If you skip this you will get an error the first time a view is rendered. Future releases of GraalJS will remove the need to disable virtual threads in Micronaut.
+
+[[react-dev-mode]]
+== Development
+
+During development you want the fastest iteration speed possible. Firstly turn off response caching so hot reload works with `npx webpack --watch`. Micronaut Views React will automatically notice the file changed on disk and reload it.
+
+[configuration]
+----
+micronaut:
+ # For development purposes only.
+ server:
+ netty:
+ responses:
+ file:
+ cache-seconds: 0
+----
+
+If using Maven turn off Micronaut's automatic restart features so that changes to the compiled bundle JS don't cause the whole server to reboot:
+
+[xml]
+----
+
+ io.micronaut.maven
+ micronaut-maven-plugin
+ ...
+
+
+
+ src/main/resources
+
+ **/*.js
+ **/*.mjs
+
+
+
+
+
+----
+
diff --git a/src/main/docs/guide/views/templates/react/reacttodo.adoc b/src/main/docs/guide/views/templates/react/reacttodo.adoc
new file mode 100644
index 000000000..c56399a92
--- /dev/null
+++ b/src/main/docs/guide/views/templates/react/reacttodo.adoc
@@ -0,0 +1,5 @@
+Micronaut React SSR has the following known issues and limitations:
+
+- There is no built-in support for server side fetching.
+- The rendering isn't streamed to the user.
+- `` is not supported.
diff --git a/test-suite-csrf/build.gradle.kts b/test-suite-csrf/build.gradle.kts
new file mode 100644
index 000000000..5e93129d7
--- /dev/null
+++ b/test-suite-csrf/build.gradle.kts
@@ -0,0 +1,21 @@
+plugins {
+ `java-library`
+ id("io.micronaut.build.internal.views-tests")
+}
+dependencies {
+ testAnnotationProcessor(mn.micronaut.inject.java)
+ testImplementation(mnTest.micronaut.test.junit5)
+ testRuntimeOnly(mnTest.junit.jupiter.engine)
+ testRuntimeOnly(mnLogging.logback.classic)
+ testImplementation(mn.micronaut.http.client)
+ testAnnotationProcessor(mnSerde.micronaut.serde.processor)
+ testImplementation(mnSerde.micronaut.serde.jackson)
+ testImplementation(mn.micronaut.http.server.netty)
+ testImplementation(projects.micronautViewsThymeleaf)
+ testImplementation(projects.micronautViewsFieldset)
+ testImplementation(mnSecurity.micronaut.security.csrf)
+ testImplementation(mnSecurity.micronaut.security.session)
+}
+tasks.withType {
+ useJUnitPlatform()
+}
diff --git a/test-suite-csrf/src/test/java/io/micronaut/views/tests/security/csrf/CsrfFormGenerationTest.java b/test-suite-csrf/src/test/java/io/micronaut/views/tests/security/csrf/CsrfFormGenerationTest.java
new file mode 100644
index 000000000..2aa4b6f0c
--- /dev/null
+++ b/test-suite-csrf/src/test/java/io/micronaut/views/tests/security/csrf/CsrfFormGenerationTest.java
@@ -0,0 +1,92 @@
+package io.micronaut.views.tests.security.csrf;
+
+import io.micronaut.context.annotation.Property;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.util.StringUtils;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.HttpStatus;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.Status;
+import io.micronaut.http.client.BlockingHttpClient;
+import io.micronaut.http.client.HttpClient;
+import io.micronaut.http.client.annotation.Client;
+import io.micronaut.security.annotation.Secured;
+import io.micronaut.security.csrf.repository.CsrfTokenRepository;
+import io.micronaut.security.rules.SecurityRule;
+import io.micronaut.serde.annotation.Serdeable;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import io.micronaut.views.fields.Form;
+import io.micronaut.views.fields.FormGenerator;
+import io.micronaut.views.fields.annotations.InputPassword;
+import io.micronaut.views.fields.elements.InputHiddenFormElement;
+import io.micronaut.views.fields.elements.InputPasswordFormElement;
+import io.micronaut.views.fields.elements.InputSubmitFormElement;
+import jakarta.inject.Singleton;
+import jakarta.validation.constraints.NotBlank;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@Property(name = "spec.name", value = "CsrfFormGeneration")
+@MicronautTest
+class CsrfFormGeneration {
+
+ @Test
+ void formIncludesAHiddenFieldForCsrfToken(FormGenerator formGenerator,
+ @Client("/") HttpClient httpClient,
+ MockController controller) {
+ BlockingHttpClient client = httpClient.toBlocking();
+ HttpResponse> response = assertDoesNotThrow(() -> client.exchange(HttpRequest.GET("/generate/form")));
+ assertEquals(HttpStatus.ACCEPTED, response.status());
+ Form form = controller.getForm();
+ assertNotNull(form);
+ assertEquals(2, form.fieldset().fields().stream().filter(f -> f instanceof InputPasswordFormElement).count());
+ assertEquals(1, form.fieldset().fields().stream().filter(f -> f instanceof InputSubmitFormElement).count());
+ assertEquals(1, form.fieldset().fields().stream().filter(f -> f instanceof InputHiddenFormElement).count());
+ assertEquals(4, form.fieldset().fields().size());
+ InputHiddenFormElement inputHiddenFormElement =(InputHiddenFormElement) form.fieldset().fields().stream().filter(f -> f instanceof InputHiddenFormElement).findFirst().get();
+ assertTrue(StringUtils.isNotEmpty(inputHiddenFormElement.value()));
+ }
+
+ @Serdeable
+ record ChangePasswordForm(@InputPassword @NotBlank String password,
+ @InputPassword @NotBlank String repeatPassword) {
+ }
+
+
+ @Requires(property = "spec.name", value = "CsrfFormGeneration")
+ @Singleton
+ static class CsrfRepositoryMock implements CsrfTokenRepository> {
+
+ @Override
+ public @NonNull Optional findCsrfToken(@NonNull HttpRequest> request) {
+ return Optional.of("abcde");
+ }
+ }
+
+ @Requires(property = "spec.name", value = "CsrfFormGeneration")
+ @Controller("/generate/form")
+ static class MockController {
+ private final FormGenerator formGenerator;
+ private Form form;
+ MockController(FormGenerator formGenerator) {
+ this.formGenerator = formGenerator;
+ }
+
+ @Secured(SecurityRule.IS_ANONYMOUS)
+ @Get
+ @Status(HttpStatus.ACCEPTED)
+ void index() {
+ this.form = formGenerator.generate("/password/change", ChangePasswordForm.class);
+ }
+
+ public Form getForm() {
+ return form;
+ }
+ }
+}
diff --git a/test-suite-csrf/src/test/java/io/micronaut/views/tests/security/csrf/CsrfSessionLogingHandlerTest.java b/test-suite-csrf/src/test/java/io/micronaut/views/tests/security/csrf/CsrfSessionLogingHandlerTest.java
new file mode 100644
index 000000000..207c12850
--- /dev/null
+++ b/test-suite-csrf/src/test/java/io/micronaut/views/tests/security/csrf/CsrfSessionLogingHandlerTest.java
@@ -0,0 +1,88 @@
+package io.micronaut.views.tests.security.csrf;
+
+import io.micronaut.context.annotation.Property;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.core.util.StringUtils;
+import io.micronaut.http.*;
+import io.micronaut.http.annotation.*;
+import io.micronaut.http.client.BlockingHttpClient;
+import io.micronaut.http.client.HttpClient;
+import io.micronaut.http.client.annotation.Client;
+import io.micronaut.http.client.exceptions.HttpClientResponseException;
+import io.micronaut.http.cookie.Cookie;
+import io.micronaut.security.annotation.Secured;
+import io.micronaut.security.authentication.AuthenticationRequest;
+import io.micronaut.security.authentication.AuthenticationResponse;
+import io.micronaut.security.authentication.provider.HttpRequestAuthenticationProvider;
+import io.micronaut.security.csrf.repository.CsrfTokenRepository;
+import io.micronaut.security.rules.SecurityRule;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import io.micronaut.views.View;
+import jakarta.inject.Singleton;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+@Property(name = "micronaut.security.authentication", value = "session")
+@Property(name = "micronaut.security.redirect.enabled", value = StringUtils.FALSE)
+@Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$")
+@Property(name = "micronaut.security.csrf.signature-key", value = "pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow")
+@Property(name = "spec.name", value = "CsrfSessionLogingHandlerTest")
+@MicronautTest
+class CsrfSessionLogingHandlerTest {
+
+ @Test
+ void loginSavesACsrfTokenInSession(@Client("/") HttpClient httpClient, CsrfTokenRepository> csrfTokenRepository) {
+ BlockingHttpClient client = httpClient.toBlocking();
+ HttpRequest> loginRequest = HttpRequest.POST("/login",Map.of("username", "sherlock", "password", "password"))
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
+
+ HttpResponse> loginRsp = assertDoesNotThrow(() -> client.exchange(loginRequest));
+ assertEquals(HttpStatus.OK, loginRsp.getStatus());
+ String cookie = loginRsp.getHeaders().get(HttpHeaders.SET_COOKIE);
+ assertNotNull(cookie);
+ assertTrue(cookie.contains("SESSION="));
+ assertTrue(cookie.contains("; HTTPOnly"));
+ String sessionId = cookie.split(";")[0].split("=")[1];
+ assertNotNull(sessionId);
+ HttpRequest> csrfEchoRequestWithSession = HttpRequest.GET("/csrf/echo")
+ .contentType(MediaType.TEXT_HTML_TYPE)
+ .cookie(Cookie.of("SESSION", sessionId));
+ String html = assertDoesNotThrow(() -> client.retrieve(csrfEchoRequestWithSession));
+ assertFalse(html.contains(""));
+
+ // request the page without session and no csrf token is present
+ HttpRequest> csrfEchoRequestWithoutSession = HttpRequest.GET("/csrf/echo")
+ .contentType(MediaType.TEXT_HTML_TYPE);
+ html = assertDoesNotThrow(() -> client.retrieve(csrfEchoRequestWithoutSession));
+ assertTrue(html.contains(""));
+ }
+
+ @Requires(property = "spec.name", value = "CsrfSessionLogingHandlerTest")
+ @Singleton
+ static class AuthenticationProviderUserPassword implements HttpRequestAuthenticationProvider {
+ @Override
+ public @NonNull AuthenticationResponse authenticate(@Nullable HttpRequest requestContext, @NonNull AuthenticationRequest authRequest) {
+ return AuthenticationResponse.success("sherlock");
+ }
+ }
+
+ @Requires(property = "spec.name", value = "CsrfSessionLogingHandlerTest")
+ @Controller("/csrf")
+ static class CsrfTokenEchoController {
+ @Secured(SecurityRule.IS_ANONYMOUS)
+ @Produces(MediaType.TEXT_HTML)
+ @Get("/echo")
+ @View("index")
+ Map index() {
+ return Collections.emptyMap();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test-suite-csrf/src/test/resources/logback.xml b/test-suite-csrf/src/test/resources/logback.xml
new file mode 100644
index 000000000..8eb8c3a81
--- /dev/null
+++ b/test-suite-csrf/src/test/resources/logback.xml
@@ -0,0 +1,10 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
diff --git a/test-suite-csrf/src/test/resources/views/index.html b/test-suite-csrf/src/test/resources/views/index.html
new file mode 100644
index 000000000..b075afbf4
--- /dev/null
+++ b/test-suite-csrf/src/test/resources/views/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test-suite-http/build.gradle b/test-suite-http/build.gradle
index 5abeffefb..224c0591a 100644
--- a/test-suite-http/build.gradle
+++ b/test-suite-http/build.gradle
@@ -6,9 +6,9 @@ plugins {
dependencies {
testAnnotationProcessor(mn.micronaut.inject.java)
- testImplementation(libs.junit.jupiter.api)
+ testImplementation(mnTest.junit.jupiter.api)
testImplementation(mnTest.micronaut.test.junit5)
- testRuntimeOnly(libs.junit.jupiter.engine)
+ testRuntimeOnly(mnTest.junit.jupiter.engine)
testRuntimeOnly(mnLogging.logback.classic)
diff --git a/test-suite-jte-fieldset/build.gradle.kts b/test-suite-jte-fieldset/build.gradle.kts
new file mode 100644
index 000000000..e6145819c
--- /dev/null
+++ b/test-suite-jte-fieldset/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ id("io.micronaut.build.internal.views-fieldset-tck")
+ alias(libs.plugins.jte)
+ alias(libs.plugins.buildtools.native)
+}
+
+dependencies {
+ annotationProcessor(platform(mn.micronaut.core.bom))
+ annotationProcessor(mn.micronaut.inject.java)
+ implementation(platform(mn.micronaut.core.bom))
+ implementation(projects.micronautViewsJte)
+ implementation(projects.micronautViewsFieldset)
+ testImplementation(projects.micronautViewsJte)
+ jteGenerate(libs.managed.jte.native.resources)
+}
+graalvmNative.toolchainDetection = true
+jte {
+ sourceDirectory = file("src/test/jte").toPath()
+ generate()
+ jteExtension("gg.jte.nativeimage.NativeResourcesExtension")
+}
+java.sourceCompatibility = JavaVersion.VERSION_21
+java.targetCompatibility = JavaVersion.VERSION_21
diff --git a/test-suite-jte-fieldset/src/test/java/io/micronaut/views/fields/jte/JteSuite.java b/test-suite-jte-fieldset/src/test/java/io/micronaut/views/fields/jte/JteSuite.java
new file mode 100644
index 000000000..0d269dc22
--- /dev/null
+++ b/test-suite-jte-fieldset/src/test/java/io/micronaut/views/fields/jte/JteSuite.java
@@ -0,0 +1,11 @@
+package io.micronaut.views.fields.jte;
+
+import org.junit.platform.suite.api.SelectPackages;
+import org.junit.platform.suite.api.Suite;
+import org.junit.platform.suite.api.SuiteDisplayName;
+
+@Suite
+@SelectPackages("io.micronaut.views.fields.tck")
+@SuiteDisplayName("Fieldset TCK for Jte")
+class JteSuite {
+}
diff --git a/test-suite-jte-fieldset/src/test/jte/fieldset/errors.jte b/test-suite-jte-fieldset/src/test/jte/fieldset/errors.jte
new file mode 100644
index 000000000..4f1caffce
--- /dev/null
+++ b/test-suite-jte-fieldset/src/test/jte/fieldset/errors.jte
@@ -0,0 +1,7 @@
+@import java.util.List
+@import io.micronaut.views.fields.messages.Message
+@param String name
+@param List errors
+@for(var error: errors)
+
${error.defaultMessage()}
+@endfor
diff --git a/test-suite-jte-fieldset/src/test/jte/fieldset/fieldset.jte b/test-suite-jte-fieldset/src/test/jte/fieldset/fieldset.jte
new file mode 100644
index 000000000..1ac92a4a0
--- /dev/null
+++ b/test-suite-jte-fieldset/src/test/jte/fieldset/fieldset.jte
@@ -0,0 +1,83 @@
+@import io.micronaut.views.fields.Fieldset
+@import io.micronaut.views.fields.elements.InputCheckboxFormElement
+@import io.micronaut.views.fields.elements.InputFormElement
+@import io.micronaut.views.fields.elements.InputHiddenFormElement
+@import io.micronaut.views.fields.elements.InputNumberFormElement
+@import io.micronaut.views.fields.elements.InputEmailFormElement
+@import io.micronaut.views.fields.elements.InputTelFormElement
+@import io.micronaut.views.fields.elements.InputTextFormElement
+@import io.micronaut.views.fields.elements.InputUrlFormElement
+@import io.micronaut.views.fields.elements.SelectFormElement
+@import io.micronaut.views.fields.elements.InputSubmitFormElement
+@import io.micronaut.views.fields.elements.InputPasswordFormElement
+@import io.micronaut.views.fields.elements.InputDateFormElement
+@import io.micronaut.views.fields.elements.InputDateTimeLocalFormElement
+@import io.micronaut.views.fields.elements.TextareaFormElement
+@import io.micronaut.views.fields.elements.TrixEditorFormElement
+@import io.micronaut.views.fields.elements.InputTimeFormElement
+@import io.micronaut.views.fields.elements.InputStringFormElement
+@import io.micronaut.views.fields.elements.InputFileFormElement
+
+@param Fieldset el
+@for(var field : el.fields())
+ @if(field instanceof InputHiddenFormElement inputHiddenFormElement)
+ @template.fieldset.inputhidden(inputHiddenFormElement)
+ @endif
+ @if(field instanceof InputCheckboxFormElement ||
+ field instanceof SelectFormElement ||
+ field instanceof TextareaFormElement ||
+ field instanceof TrixEditorFormElement ||
+ field instanceof InputTimeFormElement ||
+ field instanceof InputDateFormElement ||
+ field instanceof InputDateTimeLocalFormElement ||
+ field instanceof InputStringFormElement ||
+ field instanceof InputNumberFormElement)
+