* Updates Dependencies

* Uses webpack to bundle runtime library into a single file
* Inject the entire file into the Debugee
* Register runtime helpers (hediet_dbgVis)
This commit is contained in:
Henning Dieterichs
2020-02-12 14:20:45 +01:00
parent 1a2d5b9af0
commit d4c6e942d0
29 changed files with 809 additions and 669 deletions

View File

@@ -0,0 +1,89 @@
import {
ExtractedData,
DataExtractionResult,
} from "../../DataExtractionResult";
export interface DataExtractorApi {
/**
* Registers a single extractor.
*/
registerExtractor<TExtractedData extends ExtractedData>(
extractor: DataExtractor<TExtractedData>
): void;
/**
* Registers multiple extractors.
*/
registerExtractors(extractors: DataExtractor<ExtractedData>[]): void;
/**
* Extracts data from `value`.
*/
getData(
value: unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined
): JSONString<DataResult>;
/**
* Registers all default (built-in) extractors.
* @preferExisting if `true`, existing extractors with the same id are not overwritten.
*/
registerDefaultExtractors(preferExisting?: boolean): void;
}
export type DataResult =
| {
kind: "Data";
extractionResult: DataExtractionResult;
}
| { kind: "NoExtractors" }
| { kind: "Error"; message: string };
export interface JSONString<T> extends String {
__brand: { json: T };
}
export interface DataExtractor<T extends ExtractedData> {
/**
* Must be unique among all data extractors.
*/
id: string;
getExtractions(
data: unknown,
extractionCollector: ExtractionCollector<T>,
context: DataExtractorContext
): void;
}
export interface ExtractionCollector<T extends ExtractedData> {
/**
* Suggests a possible extraction.
*/
addExtraction(extraction: DataExtraction<T>): void;
}
export interface DataExtractorContext {
/**
* Evaluates an expression in the context of the active stack frame.
*/
evalFn: <TEval>(expression: string) => TEval;
}
export interface DataExtraction<TData extends ExtractedData> {
/**
* Higher priorities are preferred.
*/
priority: number;
/**
* A unique id identifying this extraction among all extractions.
* Required to express extraction preferences.
*/
id: string;
/**
* A user friendly name of this extraction.
*/
name: string;
extractData(): TData;
}

View File

@@ -0,0 +1,90 @@
import {
DataExtractorApi,
DataResult,
JSONString,
DataExtractor,
DataExtraction,
ExtractionCollector,
} from "./DataExtractorApi";
import { ExtractedData, DataExtractorInfo } from "../../DataExtractionResult";
import { registerDefaultDataExtractors } from "./default-extractors";
export class DataExtractorApiImpl implements DataExtractorApi {
private readonly extractors = new Map<
string,
DataExtractor<ExtractedData>
>();
private toJson<TData>(data: TData): JSONString<TData> {
return JSON.stringify(data) as any;
}
public registerExtractor<TExtractedData extends ExtractedData>(
extractor: DataExtractor<TExtractedData>
): void {
this.extractors.set(extractor.id, extractor);
}
public registerExtractors(
extractors: DataExtractor<ExtractedData>[]
): void {
for (const e of extractors) {
this.registerExtractor(e);
}
}
public getData(
value: unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined
): JSONString<DataResult> {
const extractions = new Array<DataExtraction<ExtractedData>>();
const extractionCollector: ExtractionCollector<ExtractedData> = {
addExtraction(extraction) {
extractions.push(extraction);
},
};
for (const e of this.extractors.values()) {
e.getExtractions(value, extractionCollector, { evalFn });
}
extractions.sort((a, b) => b.priority - a.priority);
let usedExtraction = extractions[0];
if (!usedExtraction) {
return this.toJson({ kind: "NoExtractors" } as DataResult);
}
if (preferredDataExtractorId) {
const preferred = extractions.find(
e => e.id === preferredDataExtractorId
);
if (preferred) {
usedExtraction = preferred;
}
}
function mapExtractor(
e: DataExtraction<ExtractedData>
): DataExtractorInfo {
return {
id: e.id as any,
name: e.name,
priority: e.priority,
};
}
const data = usedExtraction.extractData();
return this.toJson({
kind: "Data",
extractionResult: {
data,
usedExtractor: mapExtractor(usedExtraction),
availableExtractors: extractions.map(mapExtractor),
},
} as DataResult);
}
public registerDefaultExtractors(preferExisting: boolean = false): void {
registerDefaultDataExtractors(this);
}
}

View File

@@ -0,0 +1,38 @@
import { ExtractedData } from "../../../DataExtractionResult";
import {
DataExtractor,
ExtractionCollector,
DataExtractorContext,
} from "../..";
export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
readonly id = "as-is";
getExtractions(
data: unknown,
extractionCollector: ExtractionCollector<ExtractedData>,
context: DataExtractorContext
): void {
if (typeof data !== "object" || !data || !("kind" in data)) {
return;
}
const obj = data as any;
if (typeof obj.kind !== "object" || !obj.kind) {
return;
}
const areAllTrue = Object.values(obj.kind).every(val => val === true);
if (!areAllTrue) {
return;
}
extractionCollector.addExtraction({
id: this.id,
name: "As Is",
priority: 500,
extractData() {
return obj;
},
});
}
}

