Ryan Weaver Kévin Dunglas
Contributed by Ryan Weaver and Kévin Dunglas in #50112

Handling web assets in web projects is a continuously changing feature. Browsers and front-end technologies evolve a lot and Symfony has to adapt to them. In the past, Symfony included Assetic as a web asset handling pipeline. It could combine, compile and filter assets before serving them in your application.

In 2017, we introduced Webpack Encore as a modern alternative to Assetic based on Webpack and with endless features to handle your web assets. It can be a bit overwhelming to newcomers, but once set up, this asset building pipeline is simple to manage and works great.

Although we're happy with the Webpack Encore based solution, browsers have recently added support for a game-changing feature called import maps. An import map is a JSON object that tells the browser how to resolve modules when importing JavaScript modules. It maps the module names to their locations (as relative paths or absolute URLs).

For example, if you add this to the HTML of your web pages:

1
2
3
4
5
6
7
8
<script type="importmap">
  {
    "imports": {
      "square": "./module/shapes/square.js",
      "circle": "https://example.com/shapes/circle.js"
    }
  }
</script>

You can use the following in your JavaScript code:

1
2
3
4
import { name as squareName, draw } from "square";
import { name as circleName } from "circle";

// ...

You don't need to build and compile the assets. The browser can find the built/compiled modules in the paths/URLs provided by the import map. In Symfony 6.3 we're introducing a new AssetMapper component which allows you to use import maps to handle your assets. This component makes unnecessary to use Webpack, Webpack Encore, Node.js, yarn/npm, etc.

The component is divided into two main features:

  1. A feature to map assets to publicly available and versioned paths;
  2. A feature to use import maps in your front-end code.

Mapping Assets to Paths

Here's a quick overview of how this component works when mapping assets:

(1) Activate the asset mapper by telling Symfony the path that will be used to serve them:

1
2
3
4
# config/packages/framework.yaml
framework:
    asset_mapper:
        paths: ['assets/']

(2) Put your built/compiled assets in the <your-project>/assets/ directory (this is the same you do when using Webpack Encore):

1
2
3
4
5
6
7
your-project/
    assets/
        app.js
        styles/
            app.css
        images/
            logo.png

(3) Refer to those assets with the normal asset() function that you know:

1
2
3
4
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
<script src="{{ asset('app.js') }}" defer></script>

<img src="{{ asset('images/logo.png') }}">

That's all. The final paths used by the browser will look like this:

1
2
3
<link rel="stylesheet" href="/assets/styles/app-b93e5de06d9459ec9c39f10d8f9ce5b2.css">
<script src="/assets/app-1fcc5be55ce4e002a3016a5f6e1d0174.js" defer type="module"></script>
<img src="/assets/images/logo-3f24cba25ce4e114a3116b5f6f1d2159.png">

How does it work behind the scenes?

  • In the dev environment, a listener intercepts the requests to the path that you configured earlier (assets/ in this case), finds the file in the source <your-project>/assets/ directory, and returns it;
  • In the prod environment, you run a new asset-map:compile command, which copies all of the assets into public/assets/ so that the real files are returned. This command also dumps a public/assets/manifest.json so that the source paths (e.g. styles/app.css) can be exchanged for their final paths quickly.

Internally, this component provides a basic compiler to do things like updating the value of url() statements included in CSS files, to update the URLs in the source maps, etc. We're not recreating Assetic, but we need to provide these basic compilation features to make this component useful.

Working with Import Maps

The import maps feature included in AssetMapper component works as follows. In your JavaScript code, you import modules in the same way as before:

1
2
3
4
5
// assets/app.js
import { Application } from '@hotwired/stimulus';
import CoolStuff from './cool_stuff.js';

// ...

The difference is that now you don't have to use npm/yarn to install those JavaScript dependencies. Instead, run the importmap:require command to "install" those dependencies:

1
$ php bin/console importmap:require '@hotwired/stimulus';

This command will create or update an importmap.php at the root of your project:

1
2
3
4
5
6
7
8
9
return [
    'app' => [
        'path' => 'app.js',
        'preload' => true,
    ],
    '@hotwired/stimulus' => [
        'url' => 'https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js',
    ],
];

The final step is to add the new {{ importmap() }} function inside the <head> tag of all your pages. The end result will be something like:

1
2
3
4
5
6
7
8
9
10
11
<script type="importmap">
    { "imports": {
        "app": "/assets/app-cf9cfe84e945a554b2f1f64342d542bc.js",
        "cool_stuff.js": "/assets/cool_stuff-10b27bd6986c75a1e69c8658294bf22c.js",
        "@hotwired/stimulus": "https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js",
    }}
</script>

<link rel="modulepreload" href="/assets/app-cf9cfe84e945a554b2f1f64342d542bc.js">
<link rel="modulepreload" href="/assets/cool_stuff-10b27bd6986c75a1e69c8658294bf22c.js">
<script type="module">import 'app';</script>

There are many other great features provided by AssetMapper. We're still writing the docs for it and we hope to have them ready soon after the Symfony 6.3 release. Also, the upcoming SymfonyOnline June 2023 conference will include two different talks related to this component: Modern UIs with UX, a little JS & Zero Node (by Ryan Weaver) and AssetMapper: Manage Your JS Deps Without Node (by Kévin Dunglas).

Published in #Living on the edge