8000 Using AssetMapper to manage web assets of bundles · Issue #53912 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

Using AssetMapper to manage web assets of bundles #53912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
javiereguiluz opened this issue Feb 12, 2024 · 43 comments
Open

Using AssetMapper to manage web assets of bundles #53912

javiereguiluz opened this issue Feb 12, 2024 · 43 comments

Comments

@javiereguiluz
Copy link
Member
javiereguiluz commented Feb 12, 2024

AssetMapper is great and upgrading web apps to AssetMapper is also great and not too complex.

But, managing bundle's web assets with AssetMapper is not possible at the moment.

To make it work, we'd need, at least, two things:

(1) A way to run importmap commands when developing the bundle locally. Maybe include a small binary with the component like we do in other components?

(2) A way to reference to bundle's assets in {{ importmap('...') }} Twig function. Maybe allow to use the typical bundle logic path such as importmap('@AcmeBundle/app') ?

But before doing anything, we must decide if want to support AssetMapper in bundles.

I looked at the most starred GitHub repos that include the symfony-bundle tag and this is what I found:

Bundle Has Web Assets?
doctrine/DoctrineBundle
doctrine/DoctrineMigrationsBundle
EasyCorp/EasyAdminBundle
sensiolabs/SensioFrameworkExtraBundle (bundle is archived)
FriendsOfSymfony/FOSUserBundle
symfony/monolog-bundle
FriendsOfSymfony/FOSRestBundle
doctrine/DoctrineCacheBundle (bundle is archived)
nelmio/alice
lexik/LexikJWTAuthenticationBundle
doctrine/DoctrineFixturesBundle
hwi/HWIOAuthBundle
nelmio/NelmioApiDocBundle
sonata-project/SonataAdminBundle
stof/StofDoctrineExtensionsBundle
nelmio/NelmioCorsBundle
dustin10/VichUploaderBundle
KnpLabs/KnpPaginatorBundle
liip/LiipImagineBundle

So, not many popular third-party bundles need this feature.

But, there are others that could use it:

  • Apps like Sylius which internally are made out of bundles and many of them inclde web assets
  • Private apps that also use bundles internally to split their logic

Thanks!

@stloyd
Copy link
Contributor
stloyd commented Feb 12, 2024

hwi/HWIOAuthBundle is not using assets, we leave all the styling to user.

@stloyd
Copy 8000 link
Contributor
stloyd commented Feb 12, 2024

@javiereguiluz looking at that list, few of them depend on assets.

Maybe better would be to use Packagist to get a list of depending packages for webpack-encore-bundle?

@mbabker
Copy link
Contributor
mbabker commented Feb 12, 2024

