@vizualmodel/vmblu-cli 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -160,7 +160,7 @@ async function initProject(opts) {
|
|
|
160
160
|
|
|
161
161
|
const absTarget = path.resolve(targetDir);
|
|
162
162
|
const modelFile = path.join(absTarget, `${projectName}.vmblu`);
|
|
163
|
-
const docFile = path.join(absTarget, `${projectName}
|
|
163
|
+
const docFile = path.join(absTarget, `${projectName}.prf.json`);
|
|
164
164
|
|
|
165
165
|
const llmDir = path.join(absTarget, 'llm');
|
|
166
166
|
const sessionDir = path.join(llmDir, 'session');
|
|
@@ -2855,23 +2855,23 @@ async getFolderContent(){
|
|
|
2855
2855
|
const sourceMapHandling = {
|
|
2856
2856
|
|
|
2857
2857
|
// reads the source doc file and parses it into documentation
|
|
2858
|
-
async
|
|
2858
|
+
async handleSourceMap() {
|
|
2859
2859
|
|
|
2860
2860
|
// read the source doc file
|
|
2861
|
-
const
|
|
2861
|
+
const rawSourceMap = await this.readSourceMap();
|
|
2862
2862
|
|
|
2863
2863
|
// check
|
|
2864
|
-
if (!
|
|
2864
|
+
if (! rawSourceMap) return;
|
|
2865
2865
|
|
|
2866
2866
|
// parse to extract the juicy bits
|
|
2867
|
-
this.sourceMap = this.
|
|
2867
|
+
this.sourceMap = this.parseSourceMap(rawSourceMap);
|
|
2868
2868
|
|
|
2869
2869
|
// ok
|
|
2870
|
-
// console.log('**
|
|
2870
|
+
// console.log('** SourceMap **', this.sourceMap)
|
|
2871
2871
|
},
|
|
2872
2872
|
|
|
2873
2873
|
// Reads the sourceMap of the model
|
|
2874
|
-
async
|
|
2874
|
+
async readSourceMap() {
|
|
2875
2875
|
|
|
2876
2876
|
// get the full path
|
|
2877
2877
|
const fullPath = this.arl?.getFullPath();
|
|
@@ -2880,7 +2880,7 @@ async readSourceDoc() {
|
|
|
2880
2880
|
if (!fullPath) return null
|
|
2881
2881
|
|
|
2882
2882
|
// make an arl
|
|
2883
|
-
const sourceMapArl = this.arl.resolve(removeExt(fullPath) + '
|
|
2883
|
+
const sourceMapArl = this.arl.resolve(removeExt(fullPath) + '.prf.json');
|
|
2884
2884
|
|
|
2885
2885
|
// get the file
|
|
2886
2886
|
return await sourceMapArl.get('json')
|
|
@@ -2893,7 +2893,7 @@ async readSourceDoc() {
|
|
|
2893
2893
|
* @param {Array<{node: string, handlers: Array}>} docEntries
|
|
2894
2894
|
* @returns {Map<string, Map<string, object>>} Map of nodeName -> Map of pinName -> handler metadata
|
|
2895
2895
|
*/
|
|
2896
|
-
|
|
2896
|
+
parseSourceMap(raw) {
|
|
2897
2897
|
|
|
2898
2898
|
// check
|
|
2899
2899
|
if (!raw.entries) return null;
|
|
@@ -2944,7 +2944,7 @@ parseSourceDoc(raw) {
|
|
|
2944
2944
|
/**
|
|
2945
2945
|
* Optional helper to flatten the nested map into a plain array (useful for UI).
|
|
2946
2946
|
*/
|
|
2947
|
-
|
|
2947
|
+
flattenSourceMap(nodeMap) {
|
|
2948
2948
|
const flatList = [];
|
|
2949
2949
|
for (const [node, pins] of nodeMap.entries()) {
|
|
2950
2950
|
for (const [pin, meta] of pins.entries()) {
|
|
@@ -3047,7 +3047,7 @@ makeMcpToolString(root) {
|
|
|
3047
3047
|
* Generate MCP-compatible tool specs in an LLM-neutral format.
|
|
3048
3048
|
* Only handlers with `mcp: true` will be included.
|
|
3049
3049
|
*
|
|
3050
|
-
* @param {Map<string, Map<string, object>>} nodeMap - Output from
|
|
3050
|
+
* @param {Map<string, Map<string, object>>} nodeMap - Output from parseSourceMap
|
|
3051
3051
|
* @returns {Array<object>} - Abstract tool specs
|
|
3052
3052
|
*/
|
|
3053
3053
|
generateToolSpecs() {
|
|
@@ -12010,6 +12010,9 @@ changeFactory: {
|
|
|
12010
12010
|
|
|
12011
12011
|
};
|
|
12012
12012
|
|
|
12013
|
+
/**
|
|
12014
|
+
* @node editor editor
|
|
12015
|
+
*/
|
|
12013
12016
|
const redoxWidget = {
|
|
12014
12017
|
|
|
12015
12018
|
newPin: {
|
|
@@ -22656,136 +22659,144 @@ async getFolderContent(){
|
|
|
22656
22659
|
}
|
|
22657
22660
|
};
|
|
22658
22661
|
|
|
22659
|
-
// extractHandlersFromFile.js
|
|
22660
|
-
|
|
22661
|
-
|
|
22662
|
-
let currentNode = null;
|
|
22663
|
-
let topLevelClass = null;
|
|
22664
|
-
let nodeMap = null;
|
|
22665
|
-
let filePath = null;
|
|
22666
|
-
|
|
22667
|
-
|
|
22668
|
-
|
|
22669
|
-
|
|
22670
|
-
|
|
22671
|
-
|
|
22672
|
-
//
|
|
22673
|
-
|
|
22674
|
-
|
|
22675
|
-
|
|
22676
|
-
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22684
|
-
|
|
22685
|
-
|
|
22686
|
-
|
|
22687
|
-
|
|
22688
|
-
|
|
22689
|
-
|
|
22690
|
-
|
|
22691
|
-
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
|
|
22696
|
-
|
|
22697
|
-
|
|
22698
|
-
|
|
22699
|
-
|
|
22700
|
-
|
|
22701
|
-
|
|
22702
|
-
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
22707
|
-
|
|
22708
|
-
|
|
22709
|
-
|
|
22710
|
-
|
|
22711
|
-
|
|
22712
|
-
|
|
22713
|
-
|
|
22714
|
-
|
|
22715
|
-
|
|
22716
|
-
|
|
22662
|
+
// extractHandlersFromFile.js
|
|
22663
|
+
|
|
22664
|
+
|
|
22665
|
+
let currentNode = null;
|
|
22666
|
+
let topLevelClass = null;
|
|
22667
|
+
let nodeMap = null;
|
|
22668
|
+
let filePath = null;
|
|
22669
|
+
let nodeAliases = new Map();
|
|
22670
|
+
|
|
22671
|
+
let knownIdentifiers = new Set();
|
|
22672
|
+
|
|
22673
|
+
function findHandlers(sourceFile, _filePath, _nodeMap) {
|
|
22674
|
+
|
|
22675
|
+
// Reset any node context carried over from previous files.
|
|
22676
|
+
currentNode = null;
|
|
22677
|
+
|
|
22678
|
+
// The fallback name is the top-level class
|
|
22679
|
+
topLevelClass = sourceFile.getClasses()[0]?.getName?.() || null;
|
|
22680
|
+
nodeMap = _nodeMap;
|
|
22681
|
+
filePath = _filePath;
|
|
22682
|
+
nodeAliases = new Map();
|
|
22683
|
+
knownIdentifiers = collectKnownIdentifiers(sourceFile);
|
|
22684
|
+
|
|
22685
|
+
// Check all the functions in the sourcefile - typically generator functions
|
|
22686
|
+
sourceFile.getFunctions().forEach(fn => {
|
|
22687
|
+
|
|
22688
|
+
// Capture node annotations on generator-style functions and harvest handlers returned from them.
|
|
22689
|
+
const jsdoc = getFullJsDoc(fn);
|
|
22690
|
+
updateNodeFromJsdoc(jsdoc);
|
|
22691
|
+
|
|
22692
|
+
const name = fn.getName();
|
|
22693
|
+
|
|
22694
|
+
if (isHandler(name)) {
|
|
22695
|
+
|
|
22696
|
+
const line = fn.getNameNode()?.getStartLineNumber() ?? fn.getStartLineNumber();
|
|
22697
|
+
const docTags = getParamDocs(fn);
|
|
22698
|
+
const params = fn.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22699
|
+
|
|
22700
|
+
collect(name, params, line, jsdoc);
|
|
22701
|
+
}
|
|
22702
|
+
|
|
22703
|
+
collectHandlersFromFunctionReturns(fn);
|
|
22704
|
+
});
|
|
22705
|
+
|
|
22706
|
+
// Check the variable declarations in the sourcefile
|
|
22707
|
+
sourceFile.getVariableDeclarations().forEach(decl => {
|
|
22708
|
+
|
|
22709
|
+
// check the name
|
|
22710
|
+
const name = decl.getName();
|
|
22711
|
+
const init = decl.getInitializer();
|
|
22712
|
+
const line = decl.getStartLineNumber();
|
|
22713
|
+
const declJsdoc = getFullJsDoc(decl);
|
|
22714
|
+
const statement = decl.getFirstAncestorByKind?.(SyntaxKind.VariableStatement);
|
|
22715
|
+
const statementJsdoc = statement ? getFullJsDoc(statement) : null;
|
|
22716
|
+
const jsdoc = hasDocMetadata(declJsdoc) ? declJsdoc : statementJsdoc ?? declJsdoc;
|
|
22717
|
+
updateNodeFromJsdoc(jsdoc);
|
|
22718
|
+
|
|
22719
|
+
// check if the name is a handler and initialised with a function
|
|
22720
|
+
if (isHandler(name) && init && init.getKindName().includes('Function')) {
|
|
22721
|
+
|
|
22722
|
+
const docTags = getParamDocs(decl);
|
|
22723
|
+
const params = init.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22724
|
+
|
|
22725
|
+
collect(name, params, line, jsdoc);
|
|
22726
|
+
}
|
|
22727
|
+
|
|
22717
22728
|
const objectLiteral = resolveObjectLiteralExpression(init);
|
|
22718
22729
|
if (objectLiteral) {
|
|
22719
22730
|
collectObjectLiteralHandlers(objectLiteral);
|
|
22720
22731
|
}
|
|
22721
|
-
});
|
|
22722
|
-
|
|
22723
|
-
// check all the classes in the file
|
|
22724
|
-
sourceFile.getClasses().forEach(cls => {
|
|
22725
|
-
|
|
22726
|
-
// get the name of the node
|
|
22727
|
-
cls.getName?.() || topLevelClass;
|
|
22728
|
-
|
|
22729
|
-
// check all the methods
|
|
22730
|
-
cls.getMethods().forEach(method => {
|
|
22731
|
-
|
|
22732
|
-
// check the name
|
|
22733
|
-
const name = method.getName();
|
|
22734
|
-
if (!isHandler(name)) return;
|
|
22735
|
-
|
|
22736
|
-
// extract
|
|
22737
|
-
const line = method.getNameNode()?.getStartLineNumber() ?? method.getStartLineNumber();
|
|
22738
|
-
const jsdoc = getFullJsDoc(method);
|
|
22739
|
-
const docTags = getParamDocs(method);
|
|
22740
|
-
const params = method.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22741
|
-
|
|
22742
|
-
// and collect
|
|
22743
|
-
collect(name, params, line, jsdoc);
|
|
22744
|
-
});
|
|
22745
|
-
});
|
|
22746
|
-
|
|
22747
|
-
// check all the statements
|
|
22748
|
-
sourceFile.getStatements().forEach(stmt => {
|
|
22749
|
-
|
|
22750
|
-
// only binary expressions
|
|
22751
|
-
if (!stmt.isKind(
|
|
22752
|
-
const expr = stmt.getExpression();
|
|
22753
|
-
if (!expr.isKind(
|
|
22754
|
-
|
|
22755
|
-
// get the two parts of the statement
|
|
22756
|
-
const left = expr.getLeft().getText();
|
|
22757
|
-
const right = expr.getRight();
|
|
22758
|
-
|
|
22759
|
-
// check for protype
|
|
22760
|
-
if (left.includes('.prototype.') && right.isKind(
|
|
22761
|
-
|
|
22762
|
-
// get the name and check
|
|
22763
|
-
const parts = left.split('.');
|
|
22764
|
-
const name = parts[parts.length - 1];
|
|
22765
|
-
if (!isHandler(name)) return;
|
|
22766
|
-
|
|
22767
|
-
// extract
|
|
22768
|
-
const line = expr.getStartLineNumber();
|
|
22769
|
-
const params = right.getParameters().flatMap(p => describeParam(p));
|
|
22770
|
-
const jsdoc = getFullJsDoc(expr);
|
|
22771
|
-
|
|
22772
|
-
// and save in nodemap
|
|
22773
|
-
collect(name, params, line, jsdoc);
|
|
22774
|
-
}
|
|
22775
|
-
|
|
22732
|
+
});
|
|
22733
|
+
|
|
22734
|
+
// check all the classes in the file
|
|
22735
|
+
sourceFile.getClasses().forEach(cls => {
|
|
22736
|
+
|
|
22737
|
+
// get the name of the node
|
|
22738
|
+
const nodeName = cls.getName?.() || topLevelClass;
|
|
22739
|
+
|
|
22740
|
+
// check all the methods
|
|
22741
|
+
cls.getMethods().forEach(method => {
|
|
22742
|
+
|
|
22743
|
+
// check the name
|
|
22744
|
+
const name = method.getName();
|
|
22745
|
+
if (!isHandler(name)) return;
|
|
22746
|
+
|
|
22747
|
+
// extract
|
|
22748
|
+
const line = method.getNameNode()?.getStartLineNumber() ?? method.getStartLineNumber();
|
|
22749
|
+
const jsdoc = getFullJsDoc(method);
|
|
22750
|
+
const docTags = getParamDocs(method);
|
|
22751
|
+
const params = method.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22752
|
+
|
|
22753
|
+
// and collect
|
|
22754
|
+
collect(name, params, line, jsdoc, nodeName);
|
|
22755
|
+
});
|
|
22756
|
+
});
|
|
22757
|
+
|
|
22758
|
+
// check all the statements
|
|
22759
|
+
sourceFile.getStatements().forEach(stmt => {
|
|
22760
|
+
|
|
22761
|
+
// only binary expressions
|
|
22762
|
+
if (!stmt.isKind(SyntaxKind.ExpressionStatement)) return;
|
|
22763
|
+
const expr = stmt.getExpression();
|
|
22764
|
+
if (!expr.isKind(SyntaxKind.BinaryExpression)) return;
|
|
22765
|
+
|
|
22766
|
+
// get the two parts of the statement
|
|
22767
|
+
const left = expr.getLeft().getText();
|
|
22768
|
+
const right = expr.getRight();
|
|
22769
|
+
|
|
22770
|
+
// check for protype
|
|
22771
|
+
if (left.includes('.prototype.') && right.isKind(SyntaxKind.FunctionExpression)) {
|
|
22772
|
+
|
|
22773
|
+
// get the name and check
|
|
22774
|
+
const parts = left.split('.');
|
|
22775
|
+
const name = parts[parts.length - 1];
|
|
22776
|
+
if (!isHandler(name)) return;
|
|
22777
|
+
|
|
22778
|
+
// extract
|
|
22779
|
+
const line = expr.getStartLineNumber();
|
|
22780
|
+
const params = right.getParameters().flatMap(p => describeParam(p));
|
|
22781
|
+
const jsdoc = getFullJsDoc(expr);
|
|
22782
|
+
|
|
22783
|
+
// and save in nodemap
|
|
22784
|
+
collect(name, params, line, jsdoc);
|
|
22785
|
+
}
|
|
22786
|
+
|
|
22776
22787
|
const objectLiteral = resolveObjectLiteralExpression(right);
|
|
22777
22788
|
if (left.endsWith('.prototype') && objectLiteral) {
|
|
22778
22789
|
collectObjectLiteralHandlers(objectLiteral);
|
|
22779
22790
|
}
|
|
22780
|
-
});
|
|
22781
|
-
}
|
|
22782
|
-
|
|
22783
|
-
|
|
22784
|
-
function collectHandlersFromFunctionReturns(fn) {
|
|
22785
|
-
|
|
22786
|
-
// Look for factory-style returns that expose handlers via object literals.
|
|
22787
|
-
fn.getDescendantsOfKind(
|
|
22788
|
-
const expr = ret.getExpression();
|
|
22791
|
+
});
|
|
22792
|
+
}
|
|
22793
|
+
|
|
22794
|
+
|
|
22795
|
+
function collectHandlersFromFunctionReturns(fn) {
|
|
22796
|
+
|
|
22797
|
+
// Look for factory-style returns that expose handlers via object literals.
|
|
22798
|
+
fn.getDescendantsOfKind(SyntaxKind.ReturnStatement).forEach(ret => {
|
|
22799
|
+
const expr = ret.getExpression();
|
|
22789
22800
|
const objectLiteral = resolveObjectLiteralExpression(expr);
|
|
22790
22801
|
if (!objectLiteral) return;
|
|
22791
22802
|
|
|
@@ -22802,16 +22813,16 @@ function resolveObjectLiteralExpression(expression) {
|
|
|
22802
22813
|
return expression;
|
|
22803
22814
|
}
|
|
22804
22815
|
|
|
22805
|
-
if (expression.isKind?.(
|
|
22816
|
+
if (expression.isKind?.(SyntaxKind.ParenthesizedExpression)) {
|
|
22806
22817
|
return resolveObjectLiteralExpression(expression.getExpression());
|
|
22807
22818
|
}
|
|
22808
22819
|
|
|
22809
|
-
if (expression.isKind?.(
|
|
22810
|
-
|| expression.isKind?.(
|
|
22811
|
-
|| expression.isKind?.(
|
|
22812
|
-
|| expression.isKind?.(
|
|
22820
|
+
if (expression.isKind?.(SyntaxKind.AsExpression)
|
|
22821
|
+
|| expression.isKind?.(SyntaxKind.TypeAssertionExpression)
|
|
22822
|
+
|| expression.isKind?.(SyntaxKind.SatisfiesExpression)
|
|
22823
|
+
|| expression.isKind?.(SyntaxKind.NonNullExpression)
|
|
22813
22824
|
) {
|
|
22814
|
-
return resolveObjectLiteralExpression(expression.getExpression());
|
|
22825
|
+
return resolveObjectLiteralExpression(expression.getExpression?.());
|
|
22815
22826
|
}
|
|
22816
22827
|
|
|
22817
22828
|
return null;
|
|
@@ -22826,227 +22837,385 @@ function collectObjectLiteralHandlers(objectLiteral) {
|
|
|
22826
22837
|
|
|
22827
22838
|
const propName = prop.getName?.();
|
|
22828
22839
|
if (!isHandler(propName)) return;
|
|
22829
|
-
|
|
22830
|
-
let params = [];
|
|
22831
|
-
if (prop.getKind() ===
|
|
22832
|
-
const docTags = getParamDocs(prop);
|
|
22833
|
-
params = prop.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22834
|
-
} else if (prop.getKind() ===
|
|
22835
|
-
const fn = prop.getInitializerIfKind(
|
|
22836
|
-
if (fn) {
|
|
22837
|
-
const docTags = getParamDocs(fn);
|
|
22838
|
-
params = fn.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22839
|
-
}
|
|
22840
|
-
}
|
|
22841
|
-
|
|
22842
|
-
const jsdoc = getFullJsDoc(prop);
|
|
22843
|
-
const line = prop.getStartLineNumber();
|
|
22844
|
-
|
|
22845
|
-
collect(propName, params, line, jsdoc);
|
|
22846
|
-
});
|
|
22847
|
-
}
|
|
22848
|
-
|
|
22849
|
-
function updateNodeFromJsdoc(jsdoc = {}) {
|
|
22850
|
-
|
|
22851
|
-
const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
|
|
22852
|
-
if (nodeTag) currentNode = nodeTag.trim();
|
|
22853
|
-
}
|
|
22854
|
-
|
|
22855
|
-
function collect(rawName, params, line, jsdoc = {}) {
|
|
22856
|
-
|
|
22857
|
-
//if (!isHandler(rawName)) return;
|
|
22858
|
-
const cleanHandler = rawName.replace(/^['"]|['"]$/g, '');
|
|
22859
|
-
|
|
22860
|
-
let pin = null;
|
|
22861
|
-
let node = null;
|
|
22862
|
-
|
|
22863
|
-
const pinTag = jsdoc.tags?.find(t => t.tagName === 'pin')?.comment;
|
|
22864
|
-
const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
|
|
22865
|
-
const mcpTag = jsdoc.tags?.find(t => t.tagName === 'mcp')?.comment ?? null;
|
|
22866
|
-
|
|
22867
|
-
// if there is a node tag, change the name of the current node
|
|
22868
|
-
if (nodeTag) currentNode = nodeTag.trim();
|
|
22869
|
-
|
|
22870
|
-
// check the pin tag to get a pin name and node name
|
|
22871
|
-
if (pinTag) {
|
|
22872
|
-
|
|
22873
|
-
if (pinTag.includes('@')) {
|
|
22874
|
-
const [p, n] = pinTag.split('@').map(s => s.trim());
|
|
22875
|
-
pin = p;
|
|
22876
|
-
node = n;
|
|
22877
|
-
}
|
|
22878
|
-
else pin = pinTag.trim();
|
|
22879
|
-
|
|
22880
|
-
// Use the current context when the pin tag does not specify a node.
|
|
22881
|
-
if (!node) node = currentNode || topLevelClass || null;
|
|
22882
|
-
}
|
|
22883
|
-
|
|
22884
|
-
// check the pin tag to get a pin name and node name
|
|
22885
|
-
// if (pinTag && pinTag.includes('@')) {
|
|
22886
|
-
// const [p, n] = pinTag.split('@').map(s => s.trim());
|
|
22887
|
-
// pin = p;
|
|
22888
|
-
// node = n;
|
|
22889
|
-
// }
|
|
22890
|
-
else {
|
|
22891
|
-
|
|
22892
|
-
// no explicit tag - try these...
|
|
22893
|
-
node = currentNode || topLevelClass || null;
|
|
22894
|
-
|
|
22895
|
-
// deduct the pin name from the handler name
|
|
22896
|
-
if (cleanHandler.startsWith('on')) {
|
|
22897
|
-
pin = cleanHandler.slice(2).replace(/([A-Z])/g, ' $1').trim().toLowerCase();
|
|
22898
|
-
} else if (cleanHandler.startsWith('->')) {
|
|
22899
|
-
pin = cleanHandler.slice(2).trim();
|
|
22900
|
-
}
|
|
22901
|
-
}
|
|
22902
|
-
|
|
22903
|
-
// if there is no node we just don't save the data
|
|
22904
|
-
if (!node) return
|
|
22905
|
-
|
|
22906
|
-
// check if we have an entry for the node
|
|
22907
|
-
if (!nodeMap.has(node)) nodeMap.set(node, { handles: [], transmits: [] });
|
|
22908
|
-
|
|
22909
|
-
// The handler data to save
|
|
22910
|
-
const handlerData = {
|
|
22911
|
-
pin,
|
|
22912
|
-
handler: cleanHandler,
|
|
22913
|
-
file: filePath,
|
|
22914
|
-
line,
|
|
22915
|
-
summary: jsdoc.summary || '',
|
|
22916
|
-
returns: jsdoc.returns || '',
|
|
22917
|
-
examples: jsdoc.examples || [],
|
|
22918
|
-
params
|
|
22919
|
-
};
|
|
22920
|
-
|
|
22921
|
-
// extract the data from an mcp tag if present
|
|
22922
|
-
if (mcpTag !== null) {
|
|
22923
|
-
handlerData.mcp = true;
|
|
22924
|
-
if (mcpTag.includes('name:') || mcpTag.includes('description:')) {
|
|
22925
|
-
const nameMatch = /name:\s*\"?([^\"]+)\"?/.exec(mcpTag);
|
|
22926
|
-
const descMatch = /description:\s*\"?([^\"]+)\"?/.exec(mcpTag);
|
|
22927
|
-
if (nameMatch) handlerData.mcpName = nameMatch[1];
|
|
22928
|
-
if (descMatch) handlerData.mcpDescription = descMatch[1];
|
|
22929
|
-
}
|
|
22930
|
-
}
|
|
22931
|
-
|
|
22932
|
-
// and put it in the nodemap
|
|
22933
|
-
nodeMap.get(node).handles.push(handlerData);
|
|
22934
|
-
}
|
|
22935
|
-
// determines if a name is the name for a handler
|
|
22936
|
-
function isHandler(name) {
|
|
22937
|
-
// must be a string
|
|
22938
|
-
if (typeof name !== 'string') return false;
|
|
22939
|
-
|
|
22940
|
-
// get rid of " and '
|
|
22941
|
-
const clean = name.replace(/^['"]|['"]$/g, '');
|
|
22942
|
-
|
|
22943
|
-
// check that it starts with the right symbols...
|
|
22944
|
-
return clean.startsWith('on') || clean.startsWith('->');
|
|
22945
|
-
}
|
|
22946
|
-
|
|
22947
|
-
// Get the parameter description from the function or method
|
|
22948
|
-
function getParamDocs(fnOrMethod) {
|
|
22949
|
-
|
|
22950
|
-
// extract
|
|
22951
|
-
const docs = fnOrMethod.getJsDocs?.() ?? [];
|
|
22952
|
-
const tags = docs.flatMap(d => d.getTags());
|
|
22953
|
-
const paramDocs = {};
|
|
22954
|
-
|
|
22955
|
-
// check the tags
|
|
22956
|
-
for (const tag of tags) {
|
|
22957
|
-
if (tag.getTagName() === 'param') {
|
|
22958
|
-
const name = tag.getNameNode()?.getText?.() || tag.getName();
|
|
22959
|
-
const desc = tag.getComment() ?? '';
|
|
22960
|
-
const type = tag.getTypeNode?.()?.getText?.() || tag.getTypeExpression()?.getTypeNode()?.getText();
|
|
22961
|
-
paramDocs[name] = { description: desc, type };
|
|
22962
|
-
}
|
|
22963
|
-
}
|
|
22964
|
-
return paramDocs;
|
|
22965
|
-
}
|
|
22966
|
-
|
|
22967
|
-
// Get the jsdoc
|
|
22968
|
-
function getFullJsDoc(node) {
|
|
22969
|
-
|
|
22970
|
-
const docs = node.getJsDocs?.() ?? [];
|
|
22971
|
-
const summary = docs.map(d => d.getComment()).filter(Boolean).join('\n');
|
|
22972
|
-
const tags = docs.flatMap(d => d.getTags()).map(t => ({
|
|
22973
|
-
tagName: t.getTagName(),
|
|
22974
|
-
comment: t.getComment() || ''
|
|
22975
|
-
}));
|
|
22976
|
-
|
|
22977
|
-
const returns = tags.find(t => t.tagName === 'returns')?.comment || '';
|
|
22978
|
-
const examples = tags.filter(t => t.tagName === 'example').map(t => t.comment);
|
|
22979
|
-
|
|
22980
|
-
return { summary, returns, examples, tags };
|
|
22981
|
-
}
|
|
22982
|
-
|
|
22983
|
-
// make a parameter description
|
|
22984
|
-
function describeParam(p, docTags = {}) {
|
|
22985
|
-
|
|
22986
|
-
const nameNode = p.getNameNode();
|
|
22987
|
-
|
|
22988
|
-
// const func = p.getParent();
|
|
22989
|
-
// const funcName = func.getName?.() || '<anonymous>';
|
|
22990
|
-
// console.log(funcName)
|
|
22991
|
-
|
|
22992
|
-
if (nameNode.getKindName() === 'ObjectBindingPattern') {
|
|
22993
|
-
|
|
22994
|
-
const objType = p.getType();
|
|
22995
|
-
const properties = objType.getProperties();
|
|
22996
|
-
const isTSFallback = objType.getText() === 'any' || objType.getText() === 'string' || properties.length === 0;
|
|
22997
|
-
|
|
22998
|
-
return nameNode.getElements().map(el => {
|
|
22999
|
-
|
|
23000
|
-
const subName = el.getName();
|
|
23001
|
-
const doc = docTags[subName] ?? {};
|
|
23002
|
-
let tsType = null;
|
|
23003
|
-
|
|
23004
|
-
if (!isTSFallback) {
|
|
23005
|
-
const symbol = objType.getProperty(subName);
|
|
23006
|
-
if (symbol) {
|
|
23007
|
-
const resolvedType = symbol.getTypeAtLocation(el);
|
|
23008
|
-
const text = resolvedType.getText();
|
|
23009
|
-
if (text && text !== 'any') {
|
|
23010
|
-
tsType = text;
|
|
23011
|
-
}
|
|
23012
|
-
}
|
|
23013
|
-
}
|
|
23014
|
-
|
|
23015
|
-
const type = tsType || doc.type || 'string';
|
|
23016
|
-
const description = doc.description || '';
|
|
23017
|
-
return { name: subName, type, description };
|
|
23018
|
-
});
|
|
23019
|
-
}
|
|
23020
|
-
|
|
23021
|
-
const name = p.getName();
|
|
23022
|
-
const doc = docTags[name] ?? {};
|
|
23023
|
-
const tsType = p.getType().getText();
|
|
23024
|
-
|
|
23025
|
-
// const isTSFallback = tsType === 'any' || tsType === 'string';
|
|
23026
|
-
// if (isTSFallback && !doc.type) {
|
|
23027
|
-
// console.warn(`⚠️ No type info for param "${name}" in function "${funcName}"`);
|
|
23028
|
-
// }
|
|
23029
|
-
|
|
23030
|
-
return {
|
|
23031
|
-
name,
|
|
23032
|
-
type: doc.type || tsType || 'string',
|
|
23033
|
-
description: doc.description || '',
|
|
23034
|
-
};
|
|
22840
|
+
|
|
22841
|
+
let params = [];
|
|
22842
|
+
if (prop.getKind() === SyntaxKind.MethodDeclaration) {
|
|
22843
|
+
const docTags = getParamDocs(prop);
|
|
22844
|
+
params = prop.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22845
|
+
} else if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
22846
|
+
const fn = prop.getInitializerIfKind(SyntaxKind.FunctionExpression) || prop.getInitializerIfKind(SyntaxKind.ArrowFunction);
|
|
22847
|
+
if (fn) {
|
|
22848
|
+
const docTags = getParamDocs(fn);
|
|
22849
|
+
params = fn.getParameters().flatMap(p => describeParam(p, docTags));
|
|
22850
|
+
}
|
|
22851
|
+
}
|
|
22852
|
+
|
|
22853
|
+
const jsdoc = getFullJsDoc(prop);
|
|
22854
|
+
const line = prop.getStartLineNumber();
|
|
22855
|
+
|
|
22856
|
+
collect(propName, params, line, jsdoc);
|
|
22857
|
+
});
|
|
23035
22858
|
}
|
|
23036
22859
|
|
|
23037
|
-
|
|
23038
|
-
|
|
23039
|
-
|
|
23040
|
-
|
|
22860
|
+
function updateNodeFromJsdoc(jsdoc = {}) {
|
|
22861
|
+
|
|
22862
|
+
const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
|
|
22863
|
+
if (nodeTag) {
|
|
22864
|
+
applyNodeTag(nodeTag);
|
|
22865
|
+
}
|
|
22866
|
+
}
|
|
22867
|
+
|
|
22868
|
+
function collect(rawName, params, line, jsdoc = {}, defaultNode = null) {
|
|
22869
|
+
|
|
22870
|
+
const cleanHandler = rawName.replace(/^['"]|['"]$/g, '');
|
|
22871
|
+
|
|
22872
|
+
let pin = null;
|
|
22873
|
+
let node = defaultNode || null;
|
|
22874
|
+
|
|
22875
|
+
const pinTag = jsdoc.tags?.find(t => t.tagName === 'pin')?.comment;
|
|
22876
|
+
const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
|
|
22877
|
+
const mcpTag = jsdoc.tags?.find(t => t.tagName === 'mcp')?.comment ?? null;
|
|
22878
|
+
|
|
22879
|
+
if (nodeTag) {
|
|
22880
|
+
const context = applyNodeTag(nodeTag);
|
|
22881
|
+
if (context?.nodeName) {
|
|
22882
|
+
node = context.nodeName;
|
|
22883
|
+
}
|
|
22884
|
+
}
|
|
22885
|
+
|
|
22886
|
+
if (pinTag) {
|
|
22887
|
+
|
|
22888
|
+
if (pinTag.includes('@')) {
|
|
22889
|
+
const [p, n] = pinTag.split('@').map(s => s.trim());
|
|
22890
|
+
pin = p;
|
|
22891
|
+
node = n;
|
|
22892
|
+
}
|
|
22893
|
+
else pin = pinTag.trim();
|
|
22894
|
+
}
|
|
22895
|
+
else if (!node) {
|
|
22896
|
+
|
|
22897
|
+
// no explicit tag - try these...
|
|
22898
|
+
node = currentNode || topLevelClass || null;
|
|
22899
|
+
|
|
22900
|
+
// deduct the pin name from the handler name
|
|
22901
|
+
if (cleanHandler.startsWith('on')) {
|
|
22902
|
+
pin = cleanHandler.slice(2).replace(/([A-Z])/g, ' $1').trim().toLowerCase();
|
|
22903
|
+
} else if (cleanHandler.startsWith('->')) {
|
|
22904
|
+
pin = cleanHandler.slice(2).trim();
|
|
22905
|
+
}
|
|
22906
|
+
}
|
|
22907
|
+
|
|
22908
|
+
// if there is no node we just don't save the data
|
|
22909
|
+
if (!node) return;
|
|
22910
|
+
|
|
22911
|
+
// check if we have an entry for the node
|
|
22912
|
+
if (!nodeMap.has(node)) nodeMap.set(node, { handles: [], transmits: [] });
|
|
22913
|
+
|
|
22914
|
+
// The handler data to save
|
|
22915
|
+
const handlerData = {
|
|
22916
|
+
pin,
|
|
22917
|
+
handler: cleanHandler,
|
|
22918
|
+
file: filePath,
|
|
22919
|
+
line,
|
|
22920
|
+
summary: jsdoc.summary || '',
|
|
22921
|
+
returns: jsdoc.returns || '',
|
|
22922
|
+
examples: jsdoc.examples || [],
|
|
22923
|
+
params
|
|
22924
|
+
};
|
|
22925
|
+
|
|
22926
|
+
// extract the data from an mcp tag if present
|
|
22927
|
+
if (mcpTag !== null) {
|
|
22928
|
+
handlerData.mcp = true;
|
|
22929
|
+
if (mcpTag.includes('name:') || mcpTag.includes('description:')) {
|
|
22930
|
+
const nameMatch = /name:\s*\"?([^\"]+)\"?/.exec(mcpTag);
|
|
22931
|
+
const descMatch = /description:\s*\"?([^\"]+)\"?/.exec(mcpTag);
|
|
22932
|
+
if (nameMatch) handlerData.mcpName = nameMatch[1];
|
|
22933
|
+
if (descMatch) handlerData.mcpDescription = descMatch[1];
|
|
22934
|
+
}
|
|
22935
|
+
}
|
|
22936
|
+
|
|
22937
|
+
// and put it in the nodemap
|
|
22938
|
+
nodeMap.get(node).handles.push(handlerData);
|
|
22939
|
+
}
|
|
22940
|
+
|
|
22941
|
+
// determines if a name is the name for a handler
|
|
22942
|
+
function isHandler(name) {
|
|
22943
|
+
if (typeof name !== 'string') return false;
|
|
22944
|
+
|
|
22945
|
+
const clean = name.replace(/^['"]|['"]$/g, '');
|
|
22946
|
+
return clean.startsWith('on') || clean.startsWith('->');
|
|
22947
|
+
}
|
|
22948
|
+
|
|
22949
|
+
// Get the parameter description from the function or method
|
|
22950
|
+
function getParamDocs(fnOrMethod) {
|
|
22951
|
+
|
|
22952
|
+
const docs = fnOrMethod.getJsDocs?.() ?? [];
|
|
22953
|
+
const tags = docs.flatMap(d => d.getTags());
|
|
22954
|
+
const paramDocs = {};
|
|
22955
|
+
|
|
22956
|
+
for (const tag of tags) {
|
|
22957
|
+
if (tag.getTagName() === 'param') {
|
|
22958
|
+
const name = tag.getNameNode()?.getText?.() || tag.getName();
|
|
22959
|
+
const desc = tag.getComment() ?? '';
|
|
22960
|
+
const type = tag.getTypeNode?.()?.getText?.() || tag.getTypeExpression()?.getTypeNode()?.getText();
|
|
22961
|
+
paramDocs[name] = { description: desc, type };
|
|
22962
|
+
}
|
|
22963
|
+
}
|
|
22964
|
+
return paramDocs;
|
|
22965
|
+
}
|
|
22966
|
+
|
|
22967
|
+
// Get the jsdoc
|
|
22968
|
+
function getFullJsDoc(node) {
|
|
22969
|
+
|
|
22970
|
+
const docs = node.getJsDocs?.() ?? [];
|
|
22971
|
+
const summary = docs.map(d => d.getComment()).filter(Boolean).join('\n');
|
|
22972
|
+
const tags = docs.flatMap(d => d.getTags()).map(t => ({
|
|
22973
|
+
tagName: t.getTagName(),
|
|
22974
|
+
comment: t.getComment() || ''
|
|
22975
|
+
}));
|
|
22976
|
+
|
|
22977
|
+
const returns = tags.find(t => t.tagName === 'returns')?.comment || '';
|
|
22978
|
+
const examples = tags.filter(t => t.tagName === 'example').map(t => t.comment);
|
|
22979
|
+
|
|
22980
|
+
return { summary, returns, examples, tags };
|
|
22981
|
+
}
|
|
22982
|
+
|
|
22983
|
+
function hasDocMetadata(jsdoc) {
|
|
22984
|
+
if (!jsdoc) return false;
|
|
22985
|
+
if (jsdoc.summary && jsdoc.summary.trim()) return true;
|
|
22986
|
+
return Array.isArray(jsdoc.tags) && jsdoc.tags.length > 0;
|
|
22987
|
+
}
|
|
22988
|
+
|
|
22989
|
+
// make a parameter description
|
|
22990
|
+
function describeParam(p, docTags = {}) {
|
|
22991
|
+
|
|
22992
|
+
const nameNode = p.getNameNode();
|
|
22993
|
+
|
|
22994
|
+
if (nameNode.getKindName() === 'ObjectBindingPattern') {
|
|
22995
|
+
|
|
22996
|
+
const objType = p.getType();
|
|
22997
|
+
const properties = objType.getProperties();
|
|
22998
|
+
const isTSFallback = objType.getText() === 'any' || objType.getText() === 'string' || properties.length === 0;
|
|
22999
|
+
|
|
23000
|
+
return nameNode.getElements().map(el => {
|
|
23001
|
+
|
|
23002
|
+
const subName = el.getName();
|
|
23003
|
+
const doc = docTags[subName] ?? {};
|
|
23004
|
+
let tsType = null;
|
|
23005
|
+
|
|
23006
|
+
if (!isTSFallback) {
|
|
23007
|
+
const symbol = objType.getProperty(subName);
|
|
23008
|
+
if (symbol) {
|
|
23009
|
+
const resolvedType = symbol.getTypeAtLocation(el);
|
|
23010
|
+
const text = resolvedType.getText();
|
|
23011
|
+
if (text && text !== 'any') {
|
|
23012
|
+
tsType = text;
|
|
23013
|
+
}
|
|
23014
|
+
}
|
|
23015
|
+
}
|
|
23016
|
+
|
|
23017
|
+
const type = tsType || doc.type || 'string';
|
|
23018
|
+
const description = doc.description || '';
|
|
23019
|
+
return { name: subName, type, description };
|
|
23020
|
+
});
|
|
23021
|
+
}
|
|
23022
|
+
|
|
23023
|
+
const name = p.getName();
|
|
23024
|
+
const doc = docTags[name] ?? {};
|
|
23025
|
+
const tsType = p.getType().getText();
|
|
23026
|
+
|
|
23027
|
+
return {
|
|
23028
|
+
name,
|
|
23029
|
+
type: doc.type || tsType || 'string',
|
|
23030
|
+
description: doc.description || '',
|
|
23031
|
+
};
|
|
23032
|
+
}
|
|
23033
|
+
|
|
23034
|
+
function applyNodeTag(rawTag) {
|
|
23035
|
+
const { nodeName, aliases } = parseNodeTag(rawTag);
|
|
23036
|
+
if (!nodeName) return null;
|
|
23037
|
+
registerNodeContext(nodeName, aliases);
|
|
23038
|
+
return { nodeName, aliases };
|
|
23039
|
+
}
|
|
23040
|
+
|
|
23041
|
+
function registerNodeContext(nodeName, aliases = []) {
|
|
23042
|
+
const normalizedNode = nodeName.trim();
|
|
23043
|
+
if (!normalizedNode) return;
|
|
23044
|
+
currentNode = normalizedNode;
|
|
23045
|
+
|
|
23046
|
+
aliases.forEach(alias => registerAlias(alias, normalizedNode));
|
|
23047
|
+
|
|
23048
|
+
const derivedAlias = deriveIdentifierFromNodeName(normalizedNode);
|
|
23049
|
+
if (derivedAlias) registerAlias(derivedAlias, normalizedNode);
|
|
23050
|
+
}
|
|
23051
|
+
|
|
23052
|
+
function registerAlias(alias, nodeName) {
|
|
23053
|
+
const cleaned = alias?.trim();
|
|
23054
|
+
if (!cleaned) return;
|
|
23055
|
+
if (!isValidIdentifier(cleaned)) return;
|
|
23056
|
+
if (!nodeAliases.has(cleaned)) {
|
|
23057
|
+
nodeAliases.set(cleaned, nodeName);
|
|
23058
|
+
}
|
|
23059
|
+
}
|
|
23060
|
+
|
|
23061
|
+
function parseNodeTag(rawTag) {
|
|
23062
|
+
if (!rawTag || typeof rawTag !== 'string') return { nodeName: null, aliases: [] };
|
|
23063
|
+
|
|
23064
|
+
let text = rawTag.trim();
|
|
23065
|
+
if (!text) return { nodeName: null, aliases: [] };
|
|
23066
|
+
|
|
23067
|
+
let nodeName = text;
|
|
23068
|
+
let aliasChunk = '';
|
|
23069
|
+
|
|
23070
|
+
const explicitMatch = text.match(/^(.*?)(?:\s+(?:@|as|=>|->|\||:)\s+)(.+)$/i);
|
|
23071
|
+
if (explicitMatch) {
|
|
23072
|
+
nodeName = explicitMatch[1].trim();
|
|
23073
|
+
aliasChunk = explicitMatch[2].trim();
|
|
23074
|
+
} else {
|
|
23075
|
+
const quotedMatch = text.match(/^["']([^"']+)["']\s+(.+)$/);
|
|
23076
|
+
if (quotedMatch) {
|
|
23077
|
+
nodeName = quotedMatch[1].trim();
|
|
23078
|
+
aliasChunk = quotedMatch[2].trim();
|
|
23079
|
+
}
|
|
23080
|
+
}
|
|
23081
|
+
|
|
23082
|
+
if (!aliasChunk) {
|
|
23083
|
+
const parts = text.split(/\s+/);
|
|
23084
|
+
if (parts.length > 1) {
|
|
23085
|
+
const candidateAlias = parts[parts.length - 1];
|
|
23086
|
+
const candidateNode = parts.slice(0, -1).join(' ');
|
|
23087
|
+
if (isValidIdentifier(candidateAlias) && isKnownIdentifier(candidateAlias)) {
|
|
23088
|
+
aliasChunk = candidateAlias;
|
|
23089
|
+
nodeName = candidateNode.trim();
|
|
23090
|
+
}
|
|
23091
|
+
}
|
|
23092
|
+
}
|
|
23093
|
+
|
|
23094
|
+
const aliases = aliasChunk
|
|
23095
|
+
? aliasChunk.split(/[,\s]+/).map(a => a.trim()).filter(Boolean)
|
|
23096
|
+
: [];
|
|
23097
|
+
|
|
23098
|
+
return { nodeName, aliases };
|
|
23099
|
+
}
|
|
23100
|
+
|
|
23101
|
+
function deriveIdentifierFromNodeName(name) {
|
|
23102
|
+
const chunks = name.split(/[\s\-]+/).filter(Boolean);
|
|
23103
|
+
if (chunks.length === 0) return null;
|
|
23104
|
+
if (chunks.length === 1) {
|
|
23105
|
+
const single = chunks[0];
|
|
23106
|
+
return isValidIdentifier(single) ? single : null;
|
|
23107
|
+
}
|
|
23108
|
+
const [first, ...rest] = chunks;
|
|
23109
|
+
const camel = first.toLowerCase() + rest.map(capitalize).join('');
|
|
23110
|
+
return isValidIdentifier(camel) ? camel : null;
|
|
23111
|
+
}
|
|
23112
|
+
|
|
23113
|
+
function capitalize(word) {
|
|
23114
|
+
if (!word) return '';
|
|
23115
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
23116
|
+
}
|
|
23117
|
+
|
|
23118
|
+
function isValidIdentifier(value) {
|
|
23119
|
+
return /^[A-Za-z_$][\w$]*$/.test(value);
|
|
23120
|
+
}
|
|
23121
|
+
|
|
23122
|
+
function isKnownIdentifier(name) {
|
|
23123
|
+
return knownIdentifiers.has(name);
|
|
23124
|
+
}
|
|
23125
|
+
|
|
23126
|
+
function collectKnownIdentifiers(sourceFile) {
|
|
23127
|
+
const identifiers = new Set();
|
|
23128
|
+
|
|
23129
|
+
sourceFile.getVariableDeclarations().forEach(decl => {
|
|
23130
|
+
const name = decl.getName();
|
|
23131
|
+
if (typeof name === 'string' && isValidIdentifier(name)) {
|
|
23132
|
+
identifiers.add(name);
|
|
23133
|
+
}
|
|
23134
|
+
});
|
|
23135
|
+
|
|
23136
|
+
sourceFile.getFunctions().forEach(fn => {
|
|
23137
|
+
const name = fn.getName?.();
|
|
23138
|
+
if (name && isValidIdentifier(name)) identifiers.add(name);
|
|
23139
|
+
});
|
|
23140
|
+
|
|
23141
|
+
sourceFile.getClasses().forEach(cls => {
|
|
23142
|
+
const name = cls.getName?.();
|
|
23143
|
+
if (name && isValidIdentifier(name)) identifiers.add(name);
|
|
23144
|
+
});
|
|
23145
|
+
|
|
23146
|
+
if (typeof sourceFile.getInterfaces === 'function') {
|
|
23147
|
+
sourceFile.getInterfaces().forEach(iface => {
|
|
23148
|
+
const name = iface.getName?.();
|
|
23149
|
+
if (name && isValidIdentifier(name)) identifiers.add(name);
|
|
23150
|
+
});
|
|
23151
|
+
}
|
|
23152
|
+
|
|
23153
|
+
if (typeof sourceFile.getTypeAliases === 'function') {
|
|
23154
|
+
sourceFile.getTypeAliases().forEach(alias => {
|
|
23155
|
+
const name = alias.getName?.();
|
|
23156
|
+
if (name && isValidIdentifier(name)) identifiers.add(name);
|
|
23157
|
+
});
|
|
23158
|
+
}
|
|
23159
|
+
|
|
23160
|
+
if (typeof sourceFile.getEnums === 'function') {
|
|
23161
|
+
sourceFile.getEnums().forEach(enm => {
|
|
23162
|
+
const name = enm.getName?.();
|
|
23163
|
+
if (name && isValidIdentifier(name)) identifiers.add(name);
|
|
23164
|
+
});
|
|
23165
|
+
}
|
|
23166
|
+
|
|
23167
|
+
sourceFile.getImportDeclarations().forEach(decl => {
|
|
23168
|
+
const defaultImport = decl.getDefaultImport();
|
|
23169
|
+
if (defaultImport) {
|
|
23170
|
+
const name = defaultImport.getText();
|
|
23171
|
+
if (isValidIdentifier(name)) identifiers.add(name);
|
|
23172
|
+
}
|
|
23173
|
+
|
|
23174
|
+
const namespaceImport = decl.getNamespaceImport();
|
|
23175
|
+
if (namespaceImport) {
|
|
23176
|
+
const nsName = typeof namespaceImport.getText === 'function'
|
|
23177
|
+
? namespaceImport.getText()
|
|
23178
|
+
: namespaceImport.getName?.();
|
|
23179
|
+
if (nsName && isValidIdentifier(nsName)) identifiers.add(nsName);
|
|
23180
|
+
}
|
|
23181
|
+
|
|
23182
|
+
decl.getNamedImports().forEach(spec => {
|
|
23183
|
+
const alias = spec.getAliasNode()?.getText();
|
|
23184
|
+
if (alias && isValidIdentifier(alias)) {
|
|
23185
|
+
identifiers.add(alias);
|
|
23186
|
+
} else {
|
|
23187
|
+
const name = spec.getName();
|
|
23188
|
+
if (isValidIdentifier(name)) identifiers.add(name);
|
|
23189
|
+
}
|
|
23190
|
+
});
|
|
23191
|
+
});
|
|
23192
|
+
|
|
23193
|
+
return identifiers;
|
|
23194
|
+
}
|
|
23195
|
+
|
|
23196
|
+
/**
|
|
23197
|
+
* Finds tx.send or this.tx.send calls and maps them to their node context.
|
|
23198
|
+
*
|
|
23199
|
+
* @param {import('ts-morph').SourceFile} sourceFile - The source file being analyzed
|
|
23041
23200
|
* @param {string} filePath - The (relative) path of the source file
|
|
23042
23201
|
* @param {Map} nodeMap - Map from node name to metadata
|
|
23043
23202
|
* @param {string|null} currentNode - Explicitly set node name (takes priority)
|
|
23044
23203
|
*/
|
|
23045
|
-
function findTransmissions(sourceFile, filePath, nodeMap) {
|
|
23046
|
-
|
|
23047
|
-
//
|
|
23048
|
-
|
|
23049
|
-
|
|
23204
|
+
function findTransmissions(sourceFile, filePath, nodeMap) {
|
|
23205
|
+
|
|
23206
|
+
// Create a quick lookup from handler name + file to node name to attribute transmissions precisely.
|
|
23207
|
+
const handlerToNode = new Map();
|
|
23208
|
+
for (const [nodeName, meta] of nodeMap.entries()) {
|
|
23209
|
+
for (const handle of meta.handles) {
|
|
23210
|
+
if (!handle?.handler) continue;
|
|
23211
|
+
const key = createHandlerKey(handle.file ?? filePath, handle.handler);
|
|
23212
|
+
if (!handlerToNode.has(key)) handlerToNode.set(key, nodeName);
|
|
23213
|
+
}
|
|
23214
|
+
}
|
|
23215
|
+
|
|
23216
|
+
// Search all call expressions
|
|
23217
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(node => {
|
|
23218
|
+
const expr = node.getExpression();
|
|
23050
23219
|
|
|
23051
23220
|
// check
|
|
23052
23221
|
if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) return
|
|
@@ -23063,15 +23232,24 @@ function findTransmissions(sourceFile, filePath, nodeMap) {
|
|
|
23063
23232
|
const pin = args[0].getLiteralText();
|
|
23064
23233
|
|
|
23065
23234
|
// Try to infer the class context of the tx.send call
|
|
23066
|
-
const method = node.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
|
|
23067
|
-
const classDecl = method?.getFirstAncestorByKind(SyntaxKind.ClassDeclaration) ?? node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
|
|
23068
|
-
const className = classDecl?.getName?.();
|
|
23069
|
-
|
|
23070
|
-
|
|
23071
|
-
const
|
|
23072
|
-
|
|
23073
|
-
|
|
23074
|
-
|
|
23235
|
+
const method = node.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
|
|
23236
|
+
const classDecl = method?.getFirstAncestorByKind(SyntaxKind.ClassDeclaration) ?? node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
|
|
23237
|
+
const className = classDecl?.getName?.();
|
|
23238
|
+
const handlerName = getEnclosingHandlerName(node);
|
|
23239
|
+
const handlerKey = handlerName ? createHandlerKey(filePath, handlerName) : null;
|
|
23240
|
+
const aliasCandidate = getAliasCandidate(expr);
|
|
23241
|
+
const aliasNode = aliasCandidate ? nodeAliases.get(aliasCandidate) ?? null : null;
|
|
23242
|
+
|
|
23243
|
+
// Priority order: handler lookup > alias mapping > current context > class fallback
|
|
23244
|
+
const nodeName = (handlerKey ? handlerToNode.get(handlerKey) : null)
|
|
23245
|
+
|| aliasNode
|
|
23246
|
+
|| currentNode
|
|
23247
|
+
|| className
|
|
23248
|
+
|| topLevelClass
|
|
23249
|
+
|| null;
|
|
23250
|
+
|
|
23251
|
+
// check
|
|
23252
|
+
if (!nodeName) return
|
|
23075
23253
|
|
|
23076
23254
|
// check if there is an entry for the node or create it
|
|
23077
23255
|
nodeMap.has(nodeName) || nodeMap.set(nodeName, { handles: [], transmits: [] });
|
|
@@ -23082,7 +23260,70 @@ function findTransmissions(sourceFile, filePath, nodeMap) {
|
|
|
23082
23260
|
file: filePath,
|
|
23083
23261
|
line: node.getStartLineNumber()
|
|
23084
23262
|
});
|
|
23085
|
-
});
|
|
23263
|
+
});
|
|
23264
|
+
}
|
|
23265
|
+
|
|
23266
|
+
function getAliasCandidate(propertyAccess) {
|
|
23267
|
+
if (!propertyAccess || !propertyAccess.getExpression) return null;
|
|
23268
|
+
const target = propertyAccess.getExpression();
|
|
23269
|
+
const root = resolveRootIdentifier(target);
|
|
23270
|
+
if (!root) return null;
|
|
23271
|
+
if (root === 'tx' || root === 'this') return null;
|
|
23272
|
+
return root;
|
|
23273
|
+
}
|
|
23274
|
+
|
|
23275
|
+
function resolveRootIdentifier(expression) {
|
|
23276
|
+
if (!expression) return null;
|
|
23277
|
+
if (expression.isKind?.(SyntaxKind.Identifier)) {
|
|
23278
|
+
return expression.getText();
|
|
23279
|
+
}
|
|
23280
|
+
if (expression.isKind?.(SyntaxKind.ThisKeyword)) {
|
|
23281
|
+
return 'this';
|
|
23282
|
+
}
|
|
23283
|
+
if (expression.isKind?.(SyntaxKind.PropertyAccessExpression)) {
|
|
23284
|
+
return resolveRootIdentifier(expression.getExpression());
|
|
23285
|
+
}
|
|
23286
|
+
if (expression.isKind?.(SyntaxKind.ElementAccessExpression)) {
|
|
23287
|
+
return resolveRootIdentifier(expression.getExpression());
|
|
23288
|
+
}
|
|
23289
|
+
return null;
|
|
23290
|
+
}
|
|
23291
|
+
|
|
23292
|
+
function createHandlerKey(file, handler) {
|
|
23293
|
+
return `${file}::${handler}`;
|
|
23294
|
+
}
|
|
23295
|
+
|
|
23296
|
+
function getEnclosingHandlerName(callExpression) {
|
|
23297
|
+
const method = callExpression.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
|
|
23298
|
+
if (method?.getName?.()) return method.getName();
|
|
23299
|
+
|
|
23300
|
+
const funcDecl = callExpression.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration);
|
|
23301
|
+
if (funcDecl?.getName?.()) return funcDecl.getName();
|
|
23302
|
+
|
|
23303
|
+
const funcExpr = callExpression.getFirstAncestorByKind(SyntaxKind.FunctionExpression);
|
|
23304
|
+
if (funcExpr) {
|
|
23305
|
+
if (funcExpr.getName?.()) return funcExpr.getName();
|
|
23306
|
+
const prop = funcExpr.getFirstAncestorByKind(SyntaxKind.PropertyAssignment);
|
|
23307
|
+
if (prop?.getName?.()) return prop.getName();
|
|
23308
|
+
const variable = funcExpr.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
23309
|
+
if (variable) return variable.getName();
|
|
23310
|
+
}
|
|
23311
|
+
|
|
23312
|
+
const arrowFunc = callExpression.getFirstAncestorByKind(SyntaxKind.ArrowFunction);
|
|
23313
|
+
if (arrowFunc) {
|
|
23314
|
+
const prop = arrowFunc.getFirstAncestorByKind(SyntaxKind.PropertyAssignment);
|
|
23315
|
+
if (prop?.getName?.()) return prop.getName();
|
|
23316
|
+
const variable = arrowFunc.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
23317
|
+
if (variable) return variable.getName();
|
|
23318
|
+
}
|
|
23319
|
+
|
|
23320
|
+
const propAssignment = callExpression.getFirstAncestorByKind(SyntaxKind.PropertyAssignment);
|
|
23321
|
+
if (propAssignment?.getName?.()) return propAssignment.getName();
|
|
23322
|
+
|
|
23323
|
+
const varDecl = callExpression.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
23324
|
+
if (varDecl?.getName?.()) return varDecl.getName();
|
|
23325
|
+
|
|
23326
|
+
return null;
|
|
23086
23327
|
}
|
|
23087
23328
|
|
|
23088
23329
|
const SRC_DOC_VERSION = '0.2';
|
|
@@ -23109,7 +23350,7 @@ async function profile(argv = process.argv.slice(2)) {
|
|
|
23109
23350
|
? path.resolve(cli.outFile)
|
|
23110
23351
|
: (() => {
|
|
23111
23352
|
const { dir, name } = path.parse(absoluteModelPath);
|
|
23112
|
-
return path.join(dir, `${name}
|
|
23353
|
+
return path.join(dir, `${name}.prf.json`);
|
|
23113
23354
|
})();
|
|
23114
23355
|
|
|
23115
23356
|
if (cli.deltaFile) cli.deltaFile = path.resolve(cli.deltaFile);
|
|
@@ -23145,6 +23386,9 @@ async function profile(argv = process.argv.slice(2)) {
|
|
|
23145
23386
|
const generatedAt = new Date().toISOString();
|
|
23146
23387
|
for (const sourceFile of sourceFiles) {
|
|
23147
23388
|
|
|
23389
|
+
// display file scanned..
|
|
23390
|
+
// console.log(sourceFile.getFilePath())
|
|
23391
|
+
|
|
23148
23392
|
// A file reference is always relative to the model file
|
|
23149
23393
|
const filePath = path.relative(path.dirname(modelPath), sourceFile.getFilePath()).replace(/\\/g, '/');
|
|
23150
23394
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizualmodel/vmblu-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vmblu": "bin/vmblu.js"
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"vmblu": "vmblu",
|
|
26
26
|
"local": "node ./bin/vmblu.js profile ../browser/vmblu.vmblu",
|
|
27
|
-
"build
|
|
27
|
+
"build": "rollup -c ./commands/profile/rollup.config.js"
|
|
28
28
|
}
|
|
29
29
|
}
|