spiegel-keyman/common/tools/sourcemap-path-remapper/src/index.ts

158 lines
No EOL
4.8 KiB
TypeScript

import * as fs from 'fs';
import path from 'path';
import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats.
// Base64, stringified-JSON, end-of-file comment...
// Because we're compiling the main project based on its TS build outputs, and our cross-module references
// also link to build outputs, we need to clean up the sourcemap-paths. Also, the individual module sourcemaps.
export interface Mapping {
readonly from: RegExp | string;
readonly to: string;
}
/**
* An intermediate state for sourcemap source path remapping operations + functions to
* mutate it.
*/
class RemappingState {
constructor(srcmap: Object) {
this.sourceMap = srcmap;
this.resetChangedSourcepathTracking();
}
public readonly sourceMap: any;
private _unchangedSourcePaths: string[] = [];
public get unchangedSourcepaths() {
return this._unchangedSourcePaths;
}
public resetChangedSourcepathTracking() {
this._unchangedSourcePaths = [...this.sourceMap.sources];
}
/**
* The sourcemap's `sourceRoot` property.
*/
public get sourceRoot() {
return this.sourceMap.sourceRoot;
}
/**
* The sourcemap's `sourceRoot` property.
*/
public set sourceRoot(path: string) {
this.sourceMap.sourceRoot = path;
}
/**
* Searches for source-file paths that match specified path-remapping rules and remaps them
* accordingly.
* @param transforms Path-remapping rules to apply. For each remappable path, only the first
* matching rule in the specified set will be applied.
* @returns `this` - the original instance - in order to facilitate function-call chaining.
*/
remapPaths(transforms: Mapping | Mapping[], logger?: (from: string, to: string) => void) {
if(!(transforms instanceof Array)) {
transforms = [transforms];
}
const unusedTransforms = [...transforms];
const sourcePaths = this.sourceMap.sources;
for(let i = 0; i < sourcePaths.length; i++) {
for(const transform of transforms) {
if(sourcePaths[i].match(transform.from)) {
// Note that this source path has been remapped (if it wasn't previously)
if(this.unchangedSourcepaths.includes(sourcePaths[i])) {
this.unchangedSourcepaths.splice(this.unchangedSourcepaths.indexOf(sourcePaths[i]), 1);
}
// Note that this transform has been used.
if(unusedTransforms.includes(transform)) {
unusedTransforms.splice(unusedTransforms.indexOf(transform), 1);
}
let startPath = sourcePaths[i];
sourcePaths[i] = sourcePaths[i].replace(transform.from, transform.to);
if(logger) {
logger(startPath, sourcePaths[i]);
}
break;
}
}
}
if(unusedTransforms.length > 0) {
console.warn();
console.warn("Unused sourcemath mappings:");
for(const transform of unusedTransforms) {
let isRegex = transform.from instanceof RegExp;
let delim = isRegex ? '' : '"';
console.warn(`* ${delim}${transform.from.toString()}${delim} => "${transform.to}"`);
}
}
return this;
}
/**
* Prepends all `sources` paths with the current sourceRoot (or just the part after a
* specified index).
* @param index
*/
public normalizeWithSourceRoot(index?: number) {
const sources = this.sourceMap.sources;
const originalSrcRoot = this.sourceRoot;
index = index || 0;
const prefix = originalSrcRoot.substring(index);
this.sourceRoot = originalSrcRoot.substring(0, index);
for(let i=0; i < sources.length; i++) {
// Normalizes, then swaps Windows '\\' entries with '/' if they exist;
// path.normalize respects Windows too much for our taste here.
sources[i] = path.normalize(`${prefix}/${sources[i]}`).replace(/\\/g, '/');
}
}
/**
* Writes the current state of the remapped sourcemap out to the specified file.
* @param file
*/
public toFile(file: fs.PathLike) {
fs.writeFileSync(file, convertSourceMap.fromObject(this.sourceMap).toJSON(2)); // 2 = space count
}
}
export default class SourcemapRemapper {
private constructor() {}
/**
* Starts sourcemap source path remapping from a pre-loaded JSON sourcemap instance.
* @param obj
* @returns
*/
public static fromObject(obj: Object) {
return new RemappingState(obj);
}
/**
* Starts sourcemap source path remapping from the specified .js.map file.
* @param file
* @returns
*/
public static fromFile(file: fs.PathLike) {
return this.fromBuffer(fs.readFileSync(file));
}
/**
* Starts sourcemap source path remapping from a loaded but unparsed file.
* @param buffer
* @returns
*/
public static fromBuffer(buffer: Buffer) {
return this.fromObject(convertSourceMap.fromJSON(buffer).toObject());
}
}