Overview
| Parent issue | #3499919: [Meta] Plan for in-browser code components |
| Blocked by | #3499927: Config entity for storing code components |
#3483267: [exploratory] PoC of Preact+Tailwind components editable via CodeMirror has some proof-of-concept work for supporting JS components (initially just Astro islands implemented in Preact but this can be expanded over time), whose code can be edited within the XB UI, as components that can be used within XB just like Twig-based SDCs can be.
Per #3483267-29: [exploratory] PoC of Preact+Tailwind components editable via CodeMirror, we'll be opening a new Plan issue for turning that PoC into committable code in multiple granular steps. Once that Plan issue is opened, this issue will become one of those steps (child issues). However, this issue also is about code that has not yet been written in any form within that PoC, because we haven't gotten to it yet and it should just be straightforward Drupal code not requiring any new technologies to figure out.
Currently in that PoC, each JS component is exposed as an SDC and therefore requires a Twig file that renders the <astro-island> element. In this issue, we want to remove those Twig wrappers, so that when a new JS component is added, it just works, without needing Twig code to also be added.
Proposed resolution
XB already has two ComponentSource plugins: SingleDirectoryComponent and BlockComponent. Implement a 3rd one: JavaScriptComponent. Make this plugin, instead of a Twig file per component, render the <astro-island> element.
Also create a render element of type astro island that supports rendering the component in the site's FE. We will likely need a isPreview flag in ComponentSourceInterface::renderComponent but that is consistent with how LB works for blocks/layouts.
Add support for generating/updating component config entities when a javascript config entity is saved updated
Follow the steps in this comment to attach the appropriate JS files that you need to manually place in your files folder: #3483267-17: [exploratory] PoC of Preact+Tailwind components editable via CodeMirror. We'll create a proper library to use in #3500058: Hydration library for code components.
User interface changes
| Comment | File | Size | Author |
|---|---|---|---|
| #31 | code component source working.gif | 2.91 MB | wim leers |
Issue fork experience_builder-3498889
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
wim leersThis should update the table at the top of #3454519: [META] Support component types other than SDC, block, and code components.
Comment #3
balintbrewsComment #4
balintbrewsComment #5
effulgentsia commentedUp until now, the XB team has been following a pseudo-scrum/pseudo-kanban process, but we're now shifting into more conventional scrum. We started a new 2-week sprint last Thursday (Jan 16). I'm tagging our current sprint's issues for visibility.
Comment #6
wim leersComment #7
wim leers@balintbrews: Did you intend for this issue to deliver end-to-end working code components, in the sense that:
?
Comment #8
balintbrews#7: Yes to all of those, with the following notes.
I don't yet see that property added in the config entity. It's a different issue which is still in progress, but I wanted to highlight it, because it will be relevant here.
If this issue lands before the hydration library, we'll adjust the component source plugin as part of the hydration library issue to remove the need for the workarounds.
Comment #9
wim leersOkay! That then shows that the
bit in #3499927: Config entity for storing code components should indeed point here 👍
It is — it's inherited from
\Drupal\Core\Config\Entity\ConfigEntityBase::$status(and prescribed bytype: config_entityin config schema).Comment #10
larowlanUpdated issue summary to include items from a call with @effulgentsia
Comment #11
wim leers#3499927: Config entity for storing code components is in! 🥳
Note: as described in #8 (and the issue summary of #3499927) only if
JavaScriptComponent::status() === TRUEshould it get aComponentconfig entity generated.Comment #12
larowlanComment #13
larowlanSpent some time reviewing the original POC and how it wires up Astro
I think there's probably scope to split out some smaller stories from this one and make this a parent story as follows:
* Astro render element, this is reasonably straight forward in so far as it needs to take some #keys and render the twig like seen in the demo
* A library alter hook that should dynamically build library definitions based on defined code component entities - we will probably need to think about dependencies between components when one component makes use of another, but we can equally punt that to a followup
* Something similar to core's AssetControllerBase that serves CSS/JS from the assets:// stream wrapper to serve the compiled css and js as stored in the config entity. As mentioned we will likely need support for a unique hash of the contents and a preview flag. The preview flag will read from auto-saved entries. The hash will ensure we don't serve stale items. When we auto-generate libraries using the hook alter (item above) we will resolve these to the URLs for this controller. If the file exists on disk it will be served by apache. If it doesn't we'll write it to disk and then return it. This should be able to make use of a lot of the lazy asset work that landed in core in 10.1. The urls could be something like
like we have in core in \Drupal\system\Routing\AssetRoutes::routes but rather than passing a filename in the route we could pass the machine name and hash. We can set the hash as the version in the library alter and use it in the library definition URLS.
We can then append ?preview=1 to the URL for the preview version and likely add some access controls. At some point in the future this could use the workspace ID in the URL too, to allow discrete versions per workspace. The preview version most likely wouldn't dump to file as we would expect this to be regularly updated.
For now I'll start on these in this story but it might be worth splitting them off into smaller issues if the MR size grows too large
Comment #15
wim leers+1, because that'd also require the config entity to know about those dependencies. That was not in scope for #3499927: Config entity for storing code components (nor mentioned as future work, actually), and this is in the critical path for many other things. So let's stick to just individual code components in this issue 🙏
I think it could be much simpler:
— #3499933: Storage for CSS shared across in-browser code components (and other use cases in the future)
Why do we even need to load these assets while previewing? That'd require going from client to server and back to the client? The client-side already has literally all the information it needs (source+compiled CSS, source+compiled JS), I don't see why the server needs to be involved at all? 🤔
Un-assigning @larowlan because it's a holiday in Australia on Monday. He'll likely be the one to push this forward further, but that shouldn't prevent somebody from doing so until he returns :)
Comment #16
balintbrewsI agree that we can keep the preview client-side. It's also how we implemented it in #3483267: [exploratory] PoC of Preact+Tailwind components editable via CodeMirror.
In #3500034: Preview for code components we need to do compilation with
@swc/wasm-webandtailwindcss-in-browserin the browser anyway, at which point we have everything to display a preview. Like Wim said. 😊Comment #18
longwaveAdded the library hook and attached it to the island render array.
Wondering whether, similar to core, the paths should be something like
assets://xb/css/HASH.css?component=BLAH- this way if two components happen to share identical CSS they will share the CSS file as well. Core uses the query string to build and then the stored file doesn't otherwise care.For the preview CSS at least, I think we might need a separate library definition and separate route/controller to serve the CSS. Otherwise we have to invalidate library definitions every time the hash changes? If this is the case maybe we should do the same with the JS for consistency.Unless we don't need the preview at all as per #15/16.Comment #19
longwaveImplemented asset generation as per #15.
Comment #20
larowlanMy thinking around the client side/server preview comes down to import maps. If we need an import maps it needs to be in an IFrame. But if it's already solved, that's awesome
Comment #21
wim leersEverything in this MR so far has focused on the astro island/rendering/assets aspects.
But there's more to this issue:
ComponentSourceplugin.Working on those two aspects.
Comment #22
wim leersDelivered on much of #21, but not everthing yet. EOD, so passing context + next steps on to the next person:
\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\SingleDirectoryComponentintoGeneratedFieldExplicitInputUxComponentSourceBase. I kept it as simple as possible, and resisted doing significant refactoring 🤓SingleDirectoryComponentmerely ~275 LoC, and there's likely more still to be done.prop_field_definitionssetting for theSingleDirectoryComponentComponentSourceplugin out of its config schema type into a new config schema base type: https://git.drupalcode.org/project/experience_builder/-/merge_requests/5...JsComponentComponentSource: https://git.drupalcode.org/project/experience_builder/-/merge_requests/5...Next steps:
Componentconfig entities based onJavaScriptComponentconfig entities. @longwave suggested automatically doing that whenever aJavaScriptComponentconfig entity is saved. I agree that makes sense. This would be a::postSave()override in\Drupal\experience_builder\Entity\JavaScriptComponent.\Drupal\Tests\experience_builder\Kernel\Config\ComponentTest— which was previously expanded in #3475584: Add support for Blocks as Components to testBlock-sourcedComponentconfig entities.docs/components.md!P.S.: I did not test
\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent::renderComponent()at all, I just matched what @larowlan did in\Drupal\Tests\experience_builder\Element\IslandTest. It likely needs updating 😅Comment #23
wim leersAlso merged in upstream and resolved conflicts, because this conflicted heavily with #3500994: Remove references to 'props' outside of SDC - use 'inputs' instead.
Should make for a smooth start for the next person 😊
Comment #25
wim leersSO great to see @larowlan continued where I left off 🤩
ComponentSourceInterfaceAPI tightenings by @larowlan — best example: https://git.drupalcode.org/project/experience_builder/-/merge_requests/5... — done in a way that does not balloon scope and https://www.drupal.org/project/experience_builder/issues/3498889#:~:text... remains true 👍14 files changed, 855 insertions(+), 742 deletions(-)to30 files changed, 1632 insertions(+), 915 deletions(-)😳 Postponed the other issue: #3473275-13: Support disabled Block Components + multiple reasons for incompatibility.(And related: https://git.drupalcode.org/project/experience_builder/-/merge_requests/5....)
But it looks like @larowlan tackled things I had not even identified in #22, so I can literally work on the list I created there 😄
Comment #26
wim leersIt looks like I was wrong:
::postSave(), but in the newJavascriptComponentStorage👍 Only thing that needed changing: respect thestatusflag that #3499927: Config entity for storing code components assigned a particular meaning to.JavascriptComponentStorageTest. Still, updatingtestComponentCreation()the way that I did provides value: it ensures that identicalpropsfor an SDC and a "code component" result in the exact sameprop_field_definitions👍I did find some problems though, and I've fixed those.
I think I won’t be able to push this forward the next few hours. The only remaining thing I have partially (locally) is schema refinements, but there’s plenty more to do that won’t touch that.
Comment #27
wim leersFinished everything in #22, except for the docs (only a stub was added).
Iterated on everything @larowlan did overnight, and between his work and mine, we surfaced a bunch of oversights in both #3499927: Config entity for storing code components and the way that even the foundations of
Componentconfig entity type's support forComponentSource-specific settings were not quite right yet. Which is understandable, because it's this issue that's introducing a 3rdComponentSourceplugin (and as the adage goes: an abstraction requires 3 concrete uses), and one that is quite different in some ways and almost the same in others!I've spent most of today increasing test coverage, and in turn revealing weaknesses in our validation and hence the reliability of the
Componentconfig entity type in combination with the newComponentSourcethis MR is introducing. Those weaknesses are important to eradicate to remain on track for requirement14. Configuration management.The one bit where I'm going quite strongly against what @larowlan did last night is https://git.drupalcode.org/project/experience_builder/-/merge_requests/5..., because by using
ContribStrictConfigSchemaTestTrait, it became clear that all of those edge cases @larowlan was testing … actually are made impossible by the validation for theJavaScriptComponentconfig entity, and for good reason! (See the commit message.)I believe that now, especially after the source-specific test coverage (which revealed missing validation) and its fixes (A — note how this causes the exact same fails + B) everything is in place 🤓
The only thing I have not reviewed at all is all rendering aspects: the astro island and asset generation. But for those, I'm happy to defer to @longwave & @larowlan. I want to get the config bits to be as robust as possible as soon as possible because they affect the data model/update path/overall stability — tweaks to the rendering can easily happen in subsequent MRs.
So: approved this MR! Hopefully @larowlan & @longwave think this is RTBC by tomorrow 🤞😊
Comment #28
larowlanFWIW
I did it in the storage handler because we have DI in storage handlers, in Entity::postSave we have to use \Drupal. We use this pattern fairly regularly for client projects and I think it is cleaner.
Comment #29
wim leers#28:: ah, of course! I somehow always forget that’s an option 😇🙈
Green and lots of commits overnight 🤩
Reviewing!
Comment #30
wim leers@larowlan opened #3502982: Rename ComponentSource settings' `plugin_id` to `local_source_id` or similar to not bias towards source plugins that don't use plugins under the hood and #3502988: Move more `ComponentPluginManager` methods to the SDC `ComponentSource`.
I opened #3503038: Enable candidate `DynamicPropSource` suggestions for code components: refactor `GeneratedFieldExplicitInputUxComponentSourceBase` and `FieldForComponentSuggester` to need only SDC's ComponentMetadata, not SDC plugin instances.
The goal for this issue, confirmed by @balintbrews in #8, is this:
Ideally, we'd have an end-to-end test. But I think given that 99% is identical to SDCs, we could get away with not having that, and a future issue could do that — the scope of this one is already enormous.
I think that this MR could land if:
Comment #31
wim leersThanks to @longwave for getting the missing bits for Astro assets in place 👍 He also tells me he has essentially only nits. 😊
Good news all around, because:
(for this demo, I only commented out the
if ($this->configInstaller->isSyncing())early return inJavascriptComponentStorage— to force a correspondingComponentconfig entity to be created 👍)Given how many front-end issues this blocks (virtually everything in #3499919: [Meta] Plan for in-browser code components), plus it following the exact same pattern as the pre-existing test logic, plus the vastness of this MR (~2500 LoC added, net 😳), I'm not awaiting FE approval for the end-to-end tests. It's too important to land this sooner rather than later!
So: RTBC'ing. Only thing outstanding: docs. First: belated lunch.
Comment #32
balintbrewsCrediting @effulgentsia, who had the vision for this plugin and using Astro islands.
Comment #34
wim leersComment #36
effulgentsia commented