lavina 1.0.0

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/README.md ADDED
@@ -0,0 +1 @@
1
+ # lavina
package/cli.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import Logger from './src/cli/components/consoleLogger.js';
4
+ import scanDirForProjects from './src/cli/helpers/scanDirForProjects.js';
5
+ import mapDependencies from './src/cli/helpers/mapDependencies.js';
6
+ import { COLORS } from './src/constants.js';
7
+ import saveDependencies from './src/cli/helpers/saveDependencies.js';
8
+ import { spawn } from 'child_process';
9
+ import { getLibDirName } from './src/cli/helpers/getLibDirName.js';
10
+ import open from 'open';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('lavina')
16
+ .description('lavina - automated dependency propagation tool')
17
+ .version('0.1.0');
18
+
19
+ program
20
+ .command('scan')
21
+ .description('Scan and build dependency graph')
22
+ .option('--verbose', 'Enable verbose logging')
23
+ .action(async (opts) => {
24
+ Logger.setVerbose(opts.verbose);
25
+ const scannedProjets = await scanDirForProjects(process.cwd());
26
+ Logger.color('green').log(`Found ${scannedProjets.length} projects:`, true);
27
+
28
+ const mappedProjects = mapDependencies(scannedProjets);
29
+
30
+ const outputPath = saveDependencies(mappedProjects);
31
+
32
+ mappedProjects.map((project) => {
33
+ Logger.color(COLORS.YELLOW).log(
34
+ `\n- ${project.displayName}: ${project.version}`,
35
+ true,
36
+ );
37
+ if (project.projectDependencies) {
38
+ Logger.log(
39
+ `Dependencies: \n${Object.entries(project.projectDependencies)
40
+ .map(([dependency, version]) => `${dependency}: ${version}`)
41
+ .join('\n')}`,
42
+ true,
43
+ );
44
+ }
45
+ });
46
+
47
+ Logger.color(COLORS.GREEN).log(
48
+ `\nDependency map saved to: ${outputPath}\n`,
49
+ true,
50
+ );
51
+ });
52
+
53
+ program
54
+ .command('ui')
55
+ .description('Show dependency graph in a web UI')
56
+ .action(async () => {
57
+ const { __dirname } = getLibDirName(import.meta.url);
58
+ const uiPath = `${__dirname}/lavina-ui`;
59
+ Logger.color(COLORS.GREEN).log(
60
+ `Launching Lavina UI from ${uiPath}...`,
61
+ true,
62
+ );
63
+
64
+ const standaloneServer = path.join(
65
+ uiPath,
66
+ '.next',
67
+ 'standalone',
68
+ 'server.js',
69
+ );
70
+
71
+ const isBuilt = fs.existsSync(standaloneServer);
72
+
73
+ if (isBuilt) {
74
+ // published package
75
+ spawn('node', ['server.js'], {
76
+ cwd: path.dirname(standaloneServer),
77
+ env: commonEnv,
78
+ });
79
+ } else {
80
+ // local development
81
+ spawn('npm', ['run', 'dev'], {
82
+ cwd: uiPath,
83
+ shell: true,
84
+ env: commonEnv,
85
+ });
86
+ }
87
+
88
+ let isOpened = false;
89
+ nextProcess.stdout?.on('data', (data) => {
90
+ process.stderr.write(data);
91
+
92
+ if (isOpened) return;
93
+ if (data.toString().indexOf('Ready') > -1) {
94
+ open('http://localhost:3000');
95
+ isOpened = true;
96
+ }
97
+ });
98
+
99
+ nextProcess.stderr?.on('data', (data) => {
100
+ process.stderr.write(data);
101
+ });
102
+
103
+ nextProcess.on('close', (code) => {
104
+ Logger.color(COLORS.RED).log(`UI process exited with code ${code}`, true);
105
+ });
106
+ });
107
+
108
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "lavina",
3
+ "version": "1.0.0",
4
+ "description": "Lavina is an automated dependency propagation tool designed to scan microservice architectures and build dependency graphs between internal projects.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "dev": "npm unlink -g lavina && npm link"
9
+ },
10
+ "files": [
11
+ "cli.js",
12
+ "src"
13
+ ],
14
+ "author": "Elena Kikiova",
15
+ "license": "ISC",
16
+ "dependencies": {
17
+ "@radix-ui/themes": "^3.3.0",
18
+ "@tanstack/react-table": "^8.21.3",
19
+ "chalk": "^5.6.2",
20
+ "commander": "^14.0.2",
21
+ "cytoscape": "^3.33.3",
22
+ "cytoscape-elk": "^2.3.0",
23
+ "elkjs": "^0.11.1",
24
+ "jsonc-parser": "^3.3.1",
25
+ "open": "^11.0.0",
26
+ "react-icons": "^5.6.0"
27
+ },
28
+ "bin": {
29
+ "lavina": "cli.js"
30
+ },
31
+ "type": "module",
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.39.1",
34
+ "eslint": "^9.39.1",
35
+ "eslint-config-prettier": "^10.1.8",
36
+ "eslint-plugin-prettier": "^5.5.4",
37
+ "globals": "^16.5.0",
38
+ "prettier": "^3.6.2"
39
+ }
40
+ }
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk';
2
+ import { COLORS } from '../../constants.js';
3
+ const { WHITE, RED } = COLORS;
4
+
5
+ class ConsoleLogger {
6
+ isVerbose = false;
7
+ logColor = WHITE;
8
+ setVerbose(verbose) {
9
+ this.verbose = verbose;
10
+ }
11
+ color(messageColor) {
12
+ this.logColor = messageColor;
13
+ return this;
14
+ }
15
+ resetLogColor() {
16
+ this.logColor = WHITE;
17
+ }
18
+ log(message, isEssentialMessage = false) {
19
+ // if verbose flag is on, log all messages
20
+ // otherwise, log only messages that are RED (error) or flagged as non-verbose
21
+ let shouldLog = true;
22
+ if (!this.isVerbose) {
23
+ shouldLog = this.logColor === RED || isEssentialMessage;
24
+ }
25
+ shouldLog ? console.log(chalk[this.logColor](message)) : null;
26
+ this.resetLogColor();
27
+ }
28
+ }
29
+
30
+ const Logger = new ConsoleLogger();
31
+
32
+ export default Logger;
@@ -0,0 +1,8 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ export const getLibDirName = (url) => {
5
+ const __filename = fileURLToPath(url); // get the resolved path to the file
6
+ const __dirname = path.dirname(__filename);
7
+ return { __filename, __dirname };
8
+ };
@@ -0,0 +1,56 @@
1
+ const mapDependencies = (projects) => {
2
+ const originalToDisplayNameMap = {};
3
+ projects.forEach((project) => {
4
+ if (project.originalName) {
5
+ originalToDisplayNameMap[project.originalName] = project.displayName;
6
+ }
7
+ });
8
+
9
+ const projectOriginalNames = new Set(projects.map((p) => p.originalName));
10
+
11
+ const usedByCount = {};
12
+ projects.forEach((project) => {
13
+ if (project.dependencies) {
14
+ Object.keys(project.dependencies).forEach((dep) => {
15
+ if (projectOriginalNames.has(dep)) {
16
+ const displayName = originalToDisplayNameMap[dep];
17
+ usedByCount[displayName] = (usedByCount[displayName] || 0) + 1;
18
+ }
19
+ });
20
+ }
21
+ });
22
+
23
+ const projectsWithFilteredDependencies = projects.map((project) => {
24
+ const { displayName, dependencies } = project;
25
+
26
+ const projectDependencies = dependencies
27
+ ? Object.entries(dependencies)
28
+ .filter(([dep]) => projectOriginalNames.has(dep))
29
+ .map(([dep, version]) => [originalToDisplayNameMap[dep], version])
30
+ : [];
31
+
32
+ const hasDeps = projectDependencies.length > 0;
33
+ const isDependedOn = usedByCount[displayName] > 0;
34
+
35
+ let category = 'application';
36
+ if (!hasDeps && !isDependedOn) {
37
+ category = 'standalone';
38
+ } else if (isDependedOn) {
39
+ category = 'library';
40
+ }
41
+
42
+ let modifiedProject = {
43
+ ...project,
44
+ projectDependencies: Object.fromEntries(projectDependencies),
45
+ category,
46
+ };
47
+
48
+ delete modifiedProject.dependencies;
49
+
50
+ return modifiedProject;
51
+ });
52
+
53
+ return projectsWithFilteredDependencies;
54
+ };
55
+
56
+ export default mapDependencies;
@@ -0,0 +1,74 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { COLORS, PACKAGE_FILES, PROJECT_TYPES } from '../../constants.js';
3
+ import Logger from '../components/consoleLogger.js';
4
+
5
+ export const readProject = async (currentDir, dirContent, path, root) => {
6
+ const { JS, PHP } = PROJECT_TYPES;
7
+ const { PACKAGE_JSON_FILE, COMPOSER_JSON_FILE } = PACKAGE_FILES;
8
+
9
+ let packageInfo = {};
10
+ // Check if current directory contains package.json or composer.json
11
+ const names = new Set(dirContent.map((e) => e.name));
12
+
13
+ const packageFiles = dirContent.filter(
14
+ (e) =>
15
+ e.isFile() &&
16
+ (e.name === PACKAGE_JSON_FILE || e.name === COMPOSER_JSON_FILE),
17
+ );
18
+
19
+ if (packageFiles.length === 0) return;
20
+
21
+ let version = null;
22
+ let dependencies = {};
23
+ let name;
24
+ let type;
25
+ let packageFileNames = [];
26
+
27
+ // If there is a composer file, it's php
28
+ if (names.has(COMPOSER_JSON_FILE)) type = PHP;
29
+ else type = JS;
30
+
31
+ // Read and parse each package file and check if there is a version (indicating it's a project)
32
+ for (let file of packageFiles) {
33
+ let content = await readFile(path.join(currentDir, file.name), 'utf-8');
34
+ try {
35
+ let json = JSON.parse(content);
36
+ version = json.version;
37
+ packageFileNames.push(file.name);
38
+
39
+ if (version) {
40
+ if (file.name === PACKAGE_JSON_FILE) {
41
+ if (type == JS) name = json.name;
42
+ dependencies =
43
+ {
44
+ ...dependencies,
45
+ ...json.dependencies,
46
+ ...json.peerDependencies,
47
+ } || {};
48
+ } else if (file.name === COMPOSER_JSON_FILE) {
49
+ name = json.name;
50
+
51
+ dependencies = { ...dependencies, ...json.require } || {};
52
+ }
53
+ }
54
+ } catch (err) {
55
+ Logger.log(`Error parsing JSON in ${file.name}: ${err}`, COLORS.RED);
56
+ }
57
+ }
58
+ if (version && name) {
59
+ packageInfo = {
60
+ displayName: `${name} (${type})`,
61
+ originalName: name,
62
+ version,
63
+ dependencies,
64
+ path: currentDir,
65
+ packageFileNames,
66
+ type,
67
+ };
68
+
69
+ // Do not recurse into this directory any further (treat it as a project root).
70
+ return packageInfo;
71
+ }
72
+ };
73
+
74
+ export default readProject;
@@ -0,0 +1,14 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import path from 'path';
3
+
4
+ const saveDependencies = (mappedProjects) => {
5
+ const outputPath = path.resolve(process.cwd(), '.lavina', 'deps.json');
6
+
7
+ // Make sure /data directory exists
8
+ mkdirSync(path.dirname(outputPath), { recursive: true });
9
+
10
+ writeFileSync(outputPath, JSON.stringify(mappedProjects, null, 2), 'utf-8');
11
+ return outputPath;
12
+ };
13
+
14
+ export default saveDependencies;
@@ -0,0 +1,44 @@
1
+ const IGNORED_DIRS = new Set([
2
+ 'node_modules',
3
+ 'vendor',
4
+ '.git',
5
+ 'dist',
6
+ 'build',
7
+ ]);
8
+ import path from 'path';
9
+ import { readdir } from 'fs/promises';
10
+ import { readProject } from './readProject.js';
11
+ import { COLORS } from '../../constants.js';
12
+ import Logger from '../components/consoleLogger.js';
13
+
14
+ const scanDirForProjects = async (rootDir) => {
15
+ const root = path.resolve(rootDir);
16
+ let projects = [];
17
+
18
+ async function walk(currentDir) {
19
+ Logger.log(`Scanning ${currentDir}`, COLORS.WHITE);
20
+ let dirContent;
21
+ try {
22
+ dirContent = await readdir(currentDir, { withFileTypes: true });
23
+ } catch (err) {
24
+ Logger.log(`Directory not readable: ${err}`, COLORS.RED);
25
+ return;
26
+ }
27
+ const packageInfo = await readProject(currentDir, dirContent, path, root);
28
+ if (packageInfo) {
29
+ projects.push(packageInfo);
30
+ } else {
31
+ // Otherwise recurse into subdirectories (skipping ignored ones)
32
+ for (const entry of dirContent) {
33
+ if (!entry.isDirectory() || entry.name.indexOf('.') == 0) continue;
34
+ if (IGNORED_DIRS.has(entry.name)) continue;
35
+ await walk(path.join(currentDir, entry.name));
36
+ }
37
+ }
38
+ }
39
+
40
+ await walk(root);
41
+ return projects;
42
+ };
43
+
44
+ export default scanDirForProjects;
@@ -0,0 +1,23 @@
1
+ export const COLORS = {
2
+ WHITE: 'white',
3
+ GREEN: 'green',
4
+ RED: 'red',
5
+ YELLOW: 'yellow',
6
+ };
7
+
8
+ export const PROJECT_TYPES = {
9
+ JS: 'js',
10
+ PHP: 'php',
11
+ };
12
+
13
+ export const PACKAGE_FILES = {
14
+ COMPOSER_JSON_FILE: 'composer.json',
15
+ PACKAGE_JSON_FILE: 'package.json',
16
+ };
17
+
18
+ export const SUPPORTED_PROJECT_TYPES = [PROJECT_TYPES.JS, PROJECT_TYPES.PHP];
19
+
20
+ export const PACKAGE_DEPENDENCY_FIELDS = {
21
+ PACKAGE_JSON_FILE: 'dependencies',
22
+ COMPOSER_JSON_FILE: 'require',
23
+ };