entkapp 4.1.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/.scaffold-ignore +22 -0
- package/LICENSE +211 -0
- package/NOTICE +13 -0
- package/README.md +33 -0
- package/bin/cli.js +174 -0
- package/entkapp/config.json +42 -0
- package/entkapp/plugins/README.md +19 -0
- package/index.js +2254 -0
- package/logo.png +0 -0
- package/package.json +96 -0
- package/src/EngineContext.js +185 -0
- package/src/api/HeadlessAPI.js +376 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/ast/ASTAnalyzer.js +492 -0
- package/src/ast/BarrelParser.js +221 -0
- package/src/ast/DeadCodeDetector.js +73 -0
- package/src/ast/MagicDetector.js +203 -0
- package/src/ast/OxcAnalyzer.js +337 -0
- package/src/ast/SecretScanner.js +304 -0
- package/src/healing/GitSandbox.js +82 -0
- package/src/healing/SelfHealer.js +52 -0
- package/src/index.js +723 -0
- package/src/performance/GraphCache.js +87 -0
- package/src/performance/SupplyChainGuard.js +92 -0
- package/src/performance/WorkerPool.js +109 -0
- package/src/plugins/BasePlugin.js +71 -0
- package/src/plugins/KnipAdapter.js +106 -0
- package/src/plugins/PluginRegistry.js +197 -0
- package/src/plugins/ecosystems/BackendServices.js +61 -0
- package/src/plugins/ecosystems/GenericPlugins.js +64 -0
- package/src/plugins/ecosystems/ModernFrameworks.js +159 -0
- package/src/plugins/ecosystems/MorePlugins.js +184 -0
- package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
- package/src/plugins/ecosystems/PluginLoader.js +20 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
- package/src/refractor/ImpactAnalyzer.js +92 -0
- package/src/refractor/SourceRewriter.js +86 -0
- package/src/refractor/TransactionManager.js +138 -0
- package/src/refractor/TypeIntegrity.js +75 -0
- package/src/resolution/CircularDetector.js +91 -0
- package/src/resolution/ConfigLoader.js +87 -0
- package/src/resolution/DepencyResolver.js +133 -0
- package/src/resolution/DependencyProfiler.js +268 -0
- package/src/resolution/PathMapper.js +125 -0
- package/src/resolution/WorkSpaceGraph.js +474 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Advanced Dependency Profiling Engine.
|
|
6
|
+
* Traces Peer Dependencies and Implicit Tooling Invocations.
|
|
7
|
+
* Now supports "Unused Binaries" detection.
|
|
8
|
+
*/
|
|
9
|
+
export class DependencyProfiler {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
|
|
13
|
+
this.binaryToPackageMap = {
|
|
14
|
+
'tsc': 'typescript',
|
|
15
|
+
'ts-node': 'ts-node',
|
|
16
|
+
'tsx': 'tsx',
|
|
17
|
+
'node': 'node',
|
|
18
|
+
'bun': 'bun',
|
|
19
|
+
'deno': 'deno',
|
|
20
|
+
'jest': 'jest',
|
|
21
|
+
'vitest': 'vitest',
|
|
22
|
+
'mocha': 'mocha',
|
|
23
|
+
'jasmine': 'jasmine',
|
|
24
|
+
'ava': 'ava',
|
|
25
|
+
'tap': 'tap',
|
|
26
|
+
'uvu': 'uvu',
|
|
27
|
+
'c8': 'c8',
|
|
28
|
+
'nyc': 'nyc',
|
|
29
|
+
'playwright': '@playwright/test',
|
|
30
|
+
'cypress': 'cypress',
|
|
31
|
+
'puppeteer': 'puppeteer',
|
|
32
|
+
'webdriverio': 'webdriverio',
|
|
33
|
+
'wdio': '@wdio/cli',
|
|
34
|
+
'eslint': 'eslint',
|
|
35
|
+
'prettier': 'prettier',
|
|
36
|
+
'tslint': 'tslint',
|
|
37
|
+
'biome': '@biomejs/biome',
|
|
38
|
+
'oxlint': 'oxlint',
|
|
39
|
+
'stylelint': 'stylelint',
|
|
40
|
+
'markdownlint': 'markdownlint-cli',
|
|
41
|
+
'commitlint': '@commitlint/cli',
|
|
42
|
+
'lint-staged': 'lint-staged',
|
|
43
|
+
'vite': 'vite',
|
|
44
|
+
'webpack': 'webpack',
|
|
45
|
+
'rollup': 'rollup',
|
|
46
|
+
'esbuild': 'esbuild',
|
|
47
|
+
'parcel': 'parcel',
|
|
48
|
+
'turbo': 'turbo',
|
|
49
|
+
'nx': 'nx',
|
|
50
|
+
'lerna': 'lerna',
|
|
51
|
+
'changesets': '@changesets/cli',
|
|
52
|
+
'changeset': '@changesets/cli',
|
|
53
|
+
'tsup': 'tsup',
|
|
54
|
+
'unbuild': 'unbuild',
|
|
55
|
+
'pkgroll': 'pkgroll',
|
|
56
|
+
'microbundle': 'microbundle',
|
|
57
|
+
'ncc': '@vercel/ncc',
|
|
58
|
+
'swc': '@swc/cli',
|
|
59
|
+
'tailwind': 'tailwindcss',
|
|
60
|
+
'tailwindcss': 'tailwindcss',
|
|
61
|
+
'postcss': 'postcss',
|
|
62
|
+
'sass': 'sass',
|
|
63
|
+
'less': 'less',
|
|
64
|
+
'next': 'next',
|
|
65
|
+
'nuxt': 'nuxt',
|
|
66
|
+
'astro': 'astro',
|
|
67
|
+
'remix': '@remix-run/dev',
|
|
68
|
+
'svelte-kit': '@sveltejs/kit',
|
|
69
|
+
'expo': 'expo',
|
|
70
|
+
'react-scripts': 'react-scripts',
|
|
71
|
+
'ng': '@angular/cli',
|
|
72
|
+
'vue': '@vue/cli-service',
|
|
73
|
+
'gatsby': 'gatsby',
|
|
74
|
+
'nodemon': 'nodemon',
|
|
75
|
+
'ts-node-dev': 'ts-node-dev',
|
|
76
|
+
'concurrently': 'concurrently',
|
|
77
|
+
'cross-env': 'cross-env',
|
|
78
|
+
'dotenv': 'dotenv-cli',
|
|
79
|
+
'dotenv-cli': 'dotenv-cli',
|
|
80
|
+
'rimraf': 'rimraf',
|
|
81
|
+
'del-cli': 'del-cli',
|
|
82
|
+
'copyfiles': 'copyfiles',
|
|
83
|
+
'cpy-cli': 'cpy-cli',
|
|
84
|
+
'mkdirp': 'mkdirp',
|
|
85
|
+
'shx': 'shx',
|
|
86
|
+
'npm-run-all': 'npm-run-all',
|
|
87
|
+
'run-p': 'npm-run-all',
|
|
88
|
+
'run-s': 'npm-run-all',
|
|
89
|
+
'typedoc': 'typedoc',
|
|
90
|
+
'jsdoc': 'jsdoc',
|
|
91
|
+
'storybook': 'storybook',
|
|
92
|
+
'sb': 'storybook',
|
|
93
|
+
'husky': 'husky',
|
|
94
|
+
'simple-git-hooks': 'simple-git-hooks',
|
|
95
|
+
'lefthook': 'lefthook',
|
|
96
|
+
'pnpm': 'pnpm',
|
|
97
|
+
'yarn': 'yarn',
|
|
98
|
+
'npm': 'npm',
|
|
99
|
+
'patch-package': 'patch-package',
|
|
100
|
+
'syncpack': 'syncpack',
|
|
101
|
+
'publint': 'publint',
|
|
102
|
+
'attw': '@arethetypeswrong/cli',
|
|
103
|
+
'size-limit': 'size-limit',
|
|
104
|
+
'bundlesize': 'bundlesize',
|
|
105
|
+
'depcheck': 'depcheck',
|
|
106
|
+
'knip': 'knip',
|
|
107
|
+
'entkapp': 'entkapp'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.configFileToPackageMap = {
|
|
111
|
+
'jest.config': 'jest',
|
|
112
|
+
'vitest.config': 'vitest',
|
|
113
|
+
'playwright.config': '@playwright/test',
|
|
114
|
+
'cypress.config': 'cypress',
|
|
115
|
+
'webpack.config': 'webpack',
|
|
116
|
+
'vite.config': 'vite',
|
|
117
|
+
'rollup.config': 'rollup',
|
|
118
|
+
'tailwind.config': 'tailwindcss',
|
|
119
|
+
'postcss.config': 'postcss',
|
|
120
|
+
'.eslintrc': 'eslint',
|
|
121
|
+
'eslint.config': 'eslint',
|
|
122
|
+
'.prettierrc': 'prettier',
|
|
123
|
+
'prettier.config': 'prettier',
|
|
124
|
+
'.babelrc': '@babel/core',
|
|
125
|
+
'babel.config': '@babel/core',
|
|
126
|
+
'.stylelintrc': 'stylelint',
|
|
127
|
+
'stylelint.config': 'stylelint',
|
|
128
|
+
'svelte.config': '@sveltejs/kit',
|
|
129
|
+
'astro.config': 'astro',
|
|
130
|
+
'nuxt.config': 'nuxt',
|
|
131
|
+
'next.config': 'next',
|
|
132
|
+
'remix.config': '@remix-run/dev',
|
|
133
|
+
'.commitlintrc': '@commitlint/cli',
|
|
134
|
+
'commitlint.config': '@commitlint/cli',
|
|
135
|
+
'tsup.config': 'tsup',
|
|
136
|
+
'typedoc': 'typedoc',
|
|
137
|
+
'.lintstagedrc': 'lint-staged',
|
|
138
|
+
'lint-staged.config': 'lint-staged',
|
|
139
|
+
'lefthook': 'lefthook',
|
|
140
|
+
'knip.config': 'knip',
|
|
141
|
+
'knip.json': 'knip'
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async traceImplicitInvocations(projectRoot) {
|
|
146
|
+
const usedPackages = new Set();
|
|
147
|
+
const usedBinaries = new Set(); // NEW: Track which binaries are actually used
|
|
148
|
+
|
|
149
|
+
// 1. Scan package.json scripts
|
|
150
|
+
try {
|
|
151
|
+
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
152
|
+
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
153
|
+
|
|
154
|
+
if (pkg.scripts) {
|
|
155
|
+
for (const script of Object.values(pkg.scripts)) {
|
|
156
|
+
this.extractPackagesFromScript(script, usedPackages, usedBinaries);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Detect @types/* usage
|
|
161
|
+
if (pkg.dependencies || pkg.devDependencies) {
|
|
162
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
163
|
+
for (const depName of Object.keys(allDeps)) {
|
|
164
|
+
if (depName.startsWith('@types/')) {
|
|
165
|
+
usedPackages.add(depName);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
|
|
171
|
+
// 2. Scan CI workflows
|
|
172
|
+
try {
|
|
173
|
+
const githubWorkflows = path.join(projectRoot, '.github/workflows');
|
|
174
|
+
const files = await fs.readdir(githubWorkflows).catch(() => []);
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
177
|
+
const content = await fs.readFile(path.join(githubWorkflows, file), 'utf8');
|
|
178
|
+
this.extractPackagesFromScript(content, usedPackages, usedBinaries);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {}
|
|
182
|
+
|
|
183
|
+
// 3. Scan config files
|
|
184
|
+
try {
|
|
185
|
+
const dirEntries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
186
|
+
for (const entry of dirEntries) {
|
|
187
|
+
if (!entry.isFile()) continue;
|
|
188
|
+
const fileName = entry.name;
|
|
189
|
+
for (const [fragment, pkgName] of Object.entries(this.configFileToPackageMap)) {
|
|
190
|
+
if (fileName.startsWith(fragment) || fileName === fragment) {
|
|
191
|
+
usedPackages.add(pkgName);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {}
|
|
197
|
+
|
|
198
|
+
// 4. Identify Unused Binaries
|
|
199
|
+
// Scan node_modules/.bin for all available binaries
|
|
200
|
+
try {
|
|
201
|
+
const binDir = path.join(projectRoot, 'node_modules', '.bin');
|
|
202
|
+
const availableBinaries = await fs.readdir(binDir).catch(() => []);
|
|
203
|
+
|
|
204
|
+
for (const bin of availableBinaries) {
|
|
205
|
+
// Skip hidden files or package manager internal bins
|
|
206
|
+
if (bin.startsWith('.') || ['npm', 'pnpm', 'yarn', 'bun'].includes(bin)) continue;
|
|
207
|
+
|
|
208
|
+
if (!usedBinaries.has(bin)) {
|
|
209
|
+
this.context.unusedBinaries.add(bin);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {}
|
|
213
|
+
|
|
214
|
+
return usedPackages;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
extractPackagesFromScript(script, packageCollector, binaryCollector) {
|
|
218
|
+
const words = script.split(/[\s&|;()\n\r\t]+/);
|
|
219
|
+
for (let i = 0; i < words.length; i++) {
|
|
220
|
+
const rawWord = words[i];
|
|
221
|
+
const cleanWord = rawWord.replace(/['"]/g, '').replace(/^(npx|pnpx|bunx|yarn|pnpm)\s+/, '');
|
|
222
|
+
|
|
223
|
+
if (this.binaryToPackageMap[cleanWord]) {
|
|
224
|
+
packageCollector.add(this.binaryToPackageMap[cleanWord]);
|
|
225
|
+
if (binaryCollector) binaryCollector.add(cleanWord);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ((rawWord === 'npx' || rawWord === 'pnpx' || rawWord === 'bunx') && i + 1 < words.length) {
|
|
230
|
+
const nextWord = words[i + 1].replace(/['"]/g, '');
|
|
231
|
+
if (this.binaryToPackageMap[nextWord]) {
|
|
232
|
+
packageCollector.add(this.binaryToPackageMap[nextWord]);
|
|
233
|
+
if (binaryCollector) binaryCollector.add(nextWord);
|
|
234
|
+
}
|
|
235
|
+
if (nextWord.startsWith('@') || nextWord.includes('/')) {
|
|
236
|
+
const pkgName = nextWord.split('/').slice(0, nextWord.startsWith('@') ? 2 : 1).join('/');
|
|
237
|
+
if (pkgName) packageCollector.add(pkgName);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async resolvePeerDependencies(usedPackages, projectRoot) {
|
|
244
|
+
const peerDeps = new Set();
|
|
245
|
+
const nodeModules = path.join(projectRoot, 'node_modules');
|
|
246
|
+
|
|
247
|
+
for (const pkgName of usedPackages) {
|
|
248
|
+
try {
|
|
249
|
+
const pkgJsonPath = path.join(nodeModules, pkgName, 'package.json');
|
|
250
|
+
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
251
|
+
if (pkg.peerDependencies) {
|
|
252
|
+
Object.keys(pkg.peerDependencies).forEach(dep => peerDeps.add(dep));
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {}
|
|
255
|
+
}
|
|
256
|
+
return peerDeps;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
shouldExcludeFromUnusedCheck(packageName, depType) {
|
|
260
|
+
if (depType === 'peerDependency' || depType === 'optionalDependency') {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (packageName.startsWith('@types/')) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Advanced TSConfig / JSConfig Compilation Path Alias Mapper
|
|
6
|
+
* Resolves deeply nested route mappings, wildcards, and base URL overrides.
|
|
7
|
+
*/
|
|
8
|
+
export class PathMapper {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.baseUrl = '.';
|
|
12
|
+
this.absoluteBaseUrl = context.cwd;
|
|
13
|
+
this.mappings = []; // Collection of { prefix, suffix, targets[] }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads, cleans, and indexes custom alias entries from tsconfig.json files.
|
|
18
|
+
* @param {string} tsconfigFilename - Target designator (typically tsconfig.json)
|
|
19
|
+
*/
|
|
20
|
+
async loadMappings(tsconfigFilename = 'tsconfig.json') {
|
|
21
|
+
const configPath = path.resolve(this.context.cwd, tsconfigFilename);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(configPath);
|
|
25
|
+
const rawText = await fs.readFile(configPath, 'utf8');
|
|
26
|
+
|
|
27
|
+
// Strip inline single-line and block comments before parsing
|
|
28
|
+
// Improved regex to handle more edge cases in tsconfig comments
|
|
29
|
+
const jsonCleanText = rawText
|
|
30
|
+
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1')
|
|
31
|
+
.replace(/,(\s*[\]}])/g, '$1'); // Remove trailing commas
|
|
32
|
+
|
|
33
|
+
const tsconfig = JSON.parse(jsonCleanText);
|
|
34
|
+
|
|
35
|
+
if (!tsconfig.compilerOptions) return;
|
|
36
|
+
|
|
37
|
+
const opts = tsconfig.compilerOptions;
|
|
38
|
+
|
|
39
|
+
// v6 Path Resolution Fix (Knip Issue #1794)
|
|
40
|
+
// Ensure baseUrl is correctly resolved relative to the tsconfig file location
|
|
41
|
+
const configDir = path.dirname(configPath);
|
|
42
|
+
if (opts.baseUrl) {
|
|
43
|
+
this.baseUrl = opts.baseUrl;
|
|
44
|
+
this.absoluteBaseUrl = path.resolve(configDir, this.baseUrl);
|
|
45
|
+
} else {
|
|
46
|
+
this.absoluteBaseUrl = configDir;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (opts.paths) {
|
|
50
|
+
for (const [aliasPattern, targetArrays] of Object.entries(opts.paths)) {
|
|
51
|
+
this.registerPatternRule(aliasPattern, targetArrays);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (this.context.verbose) {
|
|
56
|
+
console.warn(`⚠️ [PathMapper Override] Proceeding without custom path configurations. Source: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Registers structural lookup parameters for alias strings.
|
|
63
|
+
*/
|
|
64
|
+
registerPatternRule(pattern, targets) {
|
|
65
|
+
const wildcardIndex = pattern.indexOf('*');
|
|
66
|
+
|
|
67
|
+
if (wildcardIndex === -1) {
|
|
68
|
+
this.mappings.push({
|
|
69
|
+
isExact: true,
|
|
70
|
+
pattern,
|
|
71
|
+
targets: targets.map(t => path.normalize(t))
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const prefix = pattern.slice(0, wildcardIndex);
|
|
77
|
+
const suffix = pattern.slice(wildcardIndex + 1);
|
|
78
|
+
|
|
79
|
+
this.mappings.push({
|
|
80
|
+
isExact: false,
|
|
81
|
+
prefix,
|
|
82
|
+
suffix,
|
|
83
|
+
targets: targets.map(t => path.normalize(t))
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves a raw import specifier against mapped path patterns.
|
|
89
|
+
* @param {string} specifier - Raw text from import declaration (e.g., '@ui/button')
|
|
90
|
+
* @returns {Array<string>} Candidates of absolute filesystem paths to try resolving
|
|
91
|
+
*/
|
|
92
|
+
resolveCandidatePaths(specifier) {
|
|
93
|
+
const matchingCandidates = [];
|
|
94
|
+
|
|
95
|
+
for (const rule of this.mappings) {
|
|
96
|
+
if (rule.isExact) {
|
|
97
|
+
if (specifier === rule.pattern) {
|
|
98
|
+
rule.targets.forEach(target => {
|
|
99
|
+
matchingCandidates.push(path.resolve(this.absoluteBaseUrl, target));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// Evaluate wildcard pattern matches
|
|
104
|
+
if (specifier.startsWith(rule.prefix) && specifier.endsWith(rule.suffix)) {
|
|
105
|
+
const extractedWildcardContent = specifier.slice(
|
|
106
|
+
rule.prefix.length,
|
|
107
|
+
specifier.length - rule.suffix.length
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
rule.targets.forEach(targetTemplate => {
|
|
111
|
+
const interpolatedTarget = targetTemplate.replace('*', extractedWildcardContent);
|
|
112
|
+
matchingCandidates.push(path.resolve(this.absoluteBaseUrl, interpolatedTarget));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fall back to direct lookup relative to the base URL
|
|
119
|
+
if (!specifier.startsWith('.') && !path.isAbsolute(specifier)) {
|
|
120
|
+
matchingCandidates.push(path.resolve(this.absoluteBaseUrl, specifier));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return matchingCandidates;
|
|
124
|
+
}
|
|
125
|
+
}
|