Contribute this plugin's beans through its descriptor - #472
codeconsole wants to merge 20 commits into
Conversation
A Spring Boot application had to declare for itself what the Grails plugin declares for a Grails application: the filter that serves what the pipeline compiled, and - where it renders its views with GSP - the tag libraries that write an asset's URL into a page. Both are auto-configured now, so an application adds the dependency and writes no configuration. The tag libraries are contributed only where the plugin that carries them and a GSP tag library lookup are both there, and either bean an application declares itself is left alone. AssetPipelineService reads the manifest through the ResourceLoader it is given rather than through the servlet context, which is the same context and is there before a servlet container is.
The tag libraries moved into the Spring Boot module's auto-configuration, which made a Grails-free module depend on this one to compile. They belong here: this plugin carries them, and it is the thing that knows how to wire them - in a Grails application through its artefacts, and in an application that renders GSP without being one through the beans this contributes. The condition is the tag library lookup GSP registers for a standalone application; a Grails application has the plugin's own, so nothing here applies to it. Two things this closes: The settings an application writes reach the pipeline itself. The plugin descriptor copies grails.assets into AssetPipelineConfigHolder as the context is built, and that never runs for an application with no plugins, so a configured mapping was quietly ignored while every other setting was honoured. The tag libraries say what they are. They were found by the directory they sit in, which is a Grails convention and nothing an application without Grails can use; @taglib is the annotation the lookup reads, and it is what makes them discoverable as beans. The specification drives them the way such an application does - Spring beans, GSP's standalone lookup, no plugin manager and no artefact scanning - so a later change that assumes Grails fails here rather than in somebody's application.
The BOM was there to supply versions for Spock, AssertJ and the JUnit launcher, which left a module whose whole point is working without Grails unable to run its tests without it. The versions it was supplying are now named in gradle.properties, where every other module in this build gets its own: spock 2.4-groovy-5.0, assertj 3.27.7, junit-platform 6.0.3. Nothing about the tests changes. They cover this module's own auto-configuration - the filter that serves the assets, and an application that registers one itself keeping it - and they import nothing from Grails.
Naming assertj and the junit platform in gradle.properties pinned numbers that belong to Spring Boot, and would sit still while the boot version moved. Spring Boot publishes a bom that manages both, and a Spring Boot module is entitled to it. Spock is not Spring Boot's, and comes from spock-bom the way it does in every other module here. It resolves to what the Grails bom was handing over - assertj 3.27.7, junit-platform 6.0.3 - which is unsurprising, since that bom was relaying Spring Boot's numbers too.
Until now the readme told an application to add asset.pipeline.springboot to its component scan, so an application upgrading into an auto-configuration has a bean of its own for the same filter. Two definitions of one bean is a startup failure, not a warning, which makes the backoff worth asserting rather than assuming: one filter, and a context that started.
Configuring itself means an application that had this on its class path without wiring it - and there was nothing to do with it unwired - now finds a filter on /assets/* where before there was none. Declaring the bean was the only way to say no, which is a strange way to ask for nothing. assets.enabled=false is the way to say it, named after assets.mapping, which the Micronaut adapter already reads. Unset means enabled, so nothing changes for anyone who wants it. The readme said to component scan the package; it now says what to configure, which is nothing, and what to set to opt out.
assets.enabled=false was read by the auto-configuration, and the auto-configuration is not what defines the filter - AssetPipelineService is, and an application that still names this package in its component scan registers that class itself. Such an application set the property, kept the filter, and had the readme tell it in one paragraph that the switch worked and in the next that its component scan was harmless. The condition goes on the configuration that declares the bean, so both routes to it obey.
Deleting @ConditionalOnMissingBean and running it: it still passes. A configuration class the runner has already registered is dropped from the auto-configuration's @import by the parser, before any condition is consulted, so the single filter it asserts arrives whether or not the condition exists. The outcome is worth keeping - two definitions of one bean is a startup failure - but the wording claimed to cover something else, and the specification below is what covers it.
The settings were written to the pipeline only when it held none. The pipeline holds them on a static, which outlives an application context, so the second context in a JVM - a devtools reload, or the next cached context in a test run - kept the settings of the first. A developer who changed grails.assets.mapping and reloaded saw the old mapping until the JVM restarted, with nothing said. The application being built is the one that asked, so its settings win, and a replacement says so at debug.
A field of type ResourceLoader named resourceLoader is satisfied by an application's own bean of that name before it is satisfied by the context, and a plain DefaultResourceLoader resolves "assets/manifest.properties" against the class path where the context resolves it against the servlet context. An application with such a bean would have lost its manifest and served uncompiled assets.
Every test drives its auto-configuration by importing the class, so deleting the line that lists it in META-INF/spring left the whole feature gone and the suite green. Each module now reads that file and looks for its own class in it.
This module contributes the tag libraries and the settings they read. It does not register a resolver or load a manifest - asset-pipeline-spring-boot does - so a standalone application with this module alone renders development markup and non-digest urls with nothing to explain why. Neither the class nor the readme said the two go together.
c2db094 to
621ffc8
Compare
| @GrailsBeans | ||
| // An ordering hint, not a dependency: everything used below comes from grails-web-url-mappings, | ||
| // while the class named lives in grails-url-mappings, which this plugin declares compileOnly. | ||
| @AutoConfiguration(beforeName = 'org.grails.plugins.web.mapping.UrlMappingsAutoConfiguration') |
There was a problem hiding this comment.
This should reference the class since it's only used in a grails project.
There was a problem hiding this comment.
This PR generates an AutoConfiguration that works inside AND outside of Grails
The generated auto-configuration is deliberately read outside Grails — that's what makes the third module unnecessary. Its only class-level condition is @ConditionalOnWebApplication(SERVLET), so a plain Spring Boot servlet application processes it, and AssetPipelineTagLibAutoConfigurationSpec drives exactly that: a WebApplicationContextRunner with GSP's standalone lookup, no plugin manager and no artefact scanning. That is the case the tag-lib beans exist for, and the one #470 was written to serve.
In that application org.grails.plugins.web.mapping.UrlMappingsAutoConfiguration may genuinely be absent. It lives in org.apache.grails:grails-url-mappings, which this plugin declares compileOnly and references nowhere else. Everything the beans actually use — grails.web.mapping.LinkGenerator, org.grails.web.mapping.DefaultLinkGenerator — is in org.apache.grails.web:grails-web-url-mappings, a different artifact. So the class literal turns an ordering hint into a hard runtime class-load of a jar nothing here needs.
It isn't theoretical. I just switched the annotation back to before = [UrlMappingsAutoConfiguration] on this branch:
AssetPipelineAutoConfigurationSpec > the auto-configuration is read where grails-url-mappings is absent FAILED
Caused by: java.lang.TypeNotPresentException
Caused by: java.lang.ClassNotFoundException
which is the startup warning #469 was opened for:
WARN o.s.core.annotation.MergedAnnotation : Failed to introspect meta-annotation @AutoConfiguration
java.lang.TypeNotPresentException: Type org.grails.plugins.web.mapping.UrlMappingsAutoConfiguration not present
The ordering is unaffected either way — beforeName and before are merged by the same sorter, and it still applies wherever the class is present.
The two positions can't both hold, though, and that's worth settling: if this module really is Grails-only, then the tag-lib beans have no purpose and #470 should be dropped rather than folded in here. If it isn't — which is what this PR argues, and why there's no asset-pipeline-gsp module — then the annotation can't name a class the standalone case doesn't have. Happy to go either way, but they need to match.
There was a problem hiding this comment.
Resolving this: beforeName is right. Under the split proposed in the review, this auto-configuration is still read by a standalone application, for assetProcessorService and grailsLinkGenerator, which is what the apache/grails-core#16184 example relies on. GspAutoConfiguration orders against UrlMappingsAutoConfiguration by name for the same reason. It changes nothing for a Grails application.
There was a problem hiding this comment.
Agreed, and unchanged here.
| @@ -1 +1 @@ | |||
| asset.pipeline.grails.AssetPipelineAutoConfiguration No newline at end of file | |||
| asset.pipeline.AssetPipelineAutoConfiguration | |||
There was a problem hiding this comment.
needs a grails-core update. currently autogeneration only works for internal grails projects.
There was a problem hiding this comment.
@jdaugherty now that 16224 has been merged do you have any other feedback on this PR?
There was a problem hiding this comment.
Understood on apache/grails-core#16224. Keep the file for now. Why is the autoconfiguration using a different package now?
There was a problem hiding this comment.
A generated sibling lands in the plugin descriptor's package, and the descriptor is asset.pipeline.AssetPipelineGrailsPlugin, so the sibling is asset.pipeline.AssetPipelineAutoConfiguration rather than asset.pipeline.grails. as before.
apache/grails-core#16221 makes the qualified name settable through autoConfigurationName, so the original is restorable - but it merged on 31 August, after the M6 tag, so not while pinned to M6. Same constraint you applied to this file, and both are noted in the PR description as things a later milestone lets us undo.
The auto-configuration named the class it registers before, and a class literal in an annotation has to be loaded when Spring reads it. That class is carried by grails-url-mappings, which a Spring Boot application using this plugin's tag libraries without the rest of Grails does not have, so every startup logged a failed introspection and the ordering was dropped. Naming it as a string asks nothing of the class path, and the ordering still applies wherever the class is there.
The plugin descriptor is about to use the beans DSL and BeanRegistrar, neither of which exists before Grails 8. 8.0.0-M6 is not published: Maven Central carries milestones only as far as M5, and the Apache staging repository does not have it either. The 8.0.0 snapshot does carry both, from the repository this build already declares, so that is what it builds against until a milestone that has them is released.
Grails 8 compiles a `beans` closure on a plugin descriptor into a sibling auto-configuration, and takes bean registration that needs more than a closure through a BeanRegistrar. Moving to both puts every bean this plugin contributes in one place and retires doWithSpring(), which Grails deprecates in 8.0. The sibling, asset.pipeline.AssetPipelineAutoConfiguration, is a plain auto-configuration listed in AutoConfiguration.imports. Spring Boot reads it wherever it reads auto-configurations, including an application that renders GSP without being a Grails application and so runs no plugin lifecycle - which is what asset.pipeline.grails.AssetPipelineTagLibAutoConfiguration was written to serve. Both it and asset.pipeline.grails.AssetPipelineAutoConfiguration are replaced by the closure, so the tag libraries are wired for such an application without a module of their own to hold them. One generated class means one set of beans, so each carries the condition that says which application it belongs to: the tag libraries where GSP's standalone lookup is present, the link generator where it is not - a Grails application, which is also the only one with the url mappings holder DefaultLinkGenerator autowires. Two definitions describe more than a class and a set of qualifiers, which is all a @bean method and BeanRegistry.Spec can express: assetResourceLocator inherits its search locations from abstractGrailsResourceLocator, a classless abstract definition, and assetPipelineFilter holds the filter as a nested definition so that ahead-of-time processing can generate source for it. Both are contributed through a BeanDefinitionRegistryPostProcessor, which reaches the registry where they can be stated directly - the shape Grails itself uses for abstractGrailsResourceLocator. The declared grailsVersion moves to 8.0.0, which is what the DSL requires.
It is generated from the beans closure on the plugin descriptor rather than written here, and its package is the descriptor's, which reads at a glance like the class this commit series removes from asset.pipeline.grails.
A beans property on a plugin descriptor is compiled without the annotation, the way doWithSpring and watchedResources are - GlobalGrailsClassInjectorTransformation claims any closure whose top-level statements are all bean/field/method calls. Declaring @GrailsBeans as well said nothing the descriptor did not already say. Also carries into the closure what the tag library auto-configuration gained while this branch was away from it: the settings a reloaded application writes replace the ones a previous context left on the pipeline's static, rather than being ignored until the JVM restarts. And the specification that notices when the class stops being auto-configured now names the generated one, that being the class this series leaves listed.
2a5ea81 to
aae8126
Compare
jdaugherty
left a comment
There was a problem hiding this comment.
Same theme as on apache/grails-core#16184: Grails plugin code changed to serve a non-Grails consumer. Here it is the three tag-lib beans in the descriptor, the StandaloneTagLibraryLookup conditions on them and on grailsLinkGenerator, the @TagLib annotations, the README section and the test dependencies. A Grails application gets nothing from any of it, and the plugin's correctness in Grails now depends on an org.grails internal that only grails-gsp-spring-boot registers. Details inline.
The Grails 8 modernisation is welcome and can merge on its own: beanRegistrar() in place of doWithSpring(), the registrar for the two definitions the DSL cannot express, and a beans block holding what the hand-written auto-configuration holds today. Pin it to 8.0.0-M6, which carries both.
The standalone wiring goes where we asked for it on #16184: a separate library the gsp-spring-boot example includes in place of its hello/AssetPipelineConfiguration. That class is the blueprint. The library depends on this plugin and on asset-pipeline-spring-boot, carries the tag-lib beans and the settings bridge, and is conditioned on the standalone lookup, which is fine there because that module is standalone by definition. It needs the grailsUrlMappingsHolder and @Artefact lookup changes pushed on #16184, so it targets the first Grails milestone that carries them.
The asset-pipeline-spring-boot changes are Grails-free and look right. Please split them into their own PR so they can merge now.
| bean('grailsLinkGenerator', LinkGenerator) | ||
| .annotate(ConditionalOnMissingBean, type: 'org.grails.web.pages.StandaloneTagLibraryLookup') |
There was a problem hiding this comment.
Please remove this condition. It gates a Grails bean on the absence of a GSP-standalone internal, and the reason given for it, that only a Grails application has a URL mappings holder, is being addressed on the other side: apache/grails-core#16184 now has GspAutoConfiguration contribute an empty grailsUrlMappingsHolder, which is where we said that fix belongs.
As written the two PRs contradict each other. #16184 adds the holder so a link generator can exist in a standalone application. This PR removes the link generator from exactly that application. Without it AssetProcessorService.assetBaseUrl dereferences grailsLinkGenerator.contextPath on the first assetPath() and every <asset:javascript> fails with a NullPointerException unless grails.assets.url is set. The new spec never renders a tag, so it does not see this.
Declare it the way url-mappings declares its own: bean('grailsLinkGenerator', LinkGenerator).conditionalOnMissingBeanName().
There was a problem hiding this comment.
Confirmed and fixed in 498ed2b. Reproduced before changing it, in a standalone context with the condition still in place:
java.lang.NullPointerException: Cannot get property 'contextPath' on null object
at asset.pipeline.grails.AssetProcessorService.assetBaseUrl(AssetProcessorService.groovy:150)
The chain is as you described. AssetMethodTagLib.assetPath uses SERVER_BASE_URL or CONTEXT_PATH, never NONE, so every call reaches assetBaseUrl; grailsLinkGenerator is set only by AssetSupportingLinkGenerator/AssetSupportingCachingLinkGenerator in their constructors, so suppressing the bean leaves it null. grails.assets.url returns before the dereference, which is why it was survivable.
Declared now as bean('grailsLinkGenerator', LinkGenerator).conditionalOnMissingBeanName().
Worth noting for #16184: with the condition gone the bean needs a UrlMappingsHolder, and without one the standalone context fails on NoSuchBeanDefinitionException rather than the NPE. With the holder GspAutoConfiguration contributes, assetBaseUrl returns /assets/. So the two sides meet where you said they should.
| bean(AssetsTagLib) | ||
| .conditionalOnMissingBean() | ||
| .annotate(ConditionalOnBean, value: AssetProcessorService, | ||
| type: 'org.grails.web.pages.StandaloneTagLibraryLookup') | ||
| { AssetProcessorService assetProcessorService, GrailsApplication grailsApplication -> | ||
| AssetsTagLib tagLib = new AssetsTagLib() | ||
| tagLib.assetProcessorService = assetProcessorService | ||
| tagLib.grailsApplication = grailsApplication | ||
| tagLib | ||
| } | ||
|
|
||
| bean(AssetMethodTagLib) | ||
| .conditionalOnMissingBean() | ||
| .annotate(ConditionalOnBean, value: AssetProcessorService, | ||
| type: 'org.grails.web.pages.StandaloneTagLibraryLookup') | ||
| { AssetProcessorService assetProcessorService -> | ||
| AssetMethodTagLib tagLib = new AssetMethodTagLib() | ||
| tagLib.assetProcessorService = assetProcessorService | ||
| tagLib | ||
| } | ||
|
|
||
| bean('assetPipelineConfiguration', InitializingBean) |
There was a problem hiding this comment.
These belong in the separate library, as asked on apache/grails-core#16184 for the example's AssetPipelineConfiguration. A Grails application never uses them, and this is Spring Boot conditional wiring inside the plugin's beans block: each bean carries a @ConditionalOnBean on a type from another module's internals, and every bean added here for Grails in future has to remember the inverse guard or it lands in a plain Boot application. Nothing enforces that, and the PR description says both conditions were wrong on the first attempt.
Once these move, nothing in this block needs a StandaloneTagLibraryLookup condition and the plugin does not have to defend against a context it was not written for.
There was a problem hiding this comment.
Accepted — moved into a new asset-pipeline-gsp module in this PR. None of the beans there carries a StandaloneTagLibraryLookup condition: adding the library is what says which kind of application it is.
It is not a Grails-free module, and cannot be - a GSP tag library is a Grails class, so the tag libraries, AssetProcessorService and the link generator stay in the plugin and the library declares api project(':asset-pipeline-grails'). What it leaves behind is the plugin lifecycle.
| import grails.core.GrailsApplication | ||
| import org.grails.buffer.GrailsPrintWriter | ||
|
|
||
| @TagLib |
There was a problem hiding this comment.
Please drop this. Grails compiles both tag libraries with @Artefact("TagLib"), and StandaloneTagLibraryLookup on apache/grails-core#16184 registers tag-library beans by that artefact type as well as by @TagLib. A Grails application gets nothing from the annotation.
There was a problem hiding this comment.
Keeping this one, because it contradicts the M6 pin below. Disassembling 8.0.0-M6, StandaloneTagLibraryLookup.detectAndRegisterTabLibBeans() is:
22: ldc // class grails/gsp/TagLib
24: invokeinterface ApplicationContext.getBeansWithAnnotation:(Ljava/lang/Class;)Ljava/util/Map;
Annotation only - Artefact appears nowhere in the class. The artefact-type registration is on #16184, which has not merged, so against the M6 this now pins, dropping @TagLib stops the standalone lookup finding the tag libraries at all. Same constraint you applied to the imports file: it can go once a milestone carries #16184. Say if you would rather unpin instead.
There was a problem hiding this comment.
Dropped in 8771a11, reversing my reply above. Having read #16184's diff, the artefact lookup and the UrlMappingsHolder the plugin's link generator needs arrive together there. Without the holder a standalone GSP application does not start on M6, so the annotation only protected a path that cannot run on M6 anyway, and once #16184 ships it is redundant, as you said. Both compiled classes still carry @Artefact(value="TagLib").
The specification that resolves the tag libraries through the standalone lookup is @PendingFeature on #16184, since M6's lookup reads only @TagLib. Checked that it fails with PendingFeatureSuccessfulError once the lookup does find them, so it flags itself when a milestone carrying #16184 is pinned.
| import grails.gsp.TagLib | ||
|
|
||
|
|
||
| @TagLib |
There was a problem hiding this comment.
Same as AssetsTagLib: please drop it.
There was a problem hiding this comment.
Same as AssetsTagLib - keeping it for now, see that thread. The M6 lookup finds tag libraries by this annotation and nothing else.
There was a problem hiding this comment.
Dropped as well in 8771a11; see the AssetsTagLib thread.
|
|
||
| For contributions to the core plugin please see the repository for the core plugin at [asset-pipeline](https://github.com/wondrify/asset-pipeline) | ||
|
|
||
| Outside Grails |
There was a problem hiding this comment.
Moves with the library. The plugin README should describe the plugin.
There was a problem hiding this comment.
Moved to asset-pipeline-gsp/README.md in this PR.
| class AssetPipelineGrailsPlugin extends Plugin { | ||
| def grailsVersion = '7.0.0 > *' | ||
|
|
||
| def grailsVersion = '8.0.0 > *' |
There was a problem hiding this comment.
Correct, since the beans DSL does not exist before M6. Please state it in the release notes with the auto-configuration rename.
There was a problem hiding this comment.
Stated in the PR description, with the rename. There is no CHANGELOG and no release-drafter config in this repository, so that is the only place release notes can be drafted from - tell me if they live somewhere I have not found.
|
|
||
| } | ||
| } | ||
| registry.registerBean('assetPipelineBeanDefinitionRegistrar', |
There was a problem hiding this comment.
Minor. The AOT spec still processes the filter definition only. This bean is registered with an instance supplier and no spec runs it through ApplicationContextAotGenerator. Since #465 is what this registrar exists to preserve, it is worth a case that processes the registrar bean itself.
There was a problem hiding this comment.
Added, and it was worth more than minor. My first attempt hand-rolled the shape - a RootBeanDefinition with an instance supplier - and it failed:
AotBeanProcessingException: Error processing bean with name 'assetPipelineBeanDefinitionRegistrar': instance supplier is not supported
But that is not the path that ships. Registering the real thing, applicationContext.register(plugin.beanRegistrar()), passes: Spring has a BeanRegistrarAotContribution for exactly this. So there is no AOT break, and the spec now covers the registrar through the path a Grails application uses, asserting it actually contributed assetPipelineFilter and assetResourceLocator rather than passing over an empty context.
| @ConditionalOnProperty(name = AssetPipelineAutoConfiguration.ENABLED, matchIfMissing = true) | ||
| @ConditionalOnMissingBean(name = AssetPipelineAutoConfiguration.FILTER_BEAN_NAME) | ||
| @Import(AssetPipelineService.class) | ||
| public class AssetPipelineAutoConfiguration { |
There was a problem hiding this comment.
This module's changes are fine and Grails-free. Please split them into their own PR so they are not held on the plugin discussion.
There was a problem hiding this comment.
Leaving these in this PR rather than splitting them out; #473 is closed.
| @GrailsBeans | ||
| // An ordering hint, not a dependency: everything used below comes from grails-web-url-mappings, | ||
| // while the class named lives in grails-url-mappings, which this plugin declares compileOnly. | ||
| @AutoConfiguration(beforeName = 'org.grails.plugins.web.mapping.UrlMappingsAutoConfiguration') |
There was a problem hiding this comment.
Resolving this: beforeName is right. Under the split proposed in the review, this auto-configuration is still read by a standalone application, for assetProcessorService and grailsLinkGenerator, which is what the apache/grails-core#16184 example relies on. GspAutoConfiguration orders against UrlMappingsAutoConfiguration by name for the same reason. It changes nothing for a Grails application.
| @@ -1 +1 @@ | |||
| asset.pipeline.grails.AssetPipelineAutoConfiguration No newline at end of file | |||
| asset.pipeline.AssetPipelineAutoConfiguration | |||
There was a problem hiding this comment.
Understood on apache/grails-core#16224. Keep the file for now. Why is the autoconfiguration using a different package now?
The plugin's beans block carried three beans a Grails application never uses,
each guarded by a condition on a type from another module's internals, and every
bean added here for Grails in future had to remember the inverse guard or land in
a plain Spring Boot application. Nothing enforced that, and both guards were wrong
the first time. They move to asset-pipeline-gsp, with the specification that drives
them, the test dependencies that exist only for it, and the README section that
describes it.
One of those guards was doing more harm than gating. AssetProcessorService reads
contextPath and serverBaseURL off grailsLinkGenerator for every asset url it
builds, and nothing but the link generator's own constructor ever sets that field,
so suppressing the bean where GSP's standalone lookup is present left the first
assetPath() throwing:
java.lang.NullPointerException: Cannot get property 'contextPath' on null object
at AssetProcessorService.assetBaseUrl(AssetProcessorService.groovy:150)
unless grails.assets.url was set, which returns before the dereference. The
specification never rendered a tag, so it never saw it. The bean is now declared
the way grails-url-mappings declares its own, by name, and an application that
brings its own keeps it.
Also pins Grails 8.0.0-M6, which has carried the beans DSL and beanRegistrar on
Maven Central since 26 August. The hand-written imports file stays: the compiler
that would write it merged after the M6 tag.
The tag libraries the Grails plugin carries also work in an application that renders GSP without being a Grails application, and until now the plugin wired them there itself - three beans a Grails application never uses, each gated on a type from another module's internals. Adding this library is what says which kind of application it is, so none of those conditions is needed and no bean added to the plugin in future has to remember the inverse of one. It is not a Grails-free module and cannot be: a GSP tag library is a Grails class, and the tag libraries, AssetProcessorService and the link generator all stay in the plugin. What an application leaves behind by using this is the plugin lifecycle, not Grails. The specification now builds a url through the tag library the lookup returns rather than only resolving it, which is the half that was missing: resolving never touched the link generator, so it never noticed when the link generator was not there. grails-bom constrains cloud.wondrify:asset-pipeline-*, which resolved the sibling projects to the last published version - and this module compiles against a class this build generates, which that version does not carry. The substitution in build.gradle keeps them the projects they are.
The Grails compiler already marks both with @ArteFact("TagLib"), which is how a Grails application finds them. The annotation was added so GSP's standalone lookup would find them too, and apache/grails-core#16184 teaches that lookup to read the artefact marker as well - naming the asset pipeline's library as the case. Against the 8.0.0-M6 this build pins, that lookup still reads only @taglib, so the specification that resolves the tag libraries through it is pending on #16184, and alerts once it starts passing. Nothing that works on M6 is lost: standalone GSP does not start there without the UrlMappingsHolder #16184 also contributes. The url case takes the tag library from the context instead of the lookup, so the link generator it guards is still covered on M6.
Grails 8 compiles a
beansclosure on a plugin descriptor into a sibling auto-configuration, and takes bean registration that needs more than a closure through aBeanRegistrar. Moving to both puts every bean this plugin contributes in one place and retiresdoWithSpring(), which Grails deprecates in 8.0.Reshaped after review: the standalone GSP beans have moved out of the plugin into a new
asset-pipeline-gspmodule, part of this PR. It also givesasset-pipeline-spring-bootan auto-configuration.Requires Grails 8
grailsVersionmoves to8.0.0 > *. The beans DSL does not exist before 8.0.0-M6. The build pins M6, which has carried the beans DSL andbeanRegistrar()on Maven Central since 26 August.The auto-configuration is renamed, from
asset.pipeline.grails.AssetPipelineAutoConfigurationtoasset.pipeline.AssetPipelineAutoConfiguration— a generated sibling lands in the descriptor's package. apache/grails-core#16221 makes the qualified name settable, but merged after the M6 tag, so the original name is restorable on a later milestone. Same constraint keeps the hand-writtenAutoConfiguration.imports: apache/grails-core#16224, which would write it, also merged after M6.Both belong in the release notes; there is no changelog in this repo, so they are stated here.
A bug this review found
AssetProcessorServicereadscontextPathandserverBaseURLoffgrailsLinkGeneratorfor every asset url it builds, and nothing but the link generator's own constructor ever sets that field. Gating the bean on the absence of GSP's standalone lookup left the firstassetPath()throwing:unless
grails.assets.urlwas set, which returns before the dereference. The specification resolved tag libraries but never rendered, so it never saw it. The bean is now declared by name, the way grails-url-mappings declares its own, and an application bringing its own keeps it.Two definitions that cannot be beans
BeanRegistry.Specand a@Beanmethod both describe a class plus qualifiers, and two definitions here say more:assetResourceLocatorinherits its search locations fromabstractGrailsResourceLocator, a classless abstract definition.assetPipelineFilterholds the filter as a nested bean definition rather than an instance, so AOT can generate source for it (Contribute the asset filter as a bean definition #465).Both come from
AssetPipelineBeanDefinitionRegistrar, aBeanDefinitionRegistryPostProcessor— the shape Grails uses forabstractGrailsResourceLocatoritself. The AOT spec now processes whatbeanRegistrar()contributes, not only the filter definition.asset-pipeline-gspThe tag libraries the plugin carries also work in an application that renders GSP without being a Grails application. The plugin used to wire them there itself: three beans a Grails application never uses, each gated on a type from another module's internals, which every future Grails bean would have had to guard against. They now live in
asset-pipeline-gsp, and adding that library is what says which kind of application it is, so none of those conditions remain.It is not a Grails-free module and cannot be — a GSP tag library is a Grails class — so it declares
api project(':asset-pipeline-grails'). Its spec builds a url through the tag library rather than only resolving it, which is what would have caught the link generator above. Resolving the tag libraries through GSP's standalone lookup is pending on apache/grails-core#16184: the 8.0.0-M6 lookup finds them only by@TagLib, which they no longer carry, while the@Artefact("TagLib")marker the Grails compiler gives them is what #16184's lookup reads.grails-bomconstrainscloud.wondrify:asset-pipeline-*, which resolved the sibling projects to the last published version; itsbuild.gradlesubstitutes them back to projects, since it compiles against a class this build generates.asset-pipeline-spring-bootThe filter
AssetPipelineServicedeclares becomes an auto-configuration, so an application no longer has to@Importit by hand. An application that declaresAssetPipelineServiceitself keeps its own, andassets.enabled=falsedeclines the filter.AssetPipelineServicereads the manifest through its injectedResourceLoaderrather than out of the servlet context. Nothing in it is Grails-aware.Tests
41 specs in
asset-pipeline-grails, 6 inasset-pipeline-gspand 6 inasset-pipeline-spring-boot, all passing, plusi18n-asset-pipeline-grails, which depends on the plugin../gradlew buildgreen.