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:
- Find the exact path in
node_modules - Manually add it to your import map
- 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.jsis 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 yourassets/app.jsfile - 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:
- Detects if
importmap.phpexists - Generates the
<script type="importmap">tag with all your packages - Loads your
app.jsas a module - 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-esfrom 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.jsas 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:
- Edit
assets/app.js - Save and refresh browser
- Symfony generates new hash automatically:
app.js?v=NEW_HASH - 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:
Full control
Simple for single-page demos
No framework required
Manual configuration
Manual file organization
Manual cache management
No built-in optimization
Symfony AssetMapper:
Automatic configuration
Organized file structure
Automatic versioning - never worry about cache issues
Built-in optimization
Easy package updates
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.