Symfony AssetMapper: Bundlers Are History
AI generated
SF
{ }
Symfony · AssetMapper · Import Maps · ES Modules
Symfony AssetMapper:
Bundlers Are History

Configuring Webpack, maintaining npm packages, debugging build pipelines, this often costs more time in Symfony projects than the actual frontend logic. Symfony AssetMapper makes the bundler unnecessary for most projects: import maps, automatic versioning and ES modules directly from PHP, without a Node.js build step.

16 min read AssetMapper · Import Maps · Stimulus · Versioning · Migration Symfony 6.3+ · PHP 8.2+ · No Node.js required

1. Why Symfony AssetMapper Replaces the Bundler

The classic frontend workflow in Symfony projects looked like this for years: configure Webpack Encore, run npm install, debug build errors after Node.js updates, run separate watcher processes alongside the PHP development server. All of this before a single line of JavaScript was even written. Symfony AssetMapper, introduced in Symfony 6.3 and stabilized in 6.4, removes this entire layer: it uses native browser features, specifically import maps and ES modules, which are supported by all modern browsers today, without requiring a bundling step.

The decisive technological shift behind this: modern browsers can resolve ES modules natively. That means an import { createApp } from 'vue' in the browser works without a bundler, as long as an import map tells the browser which URL the package lives at. Symfony AssetMapper manages exactly these import maps, it maps package names to versioned URLs, handles cache busting through file hashes and serves assets directly from the assets/ directory, without a compilation step. For projects without complex build pipelines, and in practice these are most traditional Symfony web applications, that is a substantial gain in simplicity.

2. Installation and Initial Configuration

Symfony AssetMapper has been part of the Symfony core package since Symfony 6.3 and needs no external installation. In new projects created with symfony new --webapp, it is active by default. In existing projects, you install it via composer require symfony/asset-mapper symfony/asset. The Symfony Flex recipe automatically creates the directory structure: assets/app.js as the entry point, assets/styles/app.css for global styles and the importmap.php file at the project root, which contains the import map configuration.

The configuration in config/packages/asset_mapper.yaml defines which directories Symfony AssetMapper manages as asset sources. By default this is only assets/, but you can add as many paths as you like, for example when a bundle ships its own assets or assets from multiple modules need to be merged. In templates, assets are included with the Twig function asset(), which automatically generates the versioned path with a hash suffix. The import map tag {{ importmap('app') }} injects the full import map plus the entry point into the HTML document, a single line replaces the entire script and link block that Webpack Encore used to generate.


<?php
// importmap.php, managed by Symfony AssetMapper
// Do not edit manually; use: bin/console importmap:require <package>

return [
    'app' => [
        // Entry point: assets/app.js
        'path' => 'app.js',
        'entrypoint' => true,
    ],
    '@hotwired/stimulus' => [
        // Downloaded and versioned locally by AssetMapper
        'version' => '3.2.2',
    ],
    '@symfony/stimulus-bundle' => [
        'path' => '@symfony/stimulus-bundle/loader.js',
    ],
    'bootstrap' => [
        'version' => '5.3.3',
    ],
    'bootstrap/dist/css/bootstrap.min.css' => [
        'version' => '5.3.3',
        'type' => 'css',
    ],
];

3. Import Maps: A Browser-Native Module System

Import maps are a web standard (WHATWG) that is natively available in the browser. They allow bare imports such as import { Controller } from '@hotwired/stimulus' to be resolved to concrete URLs, without a bundler rewriting the imports. The <script type="importmap"> tag in the HTML contains a JSON object that maps package names to URLs. Symfony AssetMapper generates this tag automatically from the importmap.php configuration and resolves versioned asset URLs in the process, including a content hash for reliable cache busting.

The result in the generated HTML: the browser receives an import map with all known packages and their URLs, followed by the entry point script. All imports in assets/app.js and transitive modules are resolved directly by the browser against the import map, no bundle, no Webpack output, no CommonJS wrappers. Symfony AssetMapper downloads vendor packages locally by default and serves them from your own domain, which substantially simplifies CSP configuration and fixes external dependencies at build time instead of loading them from a CDN at runtime.


