Add Source Maps locally

Add Source Maps locally

The next explanations are based on this project. To get the most of this article it is recommended to not only read it, but to practice creating a similar project.

This is a demo of the application functionality:

Sometimes we will find that our production application doesn't behave as we expect despite doing well in the development environment.

If we try to debug code using a debugger line in code, the production build will delete that line, the same with comments, etc. This is due to the process of minification and uglification. Furthermore, if we try to inspect our code inspecting the bundle we will see that the code differs from our original code, and sometimes will be hard to find what we want. The solution to this problem is the use of source maps. In the end, what source maps do is give us access to the original files and allow us to debug them.

// This is the source map that belongs to the bundle of the team component
{
  "version": 3,
  "file": "972.d5590aab453d0da7.js",
  "mappings": "wKAIA,MAAMA,EAAiB,CACrB,CACEC,KAAM,GACNC,UCAJ,MAAM,MAAOC,EAGXC,cAAgB,CAEhBC,WACEC,KAAKC,OAAS,CAAC,6BAA8B,6BAC7CD,KAAKE,MAAQF,KAAKC,OAAO,EAC1B,CAEDE,gBACEH,KAAKE,MAAQF,KAAKC,OAAOG,KAAMF,GAAUA,GAASF,KAAKE,MACxD,+CAZUL,EAAa,0BAAbA,EAAaQ,0FCP1BC,cAAIA,SAAWA,QACfA,oBAAQA,gCAASC,iBAAe,GAAED,wBAAYA,eAD1CA,4BDOST,CAAb,MDEE,CACEF,KAAM,KACNa,UAAW,OACXC,WAAY,KAQT,IAAMC,EAAb,MAAM,MAAOA,kDAAiB,0BAAjBA,gCAHDC,cAAsBjB,GACtBiB,QAECD,CAAb,KGJaE,EAAb,MAAM,MAAOA,kDAAU,0BAAVA,gCAJTC,KACAH,KAGSE,CAAb",
  "names": [
    "routes",
    "path",
    "component",
    "TeamComponent",
    "constructor",
    "ngOnInit",
    "this",
    "titles",
    "title",
    "onChangeTitle",
    "find",
    "selectors",
    "i0",
    "ctx",
    "pathMatch",
    "redirectTo",
    "TeamRoutingModule",
    "RouterModule",
    "TeamModule",
    "CommonModule"
  ],
  "sourceRoot": "webpack:///",
  "sources": [
    "./src/app/team/team-routing.module.ts",
    "./src/app/team/team.component.ts",
    "./src/app/team/team.component.html",
    "./src/app/team/team.module.ts"
  ],
  "sourcesContent": [
    "import { NgModule } from '@angular/core';\nimport { RouterModule, Routes } from '@angular/router';\nimport { TeamComponent } from './team.component';\n\nconst routes: Routes = [\n  {\n    path: '',\n    component: TeamComponent,\n  },\n  {\n    path: '**',\n    pathMatch: 'full',\n    redirectTo: '',\n  },\n];\n\n@NgModule({\n  imports: [RouterModule.forChild(routes)],\n  exports: [RouterModule],\n})\nexport class TeamRoutingModule {}\n",
    "import { Component, OnInit } from '@angular/core';\n\n@Component({\n  selector: 'app-team',\n  templateUrl: './team.component.html',\n  styleUrls: ['./team.component.scss'],\n})\nexport class TeamComponent implements OnInit {\n  title: string | undefined;\n  titles: string[];\n  constructor() {}\n\n  ngOnInit(): void {\n    this.titles = ['Teams Component - Original', 'Teams Component - Changed'];\n    this.title = this.titles[0];\n  }\n\n  onChangeTitle() {\n    this.title = this.titles.find((title) => title != this.title);\n  }\n}\n",
    "<h1>{{ title }}</h1>\n<button (click)=\"onChangeTitle()\">Change title</button>\n",
    "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\nimport { TeamRoutingModule } from './team-routing.module';\nimport { TeamComponent } from './team.component';\n\n\n@NgModule({\n  declarations: [\n    TeamComponent\n  ],\n  imports: [\n    CommonModule,\n    TeamRoutingModule\n  ]\n})\nexport class TeamModule { }\n"
  ],
  "x_google_ignoreList": ["import { NgModule }"]
}

A source map is represented by a .map file and it is an object that contains the next properties:

  • version: File version (always the first entry in the object) and must be a positive integer.
  • file: An optional name of the generated code that this source map is associated with.
  • mappings: A string with the encoded mapping data
  • names: A list of symbol names used by the “mappings” entry
  • sourceRoot: An optional source root, useful for relocating source files on a server or removing repeated values in the “sources” entry. This value is prepended to the individual entries in the “source” field
  • sources: A list of original sources used by the “mappings” entry
  • sourcesContent: An optional list of source content, useful when the “source” can’t be hosted. The contents are listed in the same order as the sources in line 5. “null” may be used if some original sources should be retrieved by name
  • x_google_ignoreList: An optional list of user-specified ignore list regular expressions, as well as source map hints.

To run this example we need to follow the next steps:

  1. npm run start:prod
  2. Go to chrome browser, open DevTools, and open the Sources tab
  3. Under localhost:4200 cloud, open the file we want to add the source map to.
  4. Right-click on the code and click on "Add source map..."
  5. Enter the source map URL as "file://myAbsolutePathToMatchingSourcemap"
// package.json
{
  "name": "source-maps",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "build:prod": "ng build --configuration=production",
    "delete:sourcemaps:prod": "rimraf ./dist/*.map",
    "build:sourcemaps": "ng build --configuration=production --output-path=localSourceMaps",
    "serve:prod": "http-server ./dist --proxy http://localhost:4200? -a localhost -p 4200",
    "start:prod": "run-s build:prod delete:sourcemaps:prod build:sourcemaps serve:prod",
    "watch": "ng build --watch --configuration development",
    "test": "ng test"
  },
  "private": true,

We see that "npm run start:prod" uses npm-run-all library to run secuentially the production build process, the deletion of generated source maps, a new production build process with a new output path to have the source maps locally and finally exposes the built application on a local server with the http-server library.

Bibliography: