feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Saphal Patro and Jessie Anh Nguyen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,87 @@
# Hermes Profile Transformer
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/v/hermes-profile-transformer">
<img alt="node-current" src="https://img.shields.io/node/v/hermes-profile-transformer">
<img alt="npm bundle size" src="https://img.shields.io/bundlephobia/min/hermes-profile-transformer">
<img alt="NPM" src="https://img.shields.io/npm/l/hermes-profile-transformer">
<img alt="npm type definitions" src="https://img.shields.io/npm/types/hermes-profile-transformer">
</p>
Visualize Facebook's [Hermes JavaScript runtime](https://github.com/facebook/hermes) profile traces in Chrome Developer Tools.
![Demo Profile](https://raw.githubusercontent.com/MLH-Fellowship/hermes-profile-transformer/master/assets/convertedProfile.png)
## Overview
The Hermes runtime, used by React Native for Android, is able to output [Chrome Trace Events](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview) in JSON Object Format.
This TypeScript package converts Hermes CPU profiles to Chrome Developer Tools compatible JSON Array Format, and enriches it with line and column numbers and event categories from JavaScript source maps.
## Usage
If you're using `hermes-profile-transformer` to debug React Native Android applications, you can use the [React Native CLI](https://github.com/react-native-community/cli) `react-native profile-hermes` command, which uses this package to convert the downloaded Hermes profiles automatically.
### As a standalone package
```js
const transformer = require('hermes-profile-transformer').default;
const { promises } = require('fs');
const hermesCpuProfilePath = './testprofile.cpuprofile';
const sourceMapPath = './index.map';
const sourceMapBundleFileName = 'index.bundle.js';
transformer(
// profile path is required
hermesCpuProfilePath,
// source maps are optional
sourceMap,
sourceMapBundleFileName
)
.then(events => {
// write converted trace to a file
return promises.writeFile(
'./chrome-supported.json',
JSON.stringify(events, null, 2),
'utf-8'
);
})
.catch(err => {
console.log(err);
});
```
## Creating Hermes CPU Profiles
## Opening converted profiles in Chrome Developer Tools
Open Developer Tools in Chrome, navigate to the **Performance** tab, and use the **Load profile...** feature.
![Loading the Profile](https://raw.githubusercontent.com/MLH-Fellowship/hermes-profile-transformer/master/assets/loading.png)
## API
### transformer(profilePath: string, sourceMapPath?: string, bundleFileName?: string)
#### Parameters
| Parameter | Type | Required | Description |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| profilePath | string | Yes | Path to a JSON-formatted `.cpuprofile` file created by the Hermes runtime |
| sourceMapPath | string | No | Path to a [source-map](https://www.npmjs.com/package/source-map) compatible Source Map file |
| bundleFileName | string | No | If `sourceMapPath` is provided, you need to also provide the name of the JavaScript bundle file that the source map applies to. This file does not need to exist on disk. |
#### Returns
`Promise<DurationEvent[]>`, where `DurationEvent` is as defined in [EventInterfaces.ts](src/types/EventInterfaces.ts).
## Resources
- [Using Hermes with React Native](https://reactnative.dev/docs/hermes).
- [Chrome Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview). Hermes uses the JSON Object format.
- [Measuring JavaScript performance in Chrome](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference)
## LICENSE
[MIT](LICENSE)

View File

@@ -0,0 +1,541 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var fs = require('fs');
var util = require('util');
var path = _interopDefault(require('path'));
var sourceMap = require('source-map');
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it;
if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
it = o[Symbol.iterator]();
return it.next.bind(it);
}
var EventsPhase;
(function (EventsPhase) {
EventsPhase["DURATION_EVENTS_BEGIN"] = "B";
EventsPhase["DURATION_EVENTS_END"] = "E";
EventsPhase["COMPLETE_EVENTS"] = "X";
EventsPhase["INSTANT_EVENTS"] = "I";
EventsPhase["COUNTER_EVENTS"] = "C";
EventsPhase["ASYNC_EVENTS_NESTABLE_START"] = "b";
EventsPhase["ASYNC_EVENTS_NESTABLE_INSTANT"] = "n";
EventsPhase["ASYNC_EVENTS_NESTABLE_END"] = "e";
EventsPhase["FLOW_EVENTS_START"] = "s";
EventsPhase["FLOW_EVENTS_STEP"] = "t";
EventsPhase["FLOW_EVENTS_END"] = "f";
EventsPhase["SAMPLE_EVENTS"] = "P";
EventsPhase["OBJECT_EVENTS_CREATED"] = "N";
EventsPhase["OBJECT_EVENTS_SNAPSHOT"] = "O";
EventsPhase["OBJECT_EVENTS_DESTROYED"] = "D";
EventsPhase["METADATA_EVENTS"] = "M";
EventsPhase["MEMORY_DUMP_EVENTS_GLOBAL"] = "V";
EventsPhase["MEMORY_DUMP_EVENTS_PROCESS"] = "v";
EventsPhase["MARK_EVENTS"] = "R";
EventsPhase["CLOCK_SYNC_EVENTS"] = "c";
EventsPhase["CONTEXT_EVENTS_ENTER"] = "(";
EventsPhase["CONTEXT_EVENTS_LEAVE"] = ")"; // Deprecated
EventsPhase["ASYNC_EVENTS_START"] = "S";
EventsPhase["ASYNC_EVENTS_STEP_INTO"] = "T";
EventsPhase["ASYNC_EVENTS_STEP_PAST"] = "p";
EventsPhase["ASYNC_EVENTS_END"] = "F";
EventsPhase["LINKED_ID_EVENTS"] = "=";
})(EventsPhase || (EventsPhase = {}));
var CpuProfilerModel = /*#__PURE__*/function () {
function CpuProfilerModel(profile) {
this._profile = profile;
this._nodesById = this._createNodeMap();
this._activeNodeArraysById = this._createActiveNodeArrays();
}
/**
* Initialization function to enable O(1) access to nodes by node ID.
* @return {Map<number, CPUProfileChunkNode}
*/
var _proto = CpuProfilerModel.prototype;
_proto._createNodeMap = function _createNodeMap() {
/** @type {Map<number, CpuProfile['nodes'][0]>} */
var map = new Map();
for (var _iterator = _createForOfIteratorHelperLoose(this._profile.nodes), _step; !(_step = _iterator()).done;) {
var node = _step.value;
map.set(node.id, node);
}
return map;
}
/**
* Initialization function to enable O(1) access to the set of active nodes in the stack by node ID.
* @return Map<number, number[]>
*/
;
_proto._createActiveNodeArrays = function _createActiveNodeArrays() {
var _this = this;
var map = new Map();
/**
* Given a nodeId, `getActiveNodes` gets all the parent nodes in reversed call order
* @param {number} id
*/
var getActiveNodes = function getActiveNodes(id) {
if (map.has(id)) return map.get(id) || [];
var node = _this._nodesById.get(id);
if (!node) throw new Error("No such node " + id);
if (node.parent) {
var array = getActiveNodes(node.parent).concat([id]);
map.set(id, array);
return array;
} else {
return [id];
}
};
for (var _iterator2 = _createForOfIteratorHelperLoose(this._profile.nodes), _step2; !(_step2 = _iterator2()).done;) {
var node = _step2.value;
map.set(node.id, getActiveNodes(node.id));
}
return map;
}
/**
* Returns all the node IDs in a stack when a specific nodeId is at the top of the stack
* (i.e. a stack's node ID and the node ID of all of its parents).
*/
;
_proto._getActiveNodeIds = function _getActiveNodeIds(nodeId) {
var activeNodeIds = this._activeNodeArraysById.get(nodeId);
if (!activeNodeIds) throw new Error("No such node ID " + nodeId);
return activeNodeIds;
}
/**
* Generates the necessary B/E-style trace events for a single transition from stack A to stack B
* at the given timestamp.
*
* Example:
*
* timestamp 1234
* previousNodeIds 1,2,3
* currentNodeIds 1,2,4
*
* yields [end 3 at ts 1234, begin 4 at ts 1234]
*
* @param {number} timestamp
* @param {Array<number>} previousNodeIds
* @param {Array<number>} currentNodeIds
* @returns {Array<DurationEvent>}
*/
;
_proto._createStartEndEventsForTransition = function _createStartEndEventsForTransition(timestamp, previousNodeIds, currentNodeIds) {
var _this2 = this;
// Start nodes are the nodes which are present only in the currentNodeIds and not in PreviousNodeIds
var startNodes = currentNodeIds.filter(function (id) {
return !previousNodeIds.includes(id);
}).map(function (id) {
return _this2._nodesById.get(id);
}); // End nodes are the nodes which are present only in the PreviousNodeIds and not in CurrentNodeIds
var endNodes = previousNodeIds.filter(function (id) {
return !currentNodeIds.includes(id);
}).map(function (id) {
return _this2._nodesById.get(id);
});
/**
* The name needs to be modified if `http://` is present as this directs us to bundle files which does not add any information for the end user
* @param name
*/
var removeLinksIfExist = function removeLinksIfExist(name) {
// If the name includes `http://`, we can filter the name
if (name.includes('http://')) {
name = name.substring(0, name.lastIndexOf('('));
}
return name || 'anonymous';
};
/**
* Create a Duration Event from CPUProfileChunkNodes.
* @param {CPUProfileChunkNode} node
* @return {DurationEvent} */
var createEvent = function createEvent(node) {
return {
ts: timestamp,
pid: _this2._profile.pid,
tid: Number(_this2._profile.tid),
ph: EventsPhase.DURATION_EVENTS_BEGIN,
name: removeLinksIfExist(node.callFrame.name),
cat: node.callFrame.category,
args: _extends({}, node.callFrame)
};
};
var startEvents = startNodes.map(createEvent).map(function (evt) {
return _extends({}, evt, {
ph: EventsPhase.DURATION_EVENTS_BEGIN
});
});
var endEvents = endNodes.map(createEvent).map(function (evt) {
return _extends({}, evt, {
ph: EventsPhase.DURATION_EVENTS_END
});
});
return [].concat(endEvents.reverse(), startEvents);
}
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @return {DurationEvent}
* @throws If the length of timeDeltas array or the samples array does not match with the length of samples in Hermes Profile
*/
;
_proto.createStartEndEvents = function createStartEndEvents() {
var profile = this._profile;
var length = profile.samples.length;
if (profile.timeDeltas.length !== length || profile.samples.length !== length) throw new Error("Invalid CPU profile length");
var events = [];
var timestamp = profile.startTime;
var lastActiveNodeIds = [];
for (var i = 0; i < profile.samples.length; i++) {
var nodeId = profile.samples[i];
var timeDelta = Math.max(profile.timeDeltas[i], 0);
var node = this._nodesById.get(nodeId);
if (!node) throw new Error("Missing node " + nodeId);
timestamp += timeDelta;
var activeNodeIds = this._getActiveNodeIds(nodeId);
events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, activeNodeIds));
lastActiveNodeIds = activeNodeIds;
}
events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, []));
return events;
}
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @param {CPUProfileChunk} profile
*/
;
CpuProfilerModel.createStartEndEvents = function createStartEndEvents(profile) {
var model = new CpuProfilerModel(profile);
return model.createStartEndEvents();
}
/**
* Converts the Hermes Sample into a single CpuProfileChunk object for consumption
* by `createStartEndEvents()`.
*
* @param {HermesCPUProfile} profile
* @throws Profile must have at least one sample
* @return {CPUProfileChunk}
*/
;
CpuProfilerModel.collectProfileEvents = function collectProfileEvents(profile) {
if (profile.samples.length >= 0) {
var samples = profile.samples,
stackFrames = profile.stackFrames; // Assumption: The sample will have a single process
var pid = samples[0].pid; // Assumption: Javascript is single threaded, so there should only be one thread throughout
var tid = samples[0].tid; // TODO: What role does id play in string parsing
var id = '0x1';
var startTime = Number(samples[0].ts);
var _this$constructNodes = this.constructNodes(samples, stackFrames),
nodes = _this$constructNodes.nodes,
sampleNumbers = _this$constructNodes.sampleNumbers,
timeDeltas = _this$constructNodes.timeDeltas;
return {
id: id,
pid: pid,
tid: tid,
startTime: startTime,
nodes: nodes,
samples: sampleNumbers,
timeDeltas: timeDeltas
};
} else {
throw new Error('The hermes profile has zero samples');
}
}
/**
* Constructs CPUProfileChunk Nodes and the resultant samples and time deltas to be inputted into the
* CPUProfileChunk object which will be processed to give createStartEndEvents()
*
* @param {HermesSample} samples
* @param {<string, HermesStackFrame>} stackFrames
* @return {CPUProfileChunker}
*/
;
CpuProfilerModel.constructNodes = function constructNodes(samples, stackFrames) {
samples = samples.map(function (sample) {
sample.stackFrameData = stackFrames[sample.sf];
return sample;
});
var stackFrameIds = Object.keys(stackFrames);
var profileNodes = stackFrameIds.map(function (stackFrameId) {
var stackFrame = stackFrames[stackFrameId];
return {
id: Number(stackFrameId),
callFrame: _extends({}, stackFrame, {
url: stackFrame.name
}),
parent: stackFrames[stackFrameId].parent
};
});
var returnedSamples = [];
var timeDeltas = [];
var lastTimeStamp = Number(samples[0].ts);
samples.forEach(function (sample, idx) {
returnedSamples.push(sample.sf);
if (idx === 0) {
timeDeltas.push(0);
} else {
var timeDiff = Number(sample.ts) - lastTimeStamp;
lastTimeStamp = Number(sample.ts);
timeDeltas.push(timeDiff);
}
});
return {
nodes: profileNodes,
sampleNumbers: returnedSamples,
timeDeltas: timeDeltas
};
};
return CpuProfilerModel;
}();
// A type of promise-like that resolves synchronously and supports only one observer
const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator";
const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator";
// Asynchronously call a function and send errors to recovery continuation
function _catch(body, recover) {
try {
var result = body();
} catch(e) {
return recover(e);
}
if (result && result.then) {
return result.then(void 0, recover);
}
return result;
}
var readFileAsync = function readFileAsync(path) {
try {
return Promise.resolve(_catch(function () {
var readFileAsync = util.promisify(fs.readFile);
return Promise.resolve(readFileAsync(path, 'utf-8')).then(function (fileString) {
if (fileString.length === 0) {
throw new Error(path + " is an empty file");
}
var obj = JSON.parse(fileString);
return obj;
});
}, function (err) {
throw err;
}));
} catch (e) {
return Promise.reject(e);
}
};
/**
* This function is a helper to the applySourceMapsToEvents. The category allocation logic is implemented here based on the sourcemap url (if available)
* @param defaultCategory The category the event is of by default without the use of Source maps
* @param url The URL which can be parsed to interpret the new category of the event (depends on node_modules)
*/
var improveCategories = function improveCategories(defaultCategory, url) {
var obtainCategory = function obtainCategory(url) {
var dirs = url.substring(url.lastIndexOf(path.sep + "node_modules" + path.sep)).split(path.sep);
return dirs.length > 2 && dirs[1] === 'node_modules' ? dirs[2] : defaultCategory;
};
return url ? obtainCategory(url) : defaultCategory;
};
/**
* Enhances the function line, column and params information and event categories
* based on JavaScript source maps to make it easier to associate trace events with
* the application code
*
* Throws error if args not set up in ChromeEvents
* @param {SourceMap} sourceMap
* @param {DurationEvent[]} chromeEvents
* @param {string} indexBundleFileName
* @throws If `args` for events are not populated
* @returns {DurationEvent[]}
*/
var applySourceMapsToEvents = function applySourceMapsToEvents(sourceMap$1, chromeEvents, indexBundleFileName) {
try {
// SEE: Should file here be an optional parameter, so take indexBundleFileName as a parameter and use
// a default name of `index.bundle`
var rawSourceMap = {
version: Number(sourceMap$1.version),
file: indexBundleFileName || 'index.bundle',
sources: sourceMap$1.sources,
mappings: sourceMap$1.mappings,
names: sourceMap$1.names
};
return Promise.resolve(new sourceMap.SourceMapConsumer(rawSourceMap)).then(function (consumer) {
var events = chromeEvents.map(function (event) {
if (event.args) {
var sm = consumer.originalPositionFor({
line: Number(event.args.line),
column: Number(event.args.column)
});
/**
* The categories can help us better visualise the profile if we modify the categories.
* We change these categories only in the root level and not deeper inside the args, just so we have our
* original categories as well as these modified categories (as the modified categories simply help with visualisation)
*/
event.cat = improveCategories(event.cat, sm.source);
event.args = _extends({}, event.args, {
url: sm.source,
line: sm.line,
column: sm.column,
params: sm.name,
allocatedCategory: event.cat,
allocatedName: event.name
});
} else {
throw new Error("Source maps could not be derived for an event at " + event.ts + " and with stackFrame ID " + event.sf);
}
return event;
});
consumer.destroy();
return events;
});
} catch (e) {
return Promise.reject(e);
}
};
/**
* This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional)
* and return a promise which resolves to Chrome Dev Tools compatible events
* @param profilePath string
* @param sourceMapPath string
* @param bundleFileName string
* @return Promise<DurationEvent[]>
*/
var transformer = function transformer(profilePath, sourceMapPath, bundleFileName) {
try {
return Promise.resolve(readFileAsync(profilePath)).then(function (hermesProfile) {
var _exit = false;
var profileChunk = CpuProfilerModel.collectProfileEvents(hermesProfile);
var profiler = new CpuProfilerModel(profileChunk);
var chromeEvents = profiler.createStartEndEvents();
var _temp = function () {
if (sourceMapPath) {
return Promise.resolve(readFileAsync(sourceMapPath)).then(function (sourceMap) {
var events = applySourceMapsToEvents(sourceMap, chromeEvents, bundleFileName);
_exit = true;
return events;
});
}
}();
return _temp && _temp.then ? _temp.then(function (_result) {
return _exit ? _result : chromeEvents;
}) : _exit ? _temp : chromeEvents;
});
} catch (e) {
return Promise.reject(e);
}
};
exports.default = transformer;
//# sourceMappingURL=hermes-profile-transformer.cjs.development.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,535 @@
import { readFile } from 'fs';
import { promisify } from 'util';
import path from 'path';
import { SourceMapConsumer } from 'source-map';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it;
if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
it = o[Symbol.iterator]();
return it.next.bind(it);
}
var EventsPhase;
(function (EventsPhase) {
EventsPhase["DURATION_EVENTS_BEGIN"] = "B";
EventsPhase["DURATION_EVENTS_END"] = "E";
EventsPhase["COMPLETE_EVENTS"] = "X";
EventsPhase["INSTANT_EVENTS"] = "I";
EventsPhase["COUNTER_EVENTS"] = "C";
EventsPhase["ASYNC_EVENTS_NESTABLE_START"] = "b";
EventsPhase["ASYNC_EVENTS_NESTABLE_INSTANT"] = "n";
EventsPhase["ASYNC_EVENTS_NESTABLE_END"] = "e";
EventsPhase["FLOW_EVENTS_START"] = "s";
EventsPhase["FLOW_EVENTS_STEP"] = "t";
EventsPhase["FLOW_EVENTS_END"] = "f";
EventsPhase["SAMPLE_EVENTS"] = "P";
EventsPhase["OBJECT_EVENTS_CREATED"] = "N";
EventsPhase["OBJECT_EVENTS_SNAPSHOT"] = "O";
EventsPhase["OBJECT_EVENTS_DESTROYED"] = "D";
EventsPhase["METADATA_EVENTS"] = "M";
EventsPhase["MEMORY_DUMP_EVENTS_GLOBAL"] = "V";
EventsPhase["MEMORY_DUMP_EVENTS_PROCESS"] = "v";
EventsPhase["MARK_EVENTS"] = "R";
EventsPhase["CLOCK_SYNC_EVENTS"] = "c";
EventsPhase["CONTEXT_EVENTS_ENTER"] = "(";
EventsPhase["CONTEXT_EVENTS_LEAVE"] = ")"; // Deprecated
EventsPhase["ASYNC_EVENTS_START"] = "S";
EventsPhase["ASYNC_EVENTS_STEP_INTO"] = "T";
EventsPhase["ASYNC_EVENTS_STEP_PAST"] = "p";
EventsPhase["ASYNC_EVENTS_END"] = "F";
EventsPhase["LINKED_ID_EVENTS"] = "=";
})(EventsPhase || (EventsPhase = {}));
var CpuProfilerModel = /*#__PURE__*/function () {
function CpuProfilerModel(profile) {
this._profile = profile;
this._nodesById = this._createNodeMap();
this._activeNodeArraysById = this._createActiveNodeArrays();
}
/**
* Initialization function to enable O(1) access to nodes by node ID.
* @return {Map<number, CPUProfileChunkNode}
*/
var _proto = CpuProfilerModel.prototype;
_proto._createNodeMap = function _createNodeMap() {
/** @type {Map<number, CpuProfile['nodes'][0]>} */
var map = new Map();
for (var _iterator = _createForOfIteratorHelperLoose(this._profile.nodes), _step; !(_step = _iterator()).done;) {
var node = _step.value;
map.set(node.id, node);
}
return map;
}
/**
* Initialization function to enable O(1) access to the set of active nodes in the stack by node ID.
* @return Map<number, number[]>
*/
;
_proto._createActiveNodeArrays = function _createActiveNodeArrays() {
var _this = this;
var map = new Map();
/**
* Given a nodeId, `getActiveNodes` gets all the parent nodes in reversed call order
* @param {number} id
*/
var getActiveNodes = function getActiveNodes(id) {
if (map.has(id)) return map.get(id) || [];
var node = _this._nodesById.get(id);
if (!node) throw new Error("No such node " + id);
if (node.parent) {
var array = getActiveNodes(node.parent).concat([id]);
map.set(id, array);
return array;
} else {
return [id];
}
};
for (var _iterator2 = _createForOfIteratorHelperLoose(this._profile.nodes), _step2; !(_step2 = _iterator2()).done;) {
var node = _step2.value;
map.set(node.id, getActiveNodes(node.id));
}
return map;
}
/**
* Returns all the node IDs in a stack when a specific nodeId is at the top of the stack
* (i.e. a stack's node ID and the node ID of all of its parents).
*/
;
_proto._getActiveNodeIds = function _getActiveNodeIds(nodeId) {
var activeNodeIds = this._activeNodeArraysById.get(nodeId);
if (!activeNodeIds) throw new Error("No such node ID " + nodeId);
return activeNodeIds;
}
/**
* Generates the necessary B/E-style trace events for a single transition from stack A to stack B
* at the given timestamp.
*
* Example:
*
* timestamp 1234
* previousNodeIds 1,2,3
* currentNodeIds 1,2,4
*
* yields [end 3 at ts 1234, begin 4 at ts 1234]
*
* @param {number} timestamp
* @param {Array<number>} previousNodeIds
* @param {Array<number>} currentNodeIds
* @returns {Array<DurationEvent>}
*/
;
_proto._createStartEndEventsForTransition = function _createStartEndEventsForTransition(timestamp, previousNodeIds, currentNodeIds) {
var _this2 = this;
// Start nodes are the nodes which are present only in the currentNodeIds and not in PreviousNodeIds
var startNodes = currentNodeIds.filter(function (id) {
return !previousNodeIds.includes(id);
}).map(function (id) {
return _this2._nodesById.get(id);
}); // End nodes are the nodes which are present only in the PreviousNodeIds and not in CurrentNodeIds
var endNodes = previousNodeIds.filter(function (id) {
return !currentNodeIds.includes(id);
}).map(function (id) {
return _this2._nodesById.get(id);
});
/**
* The name needs to be modified if `http://` is present as this directs us to bundle files which does not add any information for the end user
* @param name
*/
var removeLinksIfExist = function removeLinksIfExist(name) {
// If the name includes `http://`, we can filter the name
if (name.includes('http://')) {
name = name.substring(0, name.lastIndexOf('('));
}
return name || 'anonymous';
};
/**
* Create a Duration Event from CPUProfileChunkNodes.
* @param {CPUProfileChunkNode} node
* @return {DurationEvent} */
var createEvent = function createEvent(node) {
return {
ts: timestamp,
pid: _this2._profile.pid,
tid: Number(_this2._profile.tid),
ph: EventsPhase.DURATION_EVENTS_BEGIN,
name: removeLinksIfExist(node.callFrame.name),
cat: node.callFrame.category,
args: _extends({}, node.callFrame)
};
};
var startEvents = startNodes.map(createEvent).map(function (evt) {
return _extends({}, evt, {
ph: EventsPhase.DURATION_EVENTS_BEGIN
});
});
var endEvents = endNodes.map(createEvent).map(function (evt) {
return _extends({}, evt, {
ph: EventsPhase.DURATION_EVENTS_END
});
});
return [].concat(endEvents.reverse(), startEvents);
}
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @return {DurationEvent}
* @throws If the length of timeDeltas array or the samples array does not match with the length of samples in Hermes Profile
*/
;
_proto.createStartEndEvents = function createStartEndEvents() {
var profile = this._profile;
var length = profile.samples.length;
if (profile.timeDeltas.length !== length || profile.samples.length !== length) throw new Error("Invalid CPU profile length");
var events = [];
var timestamp = profile.startTime;
var lastActiveNodeIds = [];
for (var i = 0; i < profile.samples.length; i++) {
var nodeId = profile.samples[i];
var timeDelta = Math.max(profile.timeDeltas[i], 0);
var node = this._nodesById.get(nodeId);
if (!node) throw new Error("Missing node " + nodeId);
timestamp += timeDelta;
var activeNodeIds = this._getActiveNodeIds(nodeId);
events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, activeNodeIds));
lastActiveNodeIds = activeNodeIds;
}
events.push.apply(events, this._createStartEndEventsForTransition(timestamp, lastActiveNodeIds, []));
return events;
}
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @param {CPUProfileChunk} profile
*/
;
CpuProfilerModel.createStartEndEvents = function createStartEndEvents(profile) {
var model = new CpuProfilerModel(profile);
return model.createStartEndEvents();
}
/**
* Converts the Hermes Sample into a single CpuProfileChunk object for consumption
* by `createStartEndEvents()`.
*
* @param {HermesCPUProfile} profile
* @throws Profile must have at least one sample
* @return {CPUProfileChunk}
*/
;
CpuProfilerModel.collectProfileEvents = function collectProfileEvents(profile) {
if (profile.samples.length >= 0) {
var samples = profile.samples,
stackFrames = profile.stackFrames; // Assumption: The sample will have a single process
var pid = samples[0].pid; // Assumption: Javascript is single threaded, so there should only be one thread throughout
var tid = samples[0].tid; // TODO: What role does id play in string parsing
var id = '0x1';
var startTime = Number(samples[0].ts);
var _this$constructNodes = this.constructNodes(samples, stackFrames),
nodes = _this$constructNodes.nodes,
sampleNumbers = _this$constructNodes.sampleNumbers,
timeDeltas = _this$constructNodes.timeDeltas;
return {
id: id,
pid: pid,
tid: tid,
startTime: startTime,
nodes: nodes,
samples: sampleNumbers,
timeDeltas: timeDeltas
};
} else {
throw new Error('The hermes profile has zero samples');
}
}
/**
* Constructs CPUProfileChunk Nodes and the resultant samples and time deltas to be inputted into the
* CPUProfileChunk object which will be processed to give createStartEndEvents()
*
* @param {HermesSample} samples
* @param {<string, HermesStackFrame>} stackFrames
* @return {CPUProfileChunker}
*/
;
CpuProfilerModel.constructNodes = function constructNodes(samples, stackFrames) {
samples = samples.map(function (sample) {
sample.stackFrameData = stackFrames[sample.sf];
return sample;
});
var stackFrameIds = Object.keys(stackFrames);
var profileNodes = stackFrameIds.map(function (stackFrameId) {
var stackFrame = stackFrames[stackFrameId];
return {
id: Number(stackFrameId),
callFrame: _extends({}, stackFrame, {
url: stackFrame.name
}),
parent: stackFrames[stackFrameId].parent
};
});
var returnedSamples = [];
var timeDeltas = [];
var lastTimeStamp = Number(samples[0].ts);
samples.forEach(function (sample, idx) {
returnedSamples.push(sample.sf);
if (idx === 0) {
timeDeltas.push(0);
} else {
var timeDiff = Number(sample.ts) - lastTimeStamp;
lastTimeStamp = Number(sample.ts);
timeDeltas.push(timeDiff);
}
});
return {
nodes: profileNodes,
sampleNumbers: returnedSamples,
timeDeltas: timeDeltas
};
};
return CpuProfilerModel;
}();
// A type of promise-like that resolves synchronously and supports only one observer
const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator";
const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator";
// Asynchronously call a function and send errors to recovery continuation
function _catch(body, recover) {
try {
var result = body();
} catch(e) {
return recover(e);
}
if (result && result.then) {
return result.then(void 0, recover);
}
return result;
}
var readFileAsync = function readFileAsync(path) {
try {
return Promise.resolve(_catch(function () {
var readFileAsync = promisify(readFile);
return Promise.resolve(readFileAsync(path, 'utf-8')).then(function (fileString) {
if (fileString.length === 0) {
throw new Error(path + " is an empty file");
}
var obj = JSON.parse(fileString);
return obj;
});
}, function (err) {
throw err;
}));
} catch (e) {
return Promise.reject(e);
}
};
/**
* This function is a helper to the applySourceMapsToEvents. The category allocation logic is implemented here based on the sourcemap url (if available)
* @param defaultCategory The category the event is of by default without the use of Source maps
* @param url The URL which can be parsed to interpret the new category of the event (depends on node_modules)
*/
var improveCategories = function improveCategories(defaultCategory, url) {
var obtainCategory = function obtainCategory(url) {
var dirs = url.substring(url.lastIndexOf(path.sep + "node_modules" + path.sep)).split(path.sep);
return dirs.length > 2 && dirs[1] === 'node_modules' ? dirs[2] : defaultCategory;
};
return url ? obtainCategory(url) : defaultCategory;
};
/**
* Enhances the function line, column and params information and event categories
* based on JavaScript source maps to make it easier to associate trace events with
* the application code
*
* Throws error if args not set up in ChromeEvents
* @param {SourceMap} sourceMap
* @param {DurationEvent[]} chromeEvents
* @param {string} indexBundleFileName
* @throws If `args` for events are not populated
* @returns {DurationEvent[]}
*/
var applySourceMapsToEvents = function applySourceMapsToEvents(sourceMap, chromeEvents, indexBundleFileName) {
try {
// SEE: Should file here be an optional parameter, so take indexBundleFileName as a parameter and use
// a default name of `index.bundle`
var rawSourceMap = {
version: Number(sourceMap.version),
file: indexBundleFileName || 'index.bundle',
sources: sourceMap.sources,
mappings: sourceMap.mappings,
names: sourceMap.names
};
return Promise.resolve(new SourceMapConsumer(rawSourceMap)).then(function (consumer) {
var events = chromeEvents.map(function (event) {
if (event.args) {
var sm = consumer.originalPositionFor({
line: Number(event.args.line),
column: Number(event.args.column)
});
/**
* The categories can help us better visualise the profile if we modify the categories.
* We change these categories only in the root level and not deeper inside the args, just so we have our
* original categories as well as these modified categories (as the modified categories simply help with visualisation)
*/
event.cat = improveCategories(event.cat, sm.source);
event.args = _extends({}, event.args, {
url: sm.source,
line: sm.line,
column: sm.column,
params: sm.name,
allocatedCategory: event.cat,
allocatedName: event.name
});
} else {
throw new Error("Source maps could not be derived for an event at " + event.ts + " and with stackFrame ID " + event.sf);
}
return event;
});
consumer.destroy();
return events;
});
} catch (e) {
return Promise.reject(e);
}
};
/**
* This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional)
* and return a promise which resolves to Chrome Dev Tools compatible events
* @param profilePath string
* @param sourceMapPath string
* @param bundleFileName string
* @return Promise<DurationEvent[]>
*/
var transformer = function transformer(profilePath, sourceMapPath, bundleFileName) {
try {
return Promise.resolve(readFileAsync(profilePath)).then(function (hermesProfile) {
var _exit = false;
var profileChunk = CpuProfilerModel.collectProfileEvents(hermesProfile);
var profiler = new CpuProfilerModel(profileChunk);
var chromeEvents = profiler.createStartEndEvents();
var _temp = function () {
if (sourceMapPath) {
return Promise.resolve(readFileAsync(sourceMapPath)).then(function (sourceMap) {
var events = applySourceMapsToEvents(sourceMap, chromeEvents, bundleFileName);
_exit = true;
return events;
});
}
}();
return _temp && _temp.then ? _temp.then(function (_result) {
return _exit ? _result : chromeEvents;
}) : _exit ? _temp : chromeEvents;
});
} catch (e) {
return Promise.reject(e);
}
};
export default transformer;
//# sourceMappingURL=hermes-profile-transformer.esm.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
import { DurationEvent } from './types/EventInterfaces';
/**
* This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional)
* and return a promise which resolves to Chrome Dev Tools compatible events
* @param profilePath string
* @param sourceMapPath string
* @param bundleFileName string
* @return Promise<DurationEvent[]>
*/
declare const transformer: (profilePath: string, sourceMapPath: string | undefined, bundleFileName: string | undefined) => Promise<DurationEvent[]>;
export default transformer;
export { SourceMap } from './types/SourceMap';

