sycho/sourcemap
A Laravel package for generating and serving source maps for compiled JavaScript and CSS assets, making minified frontend files debuggable in browser devtools with references back to the original source.
Install via Composer:
composer require sycho/sourcemap
Before writing any integration code, open vendor/sycho/sourcemap/ and read src/. There is no service provider, facade, config file, or artisan command — it is a framework-agnostic leaf library. That means: no auto-discovery, no config('sourcemap.*'), no php artisan vendor:publish. You instantiate it yourself or register it in AppServiceProvider behind your own contract.
The package's job is narrow: parse a Source Map v3 JSON document and resolve a generated (line, column) back to an original (source, line, column, name). Everything it does flows from that one capability.
The canonical first use case — and the one worth proving out in a spike before anything else — is deminifying a browser stack trace:
use Sycho\SourceMap\SourceMap;
$map = SourceMap::fromString(
file_get_contents(public_path('build/assets/app-abc123.js.map'))
);
$original = $map->lookup(1, 48213);
// expected shape: ['source' => 'resources/js/app.js', 'line' => 42, 'column' => 10, 'name' => 'handleClick']
If your installed version's API differs from the above (method names, return shape, whether lookup is 1-based on line and 0-based on column), adjust to match what is actually in vendor/. Treat the return shape as an integration contract and pin it with tests immediately — see Gotchas.
A reasonable "day one" goal is a throwaway artisan command that takes a .map path and a line:column pair and prints the resolved source. It validates the whole premise (sourcesContent present? mappings decode correctly? indexed maps handled?) in about fifteen minutes.
Resolver service behind your own contract. Never call the library directly from controllers. Wrap it once:
namespace App\Support;
use Sycho\SourceMap\SourceMap;
class SourceMapResolver
{
/** @var array<string, SourceMap> */
private array $maps = [];
public function resolve(string $assetUrl, int $line, int $column): ?array
{
$mapPath = $this->mapPathFor($assetUrl);
if (! is_file($mapPath)) {
return null;
}
$map = $this->maps[$mapPath] ??= SourceMap::fromString(file_get_contents($mapPath));
return $map->lookup($line, $column);
}
public function mapPathFor(string $assetUrl): string
{
$path = ltrim((string) parse_url($assetUrl, PHP_URL_PATH), '/');
return public_path($path) . '.map';
}
}
Bind it as a singleton in AppServiceProvider (or a small dedicated provider) and inject SourceMapResolver everywhere else. Because the dependency is unmaintained and low-adoption, this wrapper is what lets you swap it for a fork, a different library, or a call out to Node without touching call sites. The wrapper returning ?array and null on failure is deliberate: design it to return "unmapped" rather than a plausible-but-wrong location.
Enriching error reports. The day-to-day production pattern is enriching a browser error beacon in a Laravel controller:
$resolved = $this->resolver->resolve($request->file, $request->line, $request->column);
Log::error('Frontend error', [
'message' => $request->message,
'stack' => $request->stack,
'resolved' => $resolved, // null when no map or no mapping
]);
Then downstream — Sentry, your internal dashboard, a support tool — shows resources/js/app.js:42:10 instead of app.min.js:1:48213. This is the highest-value workflow and the only one that justifies the dependency in most applications.
CI validation of emitted maps. Because the library parses maps too, it works well in a build-validation artisan command that runs after npm run build: for each file in public/build/assets/*.map, parse it, confirm sourcesContent is populated for every entry in sources, confirm mappings is non-empty, and fail the build with a readable error if not. This catches broken maps before they ship and is the lowest-risk place to pilot the package.
A dedicated "deminify a stack trace" artisan command. Highly useful for support engineers:
php artisan sourcemap:resolve public/build/assets/app-abc123.js "1:48213"
This is a good non-critical consumer to ship first — it exercises the API against real production maps without putting the library on any user-facing path.
Frontend error endpoint, done right. The route that receives browser errors should be rate-limited (throttle:60,1) and either authenticated or restricted by origin, because parsing multi-megabyte JSON is not free. Do the resolution inline only if you have bounded map sizes and caching; otherwise dispatch an EnrichFrontendError job. In the job, memoize parsed maps per asset hash (see next section).
Caching strategy. Do not cache the SourceMap object in Laravel's cache driver — serializability is not something this package promises. Cache the result of lookup(), keyed by asset-hash:line:column, with a TTL aligned to deploy cadence or invalidated on deploy. Or cache raw map file contents (strings) yourself and re-parse on demand; the parse is the expensive part but at least you control the failure surface.
Validate the API before you build on it. The examples above use SourceMap::fromString() and lookup(). Verify these against the version you install — with one star and zero dependents, treat the README as a hint and vendor/sycho/sourcemap/src/ as the truth.
Partial spec coverage fails silently. The dangerous failure mode of any source-map library is not an exception; it is returning the wrong line for a column it could not properly decode. Check whether your installed version handles Base64 VLQ decode edge cases, sourcesContent, sourceRoot, names, and indexed maps (sections — the wrapper format used by some bundlers and by Webpack for split chunks). If sections maps throw or are ignored, your production maps may be silently unsupported. Write golden-file tests against real maps from your build pipeline: a simple map, an indexed map, one with unicode, one with a missing sourcesContent, and a truncated/malformed JSON.
Build a fixture set and pin it. Because the package has no CI matrix you can rely on and one maintainer, your tests are the only safety net. Put production-style .map fixtures in tests/fixtures/sourcemaps/ and assert exact (source, line, column, name) outputs. When you upgrade the package or a bundler, these tests catch regressions that upstream will not.
Don't do this per request. Minified bundles produce maps in the multi-megabyte range. Whole-document parsing plus array-heavy structures means memory, not CPU, is your limit. Run it in jobs, cap acceptable file size (reject > a few MB), and set an explicit memory_limit on workers that touch it. A synchronous resolution call on a public endpoint is a memory-exhaustion vector if someone posts a crafted asset URL. Never feed it untrusted JSON — restrict which map files can be read (resolve against a canonicalized path under public/build/assets/, and reject anything that escapes it).
public_path path traversal. The mapPathFor() example above interpolates an asset URL. Sanitize it: realpath() the result, confirm it starts with your build directory, and only allow .map files. Otherwise a crafted file parameter becomes an arbitrary-file-read oracle.
Dev vs. production map serving. In local dev, Vite serves .map files automatically. In production they are typically absent from public/ for security/size reasons. Decide explicitly where maps live (private S3, a build artifact bucket, a non-served directory) and make SourceMapResolver aware of that — you will usually not be reading from public_path() in prod. Keeping maps private but parseable server-side is a legitimate reason to hold this dependency at all.
Missing sourcesContent means unmappable frames. If a build config sets build.sourcemap: true but not build.sourcemap: 'hidden' with embedded sources, the resolved source will point at a file you cannot read. Return the path but flag it as unresolvable, and don't pretend you have the source text.
Fail loudly, return null explicitly. Distinguish three outcomes in your wrapper: map file not found, map unparseable, and position has no mapping (lookup() returns null). Log the first two at warning level so missing maps are visible in CI and staging; keep the third silent because unmapped positions within a valid map are normal (e.g. code injected by a plugin).
Watch the PHP version constraint. Check vendor/sycho/sourcemap/composer.json for php and ext-* constraints against your Laravel version (Laravel 10 → PHP 8.1+, Laravel 11 → PHP 8.2+). A missing constraint here is a real risk on newer runtimes.
Vendor or fork on the first patch. With a bus factor of one, treat the day you need a bug fix as the day you fork. Keep the fork under a private VCS entry in composer.json ("repositories": [{"type": "vcs", ...}]) so the swap is a one-line change, and only ever call the library through SourceMapResolver so call sites don't change at all.
Question whether it belongs in PHP. If your build pipeline already runs Node (Vite, esbuild, Sentry CLI), source-map handling has a natural home there. The honest evaluation: use this package when you specifically need PHP-side parsing — a Laravel error endpoint, a support CLI, a CI check — and not as a general replacement for proper symbolication tools (Sentry, GlitchTip). It is infrastructure glue, not a symbolication platform.
How can I help you explore Laravel packages today?