Expanding beyond GitHub stars, packages that I know do provide some kind of asset integration:

  • My own PagerfantaBundle ships a CSS file for the default template that isn't designed against an existing CSS framework
  • FOSJsRoutingBundle has an entire JavaScript integration, including a webpack plugin to support builds with Webpack Encore (as that's kind of the point of the package 😅)
  • Any WYSIWYG editor package, including FOSCKEditorBundle for CKEditor 4.x and TinymceBundle for TinyMCE (I realize there's also Integration of a WYSIWYG editor ux#1 to look at this problem space inside the context of the UX initiative)
  • Not sure how nicely 100% external scripts would play into asset mapper, but you also have the captcha problem space to think about here and for that there are packages like EWZRecaptchaBundle

And just expanding on the Sylius comment, there are 3 of its internal bundles that do provide frontend assets, with its plugins (third-party bundles) regularly providing even more assets.

Personally, I do think looking at how bundles might integrate with AssetMapper is an interesting thing, as there is clearly a use case for supporting it.

@javiereguiluz
Copy link
Member Author

Let's ping @weaverryan, @smnandre and @kbond because they are experts in AssetMapper (and many other things) so they can guide us about what to do here. Thanks!

@smnandre
Copy link
Member

Well it depends what you* want to do i guess :)

*or any bundle developer

What kind of "importmap command" you'd like to do for instace ?

There are already some integrations.. for example the PagerfantaBundle CSS file can be imported either as a standard CSS file, or via the importmap

Personal opinion, I think that bundle should not add assets automatically into the main importmap, but there may be something missing today, to allow user to register all assets from a bundle in a simple click/action (like it's done for routes, or config already) (probably declaring an entrypoint?)

@javiereguiluz
Copy link
Member Author
javiereguiluz commented Feb 16, 2024

@smnandre there are two different needs here:

  1. As a bundle maintainer, use AssetMapper to manage and build the bundle assets
  2. Allow the bundle expose its AssetMapper assets somehow to the full app

About (1), I'm thinking a workflow like this:

$ cs projects/MyBundle/

$ <some binary> importmap:install bootstrap
$ <some binary> importmap:install tom-select
$ <some binary> importmap:install ...

// a few days later
$ <some binary> importmap:update

// maybe even
$ <some binary> tailwind:build --watch
$ <some binary> sass:build --watch --watch

What's that <some binary>? In a full Symfony app, this is php bin/console ... but when developing a bundle, you don't have a full Symfony app, so there's no binary.


About (2), I'm thinking about problems like this:

Imagine that you can use AssetMapper to build your bundle assets (it's not possible today, but just imagine you can).

You have a file at /my/projects/MyBundle/importmap.php but you cannot use this in the templates of the bundle:

{{ importmap('foo') }}

Symfony will look for foo entrypoint in the importmap.php file of the application, not in the importmap.php file of the bundle.

We'd need a way to refer to the bundle importmap ... maybe something like:

{{ importmap('@MyBundle/foo') }}

Or a way to add the bundle importmap (namespaced somehow) into the application importmap:

// main importmap.php
return [
    // ...

    'MyBundle' => [
        'path' => './vendor/some-vendor/my-bundle/importmap.php',
        'bundle' => true,
    ]
];

@smnandre
Copy link
Member

The concept of "bundle importmap" seems weird to me, as there is in the end only one importmap. So that could lead to conflicts.

Imho the concept of "entrypoints" work well for that need.

I don't know how that covers (or not) all your needs, but you can already add entries into importmap via recipes (that's what ux packages are doing for instance)

--

For your examples, very personal opinion and obviously i'm not talking for anyone else here.. but i'm not sure a bundle should bring bootstrap or tailwind (as it's not recommanded to install PHP dependencies)

--

For the dev tool, let's say this binary exists, can you precise me what this command would do ?

<some binary> importmap:install tom-select

(it's a geniune question i try to understand your needs and what you would like to do / how we can ease that part of your work ;) )

@javiereguiluz
Copy link
Member Author

I think that we're not understanding each other. I'll try to use another example:

Currently, AssetMapper only works in this specific scenario:

  1. You have a full Symfony app
  2. You want to build app.css and app.js to serve your assets
  3. You install AssetMapper
  4. You run importmap:install commands to install CSS/JS dependencies needed to build your assets
  5. This creates an importmap.php in the application
  6. You run asset-map:compile in production to build the assets that are exposed to users

Now, the scenario we are talking about is completely different:

  1. You are the author of a third-party bundle
  2. You want to build bundle.css and bundle.js which are the assets that your bundle needs to work

How do you do that?

  1. You can build those assets by hand. Cumbersome and time-consuming ... but this works.
  2. You can use Webpack Encore ... it works great, but it requires Node.js, yarn/npm, etc.

In both of these cases you end up with the bundle.css and bundle.js files and the app using the bundle can load them, etc.

Now, what I want is: build bundle.css and bundle.js with AssetMapper and link to them in my bundle Twig templates.

I can install symfony/asset-mapper in my bundle, but:

  • I can't run importmap: commands, so I can't install CSS/JS dependencies automatically and I should create/maintain the bundle's importmap.php file by hand
  • I can't link to those bundle.css and bundle.js assets via importmap('bundle') ... because only the main app can have an importmap and this is just a bundle.

@stof
Copy link
Member
stof commented Feb 16, 2024

AssetMapper will never build bundle.css or bundle.js. It is not a bundler.

@smnandre
Copy link
Member

That's why i did not fully understand .. there is currently nothing in assetmapper that would help you bundle/pack those files :|

@javiereguiluz
Copy link
Member Author

@stof yes, "build" is not the correct word ... it doesn't build a single file ... but it helps you build the definitive assets with your own assets and the dependencies.

In any case, nobody seems to understand what I'm trying to explain here. So maybe we can close this as "won't fix" and don't allow bundles to use AssetMapper?

@smnandre
Copy link
Member

Let's try once more .. if there is something AssetMapper can do for you, let's see how to :)

Could you ideally illustrate what files "you" (or any imaginary bundle developer of course) want to deal with, where they come, where you want to use theme, what would do the command files, etc etc ?

@javiereguiluz
Copy link
Member Author

Let's wait and see if @weaverryan and @kbond can chime in here. Maybe they understand the scenarios that I explained above.

@kbond
Copy link
Member
kbond commented Feb 16, 2024

I don't know too much about the asset mapper's internals so I'll let Ryan talk more to that.

I know there's more to what your asking but is it true that bundle assets are made available to asset mapper?

@smnandre
Copy link
Member

but is it true that bundle assets are made available to asset mapper?

Individually yes

$paths = $config['paths'];
foreach ($container->getParameter('kernel.bundles_metadata') as $name => $bundle) {
    if ($container->fileExists($dir = $bundle['path'].'/Resources/public') || $container->fileExists($dir = $bundle['path'].'/public')) {
        $paths[$dir] = sprintf('bundles/%s', preg_replace('/bundle$/', '', strtolower($name)));
    }
}

https://github.com/symfony/framework-bundle/blob/6a8838ad9ecfc7eb992c16970e2637f128bd5dbf/DependencyInjection/FrameworkExtension.php#L1307

@smnandre
Copy link
Member

The doc with full sentences and less code 😅

Capture d’écran 2024-02-16 à 17 16 55

@jahudka
Copy link
Contributor
jahudka commented Apr 6, 2024

Hi, lemme chime in..

I think that this question is actually two separate questions:

1. How do I use AssetMapper / importmap:require while developing a bundle?

This is actually two separate problems as well:

  • 1.a) How can I use importmap:require to download & install dependencies when developing a bundle?

    And the answer is: probably just install AssetMapper as a dev dependency and include a tiny bin/console implementation to be able to run it. Shouldn't be too much work, but yes, AssetMapper could include its own executable and expose it via e.g. composer.json / bin. So that for example as the developer of a bundle which adds a custom form type for some cool new wysiwyg I can just do vendor/bin/asset-mapper importmap:require cool-wysiwyg when working on my bundle and have the cool-wysiwyg assets appear in assets/vendor/cool-wysiwyg/ same as they would for a full project.

  • 1.b) How do I then expose the fact that my assets rely on those dependencies to projects consuming my bundle?

    And the answer is: currently you must document your dependencies and ask your bundle's consumers to run the necessary importmap:require commands themselves. Afaik unless you bundle your dependencies with your bundle (bleh) there is no way to expose them to the AssetMapper installed in a project which consumes your bundle. And it would indeed be very nice if there were such a way: to be able to composer require some-cool-dudes/cool-wysiwyg-bundle and then run bin/console importmap:install to auto-install the cool-wysiwyg dependency of the bundle.

2. How do I expose my bundle's own assets to a consumer of the bundle?

AssetMapper may not bundle assets, but it does other magic on them:

  • scans JS and CSS files for imports
  • produces a versioned URL for each asset
  • adds mapping between logical import specifiers and versioned URLs to the importmap, both for entrypoints and for all discovered dependencies / imports (at least that's what it looks like to me)

And therefore the question is: how do I do this for assets in my bundle, so that a consumer of my bundle can use them in their application? I think the answer to that is: I don't - I just publish my assets along with my bundle and it will be the consumer's local AssetMapper who takes care of all that for me and them. That's nice in theory.

In practice it runs into two issues from the consumer's viewpoint:

  • Exceptionally ugly import paths, because currently AssetMapper doesn't support logical bundle specifiers when resolving imports - ie. this works: import { foo } from '../../vendor/acme-bar/foo/src/Bridges/Symfony/Resources/public/js/foo.js', but this doesn't: import { foo } from '@AcmeBarFoo/js/foo.js'; nor does this, although the docs almost seem to suggest that it should: import { foo } from 'bundles/acmebarfoo/js/foo.js'.
  • And this also only works when the bundle files are imported from an entrypoint registered in the application's importmap (or from somewhere in an entrypoint's import tree) - you can't import "a bundle's assets" within an inline script in a template any way that I could think of.

It'd be really neat if a bundle's author could somehow expose information about the bundle's assets and entrypoints to the consuming application, so that the mappings could be added to the generated importmap without the need to go through a JS entrypoint file just to re-export stuff from the bundle.

An entirely separate issue is that there's a bug in AssetMapper which means that this works:

import { foo } from  '../../vendor/acme-bar/foo/src/Bridges/Symfony/Resources/public/js/foo.js';
export { foo };

but this doesn't:

export { foo } from  '../../vendor/acme-bar/foo/src/Bridges/Symfony/Resources/public/js/foo.js';

@justin-oh
Copy link

I understand that the asset:install command will copy files from any bundle with Resources/public/ into /public/bundles/nameofbundle/.

Could the Asset Mapper hijack that command or disable it and instead copy things into /assets/bundles/nameofbundle? Then we can reference those resources like any other asset.

This is an issue at least when it comes to compiling Sass since the files need to be referenced directly and not through the import map. Example:

/* assets/app.scss */
@import '../vendor/namespace/my-bundle/src/Resources/scss/style';

I didn't bother putting the scss folder of my bundle into a public folder. I guess if I did do that, the path would be a little cleaner with ../public/namespacemybundle/scss/style.

If the Asset Mapper could copy bundle files to /assets/bundles/, my Sass path would be even cleaner:

/* assets/app.scss */
@import 'bundles/namespacemybundle/scss/style';

It also would resemble the JS pathing which works the same in either case:

/* assets/app.js */
import '/bundles/namespacemybundle/js/index.js';

@smnandre
Copy link
Member

@justin-oh
Copy link
justin-oh commented Apr 16, 2024

Did you try with the configuration explained here : https://symfony.com/doc/current/frontend/asset_mapper.html#importing-assets-outside-of-the-assets-directory ?

I did not, but I don't think that is a sustainable option since every bundle that has assets will have to include the same instructions of editing config/packages/asset_mapper.yaml to add a path into their composer vendor folder.

# config/packages/asset_mapper.yaml
framework:
    asset_mapper:
        paths:
            - assets/
            - vendor/some/package/assets
            - vendor/some-other/package/assets
            - vendor/another/package/assets
            # etc.

What if the Asset Mapper supported the vendor path out of the box?

I made a similar request over at the SassBundle. It could be like a hard-coded path within the Asset Mapper that cannot be overridden. I assume developers could then import some/package/assets/style.css knowing the Asset Mapper will look inside the vendor folder.

It would also reduce duplication of work specifically when using the SassBundle with Bootstrap. The recommendation is to install Bootstrap via composer, then reference the Sass directly in the vendor folder. However, in order to leverage the Bootstrap JS, I have to do an importmap:require bootstrap. So now I have 2 places where Bootstrap files are stored. With vendor path support, I assume I could simply import 'twbs/bootstrap/dist/js/bootstrap'; (or import 'twbs/bootstrap';).

Edit: I guess I'd still have to importmap:require @popperjs/core manually. I think I'm finding more issues related to Sass integration than anything! Sass is more so a bundler of things into a single CSS file which is the opposite of what the Asset Mapper is trying to accomplish.

@smnandre
Copy link
Member
smnandre commented Apr 17, 2024

And as usual i failed my copy/paste... this is the section that should help you : https://symfony.com/doc/current/frontend/asset_mapper.html#third-party-bundles-custom-asset-paths

All bundles that have a Resources/public/ or public/ directory will automatically have that directory added as an "asset path", using the namespace: bundles/<BundleName>

@jahudka
Copy link
Contributor
jahudka commented Apr 17, 2024

And as usual i failed my copy/paste... this is the section that should help you : https://symfony.com/doc/current/frontend/asset_mapper.html#third-party-bundles-custom-asset-paths

All bundles that have a Resources/public/ or public/ directory will automatically have that directory added as an "asset path", using the namespace: bundles/<BundleName>

Yes, that means I don't have to add the path to asset_mapper.yaml; but when actually importing the bundle files from within my assets I still have to reference them using basically a full filesystem relative path, e.g. ../../../../vendor/jahudka/awesome-wysiwyg-bundle/src/Bridges/Symfony/Resources/public/awesome-wysiwyg.js, instead of the rather more sensible bundles/awesomewysiwyg/awesome-wysiwyg.js. And unless I import them from an entrypoint or a file that is directly or indirectly imported by an entrypoint they aren't scanned for dependencies and the dependencies aren't added to the importmap, so <script src="{{ asset('bundles/awesomewysiwyg/awesome-wysiwyg.js') }}"> will load the script I want, but its imports will then likely fail to load, because the importmap doesn't know about them.

@smnandre
Copy link
Member

instead of the rather more sensible bundles/awesomewysiwyg/awesome-wysiwyg.js

Did you try ? Because according to the link i posted, it's exactly what's available.

but its imports will then likely fail to load, because the importmap doesn't know about them

Why would a bundle provide a script not ready to use to an app, when it has no certitude AssetMapper is installed / ImportMap is used ?

@jahudka
Copy link
Contributor
jahudka commented Apr 17, 2024

Did you try ? Because according to the link i posted, it's exactly what's available.

Yes, indeed I have read the docs, and yes, I have tried - not just the way it was described in the docs, but also multiple other ways, because the way it was described in the docs didn't work. The issue is that although asset mapper knows about the bundle/ path, importmap doesn't - so {{ asset('bundle/...') }} works, but import 'bundle/...' doesn't.

Why would a bundle provide a script not ready to use to an app, when it has no certitude AssetMapper is installed / ImportMap is used ?

The bundle indeed doesn't care whether you use AssetMapper / ImportMap - it provides a pre-bundled IIFE build which you can reference using {{ asset('bundle/...') }} as usual, but also the source ES modules, so that you can either use a build toolchain of your own, or use ImportMap.. at least that was the idea. The bundle provides functionality built on top of a 3rd party WYSIWYG editor; including the editor code in pre-bundled assets may not be possible due to licensing, and even if the editor's license would allow it, it might still make sense to allow the bundle consumer to provide the dependency themselves, in case they e.g. want to use the editor for other purposes than just the features provided by the bundle.

@smnandre
Copy link
Member

So:

If the bundle has precompiled assets, they are available via asset() and versionned

If the bundle has non-precompiled assets, or if you want to import some file (JS or CSS).. you have to require them in the importmap (as it's the only way for importmap to ... know they do exist).

On none of those cases you need to use '../../../../vendor/' paths.

Is there something you would like me to detail, maybe i'm not clear on something and i'd really like everyone to be happy here :)

@jahudka
Copy link
Contributor
jahudka commented Apr 17, 2024

If the bundle has non-precompiled assets, or if you want to import some file (JS or CSS).. you have to require them in the importmap (as it's the only way for importmap to ... know they do exist).

And that is, I believe, kind of the point of this issue. Compare:

bin/console importmap:require bootstrap
  • bootstrap is downloaded
  • its main entrypoint is available using import 'bootstrap'
  • its dependencies are scanned and @popperjs/core is downloaded & available using import '@popperjs/core'

In contrast:

composer require jahudka/awesome-wysiwyg
  • package is downloaded
  • its main entrypoint is... well, there's no way for a bundle to specify its main entrypoint, but still, its assets are not available using import 'bundles/awesomewysiwyg/...' - either I have to manually importmap:require them, or I have to use ../../../../../vendor/...... paths
  • its JS dependencies are not scanned, so awesome-wysiwyg is not downloaded and not available

AssetMapper's ImportMap feature and commands like importmap:require and importmap:audit make it seem as though AssetMapper aims to be a replacement for a traditional "NPM + build toolchain" setup, but since bundles exist and they often expose their assets directly (as opposed to a separate NPM package), AssetMapper is feature-incomplete for that (very common) use case.

In other words, we're saying "AssetMapper can't do X, but it's such a common use-case, maybe X could be added to AssetMapper?" and you're responding with "AssetMapper can't do X", without addressing the actual question ("can X be added to AssetMapper?"), which makes me think it's us who aren't explaining ourselves well enough..

@smnandre
Copy link
Member

Oh that's not my intention, and i'm sorry if i made it feel that way !

Before AssetMapper, did you have a solution for this specific scenario (= a bundle exposing assets that you need to import / include, assets that themselves require external dependencies)
(it's a geniune question i promise, don't hit me ;) )

I understand you don't want to use importmap:require yourself.. would that be more ok with flex recipes ? It is maybe not documented enough but it works (so you can add automatically some import, even entrypoint in the importmap)

@smnandre
Copy link
Member

Do you have a bundle example that match your needs, so i could try to test / play with and see what solutions can be found ?

@DjordyKoert
Copy link
Contributor

Do you have a bundle example that match your needs, so i could try to test / play with and see what solutions can be found ?

Hey chiming in on behalf of NelmioApiDocBundle,

NelmioApiDocBundle currently comes with some 3rd party assets to help users easily browse their api documentation (swagger-ui & redoc)

It would be cool if we could instead utilize AssetMapper to handle importing these 3rd party packages by creating a importmap.php file inside of the bundle. In my eyes this file could either be written manually or generated by utilizing some kind of tiny bin/console (as mentioned by @jahudka in #53912 (comment)) and running importmap:require

// importmap.php
return [
    'swagger-ui' => [
        'version' => '...',
    ],
    'redoc' => [
        'version' => '...',
    ],
];

This could then allow use to use these dependencies with a simple import.

import SwaggerUI from 'swagger-ui'

I hope that I understood your questions/this discussion correctly and that this might help answer some questions 😅

@smnandre
Copy link
Member

I really think in that case the flex recipe is the best solution, allowing a Bundle to write some lines into the main importmap.php.

If you want let's have a talk on Slack / mail to see how i may help you implement this ?

@DjordyKoert
Copy link
Contributor

If you want let's have a talk on Slack / mail to see how i may help you implement this ?

Sure! You should be able to reach me through Slack in the "Symfony Devs" workspace :)

@jahudka
Copy link
Contributor
jahudka commented Apr 21, 2024

If AssetMapper & importmap are the "new standard" of Symfony apps, it stands to reason that bundle developers will want to adopt them, so that their bundles are compatible with the recommended standard practices and tools. If bundle developers only expose pre-built assets, that means that you can run into duplicated bundled dependencies (e.g. two Symfony Forms extensions which both add some feature built on top of TinyMCE would result in TinyMCE being included two times in your project). This is what package & dependency management is for, and as long as importmap:require and its siblings exist, AssetMapper is a package & dependency manager, but it only currently supports a limited feature set and notably doesn't natively & frictionlessly support something which I believe to be pretty common in the larger Symfony ecosystem.

There are projects which don't use Flex. I feel that a decent chunk of what AssetMapper would need to do to support this feature is already there. Also, using AM to install local dependencies and Flex to install bundle dependencies (even if it ends up using AM in the background) feels like using two separate tools for the same job. AssetMapper can already resolve dependencies recursively; why should you need Flex to be able to do that same thing for bundles, just because they come from Composer rather than NPM? Flex is a dependency which has many other potentially undesirable side effects in a project. There are already conventions in place for various bundle-related things, like the Resources/public directory for public assets. Why not include the bundle's importmap.php in that? I just don't see how going the Flex route ends up giving a better DX - not just for consumers of bundles, but for bundle authors as well (because they have to actually write and test the Flex recipes on top of the bundles themselves). Another aspect of this solution is that the same problems need to be solved over and over in every bundle's Flex recipe, instead of being neatly solved in a single place.

One of the problems the Flex recipes would need to solve is dependency conflicts. Consider:

  • The Flex recipe for first-vendor/some-bundle adds a JS entrypoint for the bundle and its dependencies, e.g. bootstrap @ v4;
  • Later, the Flex recipe for another-vendor/some-other-bundle attempts to add that bundle's entrypoint and dependencies, which, coincidentally, also include bootstrap - but this time at v5.

If the Flex recipes don't check for this, things will break pretty soon - depending on how the importmap.php file is updated, you either end up with only a v5 dependency, which may break first-vendor/some-bundle, or you end up with two bootstrap entries with different versions. And even if the recipes do check for existing / conflicting dependencies, they won't be able to give you a sensible error message in case a conflict occurs - just something generic along the lines of "your project is already using X at version Y, but this package needs version Z" - and now you may have to manually dig through your previous dependencies / commit history / whatever to figure out which dependency actually installed X at version Y in order to decide whether adding the new dependency would justify replacing the other one with something else...

Another scenario: when a new version of first-vendor/some-bundle with different / updated dependencies is released, you either have to remove it and require it again to get the current Flex recipe to run (assuming Flex recipes can even clean up stuff they changed when uninstalling a package), or you have to update the dependencies manually.

Instead: consider if AssetMapper can figure out the location of bundles' importmap.php files and intelligently merge them into the project importmap.php (without actually modifying the project file itself):

  • It would then be able to figure out the entire dependency tree and give you a meaningful error message if it finds a conflict.
  • It would be able to automatically dedupe dependencies.
  • It would also be able to detect new / updated / dropped dependencies and take appropriate action.

In other words, it would be much closer to an actual package & dependency manager. It would give better DX to bundle developers, who wouldn't have to write complex Flex recipes just to properly install bootstrap, and it would give better DX to bundle consumers, who would get much closer to the experience they've come to expect from Composer / NPM / others. It would also unify the DX between developing a bundle and developing a project - you'd use the same importmap.php file and the same importmap:* commands when developing a project and when developing a bundle.

I know that doing this properly is probably a lot of work. But I firmly believe that as long as you actually want to go the "JS/Node/NPM-less" route of working with frontend code (as opposed to just integrating better with existing tooling, such as NPM / Vite / Webpack / ...), then a full-featured package manager built into AssetMapper is a far, far superior solution than Flex recipes.

@smnandre
Copy link
Member

There are in this issue many ideas / points / suggestions. And i've read them all. I tried to answer regarding what is already possible, what can be with few modifications, why some can be complex / not possible currently.

But now i feel this become a way to broad topic, mixing very different things

  • tools bundle developers can use to bundle/install assets
  • compiled asset exposed by bundles
  • the way user can install/deploy those assets
  • asset "sources" exposed by bundles
  • the way final users can install / compile those sources

I just need to remind a few things here

  • i'm not in any way "owner" of AssetMapper (or anything in Symfony)... and i don't want my opinion here to be misstaken as anything "official"
  • i think i have a good vision of what AssetMapper is today and what it is not, but that has no impact on what assetmapper can be tomorrow :)
  • i take time on my personal free time to try to understand the different needs here