View File

@@ -0,0 +1,33 @@
import { ExtractedData } from "../../../DataExtractionResult";
import {
DataExtractor,
ExtractionCollector,
DataExtractorContext,
} from "../DataExtractorApi";
export class GetDebugVisualizationDataExtractor
implements DataExtractor<ExtractedData> {
readonly id = "get-debug-visualization";
getExtractions(
data: unknown,
collector: ExtractionCollector<ExtractedData>,
context: DataExtractorContext
): void {
if (
typeof data !== "object" ||
!data ||
!("getDebugVisualization" in data)
) {
return;
}
collector.addExtraction({
id: this.id,
name: "Use Method 'getDebugVisualization'",
priority: 600,
extractData() {
return (data as any).getDebugVisualization();
},
});
}
}

View File

@@ -0,0 +1,30 @@
import { ExtractedData } from "../../../DataExtractionResult";
import {
DataExtractor,
ExtractionCollector,
DataExtractorContext,
} from "../DataExtractorApi";
export class ToStringDataExtractor implements DataExtractor<ExtractedData> {
readonly id = "to-string";
getExtractions(
data: unknown,
collector: ExtractionCollector<ExtractedData>,
context: DataExtractorContext
): void {
collector.addExtraction({
id: "to-string",
name: "To String",
priority: 100,
extractData() {
return {
kind: {
text: true,
},
text: "" + data,
};
},
});
}
}

View File

@@ -0,0 +1,208 @@
import * as ts from "typescript"; // Only compile-time import!
import {
DataExtractor,
ExtractionCollector,
DataExtractorContext,
} from "../DataExtractorApi";
import { CommonDataTypes } from "../../../CommonDataTypes";
// This class is self contained and can be injected into both nodejs and browser environments.
export class TypeScriptAstDataExtractor
implements DataExtractor<CommonDataTypes.Ast> {
readonly id = "typescript-ast";
getExtractions(
data: unknown,
collector: ExtractionCollector<CommonDataTypes.Ast>,
{ evalFn }: DataExtractorContext
): void {
if (!data) {
return;
}
function getApi(): typeof ts {
if (typeof data === "object" && "typescript" in (data as object)) {
return (data as any).typescript;
} else {
const require = evalFn<(request: string) => unknown>("require");
return require("typescript") as typeof ts;
}
}
let tsApi: typeof ts;
try {
tsApi = getApi();
if (!tsApi) {
return;
}
} catch (e) {
return;
}
function findKey(value: any, object: any): string | null {
for (var key in object) {
if (key.startsWith("_")) continue;
var member = object[key];
if (member === value) return key;
if (Array.isArray(member) && member.indexOf(value) !== -1) {
return key;
}
}
return null;
}
function getChildren(node: ts.Node): ts.Node[] {
const result = new Array<ts.Node>();
tsApi.forEachChild(node, n => {
result.push(n);
});
return result;
}
function toTreeNode(
node: ts.Node,
memberName: string,
marked: Set<ts.Node>,
emphasizedValueFn: (node: ts.Node) => string | undefined
): CommonDataTypes.Ast["root"] {
const name = tsApi.SyntaxKind[node.kind];
const children = getChildren(node)
.map((childNode, idx) => {
let parentPropertyName = findKey(childNode, node) || "";
if (childNode.kind == tsApi.SyntaxKind.SyntaxList) {
const children = getChildren(childNode);
children.some(c => {
parentPropertyName = findKey(c, node) || "";
return !!parentPropertyName;
});
if (children.length === 0) return null!;
}
if (node.kind == tsApi.SyntaxKind.SyntaxList) {
parentPropertyName = "" + idx;
}
return toTreeNode(
childNode,
parentPropertyName,
marked,
emphasizedValueFn
);
})
.filter(c => c !== null);
let value: string | undefined = undefined;
if (tsApi.isIdentifier(node)) {
value = node.text || (node.escapedText as string);
} else if (tsApi.isLiteralExpression(node)) {
value = node.text;
}
return {
name: name,
id: memberName,
children: children,
data: {
length: node.end - node.pos,
position: node.pos,
},
emphasizedValue: emphasizedValueFn(node),
isMarked: marked.has(node),
value,
};
}
function isNode(node: unknown): node is ts.Node {
return (
typeof node === "object" &&
node !== null &&
(tsApi.isToken(node as any) || (tsApi as any).isNode(node))
);
}
function getSourceFile(node: ts.Node | any): ts.SourceFile {
if (!node) {
throw new Error("Detached node");
}
if (tsApi.isSourceFile(node)) {
return node;
}
if (!("getSourceFile" in node)) {
return getSourceFile(node.parent);
}
return node.getSourceFile();
}
let rootSourceFile: ts.SourceFile | undefined = undefined;
let rootNode: ts.Node | undefined = undefined;
let marked: Set<ts.Node>;
let fn: (n: ts.Node) => string | undefined = (n: ts.Node) => undefined;
if (Array.isArray(data) && data.every(isNode)) {
rootSourceFile = getSourceFile(data[0] as ts.Node);
marked = new Set(data);
} else if (isNode(data)) {
rootSourceFile = getSourceFile(data);
marked = new Set([data]);
} else if (typeof data === "object" && data) {
marked = new Set();
const map = new Map<ts.Node, string>();
fn = (n: ts.Node) => map.get(n);
for (const [key, item] of Object.entries(data)) {
if (key === "fn") {
fn = item;
} else if (key === "typescript") {
} else {
if (key === "rootNode") {
rootNode = item;
}
let nodes: Array<ts.Node>;
if (isNode(item)) {
nodes = [item];
} else if (Array.isArray(item) && item.every(isNode)) {
nodes = item;
} else {
return;
}
if (nodes.length > 0 && !rootSourceFile) {
rootSourceFile = getSourceFile(nodes[0]);
}
for (const n of nodes) {
marked.add(n);
map.set(n, key);
}
}
}
} else {
return;
}
if (!rootSourceFile) {
return;
}
const finalRootSourceFile = rootSourceFile;
collector.addExtraction({
id: "ts-ast",
name: "TypeScript AST",
priority: 1000,
extractData() {
return {
kind: { text: true, tree: true, ast: true },
root: toTreeNode(
rootNode || finalRootSourceFile,
"root",
marked,
fn
),
text: finalRootSourceFile.text,
fileName: "index.ts",
};
},
});
}
}