View File

@@ -0,0 +1,12 @@
import { DurationEvent } from './types/EventInterfaces';
/**
* This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional)
* and return a promise which resolves to Chrome Dev Tools compatible events
* @param profilePath string
* @param sourceMapPath string
* @param bundleFileName string
* @return Promise<DurationEvent[]>
*/
declare const transformer: (profilePath: string, sourceMapPath: string | undefined, bundleFileName: string | undefined) => Promise<DurationEvent[]>;
export default transformer;
export { SourceMap } from './types/SourceMap';

View File

@@ -0,0 +1,8 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./hermes-profile-transformer.cjs.production.min.js')
} else {
module.exports = require('./hermes-profile-transformer.cjs.development.js')
}

View File

@@ -0,0 +1,16 @@
import { DurationEvent } from '../types/EventInterfaces';
import { SourceMap } from '../types/SourceMap';
/**
* Enhances the function line, column and params information and event categories
* based on JavaScript source maps to make it easier to associate trace events with
* the application code
*
* Throws error if args not set up in ChromeEvents
* @param {SourceMap} sourceMap
* @param {DurationEvent[]} chromeEvents
* @param {string} indexBundleFileName
* @throws If `args` for events are not populated
* @returns {DurationEvent[]}
*/
declare const applySourceMapsToEvents: (sourceMap: SourceMap, chromeEvents: DurationEvent[], indexBundleFileName: string | undefined) => Promise<DurationEvent[]>;
export default applySourceMapsToEvents;

View File

@@ -0,0 +1,107 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*
* MODIFICATION NOTICE:
* This file is derived from `https://github.com/GoogleChrome/lighthouse/blob/0422daa9b1b8528dd8436860b153134bd0f959f1/lighthouse-core/lib/tracehouse/cpu-profile-model.js`
* and has been modified by Saphal Patro (email: saphal1998@gmail.com)
* The following changes have been made to the original file:
* 1. Converted code to Typescript and defined necessary types
* 2. Wrote a method @see collectProfileEvents to convert the Hermes Samples to Profile Chunks supported by Lighthouse Parser
* 3. Modified @see constructNodes to work with the Hermes Samples and StackFrames
*/
/**
* @fileoverview
*
* This model converts the `Profile` and `ProfileChunk` mega trace events from the `disabled-by-default-v8.cpu_profiler`
* category into B/E-style trace events that main-thread-tasks.js already knows how to parse into a task tree.
*
* The CPU profiler measures where time is being spent by sampling the stack (See https://www.jetbrains.com/help/profiler/Profiling_Guidelines__Choosing_the_Right_Profiling_Mode.html
* for a generic description of the differences between tracing and sampling).
*
* A `Profile` event is a record of the stack that was being executed at different sample points in time.
* It has a structure like this:
*
* nodes: [function A, function B, function C]
* samples: [node with id 2, node with id 1, ...]
* timeDeltas: [4125μs since last sample, 121μs since last sample, ...]
*
* Helpful prior art:
* @see https://cs.chromium.org/chromium/src/third_party/devtools-frontend/src/front_end/sdk/CPUProfileDataModel.js?sq=package:chromium&g=0&l=42
* @see https://github.com/v8/v8/blob/99ca333b0efba3236954b823101315aefeac51ab/tools/profile.js
* @see https://github.com/jlfwong/speedscope/blob/9ed1eb192cb7e9dac43a5f25bd101af169dc654a/src/import/chrome.ts#L200
*/
import { CPUProfileChunk, CPUProfileChunkNode, CPUProfileChunker } from '../types/CPUProfile';
import { DurationEvent } from '../types/EventInterfaces';
import { HermesCPUProfile, HermesSample, HermesStackFrame } from '../types/HermesProfile';
export declare class CpuProfilerModel {
_profile: CPUProfileChunk;
_nodesById: Map<number, CPUProfileChunkNode>;
_activeNodeArraysById: Map<number, number[]>;
constructor(profile: CPUProfileChunk);
/**
* Initialization function to enable O(1) access to nodes by node ID.
* @return {Map<number, CPUProfileChunkNode}
*/
_createNodeMap(): Map<number, CPUProfileChunkNode>;
/**
* Initialization function to enable O(1) access to the set of active nodes in the stack by node ID.
* @return Map<number, number[]>
*/
_createActiveNodeArrays(): Map<number, number[]>;
/**
* Returns all the node IDs in a stack when a specific nodeId is at the top of the stack
* (i.e. a stack's node ID and the node ID of all of its parents).
*/
_getActiveNodeIds(nodeId: number): number[];
/**
* Generates the necessary B/E-style trace events for a single transition from stack A to stack B
* at the given timestamp.
*
* Example:
*
* timestamp 1234
* previousNodeIds 1,2,3
* currentNodeIds 1,2,4
*
* yields [end 3 at ts 1234, begin 4 at ts 1234]
*
* @param {number} timestamp
* @param {Array<number>} previousNodeIds
* @param {Array<number>} currentNodeIds
* @returns {Array<DurationEvent>}
*/
_createStartEndEventsForTransition(timestamp: number, previousNodeIds: number[], currentNodeIds: number[]): DurationEvent[];
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @return {DurationEvent}
* @throws If the length of timeDeltas array or the samples array does not match with the length of samples in Hermes Profile
*/
createStartEndEvents(): DurationEvent[];
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @param {CPUProfileChunk} profile
*/
static createStartEndEvents(profile: CPUProfileChunk): DurationEvent[];
/**
* Converts the Hermes Sample into a single CpuProfileChunk object for consumption
* by `createStartEndEvents()`.
*
* @param {HermesCPUProfile} profile
* @throws Profile must have at least one sample
* @return {CPUProfileChunk}
*/
static collectProfileEvents(profile: HermesCPUProfile): CPUProfileChunk;
/**
* Constructs CPUProfileChunk Nodes and the resultant samples and time deltas to be inputted into the
* CPUProfileChunk object which will be processed to give createStartEndEvents()
*
* @param {HermesSample} samples
* @param {<string, HermesStackFrame>} stackFrames
* @return {CPUProfileChunker}
*/
static constructNodes(samples: HermesSample[], stackFrames: {
[key in string]: HermesStackFrame;
}): CPUProfileChunker;
}

View File

@@ -0,0 +1,39 @@
/**
* The CPUProfileChunk is the intermediate file that Lighthouse can interpret and
* hence subsequently convert to events supported by Chrome Dev Tools
*/
export interface CPUProfileChunk {
id: string;
pid: number;
tid: string;
startTime: number;
nodes: CPUProfileChunkNode[];
samples: number[];
timeDeltas: number[];
}
/**
* The CPUProfileChunkNode is an individual element of the nodes[] property in the CPUProfileChunk
* @see CPUProfileChunk
*/
export interface CPUProfileChunkNode {
id: number;
callFrame: {
line: string;
column: string;
funcLine: string;
funcColumn: string;
name: string;
url?: string;
category: string;
};
parent?: number;
}
/**
* The process of conversion of Hermes Profile Events to Lighthouse supported events are primarily focussed
* around generating the correct values of the properties in CPUProfileChunker.
*/
export declare type CPUProfileChunker = {
nodes: CPUProfileChunkNode[];
sampleNumbers: number[];
timeDeltas: number[];
};

View File

