Files
archived-symfony-docs/frontend/asset_mapper.rst
Wouter de Jong 221caaa455 Merge remote-tracking branch 'origin/7.1' into 7.2
* origin/7.1:
  Replaced `caution` directive by `warning`
  Replaced caution blocks with warning
2024-12-07 12:17:16 +01:00

1198 lines
44 KiB
ReStructuredText

AssetMapper: Simple, Modern CSS & JS Management
===============================================
The AssetMapper component lets you write modern JavaScript and CSS without the complexity
of using a bundler. Browsers *already* support many modern JavaScript features
like the ``import`` statement and ES6 classes. And the HTTP/2 protocol means that
combining your assets to reduce HTTP connections is no longer urgent. This component
is a light layer that helps serve your files directly to the browser.
The component has two main features:
* :ref:`Mapping & Versioning Assets <mapping-assets>`: All files inside of ``assets/``
are made available publicly and **versioned**. You can reference the file
``assets/images/product.jpg`` in a Twig template with ``{{ asset('images/product.jpg') }}``.
The final URL will include a version hash, like ``/assets/images/product-3c16d9220694c0e56d8648f25e6035e9.jpg``.
* :ref:`Importmaps <importmaps-javascript>`: A native browser feature that makes it easier
to use the JavaScript ``import`` statement (e.g. ``import { Modal } from 'bootstrap'``)
without a build system. It's supported in all browsers (thanks to a shim)
and is part of the `HTML standard <https://html.spec.whatwg.org/multipage/webappapis.html#import-maps>`_.
Installation
------------
To install the AssetMapper component, run:
.. code-block:: terminal
$ composer require symfony/asset-mapper symfony/asset symfony/twig-pack
In addition to ``symfony/asset-mapper``, this also makes sure that you have the
:doc:`Asset Component </components/asset>` and Twig available.
If you're using :ref:`Symfony Flex <symfony-flex>`, you're done! The recipe just
added a number of files:
* ``assets/app.js`` Your main JavaScript file;
* ``assets/styles/app.css`` Your main CSS file;
* ``config/packages/asset_mapper.yaml`` Where you define your asset "paths";
* ``importmap.php`` Your importmap config file.
It also *updated* the ``templates/base.html.twig`` file:
.. code-block:: diff
{% block javascripts %}
+ {% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
If you're not using Flex, you'll need to create & update these files manually. See
the `latest asset-mapper recipe`_ for the exact content of these files.
.. _mapping-assets:
Mapping and Referencing Assets
------------------------------
The AssetMapper component works by defining directories/paths of assets that you want to expose
publicly. These assets are then versioned and easy to reference. Thanks to the
``asset_mapper.yaml`` file, your app starts with one mapped path: the ``assets/``
directory.
If you create an ``assets/images/duck.png`` file, you can reference it in a template with:
.. code-block:: html+twig
<img src="{{ asset('images/duck.png') }}">
The path - ``images/duck.png`` - is relative to your mapped directory (``assets/``).
This is known as the **logical path** to your asset.
If you look at the HTML in your page, the URL will be something
like: ``/assets/images/duck-3c16d9220694c0e56d8648f25e6035e9.png``. If you change
the file, the version part of the URL will also change automatically.
.. _asset-mapper-compile-assets:
Serving Assets in dev vs prod
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the ``dev`` environment, the URL ``/assets/images/duck-3c16d9220694c0e56d8648f25e6035e9.png``
is handled and returned by your Symfony app.
For the ``prod`` environment, before deploy, you should run:
.. code-block:: terminal
$ php bin/console asset-map:compile
This will physically copy all the files from your mapped directories to
``public/assets/`` so that they're served directly by your web server.
See :ref:`Deployment <asset-mapper-deployment>` for more details.
.. warning::
If you run the ``asset-map:compile`` command on your development machine,
you won't see any changes made to your assets when reloading the page.
To resolve this, delete the contents of the ``public/assets/`` directory.
This will allow your Symfony application to serve those assets dynamically again.
.. tip::
If you need to copy the compiled assets to a different location (e.g. upload
them to S3), create a service that implements ``Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface``
and set its service id (or an alias) to ``asset_mapper.local_public_assets_filesystem``
(to replace the built-in service).
Debugging: Seeing All Mapped Assets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To see all of the mapped assets in your app, run:
.. code-block:: terminal
$ php bin/console debug:asset-map
This will show you all the mapped paths and the assets inside of each:
.. code-block:: text
AssetMapper Paths
------------------
--------- ------------------
Path Namespace prefix
--------- ------------------
assets
Mapped Assets
-------------
------------------ ----------------------------------------------------
Logical Path Filesystem Path
------------------ ----------------------------------------------------
app.js assets/app.js
styles/app.css assets/styles/app.css
images/duck.png assets/images/duck.png
The "Logical Path" is the path to use when referencing the asset, like
from a template.
The ``debug:asset-map`` command provides several options to filter results:
.. code-block:: terminal
# provide an asset name or dir to only show results that match it
$ php bin/console debug:asset-map bootstrap.js
$ php bin/console debug:asset-map style/
# provide an extension to only show that file type
$ php bin/console debug:asset-map --ext=css
# you can also only show assets in vendor/ dir or exclude any results from it
$ php bin/console debug:asset-map --vendor
$ php bin/console debug:asset-map --no-vendor
# you can also combine all filters (e.g. find bold web fonts in your own asset dirs)
$ php bin/console debug:asset-map bold --no-vendor --ext=woff2
.. versionadded:: 7.2
The options to filter ``debug:asset-map`` results were introduced in Symfony 7.2.
.. _importmaps-javascript:
Importmaps & Writing JavaScript
-------------------------------
All modern browsers support the JavaScript `import statement`_ and modern
`ES6`_ features like classes. So this code "just works":
.. code-block:: javascript
// assets/app.js
import Duck from './duck.js';
const duck = new Duck('Waddles');
duck.quack();
.. code-block:: javascript
// assets/duck.js
export default class {
constructor(name) {
this.name = name;
}
quack() {
console.log(`${this.name} says: Quack!`);
}
}
Thanks to the ``{{ importmap('app') }}`` Twig function call, which you'll learn about in
this section, the ``assets/app.js`` file is loaded & executed by the browser.
.. tip::
When importing relative files, be sure to include the ``.js`` filename extension.
Unlike in Node.js, this extension is required in the browser environment.
Importing 3rd Party JavaScript Packages
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Suppose you want to use an `npm package`_, like `bootstrap`_. Technically,
this can be done by importing its full URL, like from a CDN:
.. code-block:: javascript
import { Alert } from 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/+esm';
But yikes! Needing to include that URL is a pain! Instead, we can add this package
to our "importmap" via the ``importmap:require`` command. This command can be used
to add any `npm package`_:
.. code-block:: terminal
$ php bin/console importmap:require bootstrap
This adds the ``bootstrap`` package to your ``importmap.php`` file::
// importmap.php
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'bootstrap' => [
'version' => '5.3.0',
],
];
.. note::
Sometimes, a package - like ``bootstrap`` - will have one or more dependencies,
such as ``@popperjs/core``. The ``importmap:require`` command will add both the
main package *and* its dependencies. If a package includes a main CSS file,
that will also be added (see :ref:`Handling 3rd-Party CSS <asset-mapper-3rd-party-css>`).
.. note::
If you get a 404 error, there might be some issue with the JavaScript package
that prevents it from being served by the ``jsDelivr`` CDN. For example, the
package might be missing properties like ``main`` or ``module`` in its
`package.json configuration file`_. Try to contact the package maintainer to
ask them to fix those issues.
Now you can import the ``bootstrap`` package like usual:
.. code-block:: javascript
import { Alert } from 'bootstrap';
// ...
All packages in ``importmap.php`` are downloaded into an ``assets/vendor/`` directory,
which should be ignored by git (the Flex recipe adds it to ``.gitignore`` for you).
You'll need to run the following command to download the files on other computers
if some are missing:
.. code-block:: terminal
$ php bin/console importmap:install
You can update your third-party packages to their current versions by running:
.. code-block:: terminal
# lists outdated packages and shows their latest versions
$ php bin/console importmap:outdated
# updates all the outdated packages
$ php bin/console importmap:update
# you can also run the commands only for the given list of packages
$ php bin/console importmap:update bootstrap lodash
$ php bin/console importmap:outdated bootstrap lodash
How does the importmap Work?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
How does this ``importmap.php`` file allow you to import ``bootstrap``? That's
thanks to the ``{{ importmap() }}`` Twig function in ``base.html.twig``, which
outputs an `importmap`_:
.. code-block:: html
<script type="importmap">{
"imports": {
"app": "/assets/app-4e986c1a2318dd050b1d47db8d856278.js",
"/assets/duck.js": "/assets/duck-1b7a64b3b3d31219c262cf72521a5267.js",
"bootstrap": "/assets/vendor/bootstrap/bootstrap.index-f0935445d9c6022100863214b519a1f2.js"
}
}</script>
Import maps are a native browser feature. When you import ``bootstrap`` from
JavaScript, the browser will look at the ``importmap`` and see that it should
fetch the package from the associated path.
.. _automatic-import-mapping:
But where did the ``/assets/duck.js`` import entry come from? That doesn't live
in ``importmap.php``. Great question!
The ``assets/app.js`` file above imports ``./duck.js``. When you import a file using a
relative path, your browser looks for that file relative to the one importing
it. So, it would look for ``/assets/duck.js``. That URL *would* be correct,
except that the ``duck.js`` file is versioned. Fortunately, the AssetMapper component
sees the import and adds a mapping from ``/assets/duck.js`` to the correct, versioned
filename. The result: importing ``./duck.js`` just works!
The ``importmap()`` function also outputs an `ES module shim`_ so that
`older browsers <https://caniuse.com/import-maps>`_ understand importmaps
(see the :ref:`polyfill config <config-importmap-polyfill>`).
.. _app-entrypoint:
The "app" Entrypoint & Preloading
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An "entrypoint" is the main JavaScript file that the browser loads,
and your app starts with one by default::
// importmap.php
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
// ...
];
.. _importmap-app-entry:
In addition to the importmap, the ``{{ importmap('app') }}`` in
``base.html.twig`` outputs a few other things, including:
.. code-block:: html
<script type="module">import 'app';</script>
This line tells the browser to load the ``app`` importmap entry, which causes the
code in ``assets/app.js`` to be executed.
The ``importmap()`` function also outputs a set of "preloads":
.. code-block:: html
<link rel="modulepreload" href="/assets/app-4e986c1a2318dd050b1d47db8d856278.js">
<link rel="modulepreload" href="/assets/duck-1b7a64b3b3d31219c262cf72521a5267.js">
This is a performance optimization and you can learn more about below
in :ref:`Performance: Add Preloading <performance-preloading>`.
Importing Specific Files From a 3rd Party Package
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes you'll need to import a specific file from a package. For example,
suppose you're integrating `highlight.js`_ and want to import just the core
and a specific language:
.. code-block:: javascript
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
hljs.registerLanguage('javascript', javascript);
hljs.highlightAll();
In this case, adding the ``highlight.js`` package to your ``importmap.php`` file
won't work: whatever you import - e.g. ``highlight.js/lib/core`` - needs to
*exactly* match an entry in the ``importmap.php`` file.
Instead, use ``importmap:require`` and pass it the exact paths you need. This
also shows how you can require multiple packages at once:
.. code-block:: terminal
$ php bin/console importmap:require highlight.js/lib/core highlight.js/lib/languages/javascript
Global Variables like jQuery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You might be accustomed to relying on global variables - like jQuery's ``$``
variable:
.. code-block:: javascript
// assets/app.js
import 'jquery';
// app.js or any other file
$('.something').hide(); // WILL NOT WORK!
But in a module environment (like with AssetMapper), when you import
a library like ``jquery``, it does *not* create a global variable. Instead, you
should import it and set it to a variable in *every* file you need it:
.. code-block:: javascript
import $ from 'jquery';
$('.something').hide();
You can even do this from an inline script tag:
.. code-block:: html
<script type="module">
import $ from 'jquery';
$('.something').hide();
</script>
If you *do* need something to become a global variable, you do it manually
from inside ``app.js``:
.. code-block:: javascript
import $ from 'jquery';
// things on "window" become global variables
window.$ = $;
.. _asset-mapper-handling-css:
Handling CSS
------------
CSS can be added to your page by importing it from a JavaScript file. The default
``assets/app.js`` already imports ``assets/styles/app.css``:
.. code-block:: javascript
// assets/app.js
import '../styles/app.css';
// ...
When you call ``importmap('app')`` in ``base.html.twig``, AssetMapper parses
``assets/app.js`` (and any JavaScript files that it imports) looking for ``import``
statements for CSS files. The final collection of CSS files is rendered onto
the page as ``link`` tags in the order they were imported.
.. note::
Importing a CSS file is *not* something that is natively supported by
JavaScript modules. AssetMapper makes this work by adding a special importmap
entry for each CSS file. These special entries are valid, but do nothing.
AssetMapper adds a ``<link>`` tag for each CSS file, but when JavaScript
executes the ``import`` statement, nothing additional happens.
.. _asset-mapper-3rd-party-css:
Handling 3rd-Party CSS
~~~~~~~~~~~~~~~~~~~~~~
Sometimes a JavaScript package will contain one or more CSS files. For example,
the ``bootstrap`` package has a `dist/css/bootstrap.min.css file`_.
You can require CSS files in the same way as JavaScript files:
.. code-block:: terminal
$ php bin/console importmap:require bootstrap/dist/css/bootstrap.min.css
To include it on the page, import it from a JavaScript file:
.. code-block:: javascript
// assets/app.js
import 'bootstrap/dist/css/bootstrap.min.css';
// ...
.. tip::
Some packages - like ``bootstrap`` - advertise that they contain a CSS
file. In those cases, when you ``importmap:require bootstrap``, the
CSS file is also added to ``importmap.php`` for convenience. If some package
doesn't advertise its CSS file in the ``style`` property of the
`package.json configuration file`_ try to contact the package maintainer to
ask them to add that.
Paths Inside of CSS Files
~~~~~~~~~~~~~~~~~~~~~~~~~
From inside CSS, you can reference other files using the normal CSS ``url()``
function and a relative path to the target file:
.. code-block:: css
/* assets/styles/app.css */
.quack {
/* file lives at assets/images/duck.png */
background-image: url('../images/duck.png');
}
The path in the final ``app.css`` file will automatically include the versioned URL
for ``duck.png``:
.. code-block:: css
/* public/assets/styles/app-3c16d9220694c0e56d8648f25e6035e9.css */
.quack {
background-image: url('../images/duck-3c16d9220694c0e56d8648f25e6035e9.png');
}
.. _asset-mapper-tailwind:
Using Tailwind CSS
~~~~~~~~~~~~~~~~~~
To use the `Tailwind`_ CSS framework with the AssetMapper component, check out
`symfonycasts/tailwind-bundle`_.
.. _asset-mapper-sass:
Using Sass
~~~~~~~~~~
To use Sass with AssetMapper component, check out `symfonycasts/sass-bundle`_.
Lazily Importing CSS from a JavaScript File
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you have some CSS that you want to load lazily, you can do that via
the normal, "dynamic" import syntax:
.. code-block:: javascript
// assets/any-file.js
import('./lazy.css');
// ...
In this case, ``lazy.css`` will be downloaded asynchronously and then added to
the page. If you use a dynamic import to lazily-load a JavaScript file and that
file imports a CSS file (using the non-dynamic ``import`` syntax), that CSS file
will also be downloaded asynchronously.
Issues and Debugging
--------------------
There are a few common errors and problems you might run into.
Missing importmap Entry
~~~~~~~~~~~~~~~~~~~~~~~
One of the most common errors will come from your browser's console, and
will look something like this:
Failed to resolve module specifier " bootstrap". Relative references must start
with either "/", "./", or "../".
Or:
The specifier "bootstrap" was a bare specifier, but was not remapped to anything.
Relative module specifiers must start with "./", "../" or "/".
This means that, somewhere in your JavaScript, you're importing a 3rd party
package - e.g. ``import 'bootstrap'``. The browser tries to find this
package in your ``importmap`` file, but it's not there.
The fix is almost always to add it to your ``importmap``:
.. code-block:: terminal
$ php bin/console importmap:require bootstrap
.. note::
Some browsers, like Firefox, show *where* this "import" code lives, while
others like Chrome currently do not.
404 Not Found for a JavaScript, CSS or Image File
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes a JavaScript file you're importing (e.g. ``import './duck.js'``),
or a CSS/image file you're referencing won't be found, and you'll see a 404
error in your browser's console. You'll also notice that the 404 URL is missing
the version hash in the filename (e.g. a 404 to ``/assets/duck.js`` instead of
a path like ``/assets/duck.1b7a64b3b3d31219c262cf72521a5267.js``).
This is usually because the path is wrong. If you're referencing the file
directly in a Twig template:
.. code-block:: html+twig
<img src="{{ asset('images/duck.png') }}">
Then the path that you pass ``asset()`` should be the "logical path" to the
file. Use the ``debug:asset-map`` command to see all valid logical paths
in your app.
More likely, you're importing the failing asset from a CSS file (e.g.
``@import url('other.css')``) or a JavaScript file:
.. code-block:: javascript
// assets/controllers/farm-controller.js
import '../farm/chicken.js';
When doing this, the path should be *relative* to the file that's importing it
(and, in JavaScript files, should start with ``./`` or ``../``). In this case,
``../farm/chicken.js`` would point to ``assets/farm/chicken.js``. To
see a list of *all* invalid imports in your app, run:
.. code-block:: terminal
$ php bin/console cache:clear
$ php bin/console debug:asset-map
Any invalid imports will show up as warnings on top of the screen (make sure
you have ``symfony/monolog-bundle`` installed):
.. code-block:: text
WARNING [asset_mapper] Unable to find asset "../images/ducks.png" referenced in "assets/styles/app.css".
WARNING [asset_mapper] Unable to find asset "./ducks.js" imported from "assets/app.js".
Missing Asset Warnings on Commented-out Code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The AssetMapper component looks in your JavaScript files for ``import`` lines so
that it can :ref:`automatically add them to your importmap <automatic-import-mapping>`.
This is done via regex and works very well, though it isn't perfect. If you
comment-out an import, it will still be found and added to your importmap. That
doesn't harm anything, but could be surprising.
If the imported path cannot be found, you'll see warning log when that asset
is being built, which you can ignore.
.. _asset-mapper-deployment:
Deploying with the AssetMapper Component
----------------------------------------
When you're ready to deploy, "compile" your assets by running this command:
.. code-block:: terminal
$ php bin/console asset-map:compile
This will write all your versioned asset files into the ``public/assets/`` directory,
along with a few JSON files (``manifest.json``, ``importmap.json``, etc.) so that
the ``importmap`` can be rendered lightning fast.
.. _optimization:
Optimizing Performance
----------------------
To make your AssetMapper-powered site fly, there are a few things you need to
do. If you want to take a shortcut, you can use a service like `Cloudflare`_,
which will automatically do most of these things for you:
- **Use HTTP/2**: Your web server should be running HTTP/2 or HTTP/3 so the
browser can download assets in parallel. HTTP/2 is automatically enabled in Caddy
and can be activated in Nginx and Apache. Or, proxy your site through a
service like Cloudflare, which will automatically enable HTTP/2 for you.
- **Compress your assets**: Your web server should compress (e.g. using gzip)
your assets (JavaScript, CSS, images) before sending them to the browser. This
is automatically enabled in Caddy and can be activated in Nginx and Apache.
In Cloudflare, assets are compressed by default.
- **Set long-lived cache expiry**: Your web server should set a long-lived
``Cache-Control`` HTTP header on your assets. Because the AssetMapper component includes a version
hash in the filename of each asset, you can safely set ``max-age``
to a very long time (e.g. 1 year). This isn't automatic in
any web server, but can be easily enabled.
Once you've done these things, you can use a tool like `Lighthouse`_ to
check the performance of your site.
.. _performance-preloading:
Performance: Understanding Preloading
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
One issue that Lighthouse may report is:
Avoid Chaining Critical Requests
To understand the problem, imagine this theoretical setup:
- ``assets/app.js`` imports ``./duck.js``
- ``assets/duck.js`` imports ``bootstrap``
Without preloading, when the browser downloads the page, the following would happen:
1. The browser downloads ``assets/app.js``;
2. It *then* sees the ``./duck.js`` import and downloads ``assets/duck.js``;
3. It *then* sees the ``bootstrap`` import and downloads ``assets/bootstrap.js``.
Instead of downloading all 3 files in parallel, the browser would be forced to
download them one-by-one as it discovers them. That would hurt performance.
AssetMapper avoids this problem by outputting "preload" ``link`` tags.
The logic works like this:
**A) When you call ``importmap('app')`` in your template**, the AssetMapper component
looks at the ``assets/app.js`` file and finds all of the JavaScript files
that it imports or files that those files import, etc.
**B) It then outputs a ``link`` tag** for each of those files with a ``rel="preload"``
attribute. This tells the browser to start downloading those files immediately,
even though it hasn't yet seen the ``import`` statement for them.
Additionally, if the :doc:`WebLink Component </web_link>` is available in your application,
Symfony will add a ``Link`` header in the response to preload the CSS files.
Frequently Asked Questions
--------------------------
Does the AssetMapper Component Combine Assets?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nope! But that's because this is no longer necessary!
In the past, it was common to combine assets to reduce the number of HTTP
requests that were made. Thanks to advances in web servers like
HTTP/2, it's typically not a problem to keep your assets separate and let the
browser download them in parallel. In fact, by keeping them separate, when
you update one asset, the browser can continue to use the cached version of
all of your other assets.
See :ref:`Optimization <optimization>` for more details.
Does the AssetMapper Component Minify Assets?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nope! In most cases, this is perfectly fine. The web asset compression performed
by web servers before sending them is usually sufficient. However, if you think
you could benefit from minifying assets (in addition to later compressing them),
you can use the `SensioLabs Minify Bundle`_.
This bundle integrates seamlessly with AssetMapper and minifies all web assets
automatically when running the ``asset-map:compile`` command (as explained in
the :ref:`serving assets in production <asset-mapper-compile-assets>` section).
See :ref:`Optimization <optimization>` for more details.
Is the AssetMapper Component Production Ready? Is it Performant?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yes! Very! The AssetMapper component leverages advances in browser technology (like
importmaps and native ``import`` support) and web servers (like HTTP/2, which allows
assets to be downloaded in parallel). See the other questions about minimization
and combination and :ref:`Optimization <optimization>` for more details.
The https://ux.symfony.com site runs on the AssetMapper component and has a 99%
Google Lighthouse score.
Does the AssetMapper Component work in All Browsers?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yes! Features like importmaps and the ``import`` statement are supported
in all modern browsers, but the AssetMapper component ships with an `ES module shim`_
to support ``importmap`` in old browsers. So, it works everywhere (see note
below).
Inside your own code, if you're relying on modern `ES6`_ JavaScript features
like the `class syntax`_, this is supported in all but the oldest browsers.
If you *do* need to support very old browsers, you should use a tool like
:ref:`Encore <frontend-webpack-encore>` instead of the AssetMapper component.
.. note::
The `import statement`_ can't be polyfilled or shimmed to work on *every*
browser. However, only the **oldest** browsers don't support it - basically
IE 11 (which is no longer supported by Microsoft and has less than .4%
of global usage).
The ``importmap`` feature **is** shimmed to work in **all** browsers by the
AssetMapper component. However, the shim doesn't work with "dynamic" imports:
.. code-block:: javascript
// this works
import { add } from './math.js';
// this will not work in the oldest browsers
import('./math.js').then(({ add }) => {
// ...
});
If you want to use dynamic imports and need to support certain older browsers
(https://caniuse.com/import-maps), you can use an ``importShim()`` function
from the shim: https://www.npmjs.com/package/es-module-shims#user-content-polyfill-edge-case-dynamic-import
Can I Use it with Sass or Tailwind?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sure! See :ref:`Using Tailwind CSS <asset-mapper-tailwind>` or :ref:`Using Sass <asset-mapper-sass>`.
Can I Use it with TypeScript?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sure! See :ref:`Using TypeScript <asset-mapper-ts>`.
Can I Use it with JSX or Vue?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Probably not. And if you're writing an application in React, Svelte or another
frontend framework, you'll probably be better off using *their* tools directly.
JSX *can* be compiled directly to a native JavaScript file but if you're using a lot of JSX,
you'll probably want to use a tool like :ref:`Encore <frontend-webpack-encore>`.
See the `UX React Documentation`_ for more details about using it with the AssetMapper
component.
Vue files *can* be written in native JavaScript, and those *will* work with
the AssetMapper component. But you cannot write single-file components (i.e. ``.vue``
files) with component, as those must be used in a build system. See the
`UX Vue.js Documentation`_ for more details about using with the AssetMapper
component.
Can I Lint and Format My Code?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Not with AssetMapper, but you can install `kocal/biome-js-bundle`_ in your project
to lint and format your front-end assets. It's much faster than alternatives like
Prettier and requires no configuration to handle your JavaScript, TypeScript and CSS files.
.. _asset-mapper-ts:
Using TypeScript
----------------
To use TypeScript with the AssetMapper component, check out `sensiolabs/typescript-bundle`_.
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>``. For example, if you're using `BabdevPagerfantaBundle`_
and you run the ``debug:asset-map`` command, you'll see an asset whose logical
path is ``bundles/babdevpagerfanta/css/pagerfanta.css``.
This means you can render these assets in your templates using the
``asset()`` function:
.. code-block:: html+twig
<link rel="stylesheet" href="{{ asset('bundles/babdevpagerfanta/css/pagerfanta.css') }}">
Actually, this path - ``bundles/babdevpagerfanta/css/pagerfanta.css`` - already
works in applications *without* the AssetMapper component, because the ``assets:install``
command copies the assets from bundles into ``public/bundles/``. However, when
the AssetMapper component is enabled, the ``pagerfanta.css`` file will automatically
be versioned! It will output something like:
.. code-block:: html+twig
<link rel="stylesheet" href="/assets/bundles/babdevpagerfanta/css/pagerfanta-ea64fc9c55f8394e696554f8aeb81a8e.css">
Overriding 3rd-Party Assets
~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to override a 3rd-party asset, you can do that by creating a
file in your ``assets/`` directory with the same name. For example, if you
want to override the ``pagerfanta.css`` file, create a file at
``assets/bundles/babdevpagerfanta/css/pagerfanta.css``. This file will be
used instead of the original file.
.. note::
If a bundle renders their *own* assets, but they use a non-default
:ref:`asset package <asset-packages>`, then the AssetMapper component will
not be used. This happens, for example, with `EasyAdminBundle`_.
Importing Assets Outside of the ``assets/`` Directory
-----------------------------------------------------
You *can* import assets that live outside of your asset path
(i.e. the ``assets/`` directory). For example:
.. code-block:: css
/* assets/styles/app.css */
/* you can reach above assets/ */
@import url('../../vendor/babdev/pagerfanta-bundle/Resources/public/css/pagerfanta.css');
However, if you get an error like this:
The "app" importmap entry contains the path "vendor/some/package/assets/foo.js"
but it does not appear to be in any of your asset paths.
It means that you're pointing to a valid file, but that file isn't in any of
your asset paths. You can fix this by adding the path to your ``asset_mapper.yaml``
file:
.. code-block:: yaml
# config/packages/asset_mapper.yaml
framework:
asset_mapper:
paths:
- assets/
- vendor/some/package/assets
Then try the command again.
Configuration Options
---------------------
You can see every available configuration options and some info by running:
.. code-block:: terminal
$ php bin/console config:dump framework asset_mapper
Some of the more important options are described below.
``framework.asset_mapper.paths``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This config holds all of the directories that will be scanned for assets. This
can be a simple list:
.. code-block:: yaml
framework:
asset_mapper:
paths:
- assets/
- vendor/some/package/assets
Or you can give each path a "namespace" that will be used in the asset map:
.. code-block:: yaml
framework:
asset_mapper:
paths:
assets/: ''
vendor/some/package/assets/: 'some-package'
In this case, the "logical path" to all of the files in the ``vendor/some/package/assets/``
directory will be prefixed with ``some-package`` - e.g. ``some-package/foo.js``.
.. _excluded_patterns:
``framework.asset_mapper.excluded_patterns``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is a list of glob patterns that will be excluded from the asset map:
.. code-block:: yaml
framework:
asset_mapper:
excluded_patterns:
- '*/*.scss'
You can use the ``debug:asset-map`` command to double-check that the files
you expect are being included in the asset map.
``framework.asset_mapper.exclude_dotfiles``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Whether to exclude any file starting with a ``.`` from the asset mapper. This
is useful if you want to avoid leaking sensitive files like ``.env`` or
``.gitignore`` in the files published by the asset mapper.
.. code-block:: yaml
framework:
asset_mapper:
exclude_dotfiles: true
This option is enabled by default.
.. _config-importmap-polyfill:
``framework.asset_mapper.importmap_polyfill``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Configure the polyfill for older browsers. By default, the `ES module shim`_ is loaded
via a CDN (i.e. the default value for this setting is ``es-module-shims``):
.. code-block:: yaml
framework:
asset_mapper:
# set this option to false to disable the shim entirely
# (your website/web app won't work in old browsers)
importmap_polyfill: false
# you can also use a custom polyfill by adding it to your importmap.php file
# and setting this option to the key of that file in the importmap.php file
# importmap_polyfill: 'custom_polyfill'
.. tip::
You can tell the AssetMapper to load the `ES module shim`_ locally by
using the following command, without changing your configuration:
.. code-block:: terminal
$ php bin/console importmap:require es-module-shims
``framework.asset_mapper.importmap_script_attributes``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is a list of attributes that will be added to the ``<script>`` tags
rendered by the ``{{ importmap() }}`` Twig function:
.. code-block:: yaml
framework:
asset_mapper:
importmap_script_attributes:
crossorigin: 'anonymous'
Page-Specific CSS & JavaScript
------------------------------
Sometimes you may choose to include CSS or JavaScript files only on certain
pages. For JavaScript, an easy way is to load the file with a `dynamic import`_:
.. code-block:: javascript
const someCondition = '...';
if (someCondition) {
import('./some-file.js');
// or use async/await
// const something = await import('./some-file.js');
}
Another option is to create a separate :ref:`entrypoint <app-entrypoint>`. For
example, create a ``checkout.js`` file that contains whatever JavaScript and
CSS you need:
.. code-block:: javascript
// assets/checkout.js
import './checkout.css';
// ...
Next, add this to ``importmap.php`` and mark it as an entrypoint::
// importmap.php
return [
// the 'app' entrypoint ...
'checkout' => [
'path' => './assets/checkout.js',
'entrypoint' => true,
],
];
Finally, on the page that needs this JavaScript, call ``importmap()`` and pass
both ``app`` and ``checkout``:
.. code-block:: twig
{# templates/products/checkout.html.twig #}
{#
Override an "importmap" block from base.html.twig.
If you don't have that block, add it around the {{ importmap('app') }} call.
#}
{% block importmap %}
{# do NOT call parent() #}
{{ importmap(['app', 'checkout']) }}
{% endblock %}
By passing both ``app`` and ``checkout``, the ``importmap()`` function will
output the ``importmap`` and also add a ``<script type="module">`` tag that
loads the ``app.js`` file *and* the ``checkout.js`` file. It's important
to *not* call ``parent()`` in the ``importmap`` block. Each page can only
have *one* importmap, so ``importmap()`` must be called exactly once.
If, for some reason, you want to execute *only* ``checkout.js``
and *not* ``app.js``, pass only ``checkout`` to ``importmap()``.
Using a Content Security Policy (CSP)
-------------------------------------
If you're using a `Content Security Policy`_ (CSP) to prevent cross-site
scripting attacks, the inline ``<script>`` tags rendered by the ``importmap()``
function will likely violate that policy and will not be executed by the browser.
To allow these scripts to run without disabling the security provided by
the CSP, you can generate a secure random string for every request (called
a *nonce*) and include it in the CSP header and in a ``nonce`` attribute on
the ``<script>`` tags.
The ``importmap()`` function accepts an optional second argument that can be
used to pass attributes to the rendered ``<script>`` tags.
You can use the `NelmioSecurityBundle`_ to generate the nonce and include
it in the CSP header, and then pass the same nonce to the Twig function:
.. code-block:: twig
{# the csp_nonce() function is defined by the NelmioSecurityBundle #}
{{ importmap('app', {'nonce': csp_nonce('script')}) }}
Content Security Policy and CSS Files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If your importmap includes CSS files, AssetMapper uses a trick to load those by
adding ``data:application/javascript`` to the rendered importmap (see
:ref:`Handling CSS <asset-mapper-handling-css>`).
This can cause browsers to report CSP violations and block the CSS files from
being loaded. To prevent this, you can add `strict-dynamic`_ to the ``script-src``
directive of your Content Security Policy, to tell the browser that the importmap
is allowed to load other resources.
.. note::
When using ``strict-dynamic``, the browser will ignore any other sources in
``script-src`` such as ``'self'`` or ``'unsafe-inline'``, so any other
``<script>`` tags will also need to be trusted via a nonce.
The AssetMapper Component Caching System in dev
-----------------------------------------------
When developing your app in debug mode, the AssetMapper component will calculate the
content of each asset file and cache it. Whenever that file changes, the component
will automatically re-calculate the content.
The system also accounts for "dependencies": If ``app.css`` contains
``@import url('other.css')``, then the ``app.css`` file contents will also be
re-calculated whenever ``other.css`` changes. This is because the version hash of ``other.css``
will change... which will cause the final content of ``app.css`` to change, since
it includes the final ``other.css`` filename inside.
Mostly, this system just works. But if you have a file that is not being
re-calculated when you expect it to, you can run:
.. code-block:: terminal
$ php bin/console cache:clear
This will force the AssetMapper component to re-calculate the content of all files.
Run Security Audits on Your Dependencies
----------------------------------------
Similar to ``npm``, the AssetMapper component comes bundled with a
command that checks security vulnerabilities in the dependencies of your application:
.. code-block:: terminal
$ php bin/console importmap:audit
-------- --------------------------------------------- --------- ------- ---------- -----------------------------------------------------
Severity Title Package Version Patched in More info
-------- --------------------------------------------- --------- ------- ---------- -----------------------------------------------------
Medium jQuery Cross Site Scripting vulnerability jquery 3.3.1 3.5.0 https://api.github.com/advisories/GHSA-257q-pV89-V3xv
High Prototype Pollution in JSON5 via Parse Method json5 1.0.0 1.0.2 https://api.github.com/advisories/GHSA-9c47-m6qq-7p4h
Medium semver vulnerable to RegExp Denial of Service semver 4.3.0 5.7.2 https://api.github.com/advisories/GHSA-c2qf-rxjj-qqgw
Critical Prototype Pollution in minimist minimist 1.1.3 1.2.6 https://api.github.com/advisories/GHSA-xvch-5gv4-984h
Medium ESLint dependencies are vulnerable minimist 1.1.3 1.2.2 https://api.github.com/advisories/GHSA-7fhm-mqm4-2wp7
Medium Bootstrap Vulnerable to Cross-Site Scripting bootstrap 4.1.3 4.3.1 https://api.github.com/advisories/GHSA-9v3M-8fp8-mi99
-------- --------------------------------------------- --------- ------- ---------- -----------------------------------------------------
7 packages found: 7 audited / 0 skipped
6 vulnerabilities found: 1 Critical / 1 High / 4 Medium
The command will return the ``0`` exit code if no vulnerability is found, or
the ``1`` exit code otherwise. This means that you can seamlessly integrate this
command as part of your CI to be warned anytime a new vulnerability is found.
.. tip::
The command takes a ``--format`` option to choose the output format between
``txt`` and ``json``.
.. _latest asset-mapper recipe: https://github.com/symfony/recipes/tree/main/symfony/asset-mapper
.. _import statement: https://caniuse.com/es6-module-dynamic-import
.. _ES6: https://caniuse.com/es6
.. _npm package: https://www.npmjs.com
.. _importmap: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
.. _bootstrap: https://www.npmjs.com/package/bootstrap
.. _ES module shim: https://www.npmjs.com/package/es-module-shims
.. _highlight.js: https://www.npmjs.com/package/highlight.js
.. _class syntax: https://caniuse.com/es6-class
.. _UX React Documentation: https://symfony.com/bundles/ux-react/current/index.html
.. _UX Vue.js Documentation: https://symfony.com/bundles/ux-vue/current/index.html
.. _Lighthouse: https://developers.google.com/web/tools/lighthouse
.. _Tailwind: https://tailwindcss.com/
.. _BabdevPagerfantaBundle: https://github.com/BabDev/PagerfantaBundle
.. _Cloudflare: https://www.cloudflare.com/
.. _EasyAdminBundle: https://github.com/EasyCorp/EasyAdminBundle
.. _symfonycasts/tailwind-bundle: https://symfony.com/bundles/TailwindBundle/current/index.html
.. _symfonycasts/sass-bundle: https://symfony.com/bundles/SassBundle/current/index.html
.. _sensiolabs/typescript-bundle: https://github.com/sensiolabs/AssetMapperTypeScriptBundle
.. _`dist/css/bootstrap.min.css file`: https://www.jsdelivr.com/package/npm/bootstrap?tab=files&path=dist%2Fcss#tabRouteFiles
.. _`dynamic import`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
.. _`package.json configuration file`: https://docs.npmjs.com/creating-a-package-json-file
.. _Content Security Policy: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
.. _NelmioSecurityBundle: https://symfony.com/bundles/NelmioSecurityBundle/current/index.html#nonce-for-inline-script-handling
.. _strict-dynamic: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic
.. _kocal/biome-js-bundle: https://github.com/Kocal/BiomeJsBundle
.. _`SensioLabs Minify Bundle`: https://github.com/sensiolabs/minify-bundle