devcompass 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/LICENSE +0 -0
- package/README.md +41 -0
- package/bin/devcompass.js +25 -0
- package/package.json +50 -0
- package/src/analyzers/outdated.js +52 -0
- package/src/analyzers/scoring.js +37 -0
- package/src/analyzers/unused-deps.js +51 -0
- package/src/commands/analyze.js +188 -0
- package/src/index.js +0 -0
- package/src/utils/logger.js +36 -0
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# DevCompass š§
|
|
2
|
+
|
|
3
|
+
> Health check for your JavaScript project
|
|
4
|
+
|
|
5
|
+
Find unused dependencies, outdated packages, and calculate your project's health score in seconds.
|
|
6
|
+
|
|
7
|
+
## š Quick Start
|
|
8
|
+
```bash
|
|
9
|
+
npx devcompass analyze
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## ⨠Features
|
|
13
|
+
|
|
14
|
+
- š Find unused dependencies
|
|
15
|
+
- š¦ Check for outdated packages
|
|
16
|
+
- š Calculate health score (0-10)
|
|
17
|
+
- š” Get actionable recommendations
|
|
18
|
+
- ā” Fast analysis (2-3 seconds)
|
|
19
|
+
|
|
20
|
+
## š¦ Installation
|
|
21
|
+
|
|
22
|
+
### Option 1: npx (Recommended)
|
|
23
|
+
```bash
|
|
24
|
+
npx devcompass analyze
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Option 2: Global Installation
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g devcompass
|
|
30
|
+
devcompass analyze
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## š ļø Requirements
|
|
34
|
+
|
|
35
|
+
- Node.js >= 16.0.0
|
|
36
|
+
- npm >= 7.0.0
|
|
37
|
+
- A project with `package.json`
|
|
38
|
+
|
|
39
|
+
## š License
|
|
40
|
+
|
|
41
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { analyze } = require('../src/commands/analyze');
|
|
6
|
+
const packageJson = require('../package.json');
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('devcompass')
|
|
12
|
+
.description('Health check for your JavaScript project')
|
|
13
|
+
.version(packageJson.version, '-v, --version', 'Display version information')
|
|
14
|
+
.addHelpText('after', `
|
|
15
|
+
${chalk.gray('Author:')} Ajay Thorat
|
|
16
|
+
${chalk.gray('GitHub:')} ${chalk.cyan('https://github.com/AjayBThorat-20/devcompass')}
|
|
17
|
+
`);
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('analyze')
|
|
21
|
+
.description('Analyze your project dependencies')
|
|
22
|
+
.option('-p, --path <path>', 'Project path', process.cwd())
|
|
23
|
+
.action(analyze);
|
|
24
|
+
|
|
25
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devcompass",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Analyze your JavaScript projects for unused dependencies and outdated packages",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"devcompass": "./bin/devcompass.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"dependencies",
|
|
20
|
+
"npm",
|
|
21
|
+
"outdated",
|
|
22
|
+
"unused",
|
|
23
|
+
"analyzer",
|
|
24
|
+
"health",
|
|
25
|
+
"cli",
|
|
26
|
+
"devtools",
|
|
27
|
+
"package-manager",
|
|
28
|
+
"dependency-analysis"
|
|
29
|
+
],
|
|
30
|
+
"author": "Ajay Thorat <ajaythorat988@gmail.com>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"chalk": "^4.1.2",
|
|
34
|
+
"commander": "^11.1.0",
|
|
35
|
+
"depcheck": "^1.4.7",
|
|
36
|
+
"npm-check-updates": "^16.14.12",
|
|
37
|
+
"ora": "^5.4.1"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=14.0.0"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/AjayBThorat-20/devcompass.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/AjayBThorat-20/devcompass/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/AjayBThorat-20/devcompass#readme"
|
|
50
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const ncu = require('npm-check-updates');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
async function findOutdatedDeps(projectPath, dependencies) {
|
|
5
|
+
try {
|
|
6
|
+
const upgraded = await ncu.run({
|
|
7
|
+
packageFile: path.join(projectPath, 'package.json'),
|
|
8
|
+
upgrade: false,
|
|
9
|
+
silent: true,
|
|
10
|
+
jsonUpgraded: true,
|
|
11
|
+
timeout: 30000,
|
|
12
|
+
dep: 'prod,dev'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const outdatedDeps = Object.entries(upgraded || {}).map(
|
|
16
|
+
([name, latest]) => {
|
|
17
|
+
const current = (dependencies[name] || '')
|
|
18
|
+
.replace(/^[\^~]/, '');
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
name,
|
|
22
|
+
current,
|
|
23
|
+
latest,
|
|
24
|
+
versionsBehind: getVersionDiff(current, latest)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return outdatedDeps;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Error in findOutdatedDeps:', error.message);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getVersionDiff(current, latest) {
|
|
37
|
+
const currParts = current.split('.').map(Number);
|
|
38
|
+
const latestParts = latest.split('.').map(Number);
|
|
39
|
+
|
|
40
|
+
while (currParts.length < 3) currParts.push(0);
|
|
41
|
+
while (latestParts.length < 3) latestParts.push(0);
|
|
42
|
+
|
|
43
|
+
if (currParts[0] !== latestParts[0]) {
|
|
44
|
+
return 'major update';
|
|
45
|
+
} else if (currParts[1] !== latestParts[1]) {
|
|
46
|
+
return 'minor update';
|
|
47
|
+
} else {
|
|
48
|
+
return 'patch update';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { findOutdatedDeps };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function calculateScore(totalDeps, unusedCount, outdatedCount) {
|
|
2
|
+
let score = 10;
|
|
3
|
+
|
|
4
|
+
if (totalDeps === 0) {
|
|
5
|
+
return {
|
|
6
|
+
total: 10.0,
|
|
7
|
+
breakdown: {
|
|
8
|
+
unused: 0,
|
|
9
|
+
outdated: 0,
|
|
10
|
+
unusedPenalty: 0,
|
|
11
|
+
outdatedPenalty: 0
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const unusedRatio = unusedCount / totalDeps;
|
|
17
|
+
const unusedPenalty = unusedRatio * 4;
|
|
18
|
+
score -= unusedPenalty;
|
|
19
|
+
|
|
20
|
+
const outdatedRatio = outdatedCount / totalDeps;
|
|
21
|
+
const outdatedPenalty = outdatedRatio * 3;
|
|
22
|
+
score -= outdatedPenalty;
|
|
23
|
+
|
|
24
|
+
score = Math.max(0, Math.min(10, score));
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
total: parseFloat(score.toFixed(1)),
|
|
28
|
+
breakdown: {
|
|
29
|
+
unused: unusedCount,
|
|
30
|
+
outdated: outdatedCount,
|
|
31
|
+
unusedPenalty: parseFloat(unusedPenalty.toFixed(1)),
|
|
32
|
+
outdatedPenalty: parseFloat(outdatedPenalty.toFixed(1))
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { calculateScore };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const depcheck = require('depcheck');
|
|
2
|
+
|
|
3
|
+
async function findUnusedDeps(projectPath, dependencies) {
|
|
4
|
+
const options = {
|
|
5
|
+
ignoreBinPackage: false,
|
|
6
|
+
skipMissing: true,
|
|
7
|
+
ignorePatterns: [
|
|
8
|
+
'node_modules/**',
|
|
9
|
+
'dist/**',
|
|
10
|
+
'build/**',
|
|
11
|
+
'.next/**',
|
|
12
|
+
'coverage/**',
|
|
13
|
+
'out/**',
|
|
14
|
+
'*.min.js'
|
|
15
|
+
],
|
|
16
|
+
ignoreMatches: [
|
|
17
|
+
'react',
|
|
18
|
+
'react-dom',
|
|
19
|
+
'react-native',
|
|
20
|
+
'next',
|
|
21
|
+
'@angular/core',
|
|
22
|
+
'@angular/common',
|
|
23
|
+
'@angular/platform-browser',
|
|
24
|
+
'@nestjs/core',
|
|
25
|
+
'@nestjs/common',
|
|
26
|
+
'@nestjs/platform-express',
|
|
27
|
+
'typescript',
|
|
28
|
+
'@types/*',
|
|
29
|
+
'webpack',
|
|
30
|
+
'vite',
|
|
31
|
+
'rollup',
|
|
32
|
+
'esbuild',
|
|
33
|
+
'jest',
|
|
34
|
+
'vitest',
|
|
35
|
+
'mocha',
|
|
36
|
+
'@testing-library/*'
|
|
37
|
+
]
|
|
38
|
+
// REMOVED parsers - let depcheck use defaults
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const results = await depcheck(projectPath, options);
|
|
43
|
+
const unusedDeps = results.dependencies.map(name => ({ name }));
|
|
44
|
+
return unusedDeps;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error in findUnusedDeps:', error.message);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { findUnusedDeps };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// src/commands/analyze.js
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const { findUnusedDeps } = require('../analyzers/unused-deps');
|
|
8
|
+
const { findOutdatedDeps } = require('../analyzers/outdated');
|
|
9
|
+
const { calculateScore } = require('../analyzers/scoring');
|
|
10
|
+
const {
|
|
11
|
+
log,
|
|
12
|
+
logSection,
|
|
13
|
+
logDivider,
|
|
14
|
+
getScoreColor
|
|
15
|
+
} = require('../utils/logger');
|
|
16
|
+
|
|
17
|
+
async function analyze(options) {
|
|
18
|
+
const projectPath = options.path || process.cwd();
|
|
19
|
+
|
|
20
|
+
console.log('\n');
|
|
21
|
+
log(chalk.cyan.bold('š DevCompass v1.0.0') + ' - Analyzing your project...\n');
|
|
22
|
+
|
|
23
|
+
const spinner = ora({
|
|
24
|
+
text: 'Loading project...',
|
|
25
|
+
color: 'cyan'
|
|
26
|
+
}).start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
32
|
+
spinner.fail(chalk.red('No package.json found in current directory'));
|
|
33
|
+
console.log(chalk.yellow('\nš” Make sure you run this command in a project directory.\n'));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let packageJson;
|
|
38
|
+
try {
|
|
39
|
+
const fileContent = fs.readFileSync(packageJsonPath, 'utf8');
|
|
40
|
+
packageJson = JSON.parse(fileContent);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
spinner.fail(chalk.red('Invalid package.json format'));
|
|
43
|
+
console.log(chalk.yellow(`\nš” Error: ${error.message}\n`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const dependencies = {
|
|
48
|
+
...(packageJson.dependencies || {}),
|
|
49
|
+
...(packageJson.devDependencies || {})
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const totalDeps = Object.keys(dependencies).length;
|
|
53
|
+
|
|
54
|
+
if (totalDeps === 0) {
|
|
55
|
+
spinner.succeed(chalk.green('No dependencies found'));
|
|
56
|
+
console.log(chalk.gray('\n⨠Nothing to analyze - your project has no dependencies.\n'));
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
spinner.text = 'Detecting unused dependencies...';
|
|
61
|
+
|
|
62
|
+
let unusedDeps = [];
|
|
63
|
+
try {
|
|
64
|
+
unusedDeps = await findUnusedDeps(projectPath, dependencies);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.log(chalk.yellow('\nā ļø Could not detect unused dependencies'));
|
|
67
|
+
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
spinner.text = 'Checking for outdated packages...';
|
|
71
|
+
|
|
72
|
+
let outdatedDeps = [];
|
|
73
|
+
try {
|
|
74
|
+
outdatedDeps = await findOutdatedDeps(projectPath, dependencies);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.log(chalk.yellow('\nā ļø Could not check for outdated packages'));
|
|
77
|
+
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const score = calculateScore(
|
|
81
|
+
totalDeps,
|
|
82
|
+
unusedDeps.length,
|
|
83
|
+
outdatedDeps.length
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
spinner.succeed(chalk.green(`Scanned ${totalDeps} dependencies in project`));
|
|
87
|
+
|
|
88
|
+
displayResults(unusedDeps, outdatedDeps, score, totalDeps);
|
|
89
|
+
|
|
90
|
+
} catch (error) {
|
|
91
|
+
spinner.fail(chalk.red('Analysis failed'));
|
|
92
|
+
console.log(chalk.red(`\nā Error: ${error.message}\n`));
|
|
93
|
+
if (process.env.DEBUG) {
|
|
94
|
+
console.log(error.stack);
|
|
95
|
+
}
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function displayResults(unusedDeps, outdatedDeps, score, totalDeps) {
|
|
101
|
+
logDivider();
|
|
102
|
+
|
|
103
|
+
if (unusedDeps.length > 0) {
|
|
104
|
+
logSection('š“ UNUSED DEPENDENCIES', unusedDeps.length);
|
|
105
|
+
|
|
106
|
+
const displayCount = Math.min(5, unusedDeps.length);
|
|
107
|
+
unusedDeps.slice(0, displayCount).forEach(dep => {
|
|
108
|
+
log(` ${chalk.red('ā')} ${dep.name}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (unusedDeps.length > 5) {
|
|
112
|
+
log(chalk.gray(`\n ... and ${unusedDeps.length - 5} more\n`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
log(chalk.gray('\n Why marked unused:'));
|
|
116
|
+
log(chalk.gray(' ⢠No import/require found in source files'));
|
|
117
|
+
log(chalk.gray(' ⢠Excludes node_modules, build folders'));
|
|
118
|
+
log(chalk.gray(' ⢠May include false positives - verify before removing\n'));
|
|
119
|
+
} else {
|
|
120
|
+
logSection('ā
UNUSED DEPENDENCIES');
|
|
121
|
+
log(chalk.green(' No unused dependencies detected!\n'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logDivider();
|
|
125
|
+
|
|
126
|
+
if (outdatedDeps.length > 0) {
|
|
127
|
+
logSection('š” OUTDATED PACKAGES', outdatedDeps.length);
|
|
128
|
+
|
|
129
|
+
const displayCount = Math.min(5, outdatedDeps.length);
|
|
130
|
+
outdatedDeps.slice(0, displayCount).forEach(dep => {
|
|
131
|
+
const nameCol = dep.name.padEnd(20);
|
|
132
|
+
const currentVer = chalk.yellow(dep.current);
|
|
133
|
+
const arrow = chalk.gray('ā');
|
|
134
|
+
const latestVer = chalk.green(dep.latest);
|
|
135
|
+
const updateType = chalk.gray(`(${dep.versionsBehind})`);
|
|
136
|
+
|
|
137
|
+
log(` ${nameCol} ${currentVer} ${arrow} ${latestVer} ${updateType}`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (outdatedDeps.length > 5) {
|
|
141
|
+
log(chalk.gray(`\n ... and ${outdatedDeps.length - 5} more\n`));
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
logSection('ā
OUTDATED PACKAGES');
|
|
145
|
+
log(chalk.green(' All packages are up to date!\n'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
logDivider();
|
|
149
|
+
|
|
150
|
+
logSection('š PROJECT HEALTH');
|
|
151
|
+
|
|
152
|
+
const scoreColor = getScoreColor(score.total);
|
|
153
|
+
log(` Overall Score: ${scoreColor(score.total + '/10')}`);
|
|
154
|
+
log(` Total Dependencies: ${chalk.cyan(totalDeps)}`);
|
|
155
|
+
log(` Unused: ${chalk.red(unusedDeps.length)}`);
|
|
156
|
+
log(` Outdated: ${chalk.yellow(outdatedDeps.length)}\n`);
|
|
157
|
+
|
|
158
|
+
logDivider();
|
|
159
|
+
|
|
160
|
+
if (unusedDeps.length > 0) {
|
|
161
|
+
logSection('š” QUICK WIN');
|
|
162
|
+
log(' Clean up unused dependencies:\n');
|
|
163
|
+
|
|
164
|
+
const packagesToRemove = unusedDeps
|
|
165
|
+
.slice(0, 5)
|
|
166
|
+
.map(d => d.name)
|
|
167
|
+
.join(' ');
|
|
168
|
+
|
|
169
|
+
log(chalk.cyan(` npm uninstall ${packagesToRemove}\n`));
|
|
170
|
+
|
|
171
|
+
log(' Expected impact:');
|
|
172
|
+
log(` ${chalk.green('ā')} Remove ${unusedDeps.length} unused package${unusedDeps.length > 1 ? 's' : ''}`);
|
|
173
|
+
log(` ${chalk.green('ā')} Reduce node_modules size`);
|
|
174
|
+
|
|
175
|
+
const improvedScore = calculateScore(
|
|
176
|
+
totalDeps - unusedDeps.length,
|
|
177
|
+
0,
|
|
178
|
+
outdatedDeps.length
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const improvedScoreColor = getScoreColor(improvedScore.total);
|
|
182
|
+
log(` ${chalk.green('ā')} Improve health score ā ${improvedScoreColor(improvedScore.total + '/10')}\n`);
|
|
183
|
+
|
|
184
|
+
logDivider();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { analyze };
|
package/src/index.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
function log(message) {
|
|
4
|
+
console.log(message);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function logSection(title, count) {
|
|
8
|
+
const countStr = count !== undefined
|
|
9
|
+
? chalk.gray(` (${count})`)
|
|
10
|
+
: '';
|
|
11
|
+
log(chalk.bold(`\n${title}${countStr}\n`));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function logDivider() {
|
|
15
|
+
const line = 'ā'.repeat(70);
|
|
16
|
+
log(chalk.gray(line) + '\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getScoreColor(score) {
|
|
20
|
+
if (score >= 8) {
|
|
21
|
+
return chalk.green.bold;
|
|
22
|
+
} else if (score >= 6) {
|
|
23
|
+
return chalk.yellow.bold;
|
|
24
|
+
} else {
|
|
25
|
+
return chalk.red.bold;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
log,
|
|
31
|
+
logSection,
|
|
32
|
+
logDivider,
|
|
33
|
+
getScoreColor
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
|