Even if i'm probably the one to blame, my words not beeing always well chosen (english is not my best talent).. this conversation feels to me a bit tense right now.

So I'll now stop answering on this issue, and will post again when i have some news on my side...
.. but remember if you want something specific, the best option is often to open a PR :)

I'll talk with Djrody to see what we can do, and I'm personaly open to other discussions like this :)

Thanks everyone for all your messages / ideas / examples :)

@Hanmac
Copy link
Contributor
Hanmac commented Apr 26, 2024

How does/should AssetMapper work for a Bundle when the Main Project does not use it (yet)?

For example, sonata-project/form-extensions ships with some assets, some currently done with webpack.

Now, the Main Project does not use the importMap in their code right now.

Should/Could the sonataform be able to call their own importMap function to make their assets available?

Right now, the webpack stuff causes Problems with absolute/relative paths that might could be solved with AssetMapper (maybe)

@nicolas-grekas nicolas-grekas removed this from the 7.1 milestone May 21, 2024
@carsonbot
Copy link

Thank you for this suggestion.
There has not been a lot of activity here for a while. Would you still like to see this feature?
Every feature is developed by the community.
Perhaps someone would like to try?
You can read how to contribute to get started.

@carsonbot
Copy link

Friendly ping? Should this still be open? I will close if I don't hear anything.