{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    {# AssetMapper injects the full Import Map + entrypoint script #}
    {{ importmap('app') }}

    {# CSS assets, versioned automatically with content hash #}
    <link rel="stylesheet" href="{{ asset('styles/app.css') }}">
</head>
<body>
    {% block body %}{% endblock %}
</body>
</html>

{# Generated HTML output (simplified): #}
{#
<script type="importmap">
{
  "imports": {
    "app": "/assets/app-a1b2c3d4.js",
    "@hotwired/stimulus": "/assets/vendor/@hotwired/stimulus-e5f6a7b8.js",
    "bootstrap": "/assets/vendor/bootstrap-c9d0e1f2.js"
  }
}
</script>
<script type="module">import 'app';</script>
#}

4. Adding Vendor Packages Without npm

Adding external JavaScript packages with Symfony AssetMapper happens via the command bin/console importmap:require package-name. The command adds the package to importmap.php and downloads it from jsDelivr into the assets/vendor/ directory. From there it is versioned and served like a normal asset, without npm, without node_modules, without package lock conflicts. The package can be imported immediately in JavaScript by its package name, because Symfony AssetMapper updates the import map.

Updates to vendor packages happen with bin/console importmap:update, it checks all registered packages for newer versions and updates importmap.php as well as the downloaded files in the assets/vendor/ directory. The assets/vendor/ directory does not belong in version control and is excluded via .gitignore. Instead you only commit importmap.php, and after a git clone, bin/console importmap:install restores all vendor files again, analogous to composer install for PHP packages.

5. Automatic Versioning and Cache Busting

One of the central features of Symfony AssetMapper is automatic versioning of all assets. Every file in the watched assets/ directory receives a content hash as part of the file name at deployment time: app.js becomes app-a1b2c3d4.js. This enables aggressive browser caching with long Cache-Control: max-age headers, because the file name changes with every change and thus always loads fresh content. Twig templates use the asset() function, which automatically resolves the versioned path, in the template it reads asset('images/logo.svg'), in the HTML output /assets/images/logo-f7e8d9c0.svg appears.

In development mode, Symfony AssetMapper works without hashes and serves assets directly from the file system, every change is immediately visible in the browser, without a build step. In production mode, bin/console asset-map:compile generates all versioned files in the public/assets/ directory. This step replaces the Webpack build in the deployment workflow and is substantially faster: no transpilation, no tree shaking, no bundling, only file copying with hash renaming. The result is ready immediately and the public/assets/ directory can be cached directly by a CDN or reverse proxy.

6. Stimulus and Symfony UX Without a Build Step

Symfony AssetMapper is fully integrated into the Symfony UX ecosystem. Stimulus controllers, the JavaScript micro-framework behind Symfony UX, run without Webpack: after composer require symfony/ux-stimulus-bundle and bin/console importmap:require @hotwired/stimulus, Stimulus is available without an npm step. Symfony AssetMapper automatically scans the assets/controllers/ directory for Stimulus controllers and registers them, so that data-controller="hello" in the HTML is immediately connected to assets/controllers/hello_controller.js.

Symfony UX components such as Turbo, Chart.js integration or Cropperjs follow the same pattern: the PHP package is installed via Composer, Symfony Flex registers the necessary JavaScript dependencies in importmap.php, and after bin/console importmap:install they are usable. The assets/controllers.json format from the old Webpack Encore era is also supported by Symfony AssetMapper, which simplifies migrating existing UX projects. The entire UX workflow, writing a Stimulus controller, using it in templates, seeing it in the browser, works without a single npm command.


// assets/controllers/dropdown_controller.js
// Stimulus controller, no build step needed with Symfony AssetMapper
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    // Define target elements accessible via this.xTarget
    static targets = ['menu', 'button'];

    // Define values with type and default
    static values = {
        open: { type: Boolean, default: false },
    };

    connect() {
        // Called when the controller connects to the DOM
        this.updateMenu();
    }

    toggle() {
        this.openValue = !this.openValue;
    }

    openValueChanged() {
        this.updateMenu();
    }

    updateMenu() {
        // Toggle ARIA and visibility based on state value
        this.menuTarget.hidden = !this.openValue;
        this.buttonTarget.setAttribute('aria-expanded', String(this.openValue));
    }
}

7. Managing CSS With AssetMapper

CSS files are managed by Symfony AssetMapper the same way as JavaScript assets: every file in the assets/ directory is versioned and referenced via the asset() function. Global styles live in assets/styles/app.css, component-specific CSS files in subfolders. CSS imports via @import are resolved by Symfony AssetMapper, but without PostCSS transformations, because no build step runs. That means: vanilla CSS features like custom properties, :is(), :has() and CSS nesting work directly thanks to modern browsers, while CSS preprocessors like Sass are not usable without extra tooling.

Anyone who wants to use Tailwind CSS in a Symfony AssetMapper project falls back on the Tailwind CLI binary, which works independently of npm: ./tailwindcss -i assets/styles/app.css -o public/assets/styles/app.min.css --watch watches for changes and rebuilds the CSS file without a Node.js project. In the deployment workflow, the Tailwind CLI runs as a separate build step before asset-map:compile compiles the remaining assets. This combination makes Tailwind usable without the full npm ecosystem: no package.json, no node_modules, only the standalone binary.

8. Migrating From Webpack Encore to AssetMapper

Migrating an existing Symfony project from Webpack Encore to Symfony AssetMapper is, in most cases, a weekend workflow, not a month-long project. The first step: composer require symfony/asset-mapper and bin/console importmap:require for all JavaScript packages that used to be in package.json. The second step: convert JavaScript files from CommonJS syntax (require(), module.exports) to ES module syntax (import, export), since Symfony AssetMapper works exclusively with ES modules. This is usually trivial in modern projects, in older projects with many CommonJS dependencies this step can be more involved.

A critical point in migration: packages that do not offer an ES module format and are only available as CommonJS do not work directly with Symfony AssetMapper. jsDelivr delivers ESM variants for most popular packages, but there are exceptions. Legacy packages that rely internally on window globals or use CommonJS-specific features must either be replaced by ES module compatible alternatives or continue to be bundled via Webpack. For such exceptions, a hybrid approach is possible: Webpack bundles only the problematic parts as an IIFE, Symfony AssetMapper manages everything else.

Task Webpack Encore Symfony AssetMapper Difference
Add a package npm install package importmap:require package No node_modules, no package.json
Build step npm run build asset-map:compile Seconds instead of minutes
Development mode Watcher process required No watcher required Changes visible instantly
Versioning Webpack contenthash Automatic via hash Equivalent, without configuration
Legacy CommonJS Fully supported ESM only Older packages may be problematic

9. AssetMapper vs. Webpack Encore Compared

Symfony AssetMapper wins on simplicity, maintainability and deployment speed compared to Webpack Encore. Removing Node.js dependencies means fewer version conflicts, fewer breaking changes from npm updates and a cleaner project structure. In Docker based deployments, the separate build container for frontend assets is no longer needed, bin/console asset-map:compile runs in the PHP container and finishes in seconds. That is a genuine win for smaller teams who do not want to run a dedicated frontend build infrastructure.

Webpack Encore remains superior when the project needs TypeScript with complex configurations, CSS preprocessors like Sass or Less, aggressive code splitting strategies or dependencies on CommonJS only packages. For projects with dedicated frontend developers who expect a full build pipeline with linting, testing and transpilation, Webpack or Vite offers more control. Symfony AssetMapper explicitly targets the majority of Symfony projects that need modern JavaScript but cannot justify a complex build pipeline.

Mironsoft

Symfony development, frontend modernization and AssetMapper migration

Ready to leave Webpack complexity behind?

We migrate Symfony projects from Webpack Encore to AssetMapper, analysis of existing assets, ESM conversion, Stimulus integration and a clean deployment pipeline without Node.js overhead.

Migration

Analysis of existing Webpack configuration, ESM conversion and import map setup

Stimulus integration

Setting up Symfony UX components and Stimulus controllers without a build step

Deployment

Integrating asset-map:compile into the CI/CD pipeline and setting up CDN caching

10. Summary

Symfony AssetMapper makes the Node.js bundler unnecessary for most Symfony projects. Import maps enable native ES module resolution in the browser without Webpack configuration. Vendor packages are downloaded via importmap:require and served locally, without node_modules, without package.json. Automatic versioning through content hashes enables aggressive browser caching without configuration overhead. Stimulus and the entire Symfony UX ecosystem work without a build step. Deployments become faster, simpler and more independent of Node.js versions.

The switch pays off for all Symfony projects that do not need a complex build setup: traditional web applications, e-commerce systems, CMS projects and APIs with admin interfaces. Webpack Encore remains the right choice when TypeScript, Sass or legacy CommonJS packages are indispensable. For all other projects, Symfony AssetMapper is the most direct path to modern JavaScript without bundler complexity.

Symfony AssetMapper: The Key Points at a Glance

No bundler needed

Import maps + ES modules in the browser replace Webpack for most Symfony projects. No npm, no node_modules, no build watcher.

Vendor packages via CLI

bin/console importmap:require package downloads packages from jsDelivr and adds them to the import map. importmap:install restores everything after a git clone.

Automatic versioning

Content hashes for all assets with no configuration. asset() in Twig automatically resolves the versioned path.

Stimulus & UX

Symfony UX components and Stimulus controllers work fully without a build step. Controller auto-discovery included.

11. FAQ: Symfony AssetMapper

1What is Symfony AssetMapper?
A Symfony component since 6.3 for frontend assets without a Node.js bundler. Uses import maps and native ES modules, versions via content hash and makes Webpack unnecessary for many projects.
2Is Node.js required?
No. Packages are downloaded via importmap:require, asset-map:compile runs in PHP. No npm, no node_modules.
3Stimulus with AssetMapper?
Yes. After composer require symfony/ux-stimulus-bundle and importmap:require @hotwired/stimulus, Stimulus is active without a build step. Auto discovery from assets/controllers/ included.
4importmap:require vs. npm install?
importmap:require downloads from jsDelivr into assets/vendor/ and updates importmap.php. No node_modules, no package.json, directly importable as an ES module.
5TypeScript with AssetMapper?
Not directly, TypeScript requires transpilation. For TypeScript, Webpack Encore or Vite is the better choice.
6How does versioning work?
Content hash in the file name: app.js becomes app-a1b2c3.js. asset() in Twig automatically resolves the versioned path. Aggressive browser caching without cache invalidation problems.
7Tailwind with AssetMapper?
Via the Tailwind standalone CLI binary without Node.js. Runs as a separate build step before asset-map:compile in deployment.
8Migrating from Webpack Encore?
1. composer require symfony/asset-mapper, 2. convert CommonJS to ESM, 3. add packages via importmap:require. Usually 1 to 3 days of effort depending on project size.
9Vendor files on deployment?
assets/vendor/ is in .gitignore. importmap:install restores all vendor files after a git clone, analogous to composer install.
10Browser support for import maps?
All modern browsers since Chrome 89, Firefox 108, Safari 16.4. For older browsers, AssetMapper optionally injects the es-module-shims polyfill.