@@ -0,0 +1,143 @@
import { EventsPhase } from './Phases';
export interface SharedEventProperties {
/**
* name of the event
*/
name?: string;
/**
* event category
*/
cat?: string;
/**
* tracing clock timestamp
*/
ts?: number;
/**
* process ID
*/
pid?: number;
/**
* thread ID
*/
tid?: number;
/**
* event type (phase)
*/
ph: EventsPhase;
/**
* id for a stackFrame object
*/
sf?: number;
/**
* thread clock timestamp
*/
tts?: number;
/**
* a fixed color name
*/
cname?: string;
/**
* event arguments
*/
args?: {
[key in string]: any;
};
}
interface DurationEventBegin extends SharedEventProperties {
ph: EventsPhase.DURATION_EVENTS_BEGIN;
}
interface DurationEventEnd extends SharedEventProperties {
ph: EventsPhase.DURATION_EVENTS_END;
}
export declare type DurationEvent = DurationEventBegin | DurationEventEnd;
export interface CompleteEvent extends SharedEventProperties {
ph: EventsPhase.COMPLETE_EVENTS;
dur: number;
}
export interface MetadataEvent extends SharedEventProperties {
ph: EventsPhase.METADATA_EVENTS;
}
export interface SampleEvent extends SharedEventProperties {
ph: EventsPhase.SAMPLE_EVENTS;
}
interface ObjectEventCreated extends SharedEventProperties {
ph: EventsPhase.OBJECT_EVENTS_CREATED;
scope?: string;
}
interface ObjectEventSnapshot extends SharedEventProperties {
ph: EventsPhase.OBJECT_EVENTS_SNAPSHOT;
scope?: string;
}
interface ObjectEventDestroyed extends SharedEventProperties {
ph: EventsPhase.OBJECT_EVENTS_DESTROYED;
scope?: string;
}
export declare type ObjectEvent = ObjectEventCreated | ObjectEventSnapshot | ObjectEventDestroyed;
export interface ClockSyncEvent extends SharedEventProperties {
ph: EventsPhase.CLOCK_SYNC_EVENTS;
args: {
sync_id: string;
issue_ts?: number;
};
}
interface ContextEventEnter extends SharedEventProperties {
ph: EventsPhase.CONTEXT_EVENTS_ENTER;
}
interface ContextEventLeave extends SharedEventProperties {
ph: EventsPhase.CONTEXT_EVENTS_LEAVE;
}
export declare type ContextEvent = ContextEventEnter | ContextEventLeave;
interface AsyncEventStart extends SharedEventProperties {
ph: EventsPhase.ASYNC_EVENTS_NESTABLE_START;
id: number;
scope?: string;
}
interface AsyncEventInstant extends SharedEventProperties {
ph: EventsPhase.ASYNC_EVENTS_NESTABLE_INSTANT;
id: number;
scope?: string;
}
interface AsyncEventEnd extends SharedEventProperties {
ph: EventsPhase.ASYNC_EVENTS_NESTABLE_END;
id: number;
scope?: string;
}
export declare type AsyncEvent = AsyncEventStart | AsyncEventInstant | AsyncEventEnd;
export interface InstantEvent extends SharedEventProperties {
ph: EventsPhase.INSTANT_EVENTS;
s: string;
}
export interface CounterEvent extends SharedEventProperties {
ph: EventsPhase.COUNTER_EVENTS;
}
interface FlowEventStart extends SharedEventProperties {
ph: EventsPhase.FLOW_EVENTS_START;
}
interface FlowEventStep extends SharedEventProperties {
ph: EventsPhase.FLOW_EVENTS_STEP;
}
interface FlowEventEnd extends SharedEventProperties {
ph: EventsPhase.FLOW_EVENTS_END;
}
export declare type FlowEvent = FlowEventStart | FlowEventStep | FlowEventEnd;
interface MemoryDumpGlobal extends SharedEventProperties {
ph: EventsPhase.MEMORY_DUMP_EVENTS_GLOBAL;
id: string;
}
interface MemoryDumpProcess extends SharedEventProperties {
ph: EventsPhase.MEMORY_DUMP_EVENTS_PROCESS;
id: string;
}
export declare type MemoryDumpEvent = MemoryDumpGlobal | MemoryDumpProcess;
export interface MarkEvent extends SharedEventProperties {
ph: EventsPhase.MARK_EVENTS;
}
export interface LinkedIDEvent extends SharedEventProperties {
ph: EventsPhase.LINKED_ID_EVENTS;
id: number;
args: {
linked_id: number;
};
}
export declare type Event = DurationEvent | CompleteEvent | MetadataEvent | SampleEvent | ObjectEvent | ClockSyncEvent | ContextEvent | AsyncEvent | InstantEvent | CounterEvent | FlowEvent | MemoryDumpEvent | MarkEvent | LinkedIDEvent;
export {};

View File

@@ -0,0 +1,42 @@
import { SharedEventProperties } from './EventInterfaces';
/**
* Each item in the stackFrames object of the hermes profile
*/
export interface HermesStackFrame {
line: string;
column: string;
funcLine: string;
funcColumn: string;
name: string;
category: string;
/**
* A parent function may or may not exist
*/
parent?: number;
}
/**
* Each item in the samples array of the hermes profile
*/
export interface HermesSample {
cpu: string;
name: string;
ts: string;
pid: number;
tid: string;
weight: string;
/**
* Will refer to an element in the stackFrames object of the Hermes Profile
*/
sf: number;
stackFrameData?: HermesStackFrame;
}
/**
* Hermes Profile Interface
*/
export interface HermesCPUProfile {
traceEvents: SharedEventProperties[];
samples: HermesSample[];
stackFrames: {
[key in string]: HermesStackFrame;
};
}

View File

@@ -0,0 +1,29 @@
export declare enum EventsPhase {
DURATION_EVENTS_BEGIN = "B",
DURATION_EVENTS_END = "E",
COMPLETE_EVENTS = "X",
INSTANT_EVENTS = "I",
COUNTER_EVENTS = "C",
ASYNC_EVENTS_NESTABLE_START = "b",
ASYNC_EVENTS_NESTABLE_INSTANT = "n",
ASYNC_EVENTS_NESTABLE_END = "e",
FLOW_EVENTS_START = "s",
FLOW_EVENTS_STEP = "t",
FLOW_EVENTS_END = "f",
SAMPLE_EVENTS = "P",
OBJECT_EVENTS_CREATED = "N",
OBJECT_EVENTS_SNAPSHOT = "O",
OBJECT_EVENTS_DESTROYED = "D",
METADATA_EVENTS = "M",
MEMORY_DUMP_EVENTS_GLOBAL = "V",
MEMORY_DUMP_EVENTS_PROCESS = "v",
MARK_EVENTS = "R",
CLOCK_SYNC_EVENTS = "c",
CONTEXT_EVENTS_ENTER = "(",
CONTEXT_EVENTS_LEAVE = ")",
ASYNC_EVENTS_START = "S",
ASYNC_EVENTS_STEP_INTO = "T",
ASYNC_EVENTS_STEP_PAST = "p",
ASYNC_EVENTS_END = "F",
LINKED_ID_EVENTS = "="
}

View File

@@ -0,0 +1,11 @@
export interface SourceMap {
version: string;
sources: string[];
sourceContent: string[];
x_facebook_sources: {
names: string[];
mappings: string;
}[] | null;
names: string[];
mappings: string;
}

View File

@@ -0,0 +1 @@
export declare const readFileAsync: (path: string) => Promise<any>;

View File

