@backtrace/sourcemap-tools
Version:
Backtrace-JavaScript sourcemap tools
261 lines • 13.4 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceProcessor = void 0;
const path_1 = __importDefault(require("path"));
const common_1 = require("./helpers/common");
const flow_1 = require("./helpers/flow");
const stringHelpers_1 = require("./helpers/stringHelpers");
const stringToUuid_1 = require("./helpers/stringToUuid");
const Result_1 = require("./models/Result");
class SourceProcessor {
constructor(_debugIdGenerator) {
this._debugIdGenerator = _debugIdGenerator;
}
isSourceProcessed(source) {
return !!this._debugIdGenerator.getSourceDebugIdFromComment(source);
}
isSourceMapProcessed(sourceMap) {
return !!this._debugIdGenerator.getSourceMapDebugId(sourceMap);
}
isSourceFileProcessed(sourcePath) {
return __awaiter(this, void 0, void 0, function* () {
return (0, flow_1.pipe)(sourcePath, common_1.readFile, Result_1.R.map((v) => this.isSourceProcessed(v)));
});
}
isSourceMapFileProcessed(sourceMapPath) {
return __awaiter(this, void 0, void 0, function* () {
return (0, flow_1.pipe)(sourceMapPath, common_1.readFile, Result_1.R.map((common_1.parseJSON)), Result_1.R.map((v) => this.isSourceMapProcessed(v)));
});
}
getSourceDebugId(source) {
return this._debugIdGenerator.getSourceDebugIdFromComment(source);
}
getSourceMapDebugId(sourceMap) {
return this._debugIdGenerator.getSourceMapDebugId(sourceMap);
}
getSourceMapFileDebugId(sourceMapPath) {
return __awaiter(this, void 0, void 0, function* () {
return (0, flow_1.pipe)(sourceMapPath, common_1.readFile, Result_1.R.map((common_1.parseJSON)), Result_1.R.map((sourceMap) => this.getSourceMapDebugId(sourceMap)));
});
}
/**
* Adds required snippets and comments to source
* @param source Source content.
* @param debugId Debug ID. If not provided, one will be generated from `source`.
* @param force Force adding changes.
* @returns Used debug ID, new source and new sourcemap.
*/
processSource(source, debugId, force) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.processSourceAndAvailableSourceMap(source, undefined, debugId, force);
});
}
/**
* Adds required snippets and comments to source, and modifies sourcemap to include debug ID.
* @param source Source content.
* @param sourceMap Sourcemap object or JSON.
* @param debugId Debug ID. If not provided, one will be generated from `source`.
* @param force Force adding changes.
* @returns Used debug ID, new source and new sourcemap.
*/
processSourceAndSourceMap(source, sourceMap, debugId, force) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.processSourceAndAvailableSourceMap(source, sourceMap, debugId, force);
});
}
processSourceAndAvailableSourceMap(source, sourceMap, debugId, force) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const sourceDebugId = this.getSourceDebugId(source);
if (!debugId) {
debugId = sourceDebugId !== null && sourceDebugId !== void 0 ? sourceDebugId : (0, stringToUuid_1.stringToUuid)(source);
}
let newSource = source;
let offsetSourceMap;
// If source has debug ID, but it is different, we need to only replace it
if (sourceDebugId && debugId !== sourceDebugId) {
newSource = this._debugIdGenerator.replaceDebugId(source, sourceDebugId, debugId);
}
if (force || !sourceDebugId || !this._debugIdGenerator.hasCodeSnippet(source, debugId)) {
const sourceSnippet = this._debugIdGenerator.generateSourceSnippet(debugId);
const shebang = (_a = source.match(/^(#!.+\n)/)) === null || _a === void 0 ? void 0 : _a[1];
newSource = shebang
? shebang + sourceSnippet + '\n' + source.substring(shebang.length)
: sourceSnippet + '\n' + source;
if (sourceMap) {
// We need to offset the source map by amount of lines that we're inserting to the source code
// Sourcemaps map code like this:
// original code X:Y => generated code A:B
// So if we add any code to generated code, mappings after that code will become invalid
// We need to offset the mapping lines by sourceSnippetNewlineCount:
// original code X:Y => generated code (A + sourceSnippetNewlineCount):B
const sourceSnippetNewlineCount = (_c = (_b = sourceSnippet.match(/\n/g)) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
offsetSourceMap = yield this.offsetSourceMap(sourceMap, sourceSnippetNewlineCount + 1);
}
}
if (force || !sourceDebugId || !this._debugIdGenerator.hasCommentSnippet(source, debugId)) {
const sourceComment = this._debugIdGenerator.generateSourceComment(debugId);
newSource = (0, stringHelpers_1.appendBeforeWhitespaces)(newSource, '\n' + sourceComment);
}
if (!sourceMap) {
return { debugId, source: newSource };
}
const newSourceMap = this._debugIdGenerator.addSourceMapDebugId(offsetSourceMap !== null && offsetSourceMap !== void 0 ? offsetSourceMap : sourceMap, debugId);
return { debugId, source: newSource, sourceMap: newSourceMap };
});
}
/**
* Adds required snippets and comments to source, and modifies sourcemap to include debug ID.
* Will write modified content to the files.
* @param sourcePath Path to the source.
* @param sourceMapPath Path to the sourcemap. If not specified, will try to resolve from sourceMapURL.
* @param debugId Debug ID. If not provided, one will be generated from `source`.
* @returns Used debug ID.
*/
processSourceAndSourceMapFiles(sourcePath, sourceMapPath, debugId, force) {
return __awaiter(this, void 0, void 0, function* () {
const sourceReadResult = yield (0, common_1.readFile)(sourcePath);
if (sourceReadResult.isErr()) {
return sourceReadResult;
}
const source = sourceReadResult.data;
if (!sourceMapPath) {
const pathFromSource = yield this.getSourceMapPathFromSource(source, sourcePath);
if (!pathFromSource) {
return (0, Result_1.Err)('could not find source map for source');
}
sourceMapPath = pathFromSource;
}
const sourceMapReadResult = yield (0, common_1.readFile)(sourceMapPath);
if (sourceMapReadResult.isErr()) {
return sourceMapReadResult;
}
const sourceMapJson = sourceMapReadResult.data;
const parseResult = (0, common_1.parseJSON)(sourceMapJson);
if (parseResult.isErr()) {
return parseResult;
}
const sourceMap = parseResult.data;
const processResult = yield this.processSourceAndSourceMap(source, sourceMap, debugId, force);
return (0, Result_1.Ok)(Object.assign(Object.assign({}, processResult), { sourcePath,
sourceMapPath }));
});
}
getSourceMapPathFromSourceFile(sourcePath) {
return __awaiter(this, void 0, void 0, function* () {
const sourceReadResult = yield (0, common_1.readFile)(sourcePath);
if (sourceReadResult.isErr()) {
return sourceReadResult;
}
return (0, Result_1.Ok)(yield this.getSourceMapPathFromSource(sourceReadResult.data, sourcePath));
});
}
getSourceMapPathFromSource(source, sourcePath) {
return __awaiter(this, void 0, void 0, function* () {
const matchAll = (str, regex) => {
const result = [];
// eslint-disable-next-line no-constant-condition
while (true) {
const match = regex.exec(str);
if (!match) {
return result;
}
result.push(match);
}
};
const checkFile = (filePath) => (0, flow_1.pipe)(filePath, common_1.statFile, (result) => (result.isOk() && result.data.isFile() ? filePath : undefined));
const sourceMapName = path_1.default.basename(sourcePath) + '.map';
const checkFileInDir = (dir) => (0, flow_1.pipe)(dir, common_1.statFile, (result) => result.isOk() && result.data.isDirectory()
? // If path exists and is a directory, check if file exists in that dir
checkFile(path_1.default.join(dir, sourceMapName))
: // If path does not exist or is not a directory, check if file exists in dir of that path
checkFile(path_1.default.join(path_1.default.dirname(dir), sourceMapName)));
const matches = matchAll(source, /^\s*\/\/# sourceMappingURL=(.+)$/gm);
if (!matches.length) {
return checkFileInDir(sourcePath);
}
for (const match of matches.reverse()) {
const file = match[1];
if (!file) {
continue;
}
const fullPath = path_1.default.resolve(path_1.default.dirname(sourcePath), file);
if (yield checkFile(fullPath)) {
return fullPath;
}
const fileInDir = yield checkFileInDir(fullPath);
if (fileInDir) {
return fileInDir;
}
}
return checkFileInDir(sourcePath);
});
}
addSourcesToSourceMap(sourceMap, sourceMapPath, force) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (typeof sourceMap === 'string') {
const parseResult = (0, common_1.parseJSON)(sourceMap);
if (parseResult.isErr()) {
return parseResult;
}
sourceMap = parseResult.data;
}
const sourceRoot = sourceMap.sourceRoot
? path_1.default.resolve(path_1.default.dirname(sourceMapPath), sourceMap.sourceRoot)
: path_1.default.resolve(path_1.default.dirname(sourceMapPath));
const succeeded = [];
const skipped = [];
const failed = [];
const sourcesContent = (_a = sourceMap.sourcesContent) !== null && _a !== void 0 ? _a : [];
for (let i = 0; i < sourceMap.sources.length; i++) {
const sourcePath = sourceMap.sources[i];
if (sourcesContent[i] && !force) {
skipped.push(sourcePath);
continue;
}
const readResult = yield (0, common_1.readFile)(path_1.default.resolve(sourceRoot, sourcePath));
if (readResult.isErr()) {
failed.push(sourcePath);
}
else {
sourcesContent[i] = readResult.data;
succeeded.push(sourcePath);
}
}
return (0, Result_1.Ok)({
sourceMap: Object.assign(Object.assign({}, sourceMap), { sourcesContent }),
succeeded,
skipped,
failed,
});
});
}
doesSourceMapHaveSources(sourceMap) {
var _a, _b;
return ((_a = sourceMap.sources) === null || _a === void 0 ? void 0 : _a.length) === ((_b = sourceMap.sourcesContent) === null || _b === void 0 ? void 0 : _b.length);
}
offsetSourceMap(sourceMap, count) {
return __awaiter(this, void 0, void 0, function* () {
// Each line in sourcemap is separated by a semicolon.
// Offsetting source map lines is just done by prepending semicolons
const offset = ';'.repeat(count);
const mappings = offset + sourceMap.mappings;
return Object.assign(Object.assign({}, sourceMap), { mappings });
});
}
}
exports.SourceProcessor = SourceProcessor;
//# sourceMappingURL=SourceProcessor.js.map