thorns 5.1.9

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.
@@ -0,0 +1,252 @@
1
+ import { relative, dirname, resolve, join, extname } from 'path';
2
+
3
+ export function extractDependencies(tree, sourceCode, filePath, lang) {
4
+ const deps = {
5
+ imports: new Set(),
6
+ exports: new Set(),
7
+ importPaths: new Set(),
8
+ exportedNames: new Set()
9
+ };
10
+
11
+ function extractImportPath(node) {
12
+ // Find string literal in import statement
13
+ for (const child of node.children) {
14
+ if (child.type === 'string' || child.type === 'string_literal' ||
15
+ child.type === 'interpreted_string_literal') {
16
+ return child.text.replace(/['"]/g, '');
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function extractExportedName(node) {
23
+ for (const child of node.children) {
24
+ if (child.type.includes('identifier') || child.type.includes('name')) {
25
+ return child.text;
26
+ }
27
+ if (child.type.includes('declaration')) {
28
+ return extractExportedName(child);
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function extractCommonJSExports(node) {
35
+ if (node.type === 'assignment_expression' || node.type === 'expression_statement') {
36
+ const text = node.text;
37
+ if (text.startsWith('module.exports') || text.startsWith('exports.')) {
38
+ if (text.includes('{')) {
39
+ const match = text.match(/\{([^}]+)\}/);
40
+ if (match) {
41
+ const names = match[1].split(',').map(n => {
42
+ const parts = n.trim().split(':');
43
+ return parts[0].trim();
44
+ });
45
+ return names;
46
+ }
47
+ } else {
48
+ const match = text.match(/exports\.(\w+)/);
49
+ if (match) return [match[1]];
50
+ }
51
+ }
52
+ }
53
+ return [];
54
+ }
55
+
56
+ function traverse(node) {
57
+ const type = node.type;
58
+
59
+ if (type === 'import_statement' || type === 'import_from_statement' ||
60
+ type === 'import_declaration' || type === 'use_declaration') {
61
+ const path = extractImportPath(node);
62
+ if (path) {
63
+ deps.importPaths.add(path);
64
+ deps.imports.add(node.text.slice(0, 80));
65
+ }
66
+ }
67
+
68
+ if (type === 'call_expression') {
69
+ const funcNode = node.children[0];
70
+ if (funcNode && (funcNode.text === 'require' || funcNode.text === 'import')) {
71
+ for (const child of node.children) {
72
+ if (child.type === 'arguments') {
73
+ for (const arg of child.children) {
74
+ if (arg.type === 'string' || arg.type === 'string_fragment' || arg.type === 'template_string') {
75
+ const path = arg.text.replace(/['"]/g, '');
76
+ if (path && !path.includes('${')) {
77
+ deps.importPaths.add(path);
78
+ deps.imports.add(node.text.slice(0, 80));
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ if (type.includes('export')) {
88
+ const name = extractExportedName(node);
89
+ if (name) {
90
+ deps.exportedNames.add(name);
91
+ }
92
+ deps.exports.add(node.text.slice(0, 80));
93
+ }
94
+
95
+ const cjsExports = extractCommonJSExports(node);
96
+ if (cjsExports.length > 0) {
97
+ for (const name of cjsExports) {
98
+ deps.exportedNames.add(name);
99
+ }
100
+ deps.exports.add(node.text.slice(0, 80));
101
+ }
102
+
103
+ for (const child of node.children) {
104
+ traverse(child);
105
+ }
106
+ }
107
+
108
+ traverse(tree.rootNode);
109
+ return deps;
110
+ }
111
+
112
+ export function buildDependencyGraph(fileAnalysis) {
113
+ const graph = {
114
+ nodes: new Map(),
115
+ edges: [],
116
+ orphans: new Set(),
117
+ entryPoints: new Set(),
118
+ coupling: new Map()
119
+ };
120
+
121
+ // Create nodes
122
+ for (const [path, data] of Object.entries(fileAnalysis)) {
123
+ graph.nodes.set(path, {
124
+ imports: data.imports,
125
+ exports: data.exports,
126
+ importPaths: data.importPaths,
127
+ exportedNames: data.exportedNames,
128
+ importedBy: new Set(),
129
+ importsFrom: new Set()
130
+ });
131
+ }
132
+
133
+ // Build edges
134
+ for (const [fromPath, data] of graph.nodes) {
135
+ const fromDir = dirname(fromPath);
136
+
137
+ for (const impPath of data.importPaths) {
138
+ // Try to resolve the import to an actual file
139
+ const resolved = resolveImport(impPath, fromDir, fileAnalysis);
140
+ if (resolved) {
141
+ data.importsFrom.add(resolved);
142
+ const targetNode = graph.nodes.get(resolved);
143
+ if (targetNode) {
144
+ targetNode.importedBy.add(fromPath);
145
+ }
146
+ graph.edges.push({ from: fromPath, to: resolved, type: 'import' });
147
+ }
148
+ }
149
+ }
150
+
151
+ // Identify orphans (files not imported by anyone)
152
+ for (const [path, data] of graph.nodes) {
153
+ if (data.importedBy.size === 0) {
154
+ graph.orphans.add(path);
155
+ }
156
+ }
157
+
158
+ // Identify entry points (files that don't import anything)
159
+ for (const [path, data] of graph.nodes) {
160
+ if (data.importsFrom.size === 0 && data.importedBy.size > 0) {
161
+ graph.entryPoints.add(path);
162
+ }
163
+ }
164
+
165
+ // Calculate coupling (files with most dependencies)
166
+ for (const [path, data] of graph.nodes) {
167
+ const coupling = data.importsFrom.size + data.importedBy.size;
168
+ if (coupling > 0) {
169
+ graph.coupling.set(path, {
170
+ in: data.importedBy.size,
171
+ out: data.importsFrom.size,
172
+ total: coupling
173
+ });
174
+ }
175
+ }
176
+
177
+ return graph;
178
+ }
179
+
180
+ function resolveImport(importPath, fromDir, fileAnalysis) {
181
+ if (importPath.startsWith('.')) {
182
+ const normalized = join(fromDir, importPath).replace(/\\/g, '/');
183
+ const cleanPath = normalized.replace(/^\.\//, '');
184
+
185
+ if (fileAnalysis[cleanPath]) return cleanPath;
186
+ if (fileAnalysis[normalized]) return normalized;
187
+
188
+ const exts = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
189
+ const indexExts = ['/index.js', '/index.ts', '/index.jsx', '/index.tsx'];
190
+
191
+ for (const ext of exts) {
192
+ const withExt = cleanPath.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, '') + ext;
193
+ if (fileAnalysis[withExt]) return withExt;
194
+ }
195
+
196
+ for (const ext of indexExts) {
197
+ const withIdx = cleanPath.replace(/\/$/, '') + ext;
198
+ if (fileAnalysis[withIdx]) return withIdx;
199
+ }
200
+ }
201
+
202
+ const paths = Object.keys(fileAnalysis);
203
+ const fileName = importPath.split('/').pop();
204
+ const fileNameNoExt = fileName.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, '');
205
+
206
+ for (const path of paths) {
207
+ const pathParts = path.split('/');
208
+ const pathFileName = pathParts[pathParts.length - 1];
209
+ const pathFileNameNoExt = pathFileName.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, '');
210
+
211
+ if (pathFileNameNoExt === fileNameNoExt) {
212
+ const importParts = importPath.split('/').filter(p => p && p !== '.');
213
+ let matches = true;
214
+ for (let i = importParts.length - 1, j = pathParts.length - 1; i >= 0 && j >= 0; i--, j--) {
215
+ const importPart = importParts[i].replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, '');
216
+ const pathPart = pathParts[j].replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, '');
217
+ if (importPart !== pathPart) {
218
+ matches = false;
219
+ break;
220
+ }
221
+ }
222
+ if (matches) return path;
223
+ }
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ export function analyzeModules(fileAnalysis, baseDir) {
230
+ const modules = new Map();
231
+
232
+ for (const path of Object.keys(fileAnalysis)) {
233
+ const rel = relative(baseDir, path);
234
+ const parts = rel.split(/[\/\\]/);
235
+ const moduleName = parts[0];
236
+
237
+ if (!modules.has(moduleName)) {
238
+ modules.set(moduleName, {
239
+ files: 0,
240
+ imports: 0,
241
+ exports: 0,
242
+ internalDeps: 0,
243
+ externalDeps: 0
244
+ });
245
+ }
246
+
247
+ const mod = modules.get(moduleName);
248
+ mod.files++;
249
+ }
250
+
251
+ return modules;
252
+ }
@@ -0,0 +1,150 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join, dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ // Load default ignore patterns from .thornsignore
8
+ function loadDefaultIgnores() {
9
+ const ignorePath = join(__dirname, '.thornsignore');
10
+ if (!existsSync(ignorePath)) {
11
+ return getHardcodedIgnores();
12
+ }
13
+
14
+ try {
15
+ const content = readFileSync(ignorePath, 'utf8');
16
+ return parseIgnoreFile(content);
17
+ } catch (e) {
18
+ return getHardcodedIgnores();
19
+ }
20
+ }
21
+
22
+ // Fallback hardcoded ignores if file doesn't exist
23
+ function getHardcodedIgnores() {
24
+ return new Set([
25
+ 'node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out',
26
+ 'target', 'vendor', '__pycache__', '.pytest_cache', '.mypy_cache',
27
+ '.next', '.nuxt', '.cache', '.parcel-cache', '.vite', '.turbo',
28
+ 'coverage', '.nyc_output', '.firebase', '.terraform', '.aws',
29
+ '.azure', '.gcloud', '.vscode', '.idea', '.vs', 'bin', 'obj',
30
+ '.gradle', '.mvn', 'Pods', 'DerivedData', '.bundle',
31
+ // User home hidden folders and caches
32
+ '.config', '.local', '.ssh', '.gnupg', '.kube', '.docker',
33
+ '.npm', '.yarn', '.pnpm', '.bun', '.cargo', '.rustup',
34
+ '.java', '.m2', '.sbt', '.gem', '.rbenv', '.rvm', '.nvm',
35
+ '.python', '.venv_global', '.conda', '.mamba', '.nodenv',
36
+ '.pyenv', '.asdf', '.mise', '.vscode-server', '.cursor',
37
+ '.emacs.d', '.vim', '.neovim', '.nvim', '.zsh', '.bash',
38
+ '.oh-my-zsh', '.oh-my-bash', '.direnv'
39
+ ]);
40
+ }
41
+
42
+ // Parse ignore file format (gitignore-style)
43
+ function parseIgnoreFile(content) {
44
+ const patterns = new Set();
45
+ const lines = content.split('\n');
46
+
47
+ for (let line of lines) {
48
+ line = line.trim();
49
+
50
+ // Skip comments and empty lines
51
+ if (!line || line.startsWith('#')) continue;
52
+
53
+ // Remove trailing slash for directory patterns
54
+ if (line.endsWith('/')) {
55
+ line = line.slice(0, -1);
56
+ }
57
+
58
+ // Skip negation patterns (!) for now - we're only ignoring
59
+ if (line.startsWith('!')) continue;
60
+
61
+ // Handle wildcards
62
+ if (line.includes('*')) {
63
+ // Convert glob patterns to directory prefixes where possible
64
+ if (line.includes('*.')) {
65
+ // File extension patterns - skip for directory matching
66
+ continue;
67
+ }
68
+ // For patterns like .cache/, we can still use them
69
+ line = line.replace(/\/\*+$/, ''); // Remove trailing wildcards
70
+ }
71
+
72
+ if (line) {
73
+ patterns.add(line);
74
+ }
75
+ }
76
+
77
+ return patterns;
78
+ }
79
+
80
+ // Load project-specific ignore files
81
+ function loadProjectIgnores(rootPath) {
82
+ const patterns = new Set();
83
+ const ignoreFiles = [
84
+ '.gitignore',
85
+ '.dockerignore',
86
+ '.npmignore',
87
+ '.eslintignore',
88
+ '.prettierignore',
89
+ '.thornsignore'
90
+ ];
91
+
92
+ for (const file of ignoreFiles) {
93
+ const path = join(rootPath, file);
94
+ if (existsSync(path)) {
95
+ try {
96
+ const content = readFileSync(path, 'utf8');
97
+ const filePatterns = parseIgnoreFile(content);
98
+ for (const pattern of filePatterns) {
99
+ patterns.add(pattern);
100
+ }
101
+ } catch (e) {
102
+ // Ignore read errors
103
+ }
104
+ }
105
+ }
106
+
107
+ return patterns;
108
+ }
109
+
110
+ // Check if a path should be ignored
111
+ export function shouldIgnore(path, ignorePatterns) {
112
+ const parts = path.split(/[\/\\]/);
113
+
114
+ // Check each part of the path
115
+ for (const part of parts) {
116
+ if (ignorePatterns.has(part)) {
117
+ return true;
118
+ }
119
+
120
+ // Check for patterns with wildcards
121
+ for (const pattern of ignorePatterns) {
122
+ if (pattern.includes('*')) {
123
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
124
+ if (regex.test(part)) {
125
+ return true;
126
+ }
127
+ }
128
+
129
+ // Check for prefix matches (e.g., ".cache" matches ".cache-loader")
130
+ if (part.startsWith(pattern) && pattern.startsWith('.')) {
131
+ return true;
132
+ }
133
+ }
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ // Build combined ignore set
140
+ export function buildIgnoreSet(rootPath) {
141
+ const defaultIgnores = loadDefaultIgnores();
142
+ const projectIgnores = loadProjectIgnores(rootPath);
143
+
144
+ // Merge all patterns
145
+ const combined = new Set([...defaultIgnores, ...projectIgnores]);
146
+
147
+ return combined;
148
+ }
149
+
150
+ export { loadDefaultIgnores, loadProjectIgnores };
package/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { analyze } from './lib.js';
3
+
4
+ const targetPath = process.argv[2] || '.';
5
+ const output = analyze(targetPath);
6
+ console.log(output);