View File

@@ -0,0 +1 @@
export { registerDefaultDataExtractors } from "./registerDefaultDataExtractors";

View File

@@ -0,0 +1,20 @@
import { DataExtractorApi } from "../DataExtractorApi";
import { TypeScriptAstDataExtractor } from "./TypeScriptDataExtractors";
import { AsIsDataExtractor } from "./AsIsDataExtractor";
import { GetDebugVisualizationDataExtractor } from "./GetDebugVisualizationDataExtractor";
import { ToStringDataExtractor } from "./ToStringExtractor";
/**
* The default data extractors should be registered by VS Code automatically.
* Registering them manually ensures that they are up to date.
*/
export function registerDefaultDataExtractors(api: DataExtractorApi) {
for (const item of [
new TypeScriptAstDataExtractor(),
new AsIsDataExtractor(),
new GetDebugVisualizationDataExtractor(),
new ToStringDataExtractor(),
]) {
api.registerExtractor(item);
}
}

View File

@@ -0,0 +1,2 @@
export * from "./DataExtractorApi";
export * from "./injection";

View File

@@ -0,0 +1,73 @@
import * as fs from "fs";
import { DataExtractorApi } from "./DataExtractorApi";
import { DataExtractorApiImpl } from "./DataExtractorApiImpl";
import * as helpers from "../helpers";
import * as globalHelpers from "../global-helpers";
/**
* Returns standalone JS code representing an expression that initializes the data extraction API.
* This expression returns nothing.
* This function is called in the VS Code extension, the expression is evaluated in the debugee.
*/
export function getExpressionToInitializeDataExtractorApi(): string {
const _fs = require("fs") as typeof fs;
const moduleSrc = _fs.readFileSync(__filename, { encoding: "utf8" });
return `((function () {
let module = {};
${moduleSrc}
module.exports.installHelpers();
return module.exports.getDataExtractorApi();
})())`;
}
/**
* Returns standalone JS code representing an expression returns the data extraction API.
* This expression returns an object of type `DataExtractorApi`.
* If the API must be initialized with `getExpressionToInitializeDataExtractorApi()`,
* it throws an exception containing the text of `ApiHasNotBeenInitializedCode`.
* This function is called in the VS Code extension, the expression is evaluated in the debugee.
*/
export function getExpressionForDataExtractorApi(): string {
return `((${selfContainedGetInitializedDataExtractorApi.toString()})())`;
}
const apiKey = "@hediet/data-extractor-api/v2";
export function getDataExtractorApi(): DataExtractorApi {
const globalObj =
typeof window === "object" ? (window as any) : (global as any);
if (!globalObj[apiKey]) {
globalObj[apiKey] = new DataExtractorApiImpl();
}
return globalObj[apiKey];
}
/**
* This code is used to detect if the API has not been initialized yet.
* @internal
*/
export const ApiHasNotBeenInitializedCode = "EgH0cybXij1jYUozyakO" as const;
/**
* @internal
*/
function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi {
const globalObj =
typeof window === "object" ? (window as any) : (global as any);
const key: typeof apiKey = "@hediet/data-extractor-api/v2";
let api: DataExtractorApi | undefined = globalObj[key];
if (!api) {
const code: typeof ApiHasNotBeenInitializedCode =
"EgH0cybXij1jYUozyakO";
throw new Error(
`Data Extractor API has not been initialized. Code: ${code}`
);
}
return api;
}
export function installHelpers(): void {
const globalObj =
typeof window === "object" ? (window as any) : (global as any);
globalObj["hediet_dbgVis"] = { ...helpers, ...globalHelpers };
}