es-check
Version:
Checks the ECMAScript version of .js glob against a specified version of ECMAScript with a shell command
284 lines (231 loc) • 7.08 kB
JavaScript
const fs = require("fs");
const BASE64_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const VLQ_BASE_SHIFT = 5;
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
const VLQ_BASE_MASK = VLQ_BASE - 1;
const VLQ_CONTINUATION_BIT = VLQ_BASE;
function base64Decode(char) {
return BASE64_CHARS.indexOf(char);
}
function decodeVLQValue(str, startIndex) {
let result = 0;
let shift = 0;
let continuation = false;
let index = startIndex;
do {
const reachedEnd = index >= str.length;
if (reachedEnd) break;
const digit = base64Decode(str[index++]);
const invalidDigit = digit === -1;
if (invalidDigit) break;
continuation = !!(digit & VLQ_CONTINUATION_BIT);
const digitValue = digit & VLQ_BASE_MASK;
result += digitValue << shift;
shift += VLQ_BASE_SHIFT;
} while (continuation);
const isNegative = result & 1;
const signedResult = result >> 1;
const finalValue = isNegative ? -signedResult : signedResult;
return { value: finalValue, nextIndex: index };
}
function decodeVLQ(str) {
const values = [];
let index = 0;
while (index < str.length) {
const decoded = decodeVLQValue(str, index);
values.push(decoded.value);
index = decoded.nextIndex;
}
return values;
}
function parseSegment(segment, state) {
const hasNoSegment = !segment;
if (hasNoSegment) return null;
const values = decodeVLQ(segment);
const hasNoValues = values.length === 0;
if (hasNoValues) return null;
const generatedColumn = state.previousGeneratedColumn + values[0];
const hasSourceIndex = values.length > 1;
const sourceIndex = hasSourceIndex
? state.previousSourceIndex + values[1]
: null;
const source = hasSourceIndex ? state.sources[sourceIndex] : null;
const hasOriginalLine = values.length > 2;
const originalLine = hasOriginalLine
? state.previousSourceLine + values[2] + 1
: null;
const hasOriginalColumn = values.length > 3;
const originalColumn = hasOriginalColumn
? state.previousSourceColumn + values[3]
: null;
const hasNameIndex = values.length > 4;
const nameIndex = hasNameIndex ? state.previousNameIndex + values[4] : null;
const name = hasNameIndex ? state.names[nameIndex] : null;
const mapping = {
generatedLine: state.generatedLine,
generatedColumn,
sourceIndex,
source,
originalLine,
originalColumn,
nameIndex,
name,
};
const nextState = {
...state,
previousGeneratedColumn: generatedColumn,
previousSourceIndex: sourceIndex ?? state.previousSourceIndex,
previousSourceLine: hasOriginalLine
? state.previousSourceLine + values[2]
: state.previousSourceLine,
previousSourceColumn: originalColumn ?? state.previousSourceColumn,
previousNameIndex: nameIndex ?? state.previousNameIndex,
};
return { mapping, nextState };
}
function parseLine(line, state) {
const hasNoLine = !line;
if (hasNoLine) {
return {
mappings: [],
nextState: {
...state,
generatedLine: state.generatedLine + 1,
previousGeneratedColumn: 0,
},
};
}
const segments = line.split(",");
const initialState = { ...state, previousGeneratedColumn: 0 };
const result = segments.reduce(
(acc, segment) => {
const parsed = parseSegment(segment, acc.state);
const hasMapping = parsed !== null;
return hasMapping
? {
mappings: [...acc.mappings, parsed.mapping],
state: parsed.nextState,
}
: acc;
},
{ mappings: [], state: initialState },
);
return {
mappings: result.mappings,
nextState: {
...result.state,
generatedLine: state.generatedLine + 1,
},
};
}
function parseMappings(mappingsString, sources, names) {
const lines = mappingsString.split(";");
const initialState = {
generatedLine: 1,
previousGeneratedColumn: 0,
previousSourceIndex: 0,
previousSourceLine: 0,
previousSourceColumn: 0,
previousNameIndex: 0,
sources,
names,
};
const result = lines.reduce(
(acc, line) => {
const parsed = parseLine(line, acc.state);
return {
mappings: [...acc.mappings, ...parsed.mappings],
state: parsed.nextState,
};
},
{ mappings: [], state: initialState },
);
return result.mappings;
}
function findClosestMapping(mappings, targetLine, targetColumn) {
const sameLine = mappings.filter((m) => m.generatedLine === targetLine);
const hasSameLine = sameLine.length > 0;
if (!hasSameLine) return null;
const withDistance = sameLine.map((mapping) => ({
mapping,
distance: Math.abs(mapping.generatedColumn - targetColumn),
}));
const sorted = withDistance.sort((a, b) => a.distance - b.distance);
return sorted[0].mapping;
}
function buildSourcePath(sourceRoot, source) {
const hasSourceRoot = sourceRoot && sourceRoot.length > 0;
const combined = hasSourceRoot ? `${sourceRoot}/${source}` : source;
return combined.replace(/\/+/g, "/");
}
class SourceMapConsumer {
constructor(sourceMapData) {
this.version = sourceMapData.version;
this.sources = sourceMapData.sources || [];
this.names = sourceMapData.names || [];
this.mappings = sourceMapData.mappings || "";
this.file = sourceMapData.file;
this.sourceRoot = sourceMapData.sourceRoot || "";
this._parsedMappings = parseMappings(
this.mappings,
this.sources,
this.names,
);
}
originalPositionFor({ line, column }) {
const closest = findClosestMapping(this._parsedMappings, line, column);
const hasNoClosest = !closest;
const hasNoSource = hasNoClosest || !closest.source;
if (hasNoSource) {
return {
source: null,
line: null,
column: null,
name: null,
};
}
const source = buildSourcePath(this.sourceRoot, closest.source);
return {
source,
line: closest.originalLine,
column: closest.originalColumn,
name: closest.name || null,
};
}
destroy() {
this._parsedMappings = null;
}
}
function loadSourceMap(file) {
const mapFile = `${file}.map`;
try {
const mapExists = fs.existsSync(mapFile);
if (!mapExists) return null;
const mapContent = fs.readFileSync(mapFile, "utf8");
const sourceMapData = JSON.parse(mapContent);
const isValidVersion = sourceMapData.version === 3;
if (!isValidVersion) return null;
return new SourceMapConsumer(sourceMapData);
} catch {
return null;
}
}
function mapErrorPosition(file, line, column) {
const consumer = loadSourceMap(file);
const hasNoConsumer = !consumer;
if (hasNoConsumer) {
return { file, line, column };
}
const original = consumer.originalPositionFor({ line, column });
consumer.destroy();
const hasOriginalSource = original.source !== null;
return hasOriginalSource
? { file: original.source, line: original.line, column: original.column }
: { file, line, column };
}
module.exports = {
loadSourceMap,
mapErrorPosition,
SourceMapConsumer,
};