Skip to content

Commit

Permalink
Account for injectManifest replacements in sourcemaps (#2239)
Browse files Browse the repository at this point in the history
* Fix sourcemaps in injectManifest mode

* Linting workaround

* config => options
  • Loading branch information
jeffposnick committed Oct 1, 2019
1 parent 6300658 commit b8f1183
Show file tree
Hide file tree
Showing 16 changed files with 450 additions and 1,731 deletions.
40 changes: 31 additions & 9 deletions packages/workbox-build/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/workbox-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@
"@babel/preset-env": "^7.5.5",
"@babel/runtime": "^7.5.5",
"@hapi/joi": "^15.1.0",
"@surma/rollup-plugin-off-main-thread": "^1.1.1",
"common-tags": "^1.8.0",
"fast-json-stable-stringify": "^2.0.0",
"fs-extra": "^8.1.0",
"glob": "^7.1.4",
"lodash.template": "^4.5.0",
"pretty-bytes": "^5.2.0",
"rollup": "^1.17.0",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-node-resolve": "^5.2.0",
"@surma/rollup-plugin-off-main-thread": "^1.1.1",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.1.1",
"source-map": "^0.7.3",
"source-map-url": "^0.4.0",
"stringify-object": "^3.3.0",
"strip-comments": "^1.0.2",
"tempy": "^0.3.0",
Expand Down
73 changes: 55 additions & 18 deletions packages/workbox-build/src/inject-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@

const assert = require('assert');
const fse = require('fs-extra');
const sourceMapURL = require('source-map-url');
const stringify = require('fast-json-stable-stringify');
const upath = require('upath');

const errors = require('./lib/errors');
const escapeRegexp = require('./lib/escape-regexp');
const getFileManifestEntries = require('./lib/get-file-manifest-entries');
const injectManifestSchema = require('./options/schema/inject-manifest');
const rebasePath = require('./lib/rebase-path');
const replaceAndUpdateSourceMap =
require('./lib/replace-and-update-source-map');
const validate = require('./lib/validate-options');

/**
Expand Down Expand Up @@ -49,25 +53,21 @@ async function injectManifest(config) {
}));
}

if (upath.resolve(config.swSrc) === upath.resolve(config.swDest)) {
throw new Error(errors['same-src-and-dest']);
}

const globalRegexp = new RegExp(escapeRegexp(options.injectionPoint), 'g');

const {count, size, manifestEntries, warnings} =
await getFileManifestEntries(options);
let swFileContents;
try {
swFileContents = await fse.readFile(config.swSrc, 'utf8');
swFileContents = await fse.readFile(options.swSrc, 'utf8');
} catch (error) {
throw new Error(`${errors['invalid-sw-src']} ${error.message}`);
}

const injectionResults = swFileContents.match(globalRegexp);
if (!injectionResults) {
// See https://github.com/GoogleChrome/workbox/issues/2230
if (upath.resolve(config.swSrc) === upath.resolve(config.swDest)) {
if (upath.resolve(options.swSrc) === upath.resolve(options.swDest)) {
throw new Error(errors['same-src-and-dest'] + ' ' +
options.injectionPoint);
}
Expand All @@ -78,23 +78,60 @@ async function injectManifest(config) {
assert(injectionResults.length === 1, errors['multiple-injection-points'] +
options.injectionPoint);

const entriesString = JSON.stringify(manifestEntries, null, 2);
swFileContents = swFileContents.replace(globalRegexp, entriesString);
const manifestString = stringify(manifestEntries);
const filesToWrite = {};

const url = sourceMapURL.getFrom(swFileContents);
// If our swSrc file contains a sourcemap, we would invalidate that
// mapping if we just replaced injectionPoint with the stringified manifest.
// Instead, we need to update the swDest contents as well as the sourcemap
// at the same time.
// See https://github.com/GoogleChrome/workbox/issues/2235
if (url) {
const sourcemapSrcPath = upath.resolve(upath.dirname(options.swSrc), url);
const sourcemapDestPath = upath.resolve(upath.dirname(options.swDest), url);

let originalMap;
try {
originalMap = await fse.readJSON(sourcemapSrcPath, 'utf8');
} catch (error) {
throw new Error(`${errors['cant-find-sourcemap']} ${error.message}`);
}

try {
await fse.mkdirp(upath.dirname(options.swDest));
} catch (error) {
throw new Error(errors['unable-to-make-injection-directory'] +
` '${error.message}'`);
const {map, source} = await replaceAndUpdateSourceMap({
originalMap,
jsFilename: upath.basename(options.swDest),
originalSource: swFileContents,
replaceString: manifestString,
searchString: options.injectionPoint,
});

filesToWrite[options.swDest] = source;
filesToWrite[sourcemapDestPath] = map;
} else {
// If there's no sourcemap associated with swSrc, a simple string
// replacement will suffice.
filesToWrite[options.swDest] = swFileContents.replace(
globalRegexp, manifestString);
}

await fse.writeFile(config.swDest, swFileContents);
for (const [file, contents] of Object.entries(filesToWrite)) {
try {
await fse.mkdirp(upath.dirname(file));
} catch (error) {
throw new Error(errors['unable-to-make-injection-directory'] +
` '${error.message}'`);
}

// Wrap the single output file in an array to make the return value consistent
// with generateSW.
const filePaths = [config.swDest];
await fse.writeFile(file, contents);
}

return {count, filePaths, size, warnings};
return {
count,
size,
warnings,
filePaths: Object.keys(filesToWrite),
};
}

module.exports = injectManifest;
2 changes: 2 additions & 0 deletions packages/workbox-build/src/lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,6 @@ module.exports = {
either precaching or runtime caching. Please ensure that the various glob
options are set to match one or more files, and/or configure the
runtimeCaching option.`,
'cant-find-sourcemap': ol`The swSrc file refers to a sourcemap that can't be
opened:`,
};
108 changes: 108 additions & 0 deletions packages/workbox-build/src/lib/replace-and-update-source-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/

const {SourceMapConsumer, SourceMapGenerator} = require('source-map');

/**
* Adapted from https://github.com/nsams/sourcemap-aware-replace, with modern
* JavaScript updates, along with additional properties copied from originalMap.
*
* @param {Object} options
* @param {string} options.jsFilename The name for the file whose contents
* correspond to originalSource.
* @param {Object} options.originalMap The sourcemap for originalSource,
* prior to any replacements.
* @param {string} options.originalSource The source code, prior to any
* replacements.
* @param {string} options.replaceString A string to swap in for searchString.
* @param {string} options.searchString A string in originalSource to replace.
* Only the first occurrence will be replaced.
* @return {{source: string, map: string}} An object containing both
* originalSource with the replacement applied, and the modified originalMap.
*
* @private
*/
async function replaceAndUpdateSourceMap({
jsFilename,
originalMap,
originalSource,
replaceString,
searchString,
}) {
const generator = new SourceMapGenerator({
file: jsFilename,
});

const consumer = await new SourceMapConsumer(originalMap);

let pos;
let src = originalSource;
const replacements = [];
let lineNum = 0;
let filePos = 0;

const lines = src.split('\n');
for (let line of lines) {
lineNum++;
let searchPos = 0;
while ((pos = line.indexOf(searchString, searchPos)) !== -1) {
src = src.substring(0, filePos + pos) + replaceString +
src.substring(filePos + pos + searchString.length);
line = line.substring(0, pos) + replaceString +
line.substring(pos + searchString.length);
replacements.push({line: lineNum, column: pos});
searchPos = pos + replaceString.length;
}
filePos += line.length + 1;
}

replacements.reverse();

consumer.eachMapping((mapping) => {
for (const replacement of replacements) {
if (replacement.line == mapping.generatedLine &&
mapping.generatedColumn > replacement.column) {
const offset = searchString.length - replaceString.length;
mapping.generatedColumn -= offset;
}
}

if (mapping.source) {
const newMapping = {
generated: {
line: mapping.generatedLine,
column: mapping.generatedColumn,
},
original: {
line: mapping.originalLine,
column: mapping.originalColumn,
},
source: mapping.source,
};
return generator.addMapping(newMapping);
}

return mapping;
});

consumer.destroy();

const updatedSourceMap = Object.assign(JSON.parse(generator.toString()), {
names: originalMap.names,
sourceRoot: originalMap.sourceRoot,
sources: originalMap.sources,
sourcesContent: originalMap.sourcesContent,
});

return {
map: JSON.stringify(updatedSourceMap),
source: src,
};
}

module.exports = replaceAndUpdateSourceMap;

0 comments on commit b8f1183

Please sign in to comment.