Engineering Tactics · Part 5 of 7

When grep Can't Find It But the Reviewer Can: Debugging Chrome Extension Build Artifacts

Feb 24, 2026

You grep your source code. Zero results. You grep again with different flags. Still nothing. But the Chrome Web Store reviewer is pointing at a specific string in your submitted build, and it’s definitely there.

This happened to me several times while shipping ChatShuttle. The mismatch between what’s in your src/ and what ends up in dist/ is genuinely disorienting, because grep finds nothing and you start questioning whether you’re looking at the right project.

Why src/ and dist/ Diverge

Your source code is not what ships. What ships is the output of your build tool (Webpack, Vite, Rollup, esbuild, whatever). That build tool takes your source, pulls in dependencies from node_modules, applies transforms, and produces a bundle. The bundle contains code you wrote and code you didn’t write.

The divergence comes from a few places:

  • Inlined dependencies. Your bundler pulls the full source of every import into the output. If a dependency references a CDN URL, that URL is now in your output.
  • Transitive dependencies. Your dependency imports another dependency, which imports another. You may never see these in your package.json, but they’re in your bundle.
  • Build-time transforms. Babel, TypeScript, PostCSS, or other tools can inject polyfills, helpers, or runtime code that doesn’t exist in your source.
  • Dead code that isn’t eliminated. Tree shaking isn’t perfect. Unreachable code paths in dependencies often survive the build.

The Tracing Process

When you find a string in dist/ that doesn’t exist in src/, the question is: where did it come from? Here’s the process I use, in order:

Step 1: Confirm the String Exists in dist/

# Be specific about what you're looking for
$ grep -rn "cdnjs.cloudflare" dist/
dist/assets/sidepanel-abc123.js:42:..."cdnjs.cloudflare.com/ajax/libs/pdfobject"...

# Note the filename and line number

This tells you which output file contains the string and roughly where. In minified output, the line number may not help much, but the filename tells you which entry point or chunk the code ended up in.

Step 2: Search node_modules

# Search all JS files in node_modules for the same string
$ grep -rn "cdnjs.cloudflare" node_modules/ --include="*.js" -l
node_modules/pdfobject/pdfobject.js
node_modules/pdfobject/pdfobject.min.js

# Now you know which package owns this string

The -l flag shows filenames only, which is faster when you have a large node_modules. Once you know the package, you can look at the actual line:

$ grep -n "cdnjs.cloudflare" node_modules/pdfobject/pdfobject.js
42:var defined = "https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.3.0/pdfobject.min.js";

Step 3: Understand the Import Chain

Sometimes the dependency isn’t one you installed directly. Check who imported it:

# Is this package in your package.json?
$ grep "pdfobject" package.json
"pdfobject": "^2.3.0"

# If not, find who depends on it
$ npm ls pdfobject
myapp@1.0.0
└── pdfobject@2.3.0

If npm ls shows a nesting like some-other-package → pdfobject, you’ve found a transitive dependency. The fix might be replacing the parent package, not just patching the child.

Step 4: Decide on a Fix

Once you know the source, you have options:

  1. Remove the dependency. If you don’t need it, remove it from package.json and rebuild.
  2. Replace the dependency. Find an alternative library that doesn’t include the problematic string.
  3. Localize the dependency. Download the relevant file and include it as a local asset. (This is what I did with pdfobject.)
  4. Patch the dependency. Use a postinstall script or patch-package to modify the dependency’s source before building.

The Patching Problem

Option 4 sounds clean but has sharp edges. I learned the hard way when patching jspdf.

The dependency had a URL I needed to remove. I wrote a postinstall script using sed:

# postinstall.sh
sed -i 's|https://cdnjs.cloudflare.com/ajax/libs/jspdf/.*.js||g' \
  node_modules/jspdf/dist/jspdf.es.min.js

This worked on Linux. On macOS, it created a backup file (jspdf.es.min.js-e) and didn’t modify the original. The sed -i syntax is different between GNU sed (Linux) and BSD sed (macOS).

The fix:

# macOS-compatible sed
sed -i '' 's|https://cdnjs.cloudflare.com/ajax/libs/jspdf/.*\.js||g' \
  node_modules/jspdf/dist/jspdf.es.min.js

But even this wasn’t enough. The library had multiple distribution files:

$ find node_modules/jspdf/dist -name "*.js" -type f
node_modules/jspdf/dist/jspdf.es.min.js
node_modules/jspdf/dist/jspdf.umd.min.js
node_modules/jspdf/dist/jspdf.node.min.js
node_modules/jspdf/dist/jspdf.es.js
node_modules/jspdf/dist/jspdf.umd.js

Patching one file wasn’t enough. The build tool might pick any of these depending on the module resolution strategy. I had to patch all of them:

# Patch all dist files
find node_modules/jspdf/dist -name "*.js" -type f -exec \
  sed -i '' 's|https://cdnjs.cloudflare.com/ajax/libs/jspdf/.*\.js||g' {} +

And then verify:

# Verify the patch landed in every file
$ grep -r "cdnjs" node_modules/jspdf/dist/
# Should return zero results

# Then rebuild and verify the output too
$ npm run build
$ grep -r "cdnjs" dist/
# Should return zero results

Build Artifact Debugging Checklist

  1. Always grep dist/, not src/. Your source is not what ships.
  2. When you find something unexpected in dist/, search node_modules/. The string almost certainly comes from a dependency.
  3. Use npm ls to find transitive dependencies. The package you need to fix might not be in your package.json.
  4. If patching, patch all dist files. Libraries ship multiple formats (ES, UMD, CJS). Your bundler might pick any of them.
  5. Test patches on both macOS and Linux. The sed -i syntax differs between platforms.
  6. Verify twice. Grep node_modules/ after patching, then grep dist/ after building. Both must be clean.

When to Give Up and Replace

Patching is a maintenance burden. Every npm install re-downloads the original, unpatched source. Your postinstall script runs again. If the dependency updates and changes the string format, your sed pattern breaks silently.

My rule: if the patch is more than 3 lines or targets more than 2 files, replace the dependency entirely. For pdfobject, localizing the file was simpler and more reliable than patching it on every install.

The broader workflow that wraps around all of this, the Checkpoint Loop, makes sure you catch these issues before they reach the reviewer. And the pre-submission URL scan is the specific step that would have caught this particular class of bug.

If you’re building a Chrome extension and want to see how all these lessons came together in a real product, the ChatShuttle docs cover the architecture.