@Hanmac
Copy link
Contributor
Hanmac commented Dec 6, 2024

@javiereguiluz can you add sonata-project/form-extensions as separate point in your list?

i still don't know yet what the best way for Sonata would be

@tacman
Copy link
Contributor
tacman commented Jan 7, 2025

gut feeling is that there's some way to solve this. How about using the --path option when installing the asset, and point to the relevant vendor directory/file?

@Guarrakesh
Copy link
Guarrakesh commented Jan 14, 2025

I'd like to give my contribution on this issue as well. I've been facing the same issue while developing a bundle with its own dependency. Starting from the points made by @jahudka about

1.b) How do I then expose the fact that my assets rely on those dependencies to projects consuming my bundle?

As said, a bundle may have JS & CSS dependencies that would be required by the customer to be installed via importmap:require and the bundle to expose that in its documentation. As mentioned earlier this is not ideal for a bundle developer and for what I think too that the AssetMapper should be, ie a replacement for JS-based Package & Dependency Managers.

My project's solution was basically as follows:

  • Each bundles provides a importmap.php in its src/Resources folder and that importmap.php defines the assets dependency for that bundle.
  • A custom command, ran by the Symfony App, would look into each bundle's importmap and merges those with the one in the app. The code doesn't do any special dependency solving and deduping, instead it naively tries to solve version conflicts by either giving priority to the App's one or asking the user which one to install.
  • The user runs the command with bin/console import-assets or something like that.
    (The last two points would be integrated into the AssetMapper component when running command like importmap:install)

