@allansantos-dev/smart-tool 0.9.0 → 0.9.1
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.
- package/CHANGELOG.md +35 -23
- package/NOTICE +32 -32
- package/README.md +117 -116
- package/atomic_io.py +94 -94
- package/bin/smart-tool.mjs +5 -1
- package/client_hooks.py +192 -192
- package/code_graph_java.mjs +49 -49
- package/code_graph_js.cjs +135 -135
- package/duplicates.py +313 -313
- package/embedding_cache.py +99 -99
- package/endpoint_sync.py +179 -179
- package/gateway.py +220 -220
- package/hook_decision.py +96 -96
- package/index_inventory.py +75 -75
- package/indexer.py +878 -878
- package/install.py +113 -11
- package/install_runtime.py +67 -33
- package/integration_metrics.py +82 -82
- package/local_embedder.py +172 -172
- package/model_defaults.py +70 -70
- package/package.json +35 -35
- package/project_insights_ui.py +38 -38
- package/project_monitor.py +204 -204
- package/project_store.py +220 -220
- package/projects_ui.py +146 -146
- package/requirements.txt +8 -8
- package/research_cache.py +171 -171
- package/research_metrics.py +110 -110
- package/router.py +582 -582
- package/secret_store.py +121 -121
- package/setup_ui.py +986 -986
- package/smart_tool_daemon.py +3604 -3604
- package/version.py +1 -1
- package/web_adapters/browser/fetch_camoufox.py +28 -0
- package/web_adapters/node/ddg-search.mjs +89 -89
- package/web_adapters/node/open-websearch-adapter.mjs +77 -77
- package/web_adapters/node/package.json +12 -12
- package/web_adapters/node/wikipedia-search.mjs +44 -44
- package/web_search_adapters.py +193 -193
- package/web_search_external.py +288 -288
- package/web_search_health.py +88 -88
package/code_graph_js.cjs
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
/* Análise estática de snapshots JS/TS. Não executa código nem lê arquivos do projeto. */
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const path = require('node:path').posix;
|
|
4
|
-
const ts = require('./web_adapters/node/node_modules/typescript');
|
|
5
|
-
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
6
|
-
const clean = name => path.normalize(name.replaceAll('\\', '/')).replace(/^\.\//, '');
|
|
7
|
-
const docs = new Map(input.files.map(f => ['/snapshot/' + clean(f.path), f]));
|
|
8
|
-
const knownFiles = new Set([...docs.keys(),...(input.paths||[]).map(p=>'/snapshot/'+clean(p))]);
|
|
9
|
-
const names = [...docs.keys()].filter(n => /\.(?:[cm]?[jt]s|[jt]sx)$/.test(n));
|
|
10
|
-
const options = {allowJs:true, checkJs:true, jsx:ts.JsxEmit.Preserve, target:ts.ScriptTarget.Latest,
|
|
11
|
-
module:ts.ModuleKind.ESNext, moduleResolution:ts.ModuleResolutionKind.Bundler,
|
|
12
|
-
noLib:true, noEmit:true, skipLibCheck:true, allowImportingTsExtensions:true};
|
|
13
|
-
let configPath = docs.has('/snapshot/tsconfig.json') ? '/snapshot/tsconfig.json' : '/snapshot/jsconfig.json';
|
|
14
|
-
const configSeen = new Set();
|
|
15
|
-
function readConfig(name, depth=0) {
|
|
16
|
-
if (depth>5 || configSeen.has(name) || !docs.has(name)) return {};
|
|
17
|
-
configSeen.add(name);
|
|
18
|
-
const raw = ts.parseConfigFileTextToJson(name, docs.get(name).text).config || {};
|
|
19
|
-
const base = typeof raw.extends==='string' && raw.extends.startsWith('.') ? readConfig(clean(path.join(path.dirname(name),raw.extends.endsWith('.json')?raw.extends:raw.extends+'.json')),depth+1) : {};
|
|
20
|
-
return {...base,...raw.compilerOptions};
|
|
21
|
-
}
|
|
22
|
-
const config=readConfig(configPath);
|
|
23
|
-
const aliases=config.paths||{},baseUrl=clean(path.join(path.dirname(configPath),config.baseUrl||'.'));
|
|
24
|
-
function candidate(base) {
|
|
25
|
-
const variants=[base,base.replace(/\.[cm]?jsx?$/,'.ts'),base.replace(/\.jsx?$/,'.tsx'),
|
|
26
|
-
...['.ts','.tsx','.js','.jsx','.mts','.cts','.mjs','.cjs'].map(e=>base+e),
|
|
27
|
-
...['.ts','.tsx','.js','.jsx'].map(e=>base+'/index'+e)];
|
|
28
|
-
return variants.find(name=>knownFiles.has(clean(name)));
|
|
29
|
-
}
|
|
30
|
-
function resolve(spec, from) {
|
|
31
|
-
if (spec.startsWith('.')) return candidate(clean(path.join(path.dirname(from),spec)));
|
|
32
|
-
for (const [pattern, targets] of Object.entries(aliases)) {
|
|
33
|
-
const [prefix,suffix='']=pattern.split('*');
|
|
34
|
-
if (pattern.includes('*') ? spec.startsWith(prefix)&&spec.endsWith(suffix) : spec===pattern) {
|
|
35
|
-
const middle=pattern.includes('*')?spec.slice(prefix.length,suffix? -suffix.length:undefined):'';
|
|
36
|
-
for (const target of Array.isArray(targets)?targets:[]) {
|
|
37
|
-
const found=candidate(clean(path.join(baseUrl,target.replace('*',middle))));
|
|
38
|
-
if(found)return found;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if(config.baseUrl)return candidate(clean(path.join(baseUrl,spec)));
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
const host={
|
|
46
|
-
getSourceFile:(name,version)=>docs.has(clean(name))?ts.createSourceFile(clean(name),docs.get(clean(name)).text,version,true):undefined,
|
|
47
|
-
getDefaultLibFileName:()=>'/lib.d.ts',writeFile:()=>{},getCurrentDirectory:()=>'/snapshot',
|
|
48
|
-
getDirectories:()=>[],fileExists:name=>docs.has(clean(name)),readFile:name=>docs.get(clean(name))?.text,
|
|
49
|
-
getCanonicalFileName:name=>clean(name),useCaseSensitiveFileNames:()=>true,getNewLine:()=> '\n',
|
|
50
|
-
resolveModuleNames:(modules,containing)=>modules.map(spec=>{const resolved=resolve(spec,containing);return resolved&&docs.has(resolved)&&/\.(?:[cm]?[jt]s|[jt]sx|json)$/.test(resolved)?{resolvedFileName:resolved,extension:ts.extensionFromPath(resolved),isExternalLibraryImport:false}:undefined;})
|
|
51
|
-
};
|
|
52
|
-
const program=ts.createProgram(names,options,host),checker=program.getTypeChecker();
|
|
53
|
-
const symbols=[],dependencies=[],calls=[],unresolved=[],diagnostics=[],declarations=new Map(),moduleNodes=new Map();
|
|
54
|
-
const maxSymbols=8000,maxEdges=20000;
|
|
55
|
-
function filePath(source){return source.fileName.replace('/snapshot/','');}
|
|
56
|
-
function line(source,pos){return source.getLineAndCharacterOfPosition(pos).line+1;}
|
|
57
|
-
function displayName(node) {
|
|
58
|
-
if(node.name)return node.name.getText().replace(/^['"]|['"]$/g,'');
|
|
59
|
-
if(ts.isConstructorDeclaration(node))return 'constructor';
|
|
60
|
-
if(node.parent && ts.isVariableDeclaration(node.parent))return node.parent.name.getText();
|
|
61
|
-
if(node.parent && ts.isPropertyAssignment(node.parent))return node.parent.name.getText();
|
|
62
|
-
if(node.parent && ts.isPropertyDeclaration(node.parent))return node.parent.name.getText();
|
|
63
|
-
if(node.parent && ts.isExportAssignment(node.parent))return 'default';
|
|
64
|
-
return 'callback';
|
|
65
|
-
}
|
|
66
|
-
function isFunction(node){return !!node.body && (ts.isFunctionDeclaration(node)||ts.isMethodDeclaration(node)||ts.isFunctionExpression(node)||ts.isArrowFunction(node)||ts.isConstructorDeclaration(node)||ts.isGetAccessor(node)||ts.isSetAccessor(node));}
|
|
67
|
-
function addSymbol(node,source,owner){
|
|
68
|
-
if(symbols.length>=maxSymbols)return null;
|
|
69
|
-
let name=displayName(node),parent=node.parent;
|
|
70
|
-
const classParent=parent&&ts.isPropertyDeclaration(parent)?parent.parent:parent;
|
|
71
|
-
const cls=classParent && (ts.isClassDeclaration(classParent)||ts.isClassExpression(classParent)) ? classParent.name?.getText() : null;
|
|
72
|
-
if(cls)name=cls+'.'+name;
|
|
73
|
-
const start=line(source,node.getStart(source)),id=filePath(source)+'::'+name+'@'+start+':'+node.pos;
|
|
74
|
-
const symbol={id,path:filePath(source),name,start_line:start,end_line:line(source,node.end),kind:cls?'method':'function',language:'typescript',parent:owner?.id||null};
|
|
75
|
-
symbols.push(symbol);declarations.set(node,symbol);return symbol;
|
|
76
|
-
}
|
|
77
|
-
const parsed=[];
|
|
78
|
-
for(const source of program.getSourceFiles()){
|
|
79
|
-
if(!docs.has(source.fileName))continue;
|
|
80
|
-
if(source.parseDiagnostics.length){diagnostics.push({path:filePath(source),reason:'Incomplete or invalid syntax in the snapshot.',line:line(source,source.parseDiagnostics[0].start||0)});continue;}
|
|
81
|
-
parsed.push(source);
|
|
82
|
-
function collect(node,owner){const current=isFunction(node)?addSymbol(node,source,owner):owner;ts.forEachChild(node,child=>collect(child,current));}
|
|
83
|
-
collect(source,null);
|
|
84
|
-
}
|
|
85
|
-
function moduleSymbol(source){if(!moduleNodes.has(source.fileName)){const s={id:filePath(source)+'::<module>',path:filePath(source),name:'<module>',start_line:1,end_line:1,kind:'module',language:'typescript'};moduleNodes.set(source.fileName,s);symbols.push(s);}return moduleNodes.get(source.fileName);}
|
|
86
|
-
function targetFor(node){
|
|
87
|
-
const signature=checker.getResolvedSignature(node);let target=declarations.get(signature?.declaration);
|
|
88
|
-
if(target)return target;
|
|
89
|
-
let symbol=checker.getSymbolAtLocation(node.expression);
|
|
90
|
-
if(symbol?.flags & ts.SymbolFlags.Alias)symbol=checker.getAliasedSymbol(symbol);
|
|
91
|
-
for(const declaration of symbol?.declarations||[]){
|
|
92
|
-
if(declarations.has(declaration))return declarations.get(declaration);
|
|
93
|
-
if(ts.isVariableDeclaration(declaration)&&declarations.has(declaration.initializer))return declarations.get(declaration.initializer);
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
for(const source of parsed){
|
|
98
|
-
function visit(node,owner){
|
|
99
|
-
if(isFunction(node) && !declarations.has(node))return;
|
|
100
|
-
const active=declarations.get(node)||owner;
|
|
101
|
-
if((ts.isImportDeclaration(node)||ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
102
|
-
const spec=node.moduleSpecifier.text,target=resolve(spec,source.fileName);
|
|
103
|
-
if(dependencies.length<maxEdges)dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:spec,line:line(source,node.getStart(source)),kind:node.importClause?.isTypeOnly||node.isTypeOnly?'type_import':'import',resolution:target?'resolved':spec.startsWith('.')?'unresolved':'external'});
|
|
104
|
-
}
|
|
105
|
-
if(ts.isCallExpression(node)||ts.isNewExpression(node)){
|
|
106
|
-
const expression=node.expression,at=line(source,node.getStart(source));
|
|
107
|
-
if(ts.isCallExpression(node) && (expression.kind===ts.SyntaxKind.ImportKeyword || ts.isIdentifier(expression)&&expression.text==='require') && node.arguments?.length && ts.isStringLiteral(node.arguments[0])){
|
|
108
|
-
const spec=node.arguments[0].text,target=resolve(spec,source.fileName);
|
|
109
|
-
if(dependencies.length<maxEdges)dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:spec,line:at,kind:'dynamic_import',resolution:target?'resolved':spec.startsWith('.')?'unresolved':'external'});
|
|
110
|
-
}else{
|
|
111
|
-
const target=targetFor(node);
|
|
112
|
-
if(target && calls.length<maxEdges)calls.push({source:(active||moduleSymbol(source)).id,target:target.id,line:at,kind:ts.isNewExpression(node)?'construct':'call',resolution:'static'});
|
|
113
|
-
else if(unresolved.length<1000)unresolved.push({path:filePath(source),line:at,expression:expression.getText(source).slice(0,120),reason:'External or dynamic target, or no resolved implementation in the snapshot.'});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
ts.forEachChild(node,child=>visit(child,active));
|
|
117
|
-
}
|
|
118
|
-
visit(source,null);
|
|
119
|
-
}
|
|
120
|
-
// Metadados Angular são lidos da AST; templates são analisados pelo compilador,
|
|
121
|
-
// sem inicializar a aplicação, resolver DI em runtime ou executar seus decorators.
|
|
122
|
-
const components=[],pipes=[],templatePaths=new Set();
|
|
123
|
-
for(const source of parsed){
|
|
124
|
-
const angularAliases=new Map(),angularNamespaces=new Set();
|
|
125
|
-
for(const statement of source.statements){if(ts.isImportDeclaration(statement)&&statement.moduleSpecifier.text==='@angular/core'){const bindings=statement.importClause?.namedBindings;if(bindings&&ts.isNamedImports(bindings))for(const spec of bindings.elements)angularAliases.set(spec.name.text,(spec.propertyName||spec.name).text);else if(bindings&&ts.isNamespaceImport(bindings))angularNamespaces.add(bindings.name.text)}}
|
|
126
|
-
function visitClasses(node){if(ts.isClassDeclaration(node))for(const decorator of ts.canHaveDecorators(node)?ts.getDecorators(node)||[]:[]){const call=decorator.expression;if(!ts.isCallExpression(call)||!call.arguments.length||!ts.isObjectLiteralExpression(call.arguments[0]))continue;let type=null;if(ts.isIdentifier(call.expression)){const symbol=checker.getSymbolAtLocation(call.expression);if(symbol?.declarations?.some(ts.isImportSpecifier))type=angularAliases.get(call.expression.text)}else if(ts.isPropertyAccessExpression(call.expression)&&angularNamespaces.has(call.expression.expression.getText(source)))type=call.expression.name.text;if(!['Component','Pipe'].includes(type))continue;const metadata=new Map(call.arguments[0].properties.filter(ts.isPropertyAssignment).map(p=>[p.name.getText(source).replace(/^['"]|['"]$/g,''),p.initializer]));const methods=new Map();for(const member of node.members){const symbol=declarations.get(member)||declarations.get(member.initializer);if(symbol&&member.name)methods.set(member.name.getText(source).replace(/^['"]|['"]$/g,''),symbol)}const literal=n=>n&&ts.isStringLiteralLike(n)?n.text:null;const entry={source,node,name:node.name?.text||'<component>',methods,selector:literal(metadata.get('selector'))};if(type==='Pipe'){entry.pipeName=literal(metadata.get('name'));pipes.push(entry);continue}const template=metadata.get('template'),url=literal(metadata.get('templateUrl'));entry.template=literal(template);entry.templatePath=source.fileName;entry.inlineLine=template?line(source,template.getStart(source)):1;if(url){const target=resolve(url.startsWith('.')?url:'./'+url,source.fileName);entry.templatePath=target;entry.template=target?docs.get(target)?.text:null;entry.inlineLine=1;dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:url,line:line(source,node.getStart(source)),kind:'angular_template',resolution:target?'resolved':'unresolved'})}for(const key of ['styleUrl','styleUrls']){const value=metadata.get(key),values=value&&ts.isArrayLiteralExpression(value)?value.elements:[value];for(const item of values){const style=literal(item);if(!style)continue;const target=resolve(style.startsWith('.')?style:'./'+style,source.fileName);dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:style,line:line(source,item.getStart(source)),kind:'angular_style',resolution:target?'resolved':'unresolved'})}}components.push(entry)}ts.forEachChild(node,visitClasses)}visitClasses(source)
|
|
127
|
-
}
|
|
128
|
-
if(components.some(c=>c.template!=null)){
|
|
129
|
-
try{
|
|
130
|
-
const {parseTemplate}=require('./web_adapters/node/node_modules/@angular/compiler/fesm2022/compiler.mjs');
|
|
131
|
-
for(const component of components){if(component.template==null||!component.templatePath)continue;const pathName=component.templatePath.replace('/snapshot/',''),tree=parseTemplate(component.template,pathName);if(tree.errors?.length){diagnostics.push({path:pathName,reason:'Invalid Angular template or syntax not supported in this version.'});continue}templatePaths.add(pathName);const owner={id:pathName+'::'+filePath(component.source)+'::'+component.name+'.template@'+component.inlineLine,path:pathName,name:component.name+'.template',kind:'template',language:'angular',start_line:component.inlineLine,end_line:component.inlineLine+component.template.split('\n').length-1,location_kind:component.templatePath===component.source.fileName?'inline_template':'source_line'};if(symbols.length>=maxSymbols)break;symbols.push(owner);const shadow=new Set(),seen=new WeakSet();function walk(n,fn){if(!n||typeof n!=='object'||seen.has(n))return;seen.add(n);fn(n);for(const [key,value] of Object.entries(n)){if(['sourceSpan','span','startSourceSpan','endSourceSpan','keySpan','valueSpan','parent'].includes(key))continue;if(Array.isArray(value))value.forEach(x=>walk(x,fn));else if(value&&typeof value==='object')walk(value,fn)}}walk(tree.nodes,n=>{if(['Variable','Reference','LetDeclaration'].includes(n.constructor.name)&&n.name)shadow.add(n.name);if(n.constructor.name==='ForLoopBlock'&&n.item?.name)shadow.add(n.item.name)});const seen2=new WeakSet();function inspect(n){if(!n||typeof n!=='object'||seen2.has(n))return;seen2.add(n);const start=n.sourceSpan?.start,at=(typeof start==='number'?component.template.slice(0,start).split('\n').length-1:(start?.line??0))+component.inlineLine;if(n.constructor.name==='Call'||n.constructor.name==='SafeCall'){const receiver=n.receiver;if(receiver?.constructor.name==='PropertyRead'&&['ImplicitReceiver','ThisReceiver'].includes(receiver.receiver?.constructor.name)){const target=shadow.has(receiver.name)?null:component.methods.get(receiver.name);if(target&&calls.length<maxEdges)calls.push({source:owner.id,target:target.id,line:at,kind:'template_call',resolution:'static'});else if(unresolved.length<1000)unresolved.push({path:pathName,line:at,expression:receiver.name+'()',reason:'Unresolved template method, or a local variable that shadows the component.'})}}if(n.constructor.name==='Element'){const matches=components.filter(c=>c.selector===n.name);if(matches.length===1&&filePath(matches[0].source)!==pathName)dependencies.push({source:pathName,target:filePath(matches[0].source),specifier:n.name,line:at,kind:'angular_component',resolution:'resolved'})}if(n.constructor.name==='BindingPipe'){const matches=pipes.filter(p=>p.pipeName===n.name&&p.methods.has('transform'));if(matches.length===1){const pipe=matches[0];dependencies.push({source:pathName,target:filePath(pipe.source),specifier:n.name,line:at,kind:'angular_pipe',resolution:'resolved'});calls.push({source:owner.id,target:pipe.methods.get('transform').id,line:at,kind:'template_pipe',resolution:'static'})}}for(const [key,value] of Object.entries(n)){if(['sourceSpan','span','startSourceSpan','endSourceSpan','keySpan','valueSpan','parent'].includes(key))continue;if(Array.isArray(value))value.forEach(inspect);else if(value&&typeof value==='object')inspect(value)}}inspect(tree.nodes)
|
|
132
|
-
}
|
|
133
|
-
}catch{diagnostics.push({path:'Angular',reason:'The Angular template analyzer did not finish. Check the local runtime.'})}
|
|
134
|
-
}
|
|
135
|
-
process.stdout.write(JSON.stringify({symbols,dependencies:dependencies.slice(0,maxEdges),calls:calls.slice(0,maxEdges),unresolved,diagnostics,parsed:[...parsed.map(filePath),...templatePaths],truncated:symbols.length>=maxSymbols||calls.length>=maxEdges||dependencies.length>=maxEdges}));
|
|
1
|
+
/* Análise estática de snapshots JS/TS. Não executa código nem lê arquivos do projeto. */
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path').posix;
|
|
4
|
+
const ts = require('./web_adapters/node/node_modules/typescript');
|
|
5
|
+
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
6
|
+
const clean = name => path.normalize(name.replaceAll('\\', '/')).replace(/^\.\//, '');
|
|
7
|
+
const docs = new Map(input.files.map(f => ['/snapshot/' + clean(f.path), f]));
|
|
8
|
+
const knownFiles = new Set([...docs.keys(),...(input.paths||[]).map(p=>'/snapshot/'+clean(p))]);
|
|
9
|
+
const names = [...docs.keys()].filter(n => /\.(?:[cm]?[jt]s|[jt]sx)$/.test(n));
|
|
10
|
+
const options = {allowJs:true, checkJs:true, jsx:ts.JsxEmit.Preserve, target:ts.ScriptTarget.Latest,
|
|
11
|
+
module:ts.ModuleKind.ESNext, moduleResolution:ts.ModuleResolutionKind.Bundler,
|
|
12
|
+
noLib:true, noEmit:true, skipLibCheck:true, allowImportingTsExtensions:true};
|
|
13
|
+
let configPath = docs.has('/snapshot/tsconfig.json') ? '/snapshot/tsconfig.json' : '/snapshot/jsconfig.json';
|
|
14
|
+
const configSeen = new Set();
|
|
15
|
+
function readConfig(name, depth=0) {
|
|
16
|
+
if (depth>5 || configSeen.has(name) || !docs.has(name)) return {};
|
|
17
|
+
configSeen.add(name);
|
|
18
|
+
const raw = ts.parseConfigFileTextToJson(name, docs.get(name).text).config || {};
|
|
19
|
+
const base = typeof raw.extends==='string' && raw.extends.startsWith('.') ? readConfig(clean(path.join(path.dirname(name),raw.extends.endsWith('.json')?raw.extends:raw.extends+'.json')),depth+1) : {};
|
|
20
|
+
return {...base,...raw.compilerOptions};
|
|
21
|
+
}
|
|
22
|
+
const config=readConfig(configPath);
|
|
23
|
+
const aliases=config.paths||{},baseUrl=clean(path.join(path.dirname(configPath),config.baseUrl||'.'));
|
|
24
|
+
function candidate(base) {
|
|
25
|
+
const variants=[base,base.replace(/\.[cm]?jsx?$/,'.ts'),base.replace(/\.jsx?$/,'.tsx'),
|
|
26
|
+
...['.ts','.tsx','.js','.jsx','.mts','.cts','.mjs','.cjs'].map(e=>base+e),
|
|
27
|
+
...['.ts','.tsx','.js','.jsx'].map(e=>base+'/index'+e)];
|
|
28
|
+
return variants.find(name=>knownFiles.has(clean(name)));
|
|
29
|
+
}
|
|
30
|
+
function resolve(spec, from) {
|
|
31
|
+
if (spec.startsWith('.')) return candidate(clean(path.join(path.dirname(from),spec)));
|
|
32
|
+
for (const [pattern, targets] of Object.entries(aliases)) {
|
|
33
|
+
const [prefix,suffix='']=pattern.split('*');
|
|
34
|
+
if (pattern.includes('*') ? spec.startsWith(prefix)&&spec.endsWith(suffix) : spec===pattern) {
|
|
35
|
+
const middle=pattern.includes('*')?spec.slice(prefix.length,suffix? -suffix.length:undefined):'';
|
|
36
|
+
for (const target of Array.isArray(targets)?targets:[]) {
|
|
37
|
+
const found=candidate(clean(path.join(baseUrl,target.replace('*',middle))));
|
|
38
|
+
if(found)return found;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if(config.baseUrl)return candidate(clean(path.join(baseUrl,spec)));
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const host={
|
|
46
|
+
getSourceFile:(name,version)=>docs.has(clean(name))?ts.createSourceFile(clean(name),docs.get(clean(name)).text,version,true):undefined,
|
|
47
|
+
getDefaultLibFileName:()=>'/lib.d.ts',writeFile:()=>{},getCurrentDirectory:()=>'/snapshot',
|
|
48
|
+
getDirectories:()=>[],fileExists:name=>docs.has(clean(name)),readFile:name=>docs.get(clean(name))?.text,
|
|
49
|
+
getCanonicalFileName:name=>clean(name),useCaseSensitiveFileNames:()=>true,getNewLine:()=> '\n',
|
|
50
|
+
resolveModuleNames:(modules,containing)=>modules.map(spec=>{const resolved=resolve(spec,containing);return resolved&&docs.has(resolved)&&/\.(?:[cm]?[jt]s|[jt]sx|json)$/.test(resolved)?{resolvedFileName:resolved,extension:ts.extensionFromPath(resolved),isExternalLibraryImport:false}:undefined;})
|
|
51
|
+
};
|
|
52
|
+
const program=ts.createProgram(names,options,host),checker=program.getTypeChecker();
|
|
53
|
+
const symbols=[],dependencies=[],calls=[],unresolved=[],diagnostics=[],declarations=new Map(),moduleNodes=new Map();
|
|
54
|
+
const maxSymbols=8000,maxEdges=20000;
|
|
55
|
+
function filePath(source){return source.fileName.replace('/snapshot/','');}
|
|
56
|
+
function line(source,pos){return source.getLineAndCharacterOfPosition(pos).line+1;}
|
|
57
|
+
function displayName(node) {
|
|
58
|
+
if(node.name)return node.name.getText().replace(/^['"]|['"]$/g,'');
|
|
59
|
+
if(ts.isConstructorDeclaration(node))return 'constructor';
|
|
60
|
+
if(node.parent && ts.isVariableDeclaration(node.parent))return node.parent.name.getText();
|
|
61
|
+
if(node.parent && ts.isPropertyAssignment(node.parent))return node.parent.name.getText();
|
|
62
|
+
if(node.parent && ts.isPropertyDeclaration(node.parent))return node.parent.name.getText();
|
|
63
|
+
if(node.parent && ts.isExportAssignment(node.parent))return 'default';
|
|
64
|
+
return 'callback';
|
|
65
|
+
}
|
|
66
|
+
function isFunction(node){return !!node.body && (ts.isFunctionDeclaration(node)||ts.isMethodDeclaration(node)||ts.isFunctionExpression(node)||ts.isArrowFunction(node)||ts.isConstructorDeclaration(node)||ts.isGetAccessor(node)||ts.isSetAccessor(node));}
|
|
67
|
+
function addSymbol(node,source,owner){
|
|
68
|
+
if(symbols.length>=maxSymbols)return null;
|
|
69
|
+
let name=displayName(node),parent=node.parent;
|
|
70
|
+
const classParent=parent&&ts.isPropertyDeclaration(parent)?parent.parent:parent;
|
|
71
|
+
const cls=classParent && (ts.isClassDeclaration(classParent)||ts.isClassExpression(classParent)) ? classParent.name?.getText() : null;
|
|
72
|
+
if(cls)name=cls+'.'+name;
|
|
73
|
+
const start=line(source,node.getStart(source)),id=filePath(source)+'::'+name+'@'+start+':'+node.pos;
|
|
74
|
+
const symbol={id,path:filePath(source),name,start_line:start,end_line:line(source,node.end),kind:cls?'method':'function',language:'typescript',parent:owner?.id||null};
|
|
75
|
+
symbols.push(symbol);declarations.set(node,symbol);return symbol;
|
|
76
|
+
}
|
|
77
|
+
const parsed=[];
|
|
78
|
+
for(const source of program.getSourceFiles()){
|
|
79
|
+
if(!docs.has(source.fileName))continue;
|
|
80
|
+
if(source.parseDiagnostics.length){diagnostics.push({path:filePath(source),reason:'Incomplete or invalid syntax in the snapshot.',line:line(source,source.parseDiagnostics[0].start||0)});continue;}
|
|
81
|
+
parsed.push(source);
|
|
82
|
+
function collect(node,owner){const current=isFunction(node)?addSymbol(node,source,owner):owner;ts.forEachChild(node,child=>collect(child,current));}
|
|
83
|
+
collect(source,null);
|
|
84
|
+
}
|
|
85
|
+
function moduleSymbol(source){if(!moduleNodes.has(source.fileName)){const s={id:filePath(source)+'::<module>',path:filePath(source),name:'<module>',start_line:1,end_line:1,kind:'module',language:'typescript'};moduleNodes.set(source.fileName,s);symbols.push(s);}return moduleNodes.get(source.fileName);}
|
|
86
|
+
function targetFor(node){
|
|
87
|
+
const signature=checker.getResolvedSignature(node);let target=declarations.get(signature?.declaration);
|
|
88
|
+
if(target)return target;
|
|
89
|
+
let symbol=checker.getSymbolAtLocation(node.expression);
|
|
90
|
+
if(symbol?.flags & ts.SymbolFlags.Alias)symbol=checker.getAliasedSymbol(symbol);
|
|
91
|
+
for(const declaration of symbol?.declarations||[]){
|
|
92
|
+
if(declarations.has(declaration))return declarations.get(declaration);
|
|
93
|
+
if(ts.isVariableDeclaration(declaration)&&declarations.has(declaration.initializer))return declarations.get(declaration.initializer);
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
for(const source of parsed){
|
|
98
|
+
function visit(node,owner){
|
|
99
|
+
if(isFunction(node) && !declarations.has(node))return;
|
|
100
|
+
const active=declarations.get(node)||owner;
|
|
101
|
+
if((ts.isImportDeclaration(node)||ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
102
|
+
const spec=node.moduleSpecifier.text,target=resolve(spec,source.fileName);
|
|
103
|
+
if(dependencies.length<maxEdges)dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:spec,line:line(source,node.getStart(source)),kind:node.importClause?.isTypeOnly||node.isTypeOnly?'type_import':'import',resolution:target?'resolved':spec.startsWith('.')?'unresolved':'external'});
|
|
104
|
+
}
|
|
105
|
+
if(ts.isCallExpression(node)||ts.isNewExpression(node)){
|
|
106
|
+
const expression=node.expression,at=line(source,node.getStart(source));
|
|
107
|
+
if(ts.isCallExpression(node) && (expression.kind===ts.SyntaxKind.ImportKeyword || ts.isIdentifier(expression)&&expression.text==='require') && node.arguments?.length && ts.isStringLiteral(node.arguments[0])){
|
|
108
|
+
const spec=node.arguments[0].text,target=resolve(spec,source.fileName);
|
|
109
|
+
if(dependencies.length<maxEdges)dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:spec,line:at,kind:'dynamic_import',resolution:target?'resolved':spec.startsWith('.')?'unresolved':'external'});
|
|
110
|
+
}else{
|
|
111
|
+
const target=targetFor(node);
|
|
112
|
+
if(target && calls.length<maxEdges)calls.push({source:(active||moduleSymbol(source)).id,target:target.id,line:at,kind:ts.isNewExpression(node)?'construct':'call',resolution:'static'});
|
|
113
|
+
else if(unresolved.length<1000)unresolved.push({path:filePath(source),line:at,expression:expression.getText(source).slice(0,120),reason:'External or dynamic target, or no resolved implementation in the snapshot.'});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
ts.forEachChild(node,child=>visit(child,active));
|
|
117
|
+
}
|
|
118
|
+
visit(source,null);
|
|
119
|
+
}
|
|
120
|
+
// Metadados Angular são lidos da AST; templates são analisados pelo compilador,
|
|
121
|
+
// sem inicializar a aplicação, resolver DI em runtime ou executar seus decorators.
|
|
122
|
+
const components=[],pipes=[],templatePaths=new Set();
|
|
123
|
+
for(const source of parsed){
|
|
124
|
+
const angularAliases=new Map(),angularNamespaces=new Set();
|
|
125
|
+
for(const statement of source.statements){if(ts.isImportDeclaration(statement)&&statement.moduleSpecifier.text==='@angular/core'){const bindings=statement.importClause?.namedBindings;if(bindings&&ts.isNamedImports(bindings))for(const spec of bindings.elements)angularAliases.set(spec.name.text,(spec.propertyName||spec.name).text);else if(bindings&&ts.isNamespaceImport(bindings))angularNamespaces.add(bindings.name.text)}}
|
|
126
|
+
function visitClasses(node){if(ts.isClassDeclaration(node))for(const decorator of ts.canHaveDecorators(node)?ts.getDecorators(node)||[]:[]){const call=decorator.expression;if(!ts.isCallExpression(call)||!call.arguments.length||!ts.isObjectLiteralExpression(call.arguments[0]))continue;let type=null;if(ts.isIdentifier(call.expression)){const symbol=checker.getSymbolAtLocation(call.expression);if(symbol?.declarations?.some(ts.isImportSpecifier))type=angularAliases.get(call.expression.text)}else if(ts.isPropertyAccessExpression(call.expression)&&angularNamespaces.has(call.expression.expression.getText(source)))type=call.expression.name.text;if(!['Component','Pipe'].includes(type))continue;const metadata=new Map(call.arguments[0].properties.filter(ts.isPropertyAssignment).map(p=>[p.name.getText(source).replace(/^['"]|['"]$/g,''),p.initializer]));const methods=new Map();for(const member of node.members){const symbol=declarations.get(member)||declarations.get(member.initializer);if(symbol&&member.name)methods.set(member.name.getText(source).replace(/^['"]|['"]$/g,''),symbol)}const literal=n=>n&&ts.isStringLiteralLike(n)?n.text:null;const entry={source,node,name:node.name?.text||'<component>',methods,selector:literal(metadata.get('selector'))};if(type==='Pipe'){entry.pipeName=literal(metadata.get('name'));pipes.push(entry);continue}const template=metadata.get('template'),url=literal(metadata.get('templateUrl'));entry.template=literal(template);entry.templatePath=source.fileName;entry.inlineLine=template?line(source,template.getStart(source)):1;if(url){const target=resolve(url.startsWith('.')?url:'./'+url,source.fileName);entry.templatePath=target;entry.template=target?docs.get(target)?.text:null;entry.inlineLine=1;dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:url,line:line(source,node.getStart(source)),kind:'angular_template',resolution:target?'resolved':'unresolved'})}for(const key of ['styleUrl','styleUrls']){const value=metadata.get(key),values=value&&ts.isArrayLiteralExpression(value)?value.elements:[value];for(const item of values){const style=literal(item);if(!style)continue;const target=resolve(style.startsWith('.')?style:'./'+style,source.fileName);dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:style,line:line(source,item.getStart(source)),kind:'angular_style',resolution:target?'resolved':'unresolved'})}}components.push(entry)}ts.forEachChild(node,visitClasses)}visitClasses(source)
|
|
127
|
+
}
|
|
128
|
+
if(components.some(c=>c.template!=null)){
|
|
129
|
+
try{
|
|
130
|
+
const {parseTemplate}=require('./web_adapters/node/node_modules/@angular/compiler/fesm2022/compiler.mjs');
|
|
131
|
+
for(const component of components){if(component.template==null||!component.templatePath)continue;const pathName=component.templatePath.replace('/snapshot/',''),tree=parseTemplate(component.template,pathName);if(tree.errors?.length){diagnostics.push({path:pathName,reason:'Invalid Angular template or syntax not supported in this version.'});continue}templatePaths.add(pathName);const owner={id:pathName+'::'+filePath(component.source)+'::'+component.name+'.template@'+component.inlineLine,path:pathName,name:component.name+'.template',kind:'template',language:'angular',start_line:component.inlineLine,end_line:component.inlineLine+component.template.split('\n').length-1,location_kind:component.templatePath===component.source.fileName?'inline_template':'source_line'};if(symbols.length>=maxSymbols)break;symbols.push(owner);const shadow=new Set(),seen=new WeakSet();function walk(n,fn){if(!n||typeof n!=='object'||seen.has(n))return;seen.add(n);fn(n);for(const [key,value] of Object.entries(n)){if(['sourceSpan','span','startSourceSpan','endSourceSpan','keySpan','valueSpan','parent'].includes(key))continue;if(Array.isArray(value))value.forEach(x=>walk(x,fn));else if(value&&typeof value==='object')walk(value,fn)}}walk(tree.nodes,n=>{if(['Variable','Reference','LetDeclaration'].includes(n.constructor.name)&&n.name)shadow.add(n.name);if(n.constructor.name==='ForLoopBlock'&&n.item?.name)shadow.add(n.item.name)});const seen2=new WeakSet();function inspect(n){if(!n||typeof n!=='object'||seen2.has(n))return;seen2.add(n);const start=n.sourceSpan?.start,at=(typeof start==='number'?component.template.slice(0,start).split('\n').length-1:(start?.line??0))+component.inlineLine;if(n.constructor.name==='Call'||n.constructor.name==='SafeCall'){const receiver=n.receiver;if(receiver?.constructor.name==='PropertyRead'&&['ImplicitReceiver','ThisReceiver'].includes(receiver.receiver?.constructor.name)){const target=shadow.has(receiver.name)?null:component.methods.get(receiver.name);if(target&&calls.length<maxEdges)calls.push({source:owner.id,target:target.id,line:at,kind:'template_call',resolution:'static'});else if(unresolved.length<1000)unresolved.push({path:pathName,line:at,expression:receiver.name+'()',reason:'Unresolved template method, or a local variable that shadows the component.'})}}if(n.constructor.name==='Element'){const matches=components.filter(c=>c.selector===n.name);if(matches.length===1&&filePath(matches[0].source)!==pathName)dependencies.push({source:pathName,target:filePath(matches[0].source),specifier:n.name,line:at,kind:'angular_component',resolution:'resolved'})}if(n.constructor.name==='BindingPipe'){const matches=pipes.filter(p=>p.pipeName===n.name&&p.methods.has('transform'));if(matches.length===1){const pipe=matches[0];dependencies.push({source:pathName,target:filePath(pipe.source),specifier:n.name,line:at,kind:'angular_pipe',resolution:'resolved'});calls.push({source:owner.id,target:pipe.methods.get('transform').id,line:at,kind:'template_pipe',resolution:'static'})}}for(const [key,value] of Object.entries(n)){if(['sourceSpan','span','startSourceSpan','endSourceSpan','keySpan','valueSpan','parent'].includes(key))continue;if(Array.isArray(value))value.forEach(inspect);else if(value&&typeof value==='object')inspect(value)}}inspect(tree.nodes)
|
|
132
|
+
}
|
|
133
|
+
}catch{diagnostics.push({path:'Angular',reason:'The Angular template analyzer did not finish. Check the local runtime.'})}
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(JSON.stringify({symbols,dependencies:dependencies.slice(0,maxEdges),calls:calls.slice(0,maxEdges),unresolved,diagnostics,parsed:[...parsed.map(filePath),...templatePaths],truncated:symbols.length>=maxSymbols||calls.length>=maxEdges||dependencies.length>=maxEdges}));
|