Symfony AssetMapper and Import Maps

In the last topic, I have explained how import maps work with npm packages in vanilla JavaScript, let's see how Symfony makes this even simpler.

The Problem Symfony Solves

With vanilla JavaScript and npm, you had to:

  1. Find the exact path in node_modules
  2. Manually add it to your import map
  3. Manage updates manually

Symfony's AssetMapper component automates all of this.

What is AssetMapper?

AssetMapper is Symfony's built-in solution for managing JavaScript and CSS without a build step. It:

  • Manages npm packages for you
  • Automatically generates import maps
  • Serves your assets during development
  • Optimizes assets for production
  • Works with the native browser import map standard (no compilation needed!)

Think of it as a bridge between npm packages and browser import maps.

Installing AssetMapper in Symfony

You can install AssetMapper as described in the topic: How to Add Symfony Bundle to Your Project.

PHPMaker supports AssetMapper by default, so you don't need to install the symfony/asset-mapper package yourself, but you need to apply the recipe to the generated project.

Open a command prompt at the project folder and run:

php bin/console app:recipes:install symfony/asset-mapper

What Does the Recipe Install?

Symfony's recipe automatically creates and configures several files in your project:

1. The AssetMapper Bundle Configuration

config/packages/asset_mapper.yaml:

framework:
    asset_mapper:
        # The paths to make available to the asset mapper.
        paths:
            - assets/
        missing_import_mode: strict

when@prod:
    framework:
        asset_mapper:
            missing_import_mode: warn

This tells Symfony to look for assets in the assets/ folder

2. The Assets Directory

assets/

assets/
├── app.js          # Your main JavaScript file (entrypoint)
└── styles/
    └── app.css     # Your main CSS file

What this is:

  • This is where you put your JavaScript and CSS files
  • app.js is the main entry point for your JavaScript
  • You can create more files here and import them

The recipe creates a basic app.js:

assets/app.js:

import './styles/app.css';

console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');

3. The Import Map Configuration

importmap.php:

<?php

return [
    'app' => [
        'path' => './assets/app.js',
        'entrypoint' => true,
    ],
];

What this does:

  • Maps the name 'app' to your assets/app.js file
  • Marks it as an entrypoint (a file that should be loaded on the page)
  • When you install packages with importmap:require, they get added here

4. The AssetMapper Vendor Directory

When you install packages, Symfony stores them here:

assets/vendor/

You don't create this manually - Symfony creates it when you install your first package. This folder is like node_modules, but specifically for AssetMapper-managed packages.

Note: Add this to your .gitignore:

# .gitignore
/assets/vendor/

The actual packages shouldn't be committed to version control - only the importmap.php configuration file should be committed.

PHPMaker Layout Template

The layout.php in PHPMaker template contains:

<?= ImportMap('app', ['nonce' => $Nonce]) ?>

This single line:

  1. Detects if importmap.php exists
  2. Generates the <script type="importmap"> tag with all your packages
  3. Loads your app.js as a module
  4. Handles all paths automatically

Building the Same Example in Symfony

Let's recreate the exact same Lodash demo from the vanilla JavaScript example, but using Symfony's AssetMapper.

Step 1: Install Lodash

php bin/console importmap:require lodash

You'll see output like:

[OK] lodash@4.17.21 added to importmap.php

What happened:

  • Symfony downloaded lodash-es from npm
  • Saved it to assets/vendor/lodash/
  • Updated importmap.php:
<?php

return [
    'app' => [
        'path' => './assets/app.js',
        'entrypoint' => true,
    ],
    'lodash' => [
        'version' => '4.17.21',
    ],
];

Step 2: Create Your JavaScript

Edit assets/app.js:

import './styles/app.css';
import _ from 'lodash';

document.addEventListener('DOMContentLoaded', () => {
    const result = document.getElementById('result');
    const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
    
    document.getElementById('chunk-btn')?.addEventListener('click', () => {
        const chunked = _.chunk(numbers, 3);
        result.innerHTML = `
            <strong>Original Array:</strong><br>
            [${numbers.join(', ')}]<br><br>
            <strong>Split into groups of 3:</strong><br>
            ${JSON.stringify(chunked)}
        `;
    });
    
    document.getElementById('shuffle-btn')?.addEventListener('click', () => {
        const shuffled = _.shuffle(numbers);
        result.innerHTML = `
            <strong>Original Array:</strong><br>
            [${numbers.join(', ')}]<br><br>
            <strong>Shuffled:</strong><br>
            [${shuffled.join(', ')}]
        `;
    });
    
    document.getElementById('sum-btn')?.addEventListener('click', () => {
        const sum = _.sum(numbers);
        result.innerHTML = `
            <strong>Array:</strong><br>
            [${numbers.join(', ')}]<br><br>
            <strong>Sum:</strong><br>
            ${sum}
        `;
    });
});

