* 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:
89
data-extraction/src/js/api/DataExtractorApi.ts
Normal file
89
data-extraction/src/js/api/DataExtractorApi.ts
Normal 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;
|
||||
}
|
||||
90
data-extraction/src/js/api/DataExtractorApiImpl.ts
Normal file
90
data-extraction/src/js/api/DataExtractorApiImpl.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
1
data-extraction/src/js/api/default-extractors/index.ts
Normal file
1
data-extraction/src/js/api/default-extractors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { registerDefaultDataExtractors } from "./registerDefaultDataExtractors";
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
2
data-extraction/src/js/api/index.ts
Normal file
2
data-extraction/src/js/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./DataExtractorApi";
|
||||
export * from "./injection";
|
||||
73
data-extraction/src/js/api/injection.ts
Normal file
73
data-extraction/src/js/api/injection.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user