Then, to the point 2.

2. How do I expose my bundle's own assets to a consumer of the bundle?

The bundle, if it needs to expose his js/css files, it can register its AssetMapper paths & namespace as done for instance in ux-autocomplete too:

$container->prependExtensionConfig('framework', [
    'asset_mapper' => [
        'paths' => [ 
             __DIR__ .'/../../assets/dist' => '@acme/bundle'
        ]
    ]
]);

Then, it can register any file under that namespace.

//src/Resources/importmap.php
'@acme/bundle/bundle.js' => [
     'path' => '@acme/bundle/bundle.js' // this maps to $bundleDir/assets/dist/bundle.js
],
'@acme/bundle/bundle.css' => [
     'path' => '@acme/bundle/bundle.css', // this maps to $bundleDir/assets/dist/bundle.css
     'type' => 'css' // this is important to avoid MIME Type error
]

So the App app.js would just do the following and have the bundle dependencies "bundled" in the app.

import '@acme/bundle/bundle.js'
import '@acme/bundle/bundle.css'

(I believe these last steps are optional because, as mentioned by @smnandre and the docs, bundles files under src/Resources/public are automatically mapped to bundles/acme/.... and can be imported by import 'bundles/acme/bundle.js'.
I didn't follow this approach, but I believe it would work too.)

I have this thing working at the moment. I also have the bundle exposing Stimulus Controllers that I can extend from the app, but I won't dig into that.

The advantages of a similar solution are that:

  • Each bundle can define their JS & CSS dependencies and let the App import those dependencies in their app.js
  • Each bundle can define its own JS & CSS file and export the modules they want.
  • No Flex Recipe required.
  • The User needs to do no require or any install manually. Ideally all the importmap merging is handled by assets:install or importmap:install
  • By having a custom importmap.php, bundles can also expose their own Stimulus Controllers to the App.

Now my concerns are:

  • I don't know how to solve the Dedupe of the dependencies and version conflict, I would require input from someone in case such solution is accepted to be implemented.
  • I'm not sure how this fits with what the AssetMapper component is supposed to do but, in my opinion, it must support some sort of integration with dependencies coming from the bundles.

It's not a solution I'd propose in a PR as it is of course, but I hope that would give some input to get this issue's discussion going on and maybe we can come to a good solution for the AM Component .

I paste the snippet of code that "merges" the importmap just for reference and to give some input to other people that are facing the same issue. It's not a suggestion of using it in a production environment of course :)

 private function mergeImportmaps(InputInterface $input, OutputInterface $output, array $originalImportMap, array $bundleImportMaps): array
    {
        // First, we iterate over all bundles' importmaps and we put all the entries we find
        // in a entriesMap, where the Key is the entry name, and the value is another map with bundle as keys, and entry config as value
        /** @var array<string, array<string, array>> $entriesMap */
        $entriesMap = [];
        foreach ($bundleImportMaps as $bundle => $importMap) {
            foreach ($importMap as $entryName => $config) {
                if (isset($entriesMap[$entryName])) {
                    [$previousBundle, $previousConfig] = $entriesMap[$entryName];
                    // In case multiple bundles have the same entry, we keep the one with the higher version. (Here Dedupe would be needed instead)
                    if (isset($config['version'], $previousConfig['version']) && $config['version'] > $previousConfig['version']) {
                        $entriesMap[$entryName] = [$bundle, $config];
                    } elseif (!isset($config['version']) && !isset($previousConfig['version'])) {
                        // Same version, do nothing.
                    } else {
                        // some bundle have version defined, some doesn't. This is a config conflicts we highlight
                        $output->writeln(sprintf("<warning>Improtmap Conflict for entry <options=bold,underscore>%s</options>: Bundle %s's Version = %s, but Bundle %s's Version = %s</warning>",
                            $entryName, $bundle, $previousBundle, ($config['version'] ?? 'Not Defined'), $previousBundle['version'] ?? 'Not Defined'));
                    }
                } else {
                    $entriesMap[$entryName] = [$bundle, $config];
                }
            }
        }

        // No we iterate through all the entries we mapped.
        // If the entry already exists in the project importmap, we have two cases
        // 1.  Project's importmap has a higher or same version of all the mapped ones, so we don't merge it
        // 2. Project's importmap has a lower version of any of the mapped ones: we ask the user what to do.
        // Case 2. can be automated by having a command argument that defines the strategy (--update-versions=true/false)

        foreach ($entriesMap as $entryName => $map) {
            [$bundle, $config] = $map;
            if (!isset($originalImportMap[$entryName])) {
                // Simple case, project's import map doesn't have the entry, so we import the latest one.
                $originalImportMap[$entryName] = $config;
            } else {
                if (isset($originalImportMap[$entryName]['version']) && $originalImportMap[$entryName]['version'] > ($config['version'] ?? 0)) {
                    // Case 1, we keep original import map's entry
                    // Continue
                } elseif (isset($originalImportMap[$entryName]['version'], $config['version']) && $originalImportMap[$entryName]['version'] < $config['version']) {
                    $updateVersions = $input->getOption('update-versions');
                    $question = new ConfirmationQuestion(sprintf('Bundle %s requires asset %s with version %s but your project targets version %s. Do you want to update the asset to version %s',
                        $bundle, $entryName, $config['version'], $originalImportMap[$entryName]['version'], $config['version']));
                    $questionHelper = $this->getHelper('question');

                    if ($updateVersions || $questionHelper->ask($input, $output, $question)) {
                        $originalImportMap[$entryName] = $config;
                    }
                }
            }
        }

        return $originalImportMap;
    }

(Then, once the merge is done, the code proceeds to generate the `importmap.php` in the `{kernel.project_dir}/importmap.php`)

@tacman
Copy link
Contributor
tacman commented Jan 14, 2025

Thanks, @Guarrakesh, nice write-up!

@smnandre
Copy link
Member

Thank you for sharing @Guarrakesh !

Some technical comments so you can build your PR on something solid.

Using @ for namespaces, without active choice from the user, is a very dangerous thing to do, as any bundle (or dependency of any bundle could pretend to be @symfony or @bootstrap or what not. I’m 99% convinced this is the reason of the « bundles » namespace-y thing :)

So at minimum you wiill have to set up a prefix / spécial char / etc.

Regarding Stimulus controller I’m not sure to follow, as these are handled by the StimulusBundle, not the AssetMaper

You’re right about Bundles beeing automatically registered: that’s the main reason why everything you said possible with your solution is already possible without , BUT the installation without consent of dependencies.

Regarding conflicts, you should take a look at the ImportMapManager and the downloader, I think most of your suggested algorithm is already coded and used.

But this could not be a problem soon as importmap scopes are now baseline I believe, and this is to me THE most important features of import map files.

@Guarrakesh
Copy link

@smnandre thanks for your answer and for the clarification about the bundle prefix. I'll have a look at the ImportMapManager and the releated code.

But this could not be a problem soon as importmap scopes are now baseline I believe, and this is to me THE most important features of import map files.

Can you give some more context (or reference links) about importmap scopes? I'm afraid I've never came across them.

@smnandre
Copy link
Member

The main idea is to have different versions of a given dependency... resolved differently by ... scop :)

when called in /foo/foo.js you can use another/package in version 3.20
when called in /bar/bar.js, you can use another/package in version 2.10

Examples and (better) explanations here --> https://github.com/WICG/import-maps?tab=readme-ov-file#scoping-examples

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

0