Notice: The JavaScript code is almost identical to the vanilla version! The only differences are:

  • We import from 'lodash' (Symfony handles the path)
  • We wrap it in DOMContentLoaded
  • We use optional chaining (?.) for safety

Step 3: Update Your Custom File

Now the content of the custom file can be simplified to HTML only:

    <h1>Lodash Import Map Demo</h1>
    
    <p>Click a button to see Lodash in action:</p>
    
    <button id="chunk-btn">Split Array into Chunks</button>
    <button id="shuffle-btn">Shuffle Array</button>
    <button id="sum-btn">Calculate Sum</button>
    
    <div id="result" class="result">
        Click a button to see the result!
    </div>

That's it! Notice how clean the template is - no import map, no script tags. Symfony handles everything.

Step 4: Run and Test

Generate scripts again, run your site, visit the custom file and click the buttons.

What Symfony Generated Behind the Scenes

When you visit the page, view the HTML source. You'll find that Symfony automatically:

  • Generated the import map JSON
  • Mapped "lodash" to the correct path
  • Loaded your app.js as a module
  • Included your CSS

Understanding Asset Versioning

When you update JavaScript or CSS files, browsers might use old cached versions instead of downloading your new code.

Symfony automatically adds content-based hashes to asset URLs. When file content changes, the hash changes, forcing browsers to download the new version.

Development Mode

Symfony uses query parameters:

<link rel="stylesheet" href="/assets/styles/app.css?v=abc123">
<script type="importmap">
{
    "imports": {
        "app": "/assets/app.js?v=xyz789",
        "lodash": "/assets/vendor/lodash/lodash.js?v=def456"
    }
}
</script>

How it works:

  1. Edit assets/app.js
  2. Save and refresh browser
  3. Symfony generates new hash automatically: app.js?v=NEW_HASH
  4. Browser downloads fresh file

No manual work needed!

Production Mode

Compile assets for production:

php bin/console asset-map:compile

This creates versioned filenames:

public/assets/
├── app-a1b2c3d4.js
├── styles/
│   └── app-e5f6g7h8.css
└── vendor/
    └── lodash/
        └── lodash-i9j0k1l2.js

Generated HTML:

<link rel="stylesheet" href="/assets/styles/app-e5f6g7h8.css">
<script type="importmap">
{
    "imports": {
        "app": "/assets/app-a1b2c3d4.js",
        "lodash": "/assets/vendor/lodash/lodash-i9j0k1l2.js"
    }
}
</script>

What Makes Symfony Easier?

Looking at both versions, here's what Symfony simplifies:

Task Vanilla JS Symfony
Install Lodash npm install lodash-es php bin/console importmap:require lodash
Find file path Search node_modules/lodash-es/ Automatic
Write import map Manual JSON in HTML Automatic via importmap.php
Organize files Manual Automatic structure
Cache busting Manual ?v=1.0 tags, update on every change Automatic content-based hashing
Update packages npm update, update HTML, update versions php bin/console importmap:update
Production build Manual optimization php bin/console asset-map:compile

Understanding the Benefits

The JavaScript code you write is identical in both approaches. The difference is in how you manage it:

Vanilla JavaScript:

  • :white_check_mark: Full control
  • :white_check_mark: Simple for single-page demos
  • :white_check_mark: No framework required
  • :cross_mark: Manual configuration
  • :cross_mark: Manual file organization
  • :cross_mark: Manual cache management
  • :cross_mark: No built-in optimization

Symfony AssetMapper:

  • :white_check_mark: Automatic configuration
  • :white_check_mark: Organized file structure
  • :white_check_mark: Automatic versioning - never worry about cache issues
  • :white_check_mark: Built-in optimization
  • :white_check_mark: Easy package updates
  • :white_check_mark: Seamless backend integration

The Key Difference

Both use the same web standard (import maps), but:

  • Vanilla JS: You write import _ from 'lodash' and manage everything manually
  • Symfony: You write import _ from 'lodash' and Symfony handles all the complexity

The JavaScript is the same - you just get better tooling!

Also Read

Next Up

In next topic, we'll explore how to use Symfony Stimulus Bundle with AssetMapper.