@@ -0,0 +1,28 @@
Copyright (c) 2009-2011, Mozilla Foundation and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the names of the Mozilla Foundation nor the names of project
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,837 @@
# Source Map
[![NPM](https://nodei.co/npm/source-map.png?downloads=true&downloadRank=true)](https://www.npmjs.com/package/source-map)
This is a library to generate and consume the source map format
[described here][format].
[format]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
## Use with Node
$ npm install source-map
## Use on the Web
```html
<script src="https://unpkg.com/source-map@0.7.3/dist/source-map.js"></script>
<script>
sourceMap.SourceMapConsumer.initialize({
"lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm",
});
</script>
```
---
## Table of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Examples](#examples)
- [Consuming a source map](#consuming-a-source-map)
- [Generating a source map](#generating-a-source-map)
- [With SourceNode (high level API)](#with-sourcenode-high-level-api)
- [With SourceMapGenerator (low level API)](#with-sourcemapgenerator-low-level-api)
- [API](#api)
- [SourceMapConsumer](#sourcemapconsumer)
- [SourceMapConsumer.initialize(options)](#sourcemapconsumerinitializeoptions)
- [new SourceMapConsumer(rawSourceMap)](#new-sourcemapconsumerrawsourcemap)
- [SourceMapConsumer.with](#sourcemapconsumerwith)
- [SourceMapConsumer.prototype.destroy()](#sourcemapconsumerprototypedestroy)
- [SourceMapConsumer.prototype.computeColumnSpans()](#sourcemapconsumerprototypecomputecolumnspans)
- [SourceMapConsumer.prototype.originalPositionFor(generatedPosition)](#sourcemapconsumerprototypeoriginalpositionforgeneratedposition)
- [SourceMapConsumer.prototype.generatedPositionFor(originalPosition)](#sourcemapconsumerprototypegeneratedpositionfororiginalposition)
- [SourceMapConsumer.prototype.allGeneratedPositionsFor(originalPosition)](#sourcemapconsumerprototypeallgeneratedpositionsfororiginalposition)
- [SourceMapConsumer.prototype.hasContentsOfAllSources()](#sourcemapconsumerprototypehascontentsofallsources)
- [SourceMapConsumer.prototype.sourceContentFor(source[, returnNullOnMissing])](#sourcemapconsumerprototypesourcecontentforsource-returnnullonmissing)
- [SourceMapConsumer.prototype.eachMapping(callback, context, order)](#sourcemapconsumerprototypeeachmappingcallback-context-order)
- [SourceMapGenerator](#sourcemapgenerator)
- [new SourceMapGenerator([startOfSourceMap])](#new-sourcemapgeneratorstartofsourcemap)
- [SourceMapGenerator.fromSourceMap(sourceMapConsumer)](#sourcemapgeneratorfromsourcemapsourcemapconsumer)
- [SourceMapGenerator.prototype.addMapping(mapping)](#sourcemapgeneratorprototypeaddmappingmapping)
- [SourceMapGenerator.prototype.setSourceContent(sourceFile, sourceContent)](#sourcemapgeneratorprototypesetsourcecontentsourcefile-sourcecontent)
- [SourceMapGenerator.prototype.applySourceMap(sourceMapConsumer[, sourceFile[, sourceMapPath]])](#sourcemapgeneratorprototypeapplysourcemapsourcemapconsumer-sourcefile-sourcemappath)
- [SourceMapGenerator.prototype.toString()](#sourcemapgeneratorprototypetostring)
- [SourceNode](#sourcenode)
- [new SourceNode([line, column, source[, chunk[, name]]])](#new-sourcenodeline-column-source-chunk-name)
- [SourceNode.fromStringWithSourceMap(code, sourceMapConsumer[, relativePath])](#sourcenodefromstringwithsourcemapcode-sourcemapconsumer-relativepath)
- [SourceNode.prototype.add(chunk)](#sourcenodeprototypeaddchunk)
- [SourceNode.prototype.prepend(chunk)](#sourcenodeprototypeprependchunk)
- [SourceNode.prototype.setSourceContent(sourceFile, sourceContent)](#sourcenodeprototypesetsourcecontentsourcefile-sourcecontent)
- [SourceNode.prototype.walk(fn)](#sourcenodeprototypewalkfn)
- [SourceNode.prototype.walkSourceContents(fn)](#sourcenodeprototypewalksourcecontentsfn)
- [SourceNode.prototype.join(sep)](#sourcenodeprototypejoinsep)
- [SourceNode.prototype.replaceRight(pattern, replacement)](#sourcenodeprototypereplacerightpattern-replacement)
- [SourceNode.prototype.toString()](#sourcenodeprototypetostring)
- [SourceNode.prototype.toStringWithSourceMap([startOfSourceMap])](#sourcenodeprototypetostringwithsourcemapstartofsourcemap)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Examples
### Consuming a source map
```js
const rawSourceMap = {
version: 3,
file: "min.js",
names: ["bar", "baz", "n"],
sources: ["one.js", "two.js"],
sourceRoot: "http://example.com/www/js/",
mappings:
"CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA",
};
const whatever = await SourceMapConsumer.with(rawSourceMap, null, consumer => {
console.log(consumer.sources);
// [ 'http://example.com/www/js/one.js',
// 'http://example.com/www/js/two.js' ]
console.log(
consumer.originalPositionFor({
line: 2,
column: 28,
})
);
// { source: 'http://example.com/www/js/two.js',
// line: 2,
// column: 10,
// name: 'n' }
console.log(
consumer.generatedPositionFor({
source: "http://example.com/www/js/two.js",
line: 2,
column: 10,
})
);
// { line: 2, column: 28 }
consumer.eachMapping(function (m) {
// ...
});
return computeWhatever();
});
```
### Generating a source map
In depth guide:
[**Compiling to JavaScript, and Debugging with Source Maps**](https://hacks.mozilla.org/2013/05/compiling-to-javascript-and-debugging-with-source-maps/)
#### With SourceNode (high level API)
```js
function compile(ast) {
switch (ast.type) {
case "BinaryExpression":
return new SourceNode(
ast.location.line,
ast.location.column,
ast.location.source,
[compile(ast.left), " + ", compile(ast.right)]
);
case "Literal":
return new SourceNode(
ast.location.line,
ast.location.column,
ast.location.source,
String(ast.value)
);
// ...
default:
throw new Error("Bad AST");
}
}
var ast = parse("40 + 2", "add.js");
console.log(
compile(ast).toStringWithSourceMap({
file: "add.js",
})
);
// { code: '40 + 2',
// map: [object SourceMapGenerator] }
```
#### With SourceMapGenerator (low level API)
```js
var map = new SourceMapGenerator({
file: "source-mapped.js",
});
map.addMapping({
generated: {
line: 10,
column: 35,
},
source: "foo.js",
original: {
line: 33,
column: 2,
},
name: "christopher",
});
console.log(map.toString());
// '{"version":3,"file":"source-mapped.js","sources":["foo.js"],"names":["christopher"],"mappings":";;;;;;;;;mCAgCEA"}'
```
## API
Get a reference to the module:
```js
// Node.js
var sourceMap = require("source-map");
// Browser builds
var sourceMap = window.sourceMap;
// Inside Firefox
const sourceMap = require("devtools/toolkit/sourcemap/source-map.js");
```
### SourceMapConsumer
A `SourceMapConsumer` instance represents a parsed source map which we can query
for information about the original file positions by giving it a file position
in the generated source.
#### SourceMapConsumer.initialize(options)
When using `SourceMapConsumer` outside of node.js, for example on the Web, it
needs to know from what URL to load `lib/mappings.wasm`. You must inform it by
calling `initialize` before constructing any `SourceMapConsumer`s.
The options object has the following properties:
- `"lib/mappings.wasm"`: A `String` containing the URL of the
`lib/mappings.wasm` file, or an `ArrayBuffer` with the contents of `lib/mappings.wasm`.
```js
sourceMap.SourceMapConsumer.initialize({
"lib/mappings.wasm": "https://example.com/source-map/lib/mappings.wasm",
});
```
#### new SourceMapConsumer(rawSourceMap)
The only parameter is the raw source map (either as a string which can be
`JSON.parse`'d, or an object). According to the spec, source maps have the
following attributes:
- `version`: Which version of the source map spec this map is following.
- `sources`: An array of URLs to the original source files.
- `names`: An array of identifiers which can be referenced by individual
mappings.
- `sourceRoot`: Optional. The URL root from which all sources are relative.
- `sourcesContent`: Optional. An array of contents of the original source files.
- `mappings`: A string of base64 VLQs which contain the actual mappings.
- `file`: Optional. The generated filename this source map is associated with.
- `x_google_ignoreList`: Optional. An additional extension field which is an array
of indices refering to urls in the sources array. This is used to identify third-party
sources, that the developer might want to avoid when debugging. [Read more](https://developer.chrome.com/articles/x-google-ignore-list/)
The promise of the constructed souce map consumer is returned.
When the `SourceMapConsumer` will no longer be used anymore, you must call its
`destroy` method.
```js
const consumer = await new sourceMap.SourceMapConsumer(rawSourceMapJsonData);
doStuffWith(consumer);
consumer.destroy();
```
Alternatively, you can use `SourceMapConsumer.with` to avoid needing to remember
to call `destroy`.
#### SourceMapConsumer.with
Construct a new `SourceMapConsumer` from `rawSourceMap` and `sourceMapUrl`
(see the `SourceMapConsumer` constructor for details. Then, invoke the `async function f(SourceMapConsumer) -> T` with the newly constructed consumer, wait
for `f` to complete, call `destroy` on the consumer, and return `f`'s return
value.
You must not use the consumer after `f` completes!
By using `with`, you do not have to remember to manually call `destroy` on
the consumer, since it will be called automatically once `f` completes.
```js
const xSquared = await SourceMapConsumer.with(
myRawSourceMap,
null,
async function (consumer) {
// Use `consumer` inside here and don't worry about remembering
// to call `destroy`.
const x = await whatever(consumer);
return x * x;
}
);
// You may not use that `consumer` anymore out here; it has
// been destroyed. But you can use `xSquared`.
console.log(xSquared);
```
#### SourceMapConsumer.prototype.destroy()
Free this source map consumer's associated wasm data that is manually-managed.
```js
consumer.destroy();
```
Alternatively, you can use `SourceMapConsumer.with` to avoid needing to remember
to call `destroy`.
#### SourceMapConsumer.prototype.computeColumnSpans()
Compute the last column for each generated mapping. The last column is
inclusive.
```js
// Before:
consumer.allGeneratedPositionsFor({ line: 2, source: "foo.coffee" });
// [ { line: 2,
// column: 1 },
// { line: 2,
// column: 10 },
// { line: 2,
// column: 20 } ]
consumer.computeColumnSpans();
// After:
consumer.allGeneratedPositionsFor({ line: 2, source: "foo.coffee" });
// [ { line: 2,
// column: 1,
// lastColumn: 9 },
// { line: 2,
// column: 10,
// lastColumn: 19 },
// { line: 2,
// column: 20,
// lastColumn: Infinity } ]
```
#### SourceMapConsumer.prototype.originalPositionFor(generatedPosition)
Returns the original source, line, and column information for the generated
source's line and column positions provided. The only argument is an object with
the following properties:
- `line`: The line number in the generated source. Line numbers in
this library are 1-based (note that the underlying source map
specification uses 0-based line numbers -- this library handles the
translation).
- `column`: The column number in the generated source. Column numbers
in this library are 0-based.
- `bias`: Either `SourceMapConsumer.GREATEST_LOWER_BOUND` or
`SourceMapConsumer.LEAST_UPPER_BOUND`. Specifies whether to return the closest
element that is smaller than or greater than the one we are searching for,
respectively, if the exact element cannot be found. Defaults to
`SourceMapConsumer.GREATEST_LOWER_BOUND`.
and an object is returned with the following properties:
- `source`: The original source file, or null if this information is not
available.
- `line`: The line number in the original source, or null if this information is
not available. The line number is 1-based.
- `column`: The column number in the original source, or null if this
information is not available. The column number is 0-based.
- `name`: The original identifier, or null if this information is not available.
```js
consumer.originalPositionFor({ line: 2, column: 10 });
// { source: 'foo.coffee',
// line: 2,
// column: 2,
// name: null }
consumer.originalPositionFor({
line: 99999999999999999,
column: 999999999999999,
});
// { source: null,
// line: null,
// column: null,
// name: null }
```
#### SourceMapConsumer.prototype.generatedPositionFor(originalPosition)
Returns the generated line and column information for the original source,
line, and column positions provided. The only argument is an object with
the following properties:
- `source`: The filename of the original source.
- `line`: The line number in the original source. The line number is
1-based.
- `column`: The column number in the original source. The column
number is 0-based.
and an object is returned with the following properties:
- `line`: The line number in the generated source, or null. The line
number is 1-based.
- `column`: The column number in the generated source, or null. The
column number is 0-based.
```js
consumer.generatedPositionFor({ source: "example.js", line: 2, column: 10 });
// { line: 1,
// column: 56 }
```
#### SourceMapConsumer.prototype.allGeneratedPositionsFor(originalPosition)
Returns all generated line and column information for the original source, line,
and column provided. If no column is provided, returns all mappings
corresponding to a either the line we are searching for or the next closest line
that has any mappings. Otherwise, returns all mappings corresponding to the
given line and either the column we are searching for or the next closest column
that has any offsets.
The only argument is an object with the following properties:
- `source`: The filename of the original source.
- `line`: The line number in the original source. The line number is
1-based.
- `column`: Optional. The column number in the original source. The
column number is 0-based.
and an array of objects is returned, each with the following properties:
- `line`: The line number in the generated source, or null. The line
number is 1-based.
- `column`: The column number in the generated source, or null. The
column number is 0-based.
```js
consumer.allGeneratedPositionsFor({ line: 2, source: "foo.coffee" });
// [ { line: 2,
// column: 1 },
// { line: 2,
// column: 10 },
// { line: 2,
// column: 20 } ]
```
#### SourceMapConsumer.prototype.hasContentsOfAllSources()
Return true if we have the embedded source content for every source listed in
the source map, false otherwise.
In other words, if this method returns `true`, then
`consumer.sourceContentFor(s)` will succeed for every source `s` in
`consumer.sources`.
```js
// ...
if (consumer.hasContentsOfAllSources()) {
consumerReadyCallback(consumer);
} else {
fetchSources(consumer, consumerReadyCallback);
}
// ...
```
#### SourceMapConsumer.prototype.sourceContentFor(source[, returnNullOnMissing])
Returns the original source content for the source provided. The only
argument is the URL of the original source file.
If the source content for the given source is not found, then an error is
thrown. Optionally, pass `true` as the second param to have `null` returned
instead.
```js
consumer.sources;
// [ "my-cool-lib.clj" ]
consumer.sourceContentFor("my-cool-lib.clj");
// "..."
consumer.sourceContentFor("this is not in the source map");
// Error: "this is not in the source map" is not in the source map
consumer.sourceContentFor("this is not in the source map", true);
// null
```
#### SourceMapConsumer.prototype.eachMapping(callback, context, order)
Iterate over each mapping between an original source/line/column and a
generated line/column in this source map.
- `callback`: The function that is called with each mapping. Mappings have the
form `{ source, generatedLine, generatedColumn, originalLine, originalColumn, name }`
- `context`: Optional. If specified, this object will be the value of `this`
every time that `callback` is called.
- `order`: Either `SourceMapConsumer.GENERATED_ORDER` or
`SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to iterate over
the mappings sorted by the generated file's line/column order or the
original's source/line/column order, respectively. Defaults to
`SourceMapConsumer.GENERATED_ORDER`.
```js
consumer.eachMapping(function (m) {
console.log(m);
});
// ...
// { source: 'illmatic.js',
// generatedLine: 1,
// generatedColumn: 0,
// originalLine: 1,
// originalColumn: 0,
// name: null }
// { source: 'illmatic.js',
// generatedLine: 2,
// generatedColumn: 0,
// originalLine: 2,
// originalColumn: 0,
// name: null }
// ...
```
### SourceMapGenerator
An instance of the SourceMapGenerator represents a source map which is being
built incrementally.
#### new SourceMapGenerator([startOfSourceMap])
You may pass an object with the following properties:
- `file`: The filename of the generated source that this source map is
associated with.
- `sourceRoot`: A root for all relative URLs in this source map.
- `skipValidation`: Optional. When `true`, disables validation of mappings as
they are added. This can improve performance but should be used with
discretion, as a last resort. Even then, one should avoid using this flag when
running tests, if possible.
```js
var generator = new sourceMap.SourceMapGenerator({
file: "my-generated-javascript-file.js",
sourceRoot: "http://example.com/app/js/",
});
```
#### SourceMapGenerator.fromSourceMap(sourceMapConsumer)
Creates a new `SourceMapGenerator` from an existing `SourceMapConsumer` instance.
- `sourceMapConsumer` The SourceMap.
```js
var generator = sourceMap.SourceMapGenerator.fromSourceMap(consumer);
```
#### SourceMapGenerator.prototype.addMapping(mapping)
Add a single mapping from original source line and column to the generated
source's line and column for this source map being created. The mapping object
should have the following properties:
- `generated`: An object with the generated line and column positions.
- `original`: An object with the original line and column positions.
- `source`: The original source file (relative to the sourceRoot).
- `name`: An optional original token name for this mapping.
```js
generator.addMapping({
source: "module-one.scm",
original: { line: 128, column: 0 },
generated: { line: 3, column: 456 },
});
```
#### SourceMapGenerator.prototype.setSourceContent(sourceFile, sourceContent)
Set the source content for an original source file.
- `sourceFile` the URL of the original source file.
- `sourceContent` the content of the source file.
```js
generator.setSourceContent(
"module-one.scm",
fs.readFileSync("path/to/module-one.scm")
);
```
#### SourceMapGenerator.prototype.applySourceMap(sourceMapConsumer[, sourceFile[, sourceMapPath]])
Applies a SourceMap for a source file to the SourceMap.
Each mapping to the supplied source file is rewritten using the
supplied SourceMap. Note: The resolution for the resulting mappings
is the minimum of this map and the supplied map.
- `sourceMapConsumer`: The SourceMap to be applied.
- `sourceFile`: Optional. The filename of the source file.
If omitted, sourceMapConsumer.file will be used, if it exists.
Otherwise an error will be thrown.
- `sourceMapPath`: Optional. The dirname of the path to the SourceMap
to be applied. If relative, it is relative to the SourceMap.
This parameter is needed when the two SourceMaps aren't in the same
directory, and the SourceMap to be applied contains relative source
paths. If so, those relative source paths need to be rewritten
relative to the SourceMap.
If omitted, it is assumed that both SourceMaps are in the same directory,
thus not needing any rewriting. (Supplying `'.'` has the same effect.)
#### SourceMapGenerator.prototype.toString()
Renders the source map being generated to a string.
```js
generator.toString();
// '{"version":3,"sources":["module-one.scm"],"names":[],"mappings":"...snip...","file":"my-generated-javascript-file.js","sourceRoot":"http://example.com/app/js/"}'
```
### SourceNode
SourceNodes provide a way to abstract over interpolating and/or concatenating
snippets of generated JavaScript source code, while maintaining the line and
column information associated between those snippets and the original source
code. This is useful as the final intermediate representation a compiler might
use before outputting the generated JS and source map.
#### new SourceNode([line, column, source[, chunk[, name]]])
- `line`: The original line number associated with this source node, or null if
it isn't associated with an original line. The line number is 1-based.
- `column`: The original column number associated with this source node, or null
if it isn't associated with an original column. The column number
is 0-based.
- `source`: The original source's filename; null if no filename is provided.
- `chunk`: Optional. Is immediately passed to `SourceNode.prototype.add`, see
below.
- `name`: Optional. The original identifier.
```js
var node = new SourceNode(1, 2, "a.cpp", [
new SourceNode(3, 4, "b.cpp", "extern int status;\n"),
new SourceNode(5, 6, "c.cpp", "std::string* make_string(size_t n);\n"),
new SourceNode(7, 8, "d.cpp", "int main(int argc, char** argv) {}\n"),
]);
```
#### SourceNode.fromStringWithSourceMap(code, sourceMapConsumer[, relativePath])
Creates a SourceNode from generated code and a SourceMapConsumer.
- `code`: The generated code
- `sourceMapConsumer` The SourceMap for the generated code
- `relativePath` The optional path that relative sources in `sourceMapConsumer`
should be relative to.
```js
const consumer = await new SourceMapConsumer(
fs.readFileSync("path/to/my-file.js.map", "utf8")
);
const node = SourceNode.fromStringWithSourceMap(
fs.readFileSync("path/to/my-file.js"),
consumer
);
```
#### SourceNode.prototype.add(chunk)
Add a chunk of generated JS to this source node.
- `chunk`: A string snippet of generated JS code, another instance of
`SourceNode`, or an array where each member is one of those things.
```js
node.add(" + ");
node.add(otherNode);
node.add([leftHandOperandNode, " + ", rightHandOperandNode]);
```
#### SourceNode.prototype.prepend(chunk)
Prepend a chunk of generated JS to this source node.
- `chunk`: A string snippet of generated JS code, another instance of
`SourceNode`, or an array where each member is one of those things.
```js
node.prepend("/** Build Id: f783haef86324gf **/\n\n");
```
#### SourceNode.prototype.setSourceContent(sourceFile, sourceContent)
Set the source content for a source file. This will be added to the
`SourceMap` in the `sourcesContent` field.
- `sourceFile`: The filename of the source file
- `sourceContent`: The content of the source file
```js
node.setSourceContent(
"module-one.scm",
fs.readFileSync("path/to/module-one.scm")
);
```
#### SourceNode.prototype.walk(fn)
Walk over the tree of JS snippets in this node and its children. The walking
function is called once for each snippet of JS and is passed that snippet and
the its original associated source's line/column location.
- `fn`: The traversal function.
```js
var node = new SourceNode(1, 2, "a.js", [
new SourceNode(3, 4, "b.js", "uno"),
"dos",
["tres", new SourceNode(5, 6, "c.js", "quatro")],
]);
node.walk(function (code, loc) {
console.log("WALK:", code, loc);
});
// WALK: uno { source: 'b.js', line: 3, column: 4, name: null }
// WALK: dos { source: 'a.js', line: 1, column: 2, name: null }
// WALK: tres { source: 'a.js', line: 1, column: 2, name: null }
// WALK: quatro { source: 'c.js', line: 5, column: 6, name: null }
```
#### SourceNode.prototype.walkSourceContents(fn)
Walk over the tree of SourceNodes. The walking function is called for each
source file content and is passed the filename and source content.
- `fn`: The traversal function.
```js
var a = new SourceNode(1, 2, "a.js", "generated from a");
a.setSourceContent("a.js", "original a");
var b = new SourceNode(1, 2, "b.js", "generated from b");
b.setSourceContent("b.js", "original b");
var c = new SourceNode(1, 2, "c.js", "generated from c");
c.setSourceContent("c.js", "original c");
var node = new SourceNode(null, null, null, [a, b, c]);
node.walkSourceContents(function (source, contents) {
console.log("WALK:", source, ":", contents);
});
// WALK: a.js : original a
// WALK: b.js : original b
// WALK: c.js : original c
```
#### SourceNode.prototype.join(sep)
Like `Array.prototype.join` except for SourceNodes. Inserts the separator
between each of this source node's children.
- `sep`: The separator.
```js
var lhs = new SourceNode(1, 2, "a.rs", "my_copy");
var operand = new SourceNode(3, 4, "a.rs", "=");
var rhs = new SourceNode(5, 6, "a.rs", "orig.clone()");
var node = new SourceNode(null, null, null, [lhs, operand, rhs]);
var joinedNode = node.join(" ");
```
#### SourceNode.prototype.replaceRight(pattern, replacement)
Call `String.prototype.replace` on the very right-most source snippet. Useful
for trimming white space from the end of a source node, etc.
- `pattern`: The pattern to replace.
- `replacement`: The thing to replace the pattern with.
```js
// Trim trailing white space.
node.replaceRight(/\s*$/, "");
```
#### SourceNode.prototype.toString()
Return the string representation of this source node. Walks over the tree and
concatenates all the various snippets together to one string.
```js
var node = new SourceNode(1, 2, "a.js", [
new SourceNode(3, 4, "b.js", "uno"),
"dos",
["tres", new SourceNode(5, 6, "c.js", "quatro")],
]);
node.toString();
// 'unodostresquatro'
```
#### SourceNode.prototype.toStringWithSourceMap([startOfSourceMap])
Returns the string representation of this tree of source nodes, plus a
SourceMapGenerator which contains all the mappings between the generated and
original sources.
The arguments are the same as those to `new SourceMapGenerator`.
```js
var node = new SourceNode(1, 2, "a.js", [
new SourceNode(3, 4, "b.js", "uno"),
"dos",
["tres", new SourceNode(5, 6, "c.js", "quatro")],
]);
node.toStringWithSourceMap({ file: "my-output-file.js" });
// { code: 'unodostresquatro',
// map: [object SourceMapGenerator] }
```

View File

@@ -0,0 +1,100 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
/**
* A data structure which is a combination of an array and a set. Adding a new
* member is O(1), testing for membership is O(1), and finding the index of an
* element is O(1). Removing elements from the set is not supported. Only
* strings are supported for membership.
*/
class ArraySet {
constructor() {
this._array = [];
this._set = new Map();
}
/**
* Static method for creating ArraySet instances from an existing array.
*/
static fromArray(aArray, aAllowDuplicates) {
const set = new ArraySet();
for (let i = 0, len = aArray.length; i < len; i++) {
set.add(aArray[i], aAllowDuplicates);
}
return set;
}
/**
* Return how many unique items are in this ArraySet. If duplicates have been
* added, than those do not count towards the size.
*
* @returns Number
*/
size() {
return this._set.size;
}
/**
* Add the given string to this set.
*
* @param String aStr
*/
add(aStr, aAllowDuplicates) {
const isDuplicate = this.has(aStr);
const idx = this._array.length;
if (!isDuplicate || aAllowDuplicates) {
this._array.push(aStr);
}
if (!isDuplicate) {
this._set.set(aStr, idx);
}
}
/**
* Is the given string a member of this set?
*
* @param String aStr
*/
has(aStr) {
return this._set.has(aStr);
}
/**
* What is the index of the given string in the array?
*
* @param String aStr
*/
indexOf(aStr) {
const idx = this._set.get(aStr);
if (idx >= 0) {
return idx;
}
throw new Error('"' + aStr + '" is not in the set.');
}
/**
* What is the element at the given index?
*
* @param Number aIdx
*/
at(aIdx) {
if (aIdx >= 0 && aIdx < this._array.length) {
return this._array[aIdx];
}
throw new Error("No element indexed by " + aIdx);
}
/**
* Returns the array representation of this set (which has the proper indices
* indicated by indexOf). Note that this is a copy of the internal array used
* for storing the members so that no one can mess with internal state.
*/
toArray() {
return this._array.slice();
}
}
exports.ArraySet = ArraySet;

View File

@@ -0,0 +1,94 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*
* Based on the Base 64 VLQ implementation in Closure Compiler:
* https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java
*
* Copyright 2011 The Closure Compiler Authors. All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
const base64 = require("./base64");
// A single base 64 digit can contain 6 bits of data. For the base 64 variable
// length quantities we use in the source map spec, the first bit is the sign,
// the next four bits are the actual value, and the 6th bit is the
// continuation bit. The continuation bit tells us whether there are more
// digits in this value following this digit.
//
// Continuation
// | Sign
// | |
// V V
// 101011
const VLQ_BASE_SHIFT = 5;
// binary: 100000
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
// binary: 011111
const VLQ_BASE_MASK = VLQ_BASE - 1;
// binary: 100000
const VLQ_CONTINUATION_BIT = VLQ_BASE;
/**
* Converts from a two-complement value to a value where the sign bit is
* placed in the least significant bit. For example, as decimals:
* 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
* 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
*/
function toVLQSigned(aValue) {
return aValue < 0 ? (-aValue << 1) + 1 : (aValue << 1) + 0;
}
/**
* Returns the base 64 VLQ encoded value.
*/
exports.encode = function base64VLQ_encode(aValue) {
let encoded = "";
let digit;
let vlq = toVLQSigned(aValue);
do {
digit = vlq & VLQ_BASE_MASK;
vlq >>>= VLQ_BASE_SHIFT;
if (vlq > 0) {
// There are still more digits in this value, so we must make sure the
// continuation bit is marked.
digit |= VLQ_CONTINUATION_BIT;
}
encoded += base64.encode(digit);
} while (vlq > 0);
return encoded;
};

View File

@@ -0,0 +1,19 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
const intToCharMap =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");
/**
* Encode an integer in the range of 0 to 63 to a single base 64 digit.
*/
exports.encode = function (number) {
if (0 <= number && number < intToCharMap.length) {
return intToCharMap[number];
}
throw new TypeError("Must be between 0 and 63: " + number);
};

View File

@@ -0,0 +1,113 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
exports.GREATEST_LOWER_BOUND = 1;
exports.LEAST_UPPER_BOUND = 2;
/**
* Recursive implementation of binary search.
*
* @param aLow Indices here and lower do not contain the needle.
* @param aHigh Indices here and higher do not contain the needle.
* @param aNeedle The element being searched for.
* @param aHaystack The non-empty array being searched.
* @param aCompare Function which takes two elements and returns -1, 0, or 1.
* @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
* 'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
* closest element that is smaller than or greater than the one we are
* searching for, respectively, if the exact element cannot be found.
*/
function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare, aBias) {
// This function terminates when one of the following is true:
//
// 1. We find the exact element we are looking for.
//
// 2. We did not find the exact element, but we can return the index of
// the next-closest element.
//
// 3. We did not find the exact element, and there is no next-closest
// element than the one we are searching for, so we return -1.
const mid = Math.floor((aHigh - aLow) / 2) + aLow;
const cmp = aCompare(aNeedle, aHaystack[mid], true);
if (cmp === 0) {
// Found the element we are looking for.
return mid;
} else if (cmp > 0) {
// Our needle is greater than aHaystack[mid].
if (aHigh - mid > 1) {
// The element is in the upper half.
return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare, aBias);
}
// The exact needle element was not found in this haystack. Determine if
// we are in termination case (3) or (2) and return the appropriate thing.
if (aBias === exports.LEAST_UPPER_BOUND) {
return aHigh < aHaystack.length ? aHigh : -1;
}
return mid;
}
// Our needle is less than aHaystack[mid].
if (mid - aLow > 1) {
// The element is in the lower half.
return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare, aBias);
}
// we are in termination case (3) or (2) and return the appropriate thing.
if (aBias == exports.LEAST_UPPER_BOUND) {
return mid;
}
return aLow < 0 ? -1 : aLow;
}
/**
* This is an implementation of binary search which will always try and return
* the index of the closest element if there is no exact hit. This is because
* mappings between original and generated line/col pairs are single points,
* and there is an implicit region between each of them, so a miss just means
* that you aren't on the very start of a region.
*
* @param aNeedle The element you are looking for.
* @param aHaystack The array that is being searched.
* @param aCompare A function which takes the needle and an element in the
* array and returns -1, 0, or 1 depending on whether the needle is less
* than, equal to, or greater than the element, respectively.
* @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
* 'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
* closest element that is smaller than or greater than the one we are
* searching for, respectively, if the exact element cannot be found.
* Defaults to 'binarySearch.GREATEST_LOWER_BOUND'.
*/
exports.search = function search(aNeedle, aHaystack, aCompare, aBias) {
if (aHaystack.length === 0) {
return -1;
}
let index = recursiveSearch(
-1,
aHaystack.length,
aNeedle,
aHaystack,
aCompare,
aBias || exports.GREATEST_LOWER_BOUND
);
if (index < 0) {
return -1;
}
// We have found either the exact element, or the next-closest element to
// the one we are searching for. However, there may be more than one such
// element. Make sure we always return the smallest of these.
while (index - 1 >= 0) {
if (aCompare(aHaystack[index], aHaystack[index - 1], true) !== 0) {
break;
}
--index;
}
return index;
};

View File

@@ -0,0 +1,83 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2014 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
const util = require("./util");
/**
* Determine whether mappingB is after mappingA with respect to generated
* position.
*/
function generatedPositionAfter(mappingA, mappingB) {
// Optimized for most common case
const lineA = mappingA.generatedLine;
const lineB = mappingB.generatedLine;
const columnA = mappingA.generatedColumn;
const columnB = mappingB.generatedColumn;
return (
lineB > lineA ||
(lineB == lineA && columnB >= columnA) ||
util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0
);
}
/**
* A data structure to provide a sorted view of accumulated mappings in a
* performance conscious manner. It trades a negligible overhead in general
* case for a large speedup in case of mappings being added in order.
*/
class MappingList {
constructor() {
this._array = [];
this._sorted = true;
// Serves as infimum
this._last = { generatedLine: -1, generatedColumn: 0 };
}
/**
* Iterate through internal items. This method takes the same arguments that
* `Array.prototype.forEach` takes.
*
* NOTE: The order of the mappings is NOT guaranteed.
*/
unsortedForEach(aCallback, aThisArg) {
this._array.forEach(aCallback, aThisArg);
}
/**
* Add the given source mapping.
*
* @param Object aMapping
*/
add(aMapping) {
if (generatedPositionAfter(this._last, aMapping)) {
this._last = aMapping;
this._array.push(aMapping);
} else {
this._sorted = false;
this._array.push(aMapping);
}
}
/**
* Returns the flat, sorted array of mappings. The mappings are sorted by
* generated position.
*
* WARNING: This method returns internal data without copying, for
* performance. The return value must NOT be mutated, and should be treated as
* an immutable borrow. If you want to take ownership, you must make your own
* copy.
*/
toArray() {
if (!this._sorted) {
this._array.sort(util.compareByGeneratedPositionsInflated);
this._sorted = true;
}
return this._array;
}
}
exports.MappingList = MappingList;

View File

@@ -0,0 +1,23 @@
"use strict";
let mappingsWasm = null;
module.exports = function readWasm() {
if (typeof mappingsWasm === "string") {
return fetch(mappingsWasm).then(response => response.arrayBuffer());
}
if (mappingsWasm instanceof ArrayBuffer) {
return Promise.resolve(mappingsWasm);
}
throw new Error(
"You must provide the string URL or ArrayBuffer contents " +
"of lib/mappings.wasm by calling " +
"SourceMapConsumer.initialize({ 'lib/mappings.wasm': ... }) " +
"before using SourceMapConsumer"
);
};
module.exports.initialize = input => {
mappingsWasm = input;
};

View File

@@ -0,0 +1,27 @@
"use strict";
// Note: This file is replaced with "read-wasm-browser.js" when this module is
// bundled with a packager that takes package.json#browser fields into account.
const fs = require("fs");
const path = require("path");
module.exports = function readWasm() {
return new Promise((resolve, reject) => {
const wasmPath = path.join(__dirname, "mappings.wasm");
fs.readFile(wasmPath, null, (error, data) => {
if (error) {
reject(error);
return;
}
resolve(data.buffer);
});
});
};
module.exports.initialize = _ => {
console.debug(
"SourceMapConsumer.initialize is a no-op when running in node.js"
);
};

View File

@@ -0,0 +1,439 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
const base64VLQ = require("./base64-vlq");
const util = require("./util");
const ArraySet = require("./array-set").ArraySet;
const MappingList = require("./mapping-list").MappingList;
/**
* An instance of the SourceMapGenerator represents a source map which is
* being built incrementally. You may pass an object with the following
* properties:
*
* - file: The filename of the generated source.
* - sourceRoot: A root for all relative URLs in this source map.
*/
class SourceMapGenerator {
constructor(aArgs) {
if (!aArgs) {
aArgs = {};
}
this._file = util.getArg(aArgs, "file", null);
this._sourceRoot = util.getArg(aArgs, "sourceRoot", null);
this._skipValidation = util.getArg(aArgs, "skipValidation", false);
this._sources = new ArraySet();
this._names = new ArraySet();
this._mappings = new MappingList();
this._sourcesContents = null;
}
/**
* Creates a new SourceMapGenerator based on a SourceMapConsumer
*
* @param aSourceMapConsumer The SourceMap.
*/
static fromSourceMap(aSourceMapConsumer) {
const sourceRoot = aSourceMapConsumer.sourceRoot;
const generator = new SourceMapGenerator({
file: aSourceMapConsumer.file,
sourceRoot,
});
aSourceMapConsumer.eachMapping(function (mapping) {
const newMapping = {
generated: {
line: mapping.generatedLine,
column: mapping.generatedColumn,
},
};
if (mapping.source != null) {
newMapping.source = mapping.source;
if (sourceRoot != null) {
newMapping.source = util.relative(sourceRoot, newMapping.source);
}
newMapping.original = {
line: mapping.originalLine,
column: mapping.originalColumn,
};
if (mapping.name != null) {
newMapping.name = mapping.name;
}
}
generator.addMapping(newMapping);
});
aSourceMapConsumer.sources.forEach(function (sourceFile) {
let sourceRelative = sourceFile;
if (sourceRoot != null) {
sourceRelative = util.relative(sourceRoot, sourceFile);
}
if (!generator._sources.has(sourceRelative)) {
generator._sources.add(sourceRelative);
}
const content = aSourceMapConsumer.sourceContentFor(sourceFile);
if (content != null) {
generator.setSourceContent(sourceFile, content);
}
});
return generator;
}
/**
* Add a single mapping from original source line and column to the generated
* source's line and column for this source map being created. The mapping
* object should have the following properties:
*
* - generated: An object with the generated line and column positions.
* - original: An object with the original line and column positions.
* - source: The original source file (relative to the sourceRoot).
* - name: An optional original token name for this mapping.
*/
addMapping(aArgs) {
const generated = util.getArg(aArgs, "generated");
const original = util.getArg(aArgs, "original", null);
let source = util.getArg(aArgs, "source", null);
let name = util.getArg(aArgs, "name", null);
if (!this._skipValidation) {
this._validateMapping(generated, original, source, name);
}
if (source != null) {
source = String(source);
if (!this._sources.has(source)) {
this._sources.add(source);
}
}
if (name != null) {
name = String(name);
if (!this._names.has(name)) {
this._names.add(name);
}
}
this._mappings.add({
generatedLine: generated.line,
generatedColumn: generated.column,
originalLine: original && original.line,
originalColumn: original && original.column,
source,
name,
});
}
/**
* Set the source content for a source file.
*/
setSourceContent(aSourceFile, aSourceContent) {
let source = aSourceFile;
if (this._sourceRoot != null) {
source = util.relative(this._sourceRoot, source);
}
if (aSourceContent != null) {
// Add the source content to the _sourcesContents map.
// Create a new _sourcesContents map if the property is null.
if (!this._sourcesContents) {
this._sourcesContents = Object.create(null);
}
this._sourcesContents[util.toSetString(source)] = aSourceContent;
} else if (this._sourcesContents) {
// Remove the source file from the _sourcesContents map.
// If the _sourcesContents map is empty, set the property to null.
delete this._sourcesContents[util.toSetString(source)];
if (Object.keys(this._sourcesContents).length === 0) {
this._sourcesContents = null;
}
}
}
/**
* Applies the mappings of a sub-source-map for a specific source file to the
* source map being generated. Each mapping to the supplied source file is
* rewritten using the supplied source map. Note: The resolution for the
* resulting mappings is the minimium of this map and the supplied map.
*
* @param aSourceMapConsumer The source map to be applied.
* @param aSourceFile Optional. The filename of the source file.
* If omitted, SourceMapConsumer's file property will be used.
* @param aSourceMapPath Optional. The dirname of the path to the source map
* to be applied. If relative, it is relative to the SourceMapConsumer.
* This parameter is needed when the two source maps aren't in the same
* directory, and the source map to be applied contains relative source
* paths. If so, those relative source paths need to be rewritten
* relative to the SourceMapGenerator.
*/
applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
let sourceFile = aSourceFile;
// If aSourceFile is omitted, we will use the file property of the SourceMap
if (aSourceFile == null) {
if (aSourceMapConsumer.file == null) {
throw new Error(
"SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, " +
'or the source map\'s "file" property. Both were omitted.'
);
}
sourceFile = aSourceMapConsumer.file;
}
const sourceRoot = this._sourceRoot;
// Make "sourceFile" relative if an absolute Url is passed.
if (sourceRoot != null) {
sourceFile = util.relative(sourceRoot, sourceFile);
}
// Applying the SourceMap can add and remove items from the sources and
// the names array.
const newSources =
this._mappings.toArray().length > 0 ? new ArraySet() : this._sources;
const newNames = new ArraySet();
// Find mappings for the "sourceFile"
this._mappings.unsortedForEach(function (mapping) {
if (mapping.source === sourceFile && mapping.originalLine != null) {
// Check if it can be mapped by the source map, then update the mapping.
const original = aSourceMapConsumer.originalPositionFor({
line: mapping.originalLine,
column: mapping.originalColumn,
});
if (original.source != null) {
// Copy mapping
mapping.source = original.source;
if (aSourceMapPath != null) {
mapping.source = util.join(aSourceMapPath, mapping.source);
}
if (sourceRoot != null) {
mapping.source = util.relative(sourceRoot, mapping.source);
}
mapping.originalLine = original.line;
mapping.originalColumn = original.column;
if (original.name != null) {
mapping.name = original.name;
}
}
}
const source = mapping.source;
if (source != null && !newSources.has(source)) {
newSources.add(source);
}
const name = mapping.name;
if (name != null && !newNames.has(name)) {
newNames.add(name);
}
}, this);
this._sources = newSources;
this._names = newNames;
// Copy sourcesContents of applied map.
aSourceMapConsumer.sources.forEach(function (srcFile) {
const content = aSourceMapConsumer.sourceContentFor(srcFile);
if (content != null) {
if (aSourceMapPath != null) {
srcFile = util.join(aSourceMapPath, srcFile);
}
if (sourceRoot != null) {
srcFile = util.relative(sourceRoot, srcFile);
}
this.setSourceContent(srcFile, content);
}
}, this);
}
/**
* A mapping can have one of the three levels of data:
*
* 1. Just the generated position.
* 2. The Generated position, original position, and original source.
* 3. Generated and original position, original source, as well as a name
* token.
*
* To maintain consistency, we validate that any new mapping being added falls
* in to one of these categories.
*/
_validateMapping(aGenerated, aOriginal, aSource, aName) {
// When aOriginal is truthy but has empty values for .line and .column,
// it is most likely a programmer error. In this case we throw a very
// specific error message to try to guide them the right way.
// For example: https://github.com/Polymer/polymer-bundler/pull/519
if (
aOriginal &&
typeof aOriginal.line !== "number" &&
typeof aOriginal.column !== "number"
) {
throw new Error(
"original.line and original.column are not numbers -- you probably meant to omit " +
"the original mapping entirely and only map the generated position. If so, pass " +
"null for the original mapping instead of an object with empty or null values."
);
}
if (
aGenerated &&
"line" in aGenerated &&
"column" in aGenerated &&
aGenerated.line > 0 &&
aGenerated.column >= 0 &&
!aOriginal &&
!aSource &&
!aName
) {
// Case 1.
} else if (
aGenerated &&
"line" in aGenerated &&
"column" in aGenerated &&
aOriginal &&
"line" in aOriginal &&
"column" in aOriginal &&
aGenerated.line > 0 &&
aGenerated.column >= 0 &&
aOriginal.line > 0 &&
aOriginal.column >= 0 &&
aSource
) {
// Cases 2 and 3.
} else {
throw new Error(
"Invalid mapping: " +
JSON.stringify({
generated: aGenerated,
source: aSource,
original: aOriginal,
name: aName,
})
);
}
}
/**
* Serialize the accumulated mappings in to the stream of base 64 VLQs
* specified by the source map format.
*/
_serializeMappings() {
let previousGeneratedColumn = 0;
let previousGeneratedLine = 1;
let previousOriginalColumn = 0;
let previousOriginalLine = 0;
let previousName = 0;
let previousSource = 0;
let result = "";
let next;
let mapping;
let nameIdx;
let sourceIdx;
const mappings = this._mappings.toArray();
for (let i = 0, len = mappings.length; i < len; i++) {
mapping = mappings[i];
next = "";
if (mapping.generatedLine !== previousGeneratedLine) {
previousGeneratedColumn = 0;
while (mapping.generatedLine !== previousGeneratedLine) {
next += ";";
previousGeneratedLine++;
}
} else if (i > 0) {
if (
!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])
) {
continue;
}
next += ",";
}
next += base64VLQ.encode(
mapping.generatedColumn - previousGeneratedColumn
);
previousGeneratedColumn = mapping.generatedColumn;
if (mapping.source != null) {
sourceIdx = this._sources.indexOf(mapping.source);
next += base64VLQ.encode(sourceIdx - previousSource);
previousSource = sourceIdx;
// lines are stored 0-based in SourceMap spec version 3
next += base64VLQ.encode(
mapping.originalLine - 1 - previousOriginalLine
);
previousOriginalLine = mapping.originalLine - 1;
next += base64VLQ.encode(
mapping.originalColumn - previousOriginalColumn
);
previousOriginalColumn = mapping.originalColumn;
if (mapping.name != null) {
nameIdx = this._names.indexOf(mapping.name);
next += base64VLQ.encode(nameIdx - previousName);
previousName = nameIdx;
}
}
result += next;
}
return result;
}
_generateSourcesContent(aSources, aSourceRoot) {
return aSources.map(function (source) {
if (!this._sourcesContents) {
return null;
}
if (aSourceRoot != null) {
source = util.relative(aSourceRoot, source);
}
const key = util.toSetString(source);
return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
? this._sourcesContents[key]
: null;
}, this);
}
/**
* Externalize the source map.
*/
toJSON() {
const map = {
version: this._version,
sources: this._sources.toArray(),
names: this._names.toArray(),
mappings: this._serializeMappings(),
};
if (this._file != null) {
map.file = this._file;
}
if (this._sourceRoot != null) {
map.sourceRoot = this._sourceRoot;
}
if (this._sourcesContents) {
map.sourcesContent = this._generateSourcesContent(
map.sources,
map.sourceRoot
);
}
return map;
}
/**
* Render the source map being generated to a string.
*/
toString() {
return JSON.stringify(this.toJSON());
}
}
SourceMapGenerator.prototype._version = 3;
exports.SourceMapGenerator = SourceMapGenerator;

View File

@@ -0,0 +1,430 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
const SourceMapGenerator = require("./source-map-generator").SourceMapGenerator;
const util = require("./util");
// Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
// operating systems these days (capturing the result).
const REGEX_NEWLINE = /(\r?\n)/;
// Newline character code for charCodeAt() comparisons
const NEWLINE_CODE = 10;
// Private symbol for identifying `SourceNode`s when multiple versions of
// the source-map library are loaded. This MUST NOT CHANGE across
// versions!
const isSourceNode = "$$$isSourceNode$$$";
/**
* SourceNodes provide a way to abstract over interpolating/concatenating
* snippets of generated JavaScript source code while maintaining the line and
* column information associated with the original source code.
*
* @param aLine The original line number.
* @param aColumn The original column number.
* @param aSource The original source's filename.
* @param aChunks Optional. An array of strings which are snippets of
* generated JS, or other SourceNodes.
* @param aName The original identifier.
*/
class SourceNode {
constructor(aLine, aColumn, aSource, aChunks, aName) {
this.children = [];
this.sourceContents = {};
this.line = aLine == null ? null : aLine;
this.column = aColumn == null ? null : aColumn;
this.source = aSource == null ? null : aSource;
this.name = aName == null ? null : aName;
this[isSourceNode] = true;
if (aChunks != null) this.add(aChunks);
}
/**
* Creates a SourceNode from generated code and a SourceMapConsumer.
*
* @param aGeneratedCode The generated code
* @param aSourceMapConsumer The SourceMap for the generated code
* @param aRelativePath Optional. The path that relative sources in the
* SourceMapConsumer should be relative to.
*/
static fromStringWithSourceMap(
aGeneratedCode,
aSourceMapConsumer,
aRelativePath
) {
// The SourceNode we want to fill with the generated code
// and the SourceMap
const node = new SourceNode();
// All even indices of this array are one line of the generated code,
// while all odd indices are the newlines between two adjacent lines
// (since `REGEX_NEWLINE` captures its match).
// Processed fragments are accessed by calling `shiftNextLine`.
const remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
let remainingLinesIndex = 0;
const shiftNextLine = function () {
const lineContents = getNextLine();
// The last line of a file might not have a newline.
const newLine = getNextLine() || "";
return lineContents + newLine;
function getNextLine() {
return remainingLinesIndex < remainingLines.length
? remainingLines[remainingLinesIndex++]
: undefined;
}
};
// We need to remember the position of "remainingLines"
let lastGeneratedLine = 1,
lastGeneratedColumn = 0;
// The generate SourceNodes we need a code range.
// To extract it current and last mapping is used.
// Here we store the last mapping.
let lastMapping = null;
let nextLine;
aSourceMapConsumer.eachMapping(function (mapping) {
if (lastMapping !== null) {
// We add the code from "lastMapping" to "mapping":
// First check if there is a new line in between.
if (lastGeneratedLine < mapping.generatedLine) {
// Associate first line with "lastMapping"
addMappingWithCode(lastMapping, shiftNextLine());
lastGeneratedLine++;
lastGeneratedColumn = 0;
// The remaining code is added without mapping
} else {
// There is no new line in between.
// Associate the code between "lastGeneratedColumn" and
// "mapping.generatedColumn" with "lastMapping"
nextLine = remainingLines[remainingLinesIndex] || "";
const code = nextLine.substr(
0,
mapping.generatedColumn - lastGeneratedColumn
);
remainingLines[remainingLinesIndex] = nextLine.substr(
mapping.generatedColumn - lastGeneratedColumn
);
lastGeneratedColumn = mapping.generatedColumn;
addMappingWithCode(lastMapping, code);
// No more remaining code, continue
lastMapping = mapping;
return;
}
}
// We add the generated code until the first mapping
// to the SourceNode without any mapping.
// Each line is added as separate string.
while (lastGeneratedLine < mapping.generatedLine) {
node.add(shiftNextLine());
lastGeneratedLine++;
}
if (lastGeneratedColumn < mapping.generatedColumn) {
nextLine = remainingLines[remainingLinesIndex] || "";
node.add(nextLine.substr(0, mapping.generatedColumn));
remainingLines[remainingLinesIndex] = nextLine.substr(
mapping.generatedColumn
);
lastGeneratedColumn = mapping.generatedColumn;
}
lastMapping = mapping;
}, this);
// We have processed all mappings.
if (remainingLinesIndex < remainingLines.length) {
if (lastMapping) {
// Associate the remaining code in the current line with "lastMapping"
addMappingWithCode(lastMapping, shiftNextLine());
}
// and add the remaining lines without any mapping
node.add(remainingLines.splice(remainingLinesIndex).join(""));
}
// Copy sourcesContent into SourceNode
aSourceMapConsumer.sources.forEach(function (sourceFile) {
const content = aSourceMapConsumer.sourceContentFor(sourceFile);
if (content != null) {
if (aRelativePath != null) {
sourceFile = util.join(aRelativePath, sourceFile);
}
node.setSourceContent(sourceFile, content);
}
});
return node;
function addMappingWithCode(mapping, code) {
if (mapping === null || mapping.source === undefined) {
node.add(code);
} else {
const source = aRelativePath
? util.join(aRelativePath, mapping.source)
: mapping.source;
node.add(
new SourceNode(
mapping.originalLine,
mapping.originalColumn,
source,
code,
mapping.name
)
);
}
}
}
/**
* Add a chunk of generated JS to this source node.
*
* @param aChunk A string snippet of generated JS code, another instance of
* SourceNode, or an array where each member is one of those things.
*/
add(aChunk) {
if (Array.isArray(aChunk)) {
aChunk.forEach(function (chunk) {
this.add(chunk);
}, this);
} else if (aChunk[isSourceNode] || typeof aChunk === "string") {
if (aChunk) {
this.children.push(aChunk);
}
} else {
throw new TypeError(
"Expected a SourceNode, string, or an array of SourceNodes and strings. Got " +
aChunk
);
}
return this;
}
/**
* Add a chunk of generated JS to the beginning of this source node.
*
* @param aChunk A string snippet of generated JS code, another instance of
* SourceNode, or an array where each member is one of those things.
*/
prepend(aChunk) {
if (Array.isArray(aChunk)) {
for (let i = aChunk.length - 1; i >= 0; i--) {
this.prepend(aChunk[i]);
}
} else if (aChunk[isSourceNode] || typeof aChunk === "string") {
this.children.unshift(aChunk);
} else {
throw new TypeError(
"Expected a SourceNode, string, or an array of SourceNodes and strings. Got " +
aChunk
);
}
return this;
}
/**
* Walk over the tree of JS snippets in this node and its children. The
* walking function is called once for each snippet of JS and is passed that
* snippet and the its original associated source's line/column location.
*
* @param aFn The traversal function.
*/
walk(aFn) {
let chunk;
for (let i = 0, len = this.children.length; i < len; i++) {
chunk = this.children[i];
if (chunk[isSourceNode]) {
chunk.walk(aFn);
} else if (chunk !== "") {
aFn(chunk, {
source: this.source,
line: this.line,
column: this.column,
name: this.name,
});
}
}
}
/**
* Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
* each of `this.children`.
*
* @param aSep The separator.
*/
join(aSep) {
let newChildren;
let i;
const len = this.children.length;
if (len > 0) {
newChildren = [];
for (i = 0; i < len - 1; i++) {
newChildren.push(this.children[i]);
newChildren.push(aSep);
}
newChildren.push(this.children[i]);
this.children = newChildren;
}
return this;
}
/**
* Call String.prototype.replace on the very right-most source snippet. Useful
* for trimming whitespace from the end of a source node, etc.
*
* @param aPattern The pattern to replace.
* @param aReplacement The thing to replace the pattern with.
*/
replaceRight(aPattern, aReplacement) {
const lastChild = this.children[this.children.length - 1];
if (lastChild[isSourceNode]) {
lastChild.replaceRight(aPattern, aReplacement);
} else if (typeof lastChild === "string") {
this.children[this.children.length - 1] = lastChild.replace(
aPattern,
aReplacement
);
} else {
this.children.push("".replace(aPattern, aReplacement));
}
return this;
}
/**
* Set the source content for a source file. This will be added to the SourceMapGenerator
* in the sourcesContent field.
*
* @param aSourceFile The filename of the source file
* @param aSourceContent The content of the source file
*/
setSourceContent(aSourceFile, aSourceContent) {
this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
}
/**
* Walk over the tree of SourceNodes. The walking function is called for each
* source file content and is passed the filename and source content.
*
* @param aFn The traversal function.
*/
walkSourceContents(aFn) {
for (let i = 0, len = this.children.length; i < len; i++) {
if (this.children[i][isSourceNode]) {
this.children[i].walkSourceContents(aFn);
}
}
const sources = Object.keys(this.sourceContents);
for (let i = 0, len = sources.length; i < len; i++) {
aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
}
}
/**
* Return the string representation of this source node. Walks over the tree
* and concatenates all the various snippets together to one string.
*/
toString() {
let str = "";
this.walk(function (chunk) {
str += chunk;
});
return str;
}
/**
* Returns the string representation of this source node along with a source
* map.
*/
toStringWithSourceMap(aArgs) {
const generated = {
code: "",
line: 1,
column: 0,
};
const map = new SourceMapGenerator(aArgs);
let sourceMappingActive = false;
let lastOriginalSource = null;
let lastOriginalLine = null;
let lastOriginalColumn = null;
let lastOriginalName = null;
this.walk(function (chunk, original) {
generated.code += chunk;
if (
original.source !== null &&
original.line !== null &&
original.column !== null
) {
if (
lastOriginalSource !== original.source ||
lastOriginalLine !== original.line ||
lastOriginalColumn !== original.column ||
lastOriginalName !== original.name
) {
map.addMapping({
source: original.source,
original: {
line: original.line,
column: original.column,
},
generated: {
line: generated.line,
column: generated.column,
},
name: original.name,
});
}
lastOriginalSource = original.source;
lastOriginalLine = original.line;
lastOriginalColumn = original.column;
lastOriginalName = original.name;
sourceMappingActive = true;
} else if (sourceMappingActive) {
map.addMapping({
generated: {
line: generated.line,
column: generated.column,
},
});
lastOriginalSource = null;
sourceMappingActive = false;
}
for (let idx = 0, length = chunk.length; idx < length; idx++) {
if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
generated.line++;
generated.column = 0;
// Mappings end at eol
if (idx + 1 === length) {
lastOriginalSource = null;
sourceMappingActive = false;
} else if (sourceMappingActive) {
map.addMapping({
source: original.source,
original: {
line: original.line,
column: original.column,
},
generated: {
line: generated.line,
column: generated.column,
},
name: original.name,
});
}
} else {
generated.column++;
}
}
});
this.walkSourceContents(function (sourceFile, sourceContent) {
map.setSourceContent(sourceFile, sourceContent);
});
return { code: generated.code, map };
}
}
exports.SourceNode = SourceNode;

View File

@@ -0,0 +1,13 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
"use strict";
// Note: This file is overridden in the 'package.json#browser' field to
// substitute lib/url-browser.js instead.
// Use the URL global for Node 10, and the 'url' module for Node 8.
module.exports = typeof URL === "function" ? URL : require("url").URL;

View File

@@ -0,0 +1,444 @@
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
const URL = require("./url");
/**
* This is a helper function for getting values from parameter/options
* objects.
*
* @param args The object we are extracting values from
* @param name The name of the property we are getting.
* @param defaultValue An optional value to return if the property is missing
* from the object. If this is not specified and the property is missing, an
* error will be thrown.
*/
function getArg(aArgs, aName, aDefaultValue) {
if (aName in aArgs) {
return aArgs[aName];
} else if (arguments.length === 3) {
return aDefaultValue;
}
throw new Error('"' + aName + '" is a required argument.');
}
exports.getArg = getArg;
const supportsNullProto = (function () {
const obj = Object.create(null);
return !("__proto__" in obj);
})();
function identity(s) {
return s;
}
/**
* Because behavior goes wacky when you set `__proto__` on objects, we
* have to prefix all the strings in our set with an arbitrary character.
*
* See https://github.com/mozilla/source-map/pull/31 and
* https://github.com/mozilla/source-map/issues/30
*
* @param String aStr
*/
function toSetString(aStr) {
if (isProtoString(aStr)) {
return "$" + aStr;
}
return aStr;
}
exports.toSetString = supportsNullProto ? identity : toSetString;
function fromSetString(aStr) {
if (isProtoString(aStr)) {
return aStr.slice(1);
}
return aStr;
}
exports.fromSetString = supportsNullProto ? identity : fromSetString;
function isProtoString(s) {
if (!s) {
return false;
}
const length = s.length;
if (length < 9 /* "__proto__".length */) {
return false;
}
/* eslint-disable no-multi-spaces */
if (
s.charCodeAt(length - 1) !== 95 /* '_' */ ||
s.charCodeAt(length - 2) !== 95 /* '_' */ ||
s.charCodeAt(length - 3) !== 111 /* 'o' */ ||
s.charCodeAt(length - 4) !== 116 /* 't' */ ||
s.charCodeAt(length - 5) !== 111 /* 'o' */ ||
s.charCodeAt(length - 6) !== 114 /* 'r' */ ||
s.charCodeAt(length - 7) !== 112 /* 'p' */ ||
s.charCodeAt(length - 8) !== 95 /* '_' */ ||
s.charCodeAt(length - 9) !== 95 /* '_' */
) {
return false;
}
/* eslint-enable no-multi-spaces */
for (let i = length - 10; i >= 0; i--) {
if (s.charCodeAt(i) !== 36 /* '$' */) {
return false;
}
}
return true;
}
function strcmp(aStr1, aStr2) {
if (aStr1 === aStr2) {
return 0;
}
if (aStr1 === null) {
return 1; // aStr2 !== null
}
if (aStr2 === null) {
return -1; // aStr1 !== null
}
if (aStr1 > aStr2) {
return 1;
}
return -1;
}
/**
* Comparator between two mappings with inflated source and name strings where
* the generated positions are compared.
*/
function compareByGeneratedPositionsInflated(mappingA, mappingB) {
let cmp = mappingA.generatedLine - mappingB.generatedLine;
if (cmp !== 0) {
return cmp;
}
cmp = mappingA.generatedColumn - mappingB.generatedColumn;
if (cmp !== 0) {
return cmp;
}
cmp = strcmp(mappingA.source, mappingB.source);
if (cmp !== 0) {
return cmp;
}
cmp = mappingA.originalLine - mappingB.originalLine;
if (cmp !== 0) {
return cmp;
}
cmp = mappingA.originalColumn - mappingB.originalColumn;
if (cmp !== 0) {
return cmp;
}
return strcmp(mappingA.name, mappingB.name);
}
exports.compareByGeneratedPositionsInflated =
compareByGeneratedPositionsInflated;
/**
* Strip any JSON XSSI avoidance prefix from the string (as documented
* in the source maps specification), and then parse the string as
* JSON.
*/
function parseSourceMapInput(str) {
return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, ""));
}
exports.parseSourceMapInput = parseSourceMapInput;
// We use 'http' as the base here because we want URLs processed relative
// to the safe base to be treated as "special" URLs during parsing using
// the WHATWG URL parsing. This ensures that backslash normalization
// applies to the path and such.
const PROTOCOL = "http:";
const PROTOCOL_AND_HOST = `${PROTOCOL}//host`;
/**
* Make it easy to create small utilities that tweak a URL's path.
*/
function createSafeHandler(cb) {
return input => {
const type = getURLType(input);
const base = buildSafeBase(input);
const url = new URL(input, base);
cb(url);
const result = url.toString();
if (type === "absolute") {
return result;
} else if (type === "scheme-relative") {
return result.slice(PROTOCOL.length);
} else if (type === "path-absolute") {
return result.slice(PROTOCOL_AND_HOST.length);
}
// This assumes that the callback will only change
// the path, search and hash values.
return computeRelativeURL(base, result);
};
}
function withBase(url, base) {
return new URL(url, base).toString();
}
function buildUniqueSegment(prefix, str) {
let id = 0;
do {
const ident = prefix + id++;
if (str.indexOf(ident) === -1) return ident;
} while (true);
}
function buildSafeBase(str) {
const maxDotParts = str.split("..").length - 1;
// If we used a segment that also existed in `str`, then we would be unable
// to compute relative paths. For example, if `segment` were just "a":
//
// const url = "../../a/"
// const base = buildSafeBase(url); // http://host/a/a/
// const joined = "http://host/a/";
// const result = relative(base, joined);
//
// Expected: "../../a/";
// Actual: "a/"
//
const segment = buildUniqueSegment("p", str);
let base = `${PROTOCOL_AND_HOST}/`;
for (let i = 0; i < maxDotParts; i++) {
base += `${segment}/`;
}
return base;
}
const ABSOLUTE_SCHEME = /^[A-Za-z0-9\+\-\.]+:/;
function getURLType(url) {
if (url[0] === "/") {
if (url[1] === "/") return "scheme-relative";
return "path-absolute";
}
return ABSOLUTE_SCHEME.test(url) ? "absolute" : "path-relative";
}
/**
* Given two URLs that are assumed to be on the same
* protocol/host/user/password build a relative URL from the
* path, params, and hash values.
*
* @param rootURL The root URL that the target will be relative to.
* @param targetURL The target that the relative URL points to.
* @return A rootURL-relative, normalized URL value.
*/
function computeRelativeURL(rootURL, targetURL) {
if (typeof rootURL === "string") rootURL = new URL(rootURL);
if (typeof targetURL === "string") targetURL = new URL(targetURL);
const targetParts = targetURL.pathname.split("/");
const rootParts = rootURL.pathname.split("/");
// If we've got a URL path ending with a "/", we remove it since we'd
// otherwise be relative to the wrong location.
if (rootParts.length > 0 && !rootParts[rootParts.length - 1]) {
rootParts.pop();
}
while (
targetParts.length > 0 &&
rootParts.length > 0 &&
targetParts[0] === rootParts[0]
) {
targetParts.shift();
rootParts.shift();
}
const relativePath = rootParts
.map(() => "..")
.concat(targetParts)
.join("/");
return relativePath + targetURL.search + targetURL.hash;
}
/**
* Given a URL, ensure that it is treated as a directory URL.
*
* @param url
* @return A normalized URL value.
*/
const ensureDirectory = createSafeHandler(url => {
url.pathname = url.pathname.replace(/\/?$/, "/");
});
/**
* Given a URL, strip off any filename if one is present.
*
* @param url
* @return A normalized URL value.
*/
const trimFilename = createSafeHandler(url => {
url.href = new URL(".", url.toString()).toString();
});
/**
* Normalize a given URL.
* * Convert backslashes.
* * Remove any ".." and "." segments.
*
* @param url
* @return A normalized URL value.
*/
const normalize = createSafeHandler(url => {});
exports.normalize = normalize;
/**
* Joins two paths/URLs.
*
* All returned URLs will be normalized.
*
* @param aRoot The root path or URL. Assumed to reference a directory.
* @param aPath The path or URL to be joined with the root.
* @return A joined and normalized URL value.
*/
function join(aRoot, aPath) {
const pathType = getURLType(aPath);
const rootType = getURLType(aRoot);
aRoot = ensureDirectory(aRoot);
if (pathType === "absolute") {
return withBase(aPath, undefined);
}
if (rootType === "absolute") {
return withBase(aPath, aRoot);
}
if (pathType === "scheme-relative") {
return normalize(aPath);
}
if (rootType === "scheme-relative") {
return withBase(aPath, withBase(aRoot, PROTOCOL_AND_HOST)).slice(
PROTOCOL.length
);
}
if (pathType === "path-absolute") {
return normalize(aPath);
}
if (rootType === "path-absolute") {
return withBase(aPath, withBase(aRoot, PROTOCOL_AND_HOST)).slice(
PROTOCOL_AND_HOST.length
);
}
const base = buildSafeBase(aPath + aRoot);
const newPath = withBase(aPath, withBase(aRoot, base));
return computeRelativeURL(base, newPath);
}
exports.join = join;
/**
* Make a path relative to a URL or another path. If returning a
* relative URL is not possible, the original target will be returned.
* All returned URLs will be normalized.
*
* @param aRoot The root path or URL.
* @param aPath The path or URL to be made relative to aRoot.
* @return A rootURL-relative (if possible), normalized URL value.
*/
function relative(rootURL, targetURL) {
const result = relativeIfPossible(rootURL, targetURL);
return typeof result === "string" ? result : normalize(targetURL);
}
exports.relative = relative;
function relativeIfPossible(rootURL, targetURL) {
const urlType = getURLType(rootURL);
if (urlType !== getURLType(targetURL)) {
return null;
}
const base = buildSafeBase(rootURL + targetURL);
const root = new URL(rootURL, base);
const target = new URL(targetURL, base);
try {
new URL("", target.toString());
} catch (err) {
// Bail if the URL doesn't support things being relative to it,
// For example, data: and blob: URLs.
return null;
}
if (
target.protocol !== root.protocol ||
target.user !== root.user ||
target.password !== root.password ||
target.hostname !== root.hostname ||
target.port !== root.port
) {
return null;
}
return computeRelativeURL(root, target);
}
/**
* Compute the URL of a source given the the source root, the source's
* URL, and the source map's URL.
*/
function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) {
// The source map spec states that "sourceRoot" and "sources" entries are to be appended. While
// that is a little vague, implementations have generally interpreted that as joining the
// URLs with a `/` between then, assuming the "sourceRoot" doesn't already end with one.
// For example,
//
// sourceRoot: "some-dir",
// sources: ["/some-path.js"]
//
// and
//
// sourceRoot: "some-dir/",
// sources: ["/some-path.js"]
//
// must behave as "some-dir/some-path.js".
//
// With this library's the transition to a more URL-focused implementation, that behavior is
// preserved here. To acheive that, we trim the "/" from absolute-path when a sourceRoot value
// is present in order to make the sources entries behave as if they are relative to the
// "sourceRoot", as they would have if the two strings were simply concated.
if (sourceRoot && getURLType(sourceURL) === "path-absolute") {
sourceURL = sourceURL.replace(/^\//, "");
}
let url = normalize(sourceURL || "");
// Parsing URLs can be expensive, so we only perform these joins when needed.
if (sourceRoot) url = join(sourceRoot, url);
if (sourceMapURL) url = join(trimFilename(sourceMapURL), url);
return url;
}
exports.computeSourceURL = computeSourceURL;

View File

@@ -0,0 +1,138 @@
const readWasm = require("../lib/read-wasm");
/**
* Provide the JIT with a nice shape / hidden class.
*/
function Mapping() {
this.generatedLine = 0;
this.generatedColumn = 0;
this.lastGeneratedColumn = null;
this.source = null;
this.originalLine = null;
this.originalColumn = null;
this.name = null;
}
let cachedWasm = null;
module.exports = function wasm() {
if (cachedWasm) {
return cachedWasm;
}
const callbackStack = [];
cachedWasm = readWasm()
.then(buffer => {
return WebAssembly.instantiate(buffer, {
env: {
mapping_callback(
generatedLine,
generatedColumn,
hasLastGeneratedColumn,
lastGeneratedColumn,
hasOriginal,
source,
originalLine,
originalColumn,
hasName,
name
) {
const mapping = new Mapping();
// JS uses 1-based line numbers, wasm uses 0-based.
mapping.generatedLine = generatedLine + 1;
mapping.generatedColumn = generatedColumn;
if (hasLastGeneratedColumn) {
// JS uses inclusive last generated column, wasm uses exclusive.
mapping.lastGeneratedColumn = lastGeneratedColumn - 1;
}
if (hasOriginal) {
mapping.source = source;
// JS uses 1-based line numbers, wasm uses 0-based.
mapping.originalLine = originalLine + 1;
mapping.originalColumn = originalColumn;
if (hasName) {
mapping.name = name;
}
}
callbackStack[callbackStack.length - 1](mapping);
},
start_all_generated_locations_for() {
console.time("all_generated_locations_for");
},
end_all_generated_locations_for() {
console.timeEnd("all_generated_locations_for");
},
start_compute_column_spans() {
console.time("compute_column_spans");
},
end_compute_column_spans() {
console.timeEnd("compute_column_spans");
},
start_generated_location_for() {
console.time("generated_location_for");
},
end_generated_location_for() {
console.timeEnd("generated_location_for");
},
start_original_location_for() {
console.time("original_location_for");
},
end_original_location_for() {
console.timeEnd("original_location_for");
},
start_parse_mappings() {
console.time("parse_mappings");
},
end_parse_mappings() {
console.timeEnd("parse_mappings");
},
start_sort_by_generated_location() {
console.time("sort_by_generated_location");
},
end_sort_by_generated_location() {
console.timeEnd("sort_by_generated_location");
},
start_sort_by_original_location() {
console.time("sort_by_original_location");
},
end_sort_by_original_location() {
console.timeEnd("sort_by_original_location");
},
},
});
})
.then(Wasm => {
return {
exports: Wasm.instance.exports,
withMappingCallback: (mappingCallback, f) => {
callbackStack.push(mappingCallback);
try {
f();
} finally {
callbackStack.pop();
}
},
};
})
.then(null, e => {
cachedWasm = null;
throw e;
});
return cachedWasm;
};

View File

@@ -0,0 +1,79 @@
{
"name": "source-map",
"description": "Generates and consumes source maps",
"version": "0.7.6",
"homepage": "https://github.com/mozilla/source-map",
"author": "Nick Fitzgerald <nfitzgerald@mozilla.com>",
"contributors": [
"Tobias Koppers <tobias.koppers@googlemail.com>",
"Duncan Beevers <duncan@dweebd.com>",
"Stephen Crane <scrane@mozilla.com>",
"Ryan Seddon <seddon.ryan@gmail.com>",
"Miles Elam <miles.elam@deem.com>",
"Mihai Bazon <mihai.bazon@gmail.com>",
"Michael Ficarra <github.public.email@michael.ficarra.me>",
"Todd Wolfson <todd@twolfson.com>",
"Alexander Solovyov <alexander@solovyov.net>",
"Felix Gnass <fgnass@gmail.com>",
"Conrad Irwin <conrad.irwin@gmail.com>",
"usrbincc <usrbincc@yahoo.com>",
"David Glasser <glasser@davidglasser.net>",
"Chase Douglas <chase@newrelic.com>",
"Evan Wallace <evan.exe@gmail.com>",
"Heather Arthur <fayearthur@gmail.com>",
"Hugh Kennedy <hughskennedy@gmail.com>",
"David Glasser <glasser@davidglasser.net>",
"Simon Lydell <simon.lydell@gmail.com>",
"Jmeas Smith <jellyes2@gmail.com>",
"Michael Z Goddard <mzgoddard@gmail.com>",
"azu <azu@users.noreply.github.com>",
"John Gozde <john@gozde.ca>",
"Adam Kirkton <akirkton@truefitinnovation.com>",
"Chris Montgomery <christopher.montgomery@dowjones.com>",
"J. Ryan Stinnett <jryans@gmail.com>",
"Jack Herrington <jherrington@walmartlabs.com>",
"Chris Truter <jeffpalentine@gmail.com>",
"Daniel Espeset <daniel@danielespeset.com>",
"Jamie Wong <jamie.lf.wong@gmail.com>",
"Eddy Bruël <ejpbruel@mozilla.com>",
"Hawken Rives <hawkrives@gmail.com>",
"Gilad Peleg <giladp007@gmail.com>",
"djchie <djchie.dev@gmail.com>",
"Gary Ye <garysye@gmail.com>",
"Nicolas Lalevée <nicolas.lalevee@hibnet.org>"
],
"repository": {
"type": "git",
"url": "http://github.com/mozilla/source-map.git"
},
"main": "./source-map.js",
"types": "./source-map.d.ts",
"browser": {
"./lib/read-wasm.js": "./lib/read-wasm-browser.js"
},
"files": [
"source-map.js",
"source-map.d.ts",
"lib/"
],
"engines": {
"node": ">= 12"
},
"license": "BSD-3-Clause",
"scripts": {
"lint": "eslint --fix *.js lib/ test/ --ignore-pattern 'test/source-map-tests/**'",
"test": "git submodule update --init --recursive; node test/run-tests.js",
"coverage": "c8 --reporter=text --reporter=html npm test",
"prettier": "prettier --write .",
"clean": "rm -rf coverage",
"toc": "doctoc --github --notitle README.md CONTRIBUTING.md"
},
"devDependencies": {
"c8": "^7.12.0",
"doctoc": "^2.2.1",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"prettier": "^2.7.1"
},
"dependencies": {}
}

View File

@@ -0,0 +1,423 @@
// Type definitions for source-map 0.7
// Project: https://github.com/mozilla/source-map
// Definitions by: Morten Houston Ludvigsen <https://github.com/MortenHoustonLudvigsen>,
// Ron Buckton <https://github.com/rbuckton>,
// John Vilk <https://github.com/jvilk>
// Definitions: https://github.com/mozilla/source-map
export type SourceMapUrl = string;
export interface StartOfSourceMap {
file?: string;
sourceRoot?: string;
skipValidation?: boolean;
}
export interface RawSourceMap {
version: number;
sources: string[];
names: string[];
sourceRoot?: string;
sourcesContent?: string[];
mappings: string;
file: string;
}
export interface RawIndexMap extends StartOfSourceMap {
version: number;
sections: RawSection[];
}
export interface RawSection {
offset: Position;
map: RawSourceMap;
}
export interface Position {
line: number;
column: number;
}
export interface NullablePosition {
line: number | null;
column: number | null;
lastColumn: number | null;
}
export interface MappedPosition {
source: string;
line: number;
column: number;
name?: string;
}
export interface NullableMappedPosition {
source: string | null;
line: number | null;
column: number | null;
name: string | null;
}
export interface MappingItem {
source: string;
generatedLine: number;
generatedColumn: number;
lastGeneratedColumn: number | null;
originalLine: number;
originalColumn: number;
name: string;
}
export interface Mapping {
generated: Position;
original: Position;
source: string;
name?: string;
}
export interface CodeWithSourceMap {
code: string;
map: SourceMapGenerator;
}
export interface SourceMappings {
"lib/mappings.wasm": SourceMapUrl | ArrayBuffer;
}
export interface SourceMapConsumer {
/**
* When using SourceMapConsumer outside of node.js, for example on the Web, it
* needs to know from what URL to load lib/mappings.wasm. You must inform it
* by calling initialize before constructing any SourceMapConsumers.
*
* @param mappings an object with the following property:
* - "lib/mappings.wasm": A String containing the URL of the
* lib/mappings.wasm file, or an ArrayBuffer with the contents of
* lib/mappings.wasm.
*/
initialize(mappings: SourceMappings): void;
/**
* Compute the last column for each generated mapping. The last column is
* inclusive.
*/
computeColumnSpans(): void;
/**
* Returns the original source, line, and column information for the generated
* source's line and column positions provided. The only argument is an object
* with the following properties:
*
* - line: The line number in the generated source.
* - column: The column number in the generated source.
* - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
* 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
* closest element that is smaller than or greater than the one we are
* searching for, respectively, if the exact element cannot be found.
* Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
*
* and an object is returned with the following properties:
*
* - source: The original source file, or null.
* - line: The line number in the original source, or null.
* - column: The column number in the original source, or null.
* - name: The original identifier, or null.
*/
originalPositionFor(
generatedPosition: Position & { bias?: number }
): NullableMappedPosition;
/**
* Returns the generated line and column information for the original source,
* line, and column positions provided. The only argument is an object with
* the following properties:
*
* - source: The filename of the original source.
* - line: The line number in the original source.
* - column: The column number in the original source.
* - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
* 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
* closest element that is smaller than or greater than the one we are
* searching for, respectively, if the exact element cannot be found.
* Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
*
* and an object is returned with the following properties:
*
* - line: The line number in the generated source, or null.
* - column: The column number in the generated source, or null.
*/
generatedPositionFor(
originalPosition: MappedPosition & { bias?: number }
): NullablePosition;
/**
* Returns all generated line and column information for the original source,
* line, and column provided. If no column is provided, returns all mappings
* corresponding to a either the line we are searching for or the next
* closest line that has any mappings. Otherwise, returns all mappings
* corresponding to the given line and either the column we are searching for
* or the next closest column that has any offsets.
*
* The only argument is an object with the following properties:
*
* - source: The filename of the original source.
* - line: The line number in the original source.
* - column: Optional. the column number in the original source.
*
* and an array of objects is returned, each with the following properties:
*
* - line: The line number in the generated source, or null.
* - column: The column number in the generated source, or null.
*/
allGeneratedPositionsFor(
originalPosition: MappedPosition
): NullablePosition[];
/**
* Return true if we have the source content for every source in the source
* map, false otherwise.
*/
hasContentsOfAllSources(): boolean;
/**
* Returns the original source content. The only argument is the url of the
* original source file. Returns null if no original source content is
* available.
*/
sourceContentFor(
source: string,
returnNullOnMissing?: boolean
): string | null;
/**
* Iterate over each mapping between an original source/line/column and a
* generated line/column in this source map.
*
* @param callback
* The function that is called with each mapping.
* @param context
* Optional. If specified, this object will be the value of `this` every
* time that `aCallback` is called.
* @param order
* Either `SourceMapConsumer.GENERATED_ORDER` or
* `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to
* iterate over the mappings sorted by the generated file's line/column
* order or the original's source/line/column order, respectively. Defaults to
* `SourceMapConsumer.GENERATED_ORDER`.
*/
eachMapping(
callback: (mapping: MappingItem) => void,
context?: any,
order?: number
): void;
/**
* Free this source map consumer's associated wasm data that is manually-managed.
* Alternatively, you can use SourceMapConsumer.with to avoid needing to remember to call destroy.
*/
destroy(): void;
}
export interface SourceMapConsumerConstructor {
prototype: SourceMapConsumer;
GENERATED_ORDER: number;
ORIGINAL_ORDER: number;
GREATEST_LOWER_BOUND: number;
LEAST_UPPER_BOUND: number;
new (
rawSourceMap: RawSourceMap,
sourceMapUrl?: SourceMapUrl
): Promise<BasicSourceMapConsumer>;
new (
rawSourceMap: RawIndexMap,
sourceMapUrl?: SourceMapUrl
): Promise<IndexedSourceMapConsumer>;
new (
rawSourceMap: RawSourceMap | RawIndexMap | string,
sourceMapUrl?: SourceMapUrl
): Promise<BasicSourceMapConsumer | IndexedSourceMapConsumer>;
/**
* Create a BasicSourceMapConsumer from a SourceMapGenerator.
*
* @param sourceMap
* The source map that will be consumed.
*/
fromSourceMap(
sourceMap: SourceMapGenerator,
sourceMapUrl?: SourceMapUrl
): Promise<BasicSourceMapConsumer>;
/**
* Construct a new `SourceMapConsumer` from `rawSourceMap` and `sourceMapUrl`
* (see the `SourceMapConsumer` constructor for details. Then, invoke the `async
* function f(SourceMapConsumer) -> T` with the newly constructed consumer, wait
* for `f` to complete, call `destroy` on the consumer, and return `f`'s return
* value.
*
* You must not use the consumer after `f` completes!
*
* By using `with`, you do not have to remember to manually call `destroy` on
* the consumer, since it will be called automatically once `f` completes.
*
* ```js
* const xSquared = await SourceMapConsumer.with(
* myRawSourceMap,
* null,
* async function (consumer) {
* // Use `consumer` inside here and don't worry about remembering
* // to call `destroy`.
*
* const x = await whatever(consumer);
* return x * x;
* }
* );
*
* // You may not use that `consumer` anymore out here; it has
* // been destroyed. But you can use `xSquared`.
* console.log(xSquared);
* ```
*/
with<T>(
rawSourceMap: RawSourceMap | RawIndexMap | string,
sourceMapUrl: SourceMapUrl | null | undefined,
callback: (
consumer: BasicSourceMapConsumer | IndexedSourceMapConsumer
) => Promise<T> | T
): Promise<T>;
}
export const SourceMapConsumer: SourceMapConsumerConstructor;
export interface BasicSourceMapConsumer extends SourceMapConsumer {
file: string;
sourceRoot: string;
sources: string[];
sourcesContent: string[];
}
export interface BasicSourceMapConsumerConstructor {
prototype: BasicSourceMapConsumer;
new (rawSourceMap: RawSourceMap | string): Promise<BasicSourceMapConsumer>;
/**
* Create a BasicSourceMapConsumer from a SourceMapGenerator.
*
* @param sourceMap
* The source map that will be consumed.
*/
fromSourceMap(sourceMap: SourceMapGenerator): Promise<BasicSourceMapConsumer>;
}
export const BasicSourceMapConsumer: BasicSourceMapConsumerConstructor;
export interface IndexedSourceMapConsumer extends SourceMapConsumer {
sources: string[];
}
export interface IndexedSourceMapConsumerConstructor {
prototype: IndexedSourceMapConsumer;
new (rawSourceMap: RawIndexMap | string): Promise<IndexedSourceMapConsumer>;
}
export const IndexedSourceMapConsumer: IndexedSourceMapConsumerConstructor;
export class SourceMapGenerator {
constructor(startOfSourceMap?: StartOfSourceMap);
/**
* Creates a new SourceMapGenerator based on a SourceMapConsumer
*
* @param sourceMapConsumer The SourceMap.
*/
static fromSourceMap(
sourceMapConsumer: SourceMapConsumer
): SourceMapGenerator;
/**
* Add a single mapping from original source line and column to the generated
* source's line and column for this source map being created. The mapping
* object should have the following properties:
*
* - generated: An object with the generated line and column positions.
* - original: An object with the original line and column positions.
* - source: The original source file (relative to the sourceRoot).
* - name: An optional original token name for this mapping.
*/
addMapping(mapping: Mapping): void;
/**
* Set the source content for a source file.
*/
setSourceContent(sourceFile: string, sourceContent: string): void;
/**
* Applies the mappings of a sub-source-map for a specific source file to the
* source map being generated. Each mapping to the supplied source file is
* rewritten using the supplied source map. Note: The resolution for the
* resulting mappings is the minimium of this map and the supplied map.
*
* @param sourceMapConsumer The source map to be applied.
* @param sourceFile Optional. The filename of the source file.
* If omitted, SourceMapConsumer's file property will be used.
* @param sourceMapPath Optional. The dirname of the path to the source map
* to be applied. If relative, it is relative to the SourceMapConsumer.
* This parameter is needed when the two source maps aren't in the same
* directory, and the source map to be applied contains relative source
* paths. If so, those relative source paths need to be rewritten
* relative to the SourceMapGenerator.
*/
applySourceMap(
sourceMapConsumer: SourceMapConsumer,
sourceFile?: string,
sourceMapPath?: string
): void;
toString(): string;
toJSON(): RawSourceMap;
}
export class SourceNode {
children: SourceNode[];
sourceContents: any;
line: number;
column: number;
source: string;
name: string;
constructor();
constructor(
line: number | null,
column: number | null,
source: string | null,
chunks?: Array<string | SourceNode> | SourceNode | string,
name?: string
);
static fromStringWithSourceMap(
code: string,
sourceMapConsumer: SourceMapConsumer,
relativePath?: string
): SourceNode;
add(chunk: Array<string | SourceNode> | SourceNode | string): SourceNode;
prepend(chunk: Array<string | SourceNode> | SourceNode | string): SourceNode;
setSourceContent(sourceFile: string, sourceContent: string): void;
walk(fn: (chunk: string, mapping: MappedPosition) => void): void;
walkSourceContents(fn: (file: string, content: string) => void): void;
join(sep: string): SourceNode;
replaceRight(pattern: string, replacement: string): SourceNode;
toString(): string;
toStringWithSourceMap(startOfSourceMap?: StartOfSourceMap): CodeWithSourceMap;
}

View File

@@ -0,0 +1,10 @@
/*
* Copyright 2009-2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
exports.SourceMapGenerator =
require("./lib/source-map-generator").SourceMapGenerator;
exports.SourceMapConsumer =
require("./lib/source-map-consumer").SourceMapConsumer;
exports.SourceNode = require("./lib/source-node").SourceNode;

View File

@@ -0,0 +1,59 @@
{
"version": "0.0.6",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=8"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test",
"lint": "tsdx lint",
"prepare": "tsdx build"
},
"peerDependencies": {},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"name": "hermes-profile-transformer",
"author": {
"name": "Saphal Patro",
"email": "saphal1998@gmail.com",
"url": "http://github.com/saphal1998"
},
"repository": {
"type": "git",
"url": "https://github.com/MLH-Fellowship/hermes-profile-transformer"
},
"module": "dist/hermes-tracing-profile-transformer.esm.js",
"devDependencies": {
"husky": "^4.2.5",
"tsdx": "^0.13.2",
"tslib": "^2.0.0",
"typescript": "^3.9.5"
},
"dependencies": {
"source-map": "^0.7.3"
},
"keywords": [
"profiling",
"hermes",
"transformation",
"transformers",
"dev-tools"
]
}

Binary file not shown.

View File

@@ -0,0 +1,126 @@
import { Event, DurationEvent, FlowEvent } from '../types/EventInterfaces';
import { EventsPhase } from '../types/Phases';
/**
* These tests are 50% about testing that the types are implemented correctly,
* and 50% documenting how to handle mapping between Event types and subtypes
* with and without EventsPhas literal.
*
* The tests rely on the @ts-expect-error pragma, which will pass the type check
* if the following line has a type error, and will error if the following line is fine.
*/
describe('Event', () => {
it('should allow constructing event objects using EventsPhase enum values', () => {
// create a new flow event
const event: DurationEvent = {
ts: 1,
ph: EventsPhase.DURATION_EVENTS_BEGIN,
};
// check that value is correct in runtime
expect(event).toEqual({ ts: 1, ph: 'B' });
});
it('should not allow constructing event objects using wrong enum values', () => {
// try to create a new flow event, should fail with TypeScript error
// @ts-expect-error
const event: DurationEvent = { ts: 1, ph: EventsPhase.INSTANT_EVENTS };
// at runtime object is still created, but we should never be here
expect(event).toEqual({ ts: 1, ph: 'I' });
});
it('should not allow constructing event objects with phase literal at type level', () => {
// try to create a new flow event, should fail with TypeScript error
// @ts-expect-error
const event: DurationEvent = { ts: 'ts', ph: 's' };
// check that value is correct in runtime
expect(event).toEqual({ ts: 'ts', ph: 's' });
});
it('should not allow coercing event objects with incorrect phase literal', () => {
// try to create a new flow event, should fail with TypeScript error
// @ts-expect-error
const event: DurationEvent = { ts: 'ts', ph: 'NOT_s' } as DurationEvent;
// check that value is correct in runtime
expect(event).toEqual({ ts: 'ts', ph: 'NOT_s' });
});
it('should allow polymorphic lists of different event types', () => {
const flow: FlowEvent = { ts: 1, ph: EventsPhase.FLOW_EVENTS_END };
const duration: DurationEvent = {
ts: 1,
ph: EventsPhase.DURATION_EVENTS_END,
};
// should not type error
const events: Event[] = [flow, duration];
expect(events).toEqual([
{ ts: 1, ph: 'f' },
{ ts: 1, ph: 'E' },
]);
});
it('should not allow polymorphic lists where any value is not a valid event type', () => {
const durationEnd: DurationEvent = {
ts: 1,
ph: EventsPhase.DURATION_EVENTS_END,
};
const durationBegin: DurationEvent = {
ts: 1,
ph: EventsPhase.DURATION_EVENTS_BEGIN,
};
const invalid = {
ts: 'ts',
ph: 'invalid',
};
// @ts-expect-error
const events: Event[] = [durationEnd, durationBegin, invalid];
expect(events).toEqual([
{ ts: 1, ph: 'E' },
{ ts: 1, ph: 'B' },
{ ts: 'ts', ph: 'invalid' },
]);
});
it('should support type guards', () => {
// If we want to ensure that a type is *actually* of the type
// we want it to be instead of relying on type coercion/casting,
// we can use a type guard
//
// See: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
function isDurationEvent(event: any): event is DurationEvent {
return (
event.ph === EventsPhase.DURATION_EVENTS_BEGIN ||
event.ph === EventsPhase.DURATION_EVENTS_END
);
}
// This function expects a duration event
function expectsDurationEvent(event: DurationEvent): any {
return event.ph;
}
// This
const durationEventLike = { ts: 1, ph: 'B' };
// This fails, because string `B` is not coerced to EventsPhase
// @ts-expect-error
expectsDurationEvent(durationEventLike);
// But if we use our type guard first...
if (isDurationEvent(durationEventLike)) {
// This will pass, because isDurationEvent type guard refines the type
// by checking that the value matches the expected type
expectsDurationEvent(durationEventLike);
} else {
// This will fail, because the value didn't match
// @ts-expect-error
expectsDurationEvent(durationEventLike);
}
});
});

View File

@@ -0,0 +1,18 @@
import { EventsPhase } from '../types/Phases';
describe('EventPhase', () => {
it('should map to corresponding string value correctly at type-level and runtime', () => {
// If you added @ts-expect-error below, the type check should
// error because the comparison always returns false
expect(EventsPhase.DURATION_EVENTS_BEGIN === 'B').toBe(true);
});
it('should cause a type error and fail runtime check when compared to incorrect literal', () => {
// If you remove @ts-expect-error below, the type check should
// error because the comparison always returns false
// @ts-expect-error
expect(EventsPhase.DURATION_EVENTS_BEGIN === 'NOT_B').toBe(false);
});
});

View File

@@ -0,0 +1,60 @@
import { CpuProfilerModel } from './profiler/cpuProfilerModel';
import { DurationEvent } from './types/EventInterfaces';
import { readFileAsync } from './utils/fileSystem';
import { HermesCPUProfile } from './types/HermesProfile';
import applySourceMapsToEvents from './profiler/applySourceMapsToEvents';
import { SourceMap } from './types/SourceMap';
// Imports from node
import path from 'path';
import { writeFile } from 'fs';
import { promisify } from 'util';
/**
* This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional)
* and return a promise which resolves to Chrome Dev Tools compatible events
* @param profilePath string
* @param sourceMapPath string
* @param bundleFileName string
* @return Promise<DurationEvent[]>
*/
const transformer = async (
profilePath: string,
sourceMapPath: string | undefined,
bundleFileName: string | undefined
): Promise<DurationEvent[]> => {
const hermesProfile: HermesCPUProfile = await readFileAsync(profilePath);
const profileChunk = CpuProfilerModel.collectProfileEvents(hermesProfile);
const profiler = new CpuProfilerModel(profileChunk);
const chromeEvents = profiler.createStartEndEvents();
if (sourceMapPath) {
const sourceMap: SourceMap = await readFileAsync(sourceMapPath);
const events = applySourceMapsToEvents(
sourceMap,
chromeEvents,
bundleFileName
);
return events;
}
return chromeEvents;
};
export default transformer;
export { SourceMap } from './types/SourceMap';
transformer(
path.join('..', 'hermes-test-profile', 'nestedFuncProfile.cpuprofile'),
path.join('..', 'hermes-test-profile', 'index.map'),
'index.bundle'
)
.then(async events => {
const writeFileAsync = promisify(writeFile);
await writeFileAsync(
path.join('..', 'hermes-test-profile', 'chrome-supported.json'),
JSON.stringify(events, null, 2),
'utf-8'
);
})
.catch(err => {
console.log(err);
});

View File

@@ -0,0 +1,38 @@
import { CpuProfilerModel } from './profiler/cpuProfilerModel';
import { DurationEvent } from './types/EventInterfaces';
import { readFileAsync } from './utils/fileSystem';
import { HermesCPUProfile } from './types/HermesProfile';
import applySourceMapsToEvents from './profiler/applySourceMapsToEvents';
import { SourceMap } from './types/SourceMap';
/**
* This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional)
* and return a promise which resolves to Chrome Dev Tools compatible events
* @param profilePath string
* @param sourceMapPath string
* @param bundleFileName string
* @return Promise<DurationEvent[]>
*/
const transformer = async (
profilePath: string,
sourceMapPath: string | undefined,
bundleFileName: string | undefined
): Promise<DurationEvent[]> => {
const hermesProfile: HermesCPUProfile = await readFileAsync(profilePath);
const profileChunk = CpuProfilerModel.collectProfileEvents(hermesProfile);
const profiler = new CpuProfilerModel(profileChunk);
const chromeEvents = profiler.createStartEndEvents();
if (sourceMapPath) {
const sourceMap: SourceMap = await readFileAsync(sourceMapPath);
const events = applySourceMapsToEvents(
sourceMap,
chromeEvents,
bundleFileName
);
return events;
}
return chromeEvents;
};
export default transformer;
export { SourceMap } from './types/SourceMap';

View File

@@ -0,0 +1,86 @@
import path from 'path';
import { SourceMapConsumer, RawSourceMap } from 'source-map';
import { DurationEvent } from '../types/EventInterfaces';
import { SourceMap } from '../types/SourceMap';
/**
* This function is a helper to the applySourceMapsToEvents. The category allocation logic is implemented here based on the sourcemap url (if available)
* @param defaultCategory The category the event is of by default without the use of Source maps
* @param url The URL which can be parsed to interpret the new category of the event (depends on node_modules)
*/
const improveCategories = (
defaultCategory: string,
url: string | null
): string => {
const obtainCategory = (url: string): string => {
const dirs = url
.substring(url.lastIndexOf(`${path.sep}node_modules${path.sep}`))
.split(path.sep);
return dirs.length > 2 && dirs[1] === 'node_modules'
? dirs[2]
: defaultCategory;
};
return url ? obtainCategory(url) : defaultCategory;
};
/**
* Enhances the function line, column and params information and event categories
* based on JavaScript source maps to make it easier to associate trace events with
* the application code
*
* Throws error if args not set up in ChromeEvents
* @param {SourceMap} sourceMap
* @param {DurationEvent[]} chromeEvents
* @param {string} indexBundleFileName
* @throws If `args` for events are not populated
* @returns {DurationEvent[]}
*/
const applySourceMapsToEvents = async (
sourceMap: SourceMap,
chromeEvents: DurationEvent[],
indexBundleFileName: string | undefined
): Promise<DurationEvent[]> => {
// SEE: Should file here be an optional parameter, so take indexBundleFileName as a parameter and use
// a default name of `index.bundle`
const rawSourceMap: RawSourceMap = {
version: Number(sourceMap.version),
file: indexBundleFileName || 'index.bundle',
sources: sourceMap.sources,
mappings: sourceMap.mappings,
names: sourceMap.names,
};
const consumer = await new SourceMapConsumer(rawSourceMap);
const events = chromeEvents.map((event: DurationEvent) => {
if (event.args) {
const sm = consumer.originalPositionFor({
line: Number(event.args.line),
column: Number(event.args.column),
});
/**
* The categories can help us better visualise the profile if we modify the categories.
* We change these categories only in the root level and not deeper inside the args, just so we have our
* original categories as well as these modified categories (as the modified categories simply help with visualisation)
*/
event.cat = improveCategories(event.cat!, sm.source);
event.args = {
...event.args,
url: sm.source,
line: sm.line,
column: sm.column,
params: sm.name,
allocatedCategory: event.cat,
allocatedName: event.name,
};
} else {
throw new Error(
`Source maps could not be derived for an event at ${event.ts} and with stackFrame ID ${event.sf}`
);
}
return event;
});
consumer.destroy();
return events;
};
export default applySourceMapsToEvents;

View File

@@ -0,0 +1,327 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*
* MODIFICATION NOTICE:
* This file is derived from `https://github.com/GoogleChrome/lighthouse/blob/0422daa9b1b8528dd8436860b153134bd0f959f1/lighthouse-core/lib/tracehouse/cpu-profile-model.js`
* and has been modified by Saphal Patro (email: saphal1998@gmail.com)
* The following changes have been made to the original file:
* 1. Converted code to Typescript and defined necessary types
* 2. Wrote a method @see collectProfileEvents to convert the Hermes Samples to Profile Chunks supported by Lighthouse Parser
* 3. Modified @see constructNodes to work with the Hermes Samples and StackFrames
*/
/**
* @fileoverview
*
* This model converts the `Profile` and `ProfileChunk` mega trace events from the `disabled-by-default-v8.cpu_profiler`
* category into B/E-style trace events that main-thread-tasks.js already knows how to parse into a task tree.
*
* The CPU profiler measures where time is being spent by sampling the stack (See https://www.jetbrains.com/help/profiler/Profiling_Guidelines__Choosing_the_Right_Profiling_Mode.html
* for a generic description of the differences between tracing and sampling).
*
* A `Profile` event is a record of the stack that was being executed at different sample points in time.
* It has a structure like this:
*
* nodes: [function A, function B, function C]
* samples: [node with id 2, node with id 1, ...]
* timeDeltas: [4125μs since last sample, 121μs since last sample, ...]
*
* Helpful prior art:
* @see https://cs.chromium.org/chromium/src/third_party/devtools-frontend/src/front_end/sdk/CPUProfileDataModel.js?sq=package:chromium&g=0&l=42
* @see https://github.com/v8/v8/blob/99ca333b0efba3236954b823101315aefeac51ab/tools/profile.js
* @see https://github.com/jlfwong/speedscope/blob/9ed1eb192cb7e9dac43a5f25bd101af169dc654a/src/import/chrome.ts#L200
*/
import {
CPUProfileChunk,
CPUProfileChunkNode,
CPUProfileChunker,
} from '../types/CPUProfile';
import { DurationEvent } from '../types/EventInterfaces';
import {
HermesCPUProfile,
HermesSample,
HermesStackFrame,
} from '../types/HermesProfile';
import { EventsPhase } from '../types/Phases';
export class CpuProfilerModel {
_profile: CPUProfileChunk;
_nodesById: Map<number, CPUProfileChunkNode>;
_activeNodeArraysById: Map<number, number[]>;
constructor(profile: CPUProfileChunk) {
this._profile = profile;
this._nodesById = this._createNodeMap();
this._activeNodeArraysById = this._createActiveNodeArrays();
}
/**
* Initialization function to enable O(1) access to nodes by node ID.
* @return {Map<number, CPUProfileChunkNode}
*/
_createNodeMap(): Map<number, CPUProfileChunkNode> {
/** @type {Map<number, CpuProfile['nodes'][0]>} */
const map: Map<number, CPUProfileChunkNode> = new Map<
number,
CPUProfileChunkNode
>();
for (const node of this._profile.nodes) {
map.set(node.id, node);
}
return map;
}
/**
* Initialization function to enable O(1) access to the set of active nodes in the stack by node ID.
* @return Map<number, number[]>
*/
_createActiveNodeArrays(): Map<number, number[]> {
const map: Map<number, number[]> = new Map<number, number[]>();
/**
* Given a nodeId, `getActiveNodes` gets all the parent nodes in reversed call order
* @param {number} id
*/
const getActiveNodes = (id: number): number[] => {
if (map.has(id)) return map.get(id) || [];
const node = this._nodesById.get(id);
if (!node) throw new Error(`No such node ${id}`);
if (node.parent) {
const array = getActiveNodes(node.parent).concat([id]);
map.set(id, array);
return array;
} else {
return [id];
}
};
for (const node of this._profile.nodes) {
map.set(node.id, getActiveNodes(node.id));
}
return map;
}
/**
* Returns all the node IDs in a stack when a specific nodeId is at the top of the stack
* (i.e. a stack's node ID and the node ID of all of its parents).
*/
_getActiveNodeIds(nodeId: number): number[] {
const activeNodeIds = this._activeNodeArraysById.get(nodeId);
if (!activeNodeIds) throw new Error(`No such node ID ${nodeId}`);
return activeNodeIds;
}
/**
* Generates the necessary B/E-style trace events for a single transition from stack A to stack B
* at the given timestamp.
*
* Example:
*
* timestamp 1234
* previousNodeIds 1,2,3
* currentNodeIds 1,2,4
*
* yields [end 3 at ts 1234, begin 4 at ts 1234]
*
* @param {number} timestamp
* @param {Array<number>} previousNodeIds
* @param {Array<number>} currentNodeIds
* @returns {Array<DurationEvent>}
*/
_createStartEndEventsForTransition(
timestamp: number,
previousNodeIds: number[],
currentNodeIds: number[]
): DurationEvent[] {
// Start nodes are the nodes which are present only in the currentNodeIds and not in PreviousNodeIds
const startNodes: CPUProfileChunkNode[] = currentNodeIds
.filter(id => !previousNodeIds.includes(id))
.map(id => this._nodesById.get(id)!);
// End nodes are the nodes which are present only in the PreviousNodeIds and not in CurrentNodeIds
const endNodes: CPUProfileChunkNode[] = previousNodeIds
.filter(id => !currentNodeIds.includes(id))
.map(id => this._nodesById.get(id)!);
/**
* The name needs to be modified if `http://` is present as this directs us to bundle files which does not add any information for the end user
* @param name
*/
const removeLinksIfExist = (name: string): string => {
// If the name includes `http://`, we can filter the name
if (name.includes('http://')) {
name = name.substring(0, name.lastIndexOf('('));
}
return name || 'anonymous';
};
/**
* Create a Duration Event from CPUProfileChunkNodes.
* @param {CPUProfileChunkNode} node
* @return {DurationEvent} */
const createEvent = (node: CPUProfileChunkNode): DurationEvent => ({
ts: timestamp,
pid: this._profile.pid,
tid: Number(this._profile.tid),
ph: EventsPhase.DURATION_EVENTS_BEGIN,
name: removeLinksIfExist(node.callFrame.name),
cat: node.callFrame.category,
args: { ...node.callFrame },
});
const startEvents: DurationEvent[] = startNodes
.map(createEvent)
.map(evt => ({ ...evt, ph: EventsPhase.DURATION_EVENTS_BEGIN }));
const endEvents: DurationEvent[] = endNodes
.map(createEvent)
.map(evt => ({ ...evt, ph: EventsPhase.DURATION_EVENTS_END }));
return [...endEvents.reverse(), ...startEvents];
}
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @return {DurationEvent}
* @throws If the length of timeDeltas array or the samples array does not match with the length of samples in Hermes Profile
*/
createStartEndEvents(): DurationEvent[] {
const profile = this._profile;
const length = profile.samples.length;
if (
profile.timeDeltas.length !== length ||
profile.samples.length !== length
)
throw new Error(`Invalid CPU profile length`);
const events: DurationEvent[] = [];
let timestamp = profile.startTime;
let lastActiveNodeIds: number[] = [];
for (let i = 0; i < profile.samples.length; i++) {
const nodeId = profile.samples[i];
const timeDelta = Math.max(profile.timeDeltas[i], 0);
const node = this._nodesById.get(nodeId);
if (!node) throw new Error(`Missing node ${nodeId}`);
timestamp += timeDelta;
const activeNodeIds = this._getActiveNodeIds(nodeId);
events.push(
...this._createStartEndEventsForTransition(
timestamp,
lastActiveNodeIds,
activeNodeIds
)
);
lastActiveNodeIds = activeNodeIds;
}
events.push(
...this._createStartEndEventsForTransition(
timestamp,
lastActiveNodeIds,
[]
)
);
return events;
}
/**
* Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`
* @param {CPUProfileChunk} profile
*/
static createStartEndEvents(profile: CPUProfileChunk) {
const model = new CpuProfilerModel(profile);
return model.createStartEndEvents();
}
/**
* Converts the Hermes Sample into a single CpuProfileChunk object for consumption
* by `createStartEndEvents()`.
*
* @param {HermesCPUProfile} profile
* @throws Profile must have at least one sample
* @return {CPUProfileChunk}
*/
static collectProfileEvents(profile: HermesCPUProfile): CPUProfileChunk {
if (profile.samples.length >= 0) {
const { samples, stackFrames } = profile;
// Assumption: The sample will have a single process
const pid: number = samples[0].pid;
// Assumption: Javascript is single threaded, so there should only be one thread throughout
const tid: string = samples[0].tid;
// TODO: What role does id play in string parsing
const id: string = '0x1';
const startTime: number = Number(samples[0].ts);
const { nodes, sampleNumbers, timeDeltas } = this.constructNodes(
samples,
stackFrames
);
return {
id,
pid,
tid,
startTime,
nodes,
samples: sampleNumbers,
timeDeltas,
};
} else {
throw new Error('The hermes profile has zero samples');
}
}
/**
* Constructs CPUProfileChunk Nodes and the resultant samples and time deltas to be inputted into the
* CPUProfileChunk object which will be processed to give createStartEndEvents()
*
* @param {HermesSample} samples
* @param {<string, HermesStackFrame>} stackFrames
* @return {CPUProfileChunker}
*/
static constructNodes(
samples: HermesSample[],
stackFrames: { [key in string]: HermesStackFrame }
): CPUProfileChunker {
samples = samples.map((sample: HermesSample) => {
sample.stackFrameData = stackFrames[sample.sf];
return sample;
});
const stackFrameIds: string[] = Object.keys(stackFrames);
const profileNodes: CPUProfileChunkNode[] = stackFrameIds.map(
(stackFrameId: string) => {
const stackFrame = stackFrames[stackFrameId];
return {
id: Number(stackFrameId),
callFrame: {
...stackFrame,
url: stackFrame.name,
},
parent: stackFrames[stackFrameId].parent,
};
}
);
const returnedSamples: number[] = [];
const timeDeltas: number[] = [];
let lastTimeStamp = Number(samples[0].ts);
samples.forEach((sample: HermesSample, idx: number) => {
returnedSamples.push(sample.sf);
if (idx === 0) {
timeDeltas.push(0);
} else {
const timeDiff = Number(sample.ts) - lastTimeStamp;
lastTimeStamp = Number(sample.ts);
timeDeltas.push(timeDiff);
}
});
return {
nodes: profileNodes,
sampleNumbers: returnedSamples,
timeDeltas,
};
}
}

View File

@@ -0,0 +1,41 @@
/**
* The CPUProfileChunk is the intermediate file that Lighthouse can interpret and
* hence subsequently convert to events supported by Chrome Dev Tools
*/
export interface CPUProfileChunk {
id: string;
pid: number;
tid: string;
startTime: number;
nodes: CPUProfileChunkNode[];
samples: number[];
timeDeltas: number[];
}
/**
* The CPUProfileChunkNode is an individual element of the nodes[] property in the CPUProfileChunk
* @see CPUProfileChunk
*/
export interface CPUProfileChunkNode {
id: number;
callFrame: {
line: string;
column: string;
funcLine: string;
funcColumn: string;
name: string;
url?: string;
category: string;
};
parent?: number;
}
/**
* The process of conversion of Hermes Profile Events to Lighthouse supported events are primarily focussed
* around generating the correct values of the properties in CPUProfileChunker.
*/
export type CPUProfileChunker = {
nodes: CPUProfileChunkNode[];
sampleNumbers: number[];
timeDeltas: number[];
};

View File

@@ -0,0 +1,192 @@
import { EventsPhase } from './Phases';
// All of the event types in this module are not currently
// being used, but they are included here for completeness
// for future implementers
export interface SharedEventProperties {
/**
* name of the event
*/
name?: string;
/**
* event category
*/
cat?: string;
/**
* tracing clock timestamp
*/
ts?: number;
/**
* process ID
*/
pid?: number;
/**
* thread ID
*/
tid?: number;
/**
* event type (phase)
*/
ph: EventsPhase;
/**
* id for a stackFrame object
*/
sf?: number;
/**
* thread clock timestamp
*/
tts?: number;
/**
* a fixed color name
*/
cname?: string;
/**
* event arguments
*/
args?: {
[key in string]: any;
};
}
interface DurationEventBegin extends SharedEventProperties {
ph: EventsPhase.DURATION_EVENTS_BEGIN;
}
interface DurationEventEnd extends SharedEventProperties {
ph: EventsPhase.DURATION_EVENTS_END;
}
export type DurationEvent = DurationEventBegin | DurationEventEnd;
export interface CompleteEvent extends SharedEventProperties {
ph: EventsPhase.COMPLETE_EVENTS;
dur: number;
}
export interface MetadataEvent extends SharedEventProperties {
ph: EventsPhase.METADATA_EVENTS;
}
export interface SampleEvent extends SharedEventProperties {
ph: EventsPhase.SAMPLE_EVENTS;
}
interface ObjectEventCreated extends SharedEventProperties {
ph: EventsPhase.OBJECT_EVENTS_CREATED;
scope?: string;
}
interface ObjectEventSnapshot extends SharedEventProperties {
ph: EventsPhase.OBJECT_EVENTS_SNAPSHOT;
scope?: string;
}
interface ObjectEventDestroyed extends SharedEventProperties {
ph: EventsPhase.OBJECT_EVENTS_DESTROYED;
scope?: string;
}
export type ObjectEvent =
| ObjectEventCreated
| ObjectEventSnapshot
| ObjectEventDestroyed;
export interface ClockSyncEvent extends SharedEventProperties {
ph: EventsPhase.CLOCK_SYNC_EVENTS;
args: {
sync_id: string;
issue_ts?: number;
};
}
interface ContextEventEnter extends SharedEventProperties {
ph: EventsPhase.CONTEXT_EVENTS_ENTER;
}
interface ContextEventLeave extends SharedEventProperties {
ph: EventsPhase.CONTEXT_EVENTS_LEAVE;
}
export type ContextEvent = ContextEventEnter | ContextEventLeave;
interface AsyncEventStart extends SharedEventProperties {
ph: EventsPhase.ASYNC_EVENTS_NESTABLE_START;
id: number;
scope?: string;
}
interface AsyncEventInstant extends SharedEventProperties {
ph: EventsPhase.ASYNC_EVENTS_NESTABLE_INSTANT;
id: number;
scope?: string;
}
interface AsyncEventEnd extends SharedEventProperties {
ph: EventsPhase.ASYNC_EVENTS_NESTABLE_END;
id: number;
scope?: string;
}
export type AsyncEvent = AsyncEventStart | AsyncEventInstant | AsyncEventEnd;
export interface InstantEvent extends SharedEventProperties {
ph: EventsPhase.INSTANT_EVENTS;
s: string;
}
export interface CounterEvent extends SharedEventProperties {
ph: EventsPhase.COUNTER_EVENTS;
}
interface FlowEventStart extends SharedEventProperties {
ph: EventsPhase.FLOW_EVENTS_START;
}
interface FlowEventStep extends SharedEventProperties {
ph: EventsPhase.FLOW_EVENTS_STEP;
}
interface FlowEventEnd extends SharedEventProperties {
ph: EventsPhase.FLOW_EVENTS_END;
}
export type FlowEvent = FlowEventStart | FlowEventStep | FlowEventEnd;
interface MemoryDumpGlobal extends SharedEventProperties {
ph: EventsPhase.MEMORY_DUMP_EVENTS_GLOBAL;
id: string;
}
interface MemoryDumpProcess extends SharedEventProperties {
ph: EventsPhase.MEMORY_DUMP_EVENTS_PROCESS;
id: string;
}
export type MemoryDumpEvent = MemoryDumpGlobal | MemoryDumpProcess;
export interface MarkEvent extends SharedEventProperties {
ph: EventsPhase.MARK_EVENTS;
}
export interface LinkedIDEvent extends SharedEventProperties {
ph: EventsPhase.LINKED_ID_EVENTS;
id: number;
args: {
linked_id: number;
};
}
export type Event =
| DurationEvent
| CompleteEvent
| MetadataEvent
| SampleEvent
| ObjectEvent
| ClockSyncEvent
| ContextEvent
| AsyncEvent
| InstantEvent
| CounterEvent
| FlowEvent
| MemoryDumpEvent
| MarkEvent
| LinkedIDEvent;

View File

@@ -0,0 +1,42 @@
import { SharedEventProperties } from './EventInterfaces';
/**
* Each item in the stackFrames object of the hermes profile
*/
export interface HermesStackFrame {
line: string;
column: string;
funcLine: string;
funcColumn: string;
name: string;
category: string;
/**
* A parent function may or may not exist
*/
parent?: number;
}
/**
* Each item in the samples array of the hermes profile
*/
export interface HermesSample {
cpu: string;
name: string;
ts: string;
pid: number;
tid: string;
weight: string;
/**
* Will refer to an element in the stackFrames object of the Hermes Profile
*/
sf: number;
stackFrameData?: HermesStackFrame;
}
/**
* Hermes Profile Interface
*/
export interface HermesCPUProfile {
traceEvents: SharedEventProperties[];
samples: HermesSample[];
stackFrames: { [key in string]: HermesStackFrame };
}

View File

@@ -0,0 +1,30 @@
export enum EventsPhase {
DURATION_EVENTS_BEGIN = 'B',
DURATION_EVENTS_END = 'E',
COMPLETE_EVENTS = 'X',
INSTANT_EVENTS = 'I',
COUNTER_EVENTS = 'C',
ASYNC_EVENTS_NESTABLE_START = 'b',
ASYNC_EVENTS_NESTABLE_INSTANT = 'n',
ASYNC_EVENTS_NESTABLE_END = 'e',
FLOW_EVENTS_START = 's',
FLOW_EVENTS_STEP = 't',
FLOW_EVENTS_END = 'f',
SAMPLE_EVENTS = 'P',
OBJECT_EVENTS_CREATED = 'N',
OBJECT_EVENTS_SNAPSHOT = 'O',
OBJECT_EVENTS_DESTROYED = 'D',
METADATA_EVENTS = 'M',
MEMORY_DUMP_EVENTS_GLOBAL = 'V',
MEMORY_DUMP_EVENTS_PROCESS = 'v',
MARK_EVENTS = 'R',
CLOCK_SYNC_EVENTS = 'c',
CONTEXT_EVENTS_ENTER = '(',
CONTEXT_EVENTS_LEAVE = ')',
// Deprecated
ASYNC_EVENTS_START = 'S',
ASYNC_EVENTS_STEP_INTO = 'T',
ASYNC_EVENTS_STEP_PAST = 'p',
ASYNC_EVENTS_END = 'F',
LINKED_ID_EVENTS = '=',
}

View File

@@ -0,0 +1,8 @@
export interface SourceMap {
version: string;
sources: string[];
sourceContent: string[];
x_facebook_sources: { names: string[]; mappings: string }[] | null;
names: string[];
mappings: string;
}

View File

@@ -0,0 +1,16 @@
import { readFile } from 'fs';
import { promisify } from 'util';
export const readFileAsync = async (path: string): Promise<any> => {
try {
const readFileAsync = promisify(readFile);
const fileString: string = (await readFileAsync(path, 'utf-8')) as string;
if (fileString.length === 0) {
throw new Error(`${path} is an empty file`);
}
const obj = JSON.parse(fileString);
return obj;
} catch (err) {
throw err;
}
};