entkapp 4.5.1 โ 5.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/bin/cli.js +4 -4
- package/docs.zip +0 -0
- package/entkapp/config.json +0 -1
- package/package.json +5 -6
- package/src/EngineContext.js +276 -78
- package/src/analyzers/OxcAnalyzer.js +8 -380
- package/src/api/HeadlessAPI.js +1 -1
- package/src/api/PluginSDK.js +23 -187
- package/src/ast/ASTAnalyzer.js +467 -253
- package/src/ast/AdvancedAnalysis.js +6 -5
- package/src/ast/BarrelParser.js +23 -16
- package/src/ast/DeadCodeDetector.js +30 -18
- package/src/ast/MagicDetector.js +1 -1
- package/src/ast/OxcAnalyzer.js +329 -273
- package/src/index.js +272 -361
- package/src/performance/GraphCache.js +21 -2
- package/src/performance/WorkerPool.js +11 -1
- package/src/performance/WorkerTaskRunner.js +72 -25
- package/src/plugins/PluginRegistry.js +5 -16
- package/src/plugins/ecosystems/GenericPlugins.js +61 -0
- package/src/refractor/TransactionManager.js +3 -136
- package/src/refractor/TypeIntegrity.js +2 -73
- package/src/resolution/CircularDetector.js +27 -66
- package/src/resolution/ConfigLoader.js +2 -85
- package/src/resolution/DepencyResolver.js +20 -124
- package/src/resolution/EntryPointDetector.js +134 -0
- package/src/resolution/PathMapper.js +3 -123
- package/src/resolution/TSConfigLoader.js +76 -0
- package/src/resolution/WorkSpaceGraph.js +4 -473
- package/src/resolution/WorkspaceDiagnostic.js +3 -57
- package/src/plugins/KnipAdapter.js +0 -106
|
@@ -9,7 +9,24 @@ import crypto from 'crypto';
|
|
|
9
9
|
export class IncrementalCacheManager {
|
|
10
10
|
constructor(context) {
|
|
11
11
|
this.context = context;
|
|
12
|
-
this.
|
|
12
|
+
this.cacheDir = context.cacheDir || path.join(context.cwd, '.entkapp-cache');
|
|
13
|
+
this.manifestPath = path.join(this.cacheDir, 'graph-manifest.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clears the entire cache directory to ensure a fresh analysis run.
|
|
18
|
+
*/
|
|
19
|
+
async clearCache() {
|
|
20
|
+
try {
|
|
21
|
+
await fs.rm(this.cacheDir, { recursive: true, force: true });
|
|
22
|
+
if (this.context.verbose) {
|
|
23
|
+
console.log(`๐งน Cache cleared at: ${this.cacheDir}`);
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (this.context.verbose) {
|
|
27
|
+
console.error(`๐จ Failed to clear cache: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
/**
|
|
@@ -68,7 +85,9 @@ export class IncrementalCacheManager {
|
|
|
68
85
|
symbolSourceLocations: Object.fromEntries(node.symbolSourceLocations),
|
|
69
86
|
externalPackageUsage: Array.from(node.externalPackageUsage),
|
|
70
87
|
isEntry: node.isEntry,
|
|
71
|
-
isFrameworkComponent: node.isFrameworkComponent
|
|
88
|
+
isFrameworkComponent: node.isFrameworkComponent,
|
|
89
|
+
calculatedDynamicImports: node.calculatedDynamicImports || [],
|
|
90
|
+
globImports: node.globImports || []
|
|
72
91
|
};
|
|
73
92
|
}
|
|
74
93
|
|
|
@@ -51,7 +51,9 @@ export class WorkerPool {
|
|
|
51
51
|
// Merge thread structural subsets back into the primary context graph nodes
|
|
52
52
|
analyticalResultsSubsets.flat().forEach(result => {
|
|
53
53
|
if (!result) return;
|
|
54
|
-
|
|
54
|
+
// UPGRADE: Normalize filePath before merging to prevent duplicate nodes in the graph
|
|
55
|
+
const normalizedPath = path.resolve(this.context.cwd, result.filePath).replace(/\\/g, '/');
|
|
56
|
+
const node = masterEngineInstanceReference.context.getOrCreateNode(normalizedPath);
|
|
55
57
|
|
|
56
58
|
result.explicitImports.forEach(i => node.explicitImports.add(i));
|
|
57
59
|
result.dynamicImports.forEach(i => node.dynamicImports.add(i));
|
|
@@ -85,6 +87,14 @@ export class WorkerPool {
|
|
|
85
87
|
if (result.isEntry) node.isEntry = true;
|
|
86
88
|
if (result.isLibraryEntry) node.isLibraryEntry = true;
|
|
87
89
|
if (result.isFrameworkComponent) node.isFrameworkComponent = true;
|
|
90
|
+
if (result.calculatedDynamicImports) {
|
|
91
|
+
if (!node.calculatedDynamicImports) node.calculatedDynamicImports = [];
|
|
92
|
+
result.calculatedDynamicImports.forEach(i => node.calculatedDynamicImports.push(i));
|
|
93
|
+
}
|
|
94
|
+
if (result.globImports) {
|
|
95
|
+
if (!node.globImports) node.globImports = [];
|
|
96
|
+
result.globImports.forEach(i => node.globImports.push(i));
|
|
97
|
+
}
|
|
88
98
|
});
|
|
89
99
|
|
|
90
100
|
return true;
|
|
@@ -6,6 +6,7 @@ import { OxcAnalyzer } from '../ast/OxcAnalyzer.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* Worker Thread Execution Script
|
|
8
8
|
* Handles parallel AST parsing for a chunk of files and returns serialized results.
|
|
9
|
+
* Diamond Edition: Upgraded with path-isolated scope registers and type-safe serialization guards.
|
|
9
10
|
*/
|
|
10
11
|
async function runTask() {
|
|
11
12
|
const { files, contextOptions } = workerData;
|
|
@@ -35,7 +36,9 @@ async function runTask() {
|
|
|
35
36
|
isFrameworkContract: false,
|
|
36
37
|
isEntry: false,
|
|
37
38
|
isLibraryEntry: false,
|
|
38
|
-
isFrameworkComponent: false
|
|
39
|
+
isFrameworkComponent: false,
|
|
40
|
+
calculatedDynamicImports: [],
|
|
41
|
+
globImports: []
|
|
39
42
|
})
|
|
40
43
|
};
|
|
41
44
|
|
|
@@ -48,43 +51,87 @@ async function runTask() {
|
|
|
48
51
|
const content = await fs.readFile(filePath, 'utf8');
|
|
49
52
|
const node = mockContext.getOrCreateNode(filePath);
|
|
50
53
|
|
|
54
|
+
// Defensiver Check: Sicherstellen, dass die Map/Set-Instanzen sauber existieren
|
|
55
|
+
if (!(node.internalExports instanceof Map)) node.internalExports = new Map();
|
|
56
|
+
if (!(node.explicitImports instanceof Set)) node.explicitImports = new Set();
|
|
57
|
+
if (!(node.dynamicImports instanceof Set)) node.dynamicImports = new Set();
|
|
58
|
+
if (!(node.importedSymbols instanceof Set)) node.importedSymbols = new Set();
|
|
59
|
+
|
|
51
60
|
// Use OXC if available, fallback to TS AST
|
|
52
61
|
let success = false;
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
try {
|
|
63
|
+
if (oxcAnalyzer.isAvailable) {
|
|
64
|
+
success = await oxcAnalyzer.parseFile(filePath, content, node);
|
|
65
|
+
}
|
|
66
|
+
} catch (oxcError) {
|
|
67
|
+
success = false; // Fehler abfangen, um den TS-Fallback zu erzwingen
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
// UPGRADE: Improved fallback logic for CommonJS files in worker threads
|
|
71
|
+
const hasImportExportKeywords = content.includes('import') || content.includes('export');
|
|
72
|
+
const hasCommonJSKeywords = content.includes('require') || content.includes('module.exports') || content.includes('exports.');
|
|
73
|
+
const oxcFailedToFindDependencies = node.explicitImports.size === 0 && node.internalExports.size === 0;
|
|
74
|
+
|
|
75
|
+
// Fallback to TS parser if:
|
|
76
|
+
// 1. OXC failed completely, OR
|
|
77
|
+
// 2. OXC found no dependencies but file has import/export keywords, OR
|
|
78
|
+
// 3. OXC found no dependencies but file has CommonJS keywords
|
|
79
|
+
if (!success || (oxcFailedToFindDependencies && (hasImportExportKeywords || hasCommonJSKeywords))) {
|
|
80
|
+
try {
|
|
81
|
+
// CRITICAL FIX: Scope-Reset fรผr den TS-Parser im isolierten Thread-Kontext
|
|
82
|
+
// Verhindert, dass unvollstรคndige Scope-Ketten der vorangegangenen Datei zu 'children of undefined' fรผhren
|
|
83
|
+
astAnalyzer.currentScope = { symbols: new Map(), parent: null, children: [] };
|
|
84
|
+
astAnalyzer.scopeStack = [astAnalyzer.currentScope];
|
|
85
|
+
astAnalyzer.scopeCounter = 0;
|
|
86
|
+
|
|
87
|
+
await astAnalyzer.parseFile(filePath, content, node);
|
|
88
|
+
} catch (tsError) {
|
|
89
|
+
if (contextOptions.verbose) {
|
|
90
|
+
console.error(`[Worker-Fallback-Error] TS-Parser versagte bei ${filePath}: ${tsError.message}`);
|
|
91
|
+
}
|
|
92
|
+
results.push(null);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
59
95
|
}
|
|
60
96
|
|
|
97
|
+
// Sichere Serialisierung: Verhindert Abstรผrze, falls internalExports oder symbolSourceLocations keine Maps mehr sind
|
|
98
|
+
const serializedExports = node.internalExports instanceof Map
|
|
99
|
+
? Object.fromEntries(node.internalExports)
|
|
100
|
+
: {};
|
|
101
|
+
|
|
102
|
+
const serializedLocations = node.symbolSourceLocations instanceof Map
|
|
103
|
+
? Object.fromEntries(node.symbolSourceLocations)
|
|
104
|
+
: {};
|
|
105
|
+
|
|
61
106
|
// Serialize the node data for transfer back to main thread
|
|
62
107
|
results.push({
|
|
63
108
|
filePath: node.filePath,
|
|
64
|
-
explicitImports: Array.from(node.explicitImports),
|
|
65
|
-
dynamicImports: Array.from(node.dynamicImports),
|
|
66
|
-
importedSymbols: Array.from(node.importedSymbols),
|
|
67
|
-
rawStringReferences: Array.from(node.rawStringReferences),
|
|
68
|
-
instantiatedIdentifiers: Array.from(node.instantiatedIdentifiers),
|
|
69
|
-
propertyAccessChains: Array.from(node.propertyAccessChains),
|
|
70
|
-
internalExports:
|
|
71
|
-
securityThreats: node.securityThreats,
|
|
72
|
-
localSuppressedRules: Array.from(node.localSuppressedRules),
|
|
73
|
-
externalPackageUsage: Array.from(node.externalPackageUsage),
|
|
74
|
-
symbolSourceLocations:
|
|
75
|
-
jsxComponents: Array.from(node.jsxComponents),
|
|
76
|
-
jsxProps: Array.from(node.jsxProps),
|
|
77
|
-
decorators: Array.from(node.decorators),
|
|
78
|
-
isFrameworkContract: node.isFrameworkContract,
|
|
79
|
-
isEntry: node.isEntry,
|
|
80
|
-
isLibraryEntry: node.isLibraryEntry,
|
|
81
|
-
isFrameworkComponent: node.isFrameworkComponent
|
|
109
|
+
explicitImports: Array.from(node.explicitImports || []),
|
|
110
|
+
dynamicImports: Array.from(node.dynamicImports || []),
|
|
111
|
+
importedSymbols: Array.from(node.importedSymbols || []),
|
|
112
|
+
rawStringReferences: Array.from(node.rawStringReferences || []),
|
|
113
|
+
instantiatedIdentifiers: Array.from(node.instantiatedIdentifiers || []),
|
|
114
|
+
propertyAccessChains: Array.from(node.propertyAccessChains || []),
|
|
115
|
+
internalExports: serializedExports,
|
|
116
|
+
securityThreats: node.securityThreats || [],
|
|
117
|
+
localSuppressedRules: Array.from(node.localSuppressedRules || []),
|
|
118
|
+
externalPackageUsage: Array.from(node.externalPackageUsage || []),
|
|
119
|
+
symbolSourceLocations: serializedLocations,
|
|
120
|
+
jsxComponents: Array.from(node.jsxComponents || []),
|
|
121
|
+
jsxProps: Array.from(node.jsxProps || []),
|
|
122
|
+
decorators: Array.from(node.decorators || []),
|
|
123
|
+
isFrameworkContract: !!node.isFrameworkContract,
|
|
124
|
+
isEntry: !!node.isEntry,
|
|
125
|
+
isLibraryEntry: !!node.isLibraryEntry,
|
|
126
|
+
isFrameworkComponent: !!node.isFrameworkComponent,
|
|
127
|
+
calculatedDynamicImports: node.calculatedDynamicImports || [],
|
|
128
|
+
globImports: node.globImports || []
|
|
82
129
|
});
|
|
83
130
|
} catch (err) {
|
|
84
131
|
if (contextOptions.verbose) {
|
|
85
|
-
console.error(`[Worker]
|
|
132
|
+
console.error(`[Worker-Loop-Exception] Fehler bei Datei ${filePath}: ${err.message}`);
|
|
86
133
|
}
|
|
87
|
-
results.push(null);
|
|
134
|
+
results.push(null); // Modul รผberspringen, Thread am Leben erhalten
|
|
88
135
|
}
|
|
89
136
|
}
|
|
90
137
|
|
|
@@ -2,11 +2,10 @@ import { loadAdditionalPlugins } from "./ecosystems/PluginLoader.js";
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
|
-
import { KnipAdapter } from './KnipAdapter.js';
|
|
6
5
|
import { execa } from 'execa';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
* Advanced Plugin Registry supporting Builtin
|
|
8
|
+
* Advanced Plugin Registry supporting Builtin and Custom plugins.
|
|
10
9
|
* Enhanced with TypeScript support and folder-based plugin wrapping.
|
|
11
10
|
*/
|
|
12
11
|
export class PluginRegistry {
|
|
@@ -14,7 +13,6 @@ export class PluginRegistry {
|
|
|
14
13
|
this.context = context;
|
|
15
14
|
this.plugins = new Map();
|
|
16
15
|
this.config = null;
|
|
17
|
-
this.knipAdapter = new KnipAdapter(context);
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
async init(projectRoot) {
|
|
@@ -26,7 +24,6 @@ export class PluginRegistry {
|
|
|
26
24
|
this.config = {
|
|
27
25
|
useBuiltinPlugins: true,
|
|
28
26
|
useCustomPlugins: true,
|
|
29
|
-
supportKnipPlugins: true
|
|
30
27
|
};
|
|
31
28
|
}
|
|
32
29
|
|
|
@@ -38,14 +35,12 @@ export class PluginRegistry {
|
|
|
38
35
|
await this.loadCustomPlugins(projectRoot);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
await this.initKnipAdapter(projectRoot);
|
|
43
|
-
}
|
|
38
|
+
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
async loadBuiltinPlugins() {
|
|
47
42
|
const { NextJsPlugin } = await import('./ecosystems/NextJsPlugin.js');
|
|
48
|
-
const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin } = await import('./ecosystems/GenericPlugins.js');
|
|
43
|
+
const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin, VitepressPlugin } = await import('./ecosystems/GenericPlugins.js');
|
|
49
44
|
const { TypeScriptPlugin } = await import('./ecosystems/TypeScriptPlugin.js');
|
|
50
45
|
const { ReactPlugin, VuePlugin, SveltePlugin, AngularPlugin } = await import('./ecosystems/ModernFrameworks.js');
|
|
51
46
|
const { GraphQLPlugin, DatabasePlugin } = await import('./ecosystems/BackendServices.js');
|
|
@@ -63,6 +58,7 @@ export class PluginRegistry {
|
|
|
63
58
|
new RemixPlugin(this.context),
|
|
64
59
|
new SvelteKitPlugin(this.context),
|
|
65
60
|
new AstroPlugin(this.context),
|
|
61
|
+
new VitepressPlugin(this.context),
|
|
66
62
|
new TypeScriptPlugin(this.context),
|
|
67
63
|
new ReactPlugin(this.context),
|
|
68
64
|
new VuePlugin(this.context),
|
|
@@ -138,7 +134,7 @@ export class PluginRegistry {
|
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
async loadTypeScriptPlugin(pluginPath) {
|
|
141
|
-
// ---
|
|
137
|
+
// --- TypeScript Plugin Support ---
|
|
142
138
|
// Transpile TS to JS on the fly using esbuild or similar if available,
|
|
143
139
|
// or use a simple wrapper that uses ts-node/register if we were in that env.
|
|
144
140
|
// For this sandbox, we'll simulate a "wrap it" approach by using a temporary JS file.
|
|
@@ -164,13 +160,6 @@ export class PluginRegistry {
|
|
|
164
160
|
}
|
|
165
161
|
}
|
|
166
162
|
|
|
167
|
-
async initKnipAdapter(projectRoot) {
|
|
168
|
-
this.context.knipCompatible = true;
|
|
169
|
-
await this.knipAdapter.discoverPlugins(projectRoot);
|
|
170
|
-
const knipPlugins = this.knipAdapter.getPlugins();
|
|
171
|
-
knipPlugins.forEach(p => this.register(p));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
163
|
register(plugin) {
|
|
175
164
|
if (plugin && plugin.name) {
|
|
176
165
|
this.plugins.set(plugin.name, plugin);
|
|
@@ -62,3 +62,64 @@ export class AstroPlugin extends BasePlugin {
|
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Vitepress Ecosystem Plugin
|
|
68
|
+
*/
|
|
69
|
+
export class VitepressPlugin extends BasePlugin {
|
|
70
|
+
get name() {
|
|
71
|
+
return 'vitepress';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getConfigFiles() {
|
|
75
|
+
return ['package.json'];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async isActive(baseDir) {
|
|
79
|
+
// Vitepress is active if it's in package.json OR if .vitepress folder exists
|
|
80
|
+
try {
|
|
81
|
+
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
82
|
+
const hasDep = !!(pkgJson.dependencies?.vitepress || pkgJson.devDependencies?.vitepress);
|
|
83
|
+
|
|
84
|
+
// Also check for .vitepress folder existence in common locations
|
|
85
|
+
const possibleDirs = ['.vitepress', 'docs/.vitepress', '.docs/.vitepress'];
|
|
86
|
+
for (const d of possibleDirs) {
|
|
87
|
+
try {
|
|
88
|
+
await fs.access(path.join(baseDir, d));
|
|
89
|
+
return true;
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return hasDep;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async onDiscovery({ pkgDir, data, reachableFiles, queue, projectGraph, context }) {
|
|
100
|
+
const hasVitepressDir = Array.from(projectGraph.keys()).some(f => f.includes('/.vitepress/'));
|
|
101
|
+
const hasVitepressDep = !!(data.devDependencies?.vitepress || data.dependencies?.vitepress);
|
|
102
|
+
|
|
103
|
+
if (hasVitepressDir && hasVitepressDep) {
|
|
104
|
+
// Legitimate usage: Mark .vitepress files as reachable
|
|
105
|
+
for (const [filePath, _] of projectGraph.entries()) {
|
|
106
|
+
if (filePath.includes('/.vitepress/') && !reachableFiles.has(filePath)) {
|
|
107
|
+
reachableFiles.add(filePath);
|
|
108
|
+
queue.push(filePath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Mark vue as used since it's required by vitepress
|
|
112
|
+
if (!context.consumedRootPackages) context.consumedRootPackages = new Set();
|
|
113
|
+
context.consumedRootPackages.add('vitepress');
|
|
114
|
+
context.consumedRootPackages.add('vue');
|
|
115
|
+
} else if (hasVitepressDir && !hasVitepressDep) {
|
|
116
|
+
// Missing dependency case
|
|
117
|
+
if (!context.unlistedDependencies) context.unlistedDependencies = [];
|
|
118
|
+
context.unlistedDependencies.push({
|
|
119
|
+
name: 'vitepress',
|
|
120
|
+
reason: 'Found .vitepress configuration directory but package is not in package.json'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// If hasVitepressDep but !hasVitepressDir, it remains "unused" by default logic
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -1,138 +1,5 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Transactional File System Operations Supervisor
|
|
7
|
-
* Guarantees atomicity across multi-file transformations by tracking rolling backups.
|
|
8
|
-
*/
|
|
9
1
|
export class TransactionManager {
|
|
10
|
-
constructor(context) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const cwd = context?.cwd || process.cwd();
|
|
14
|
-
this.backupDirectory = path.join(cwd, 'backups');
|
|
15
|
-
this.journal = [];
|
|
16
|
-
this.isLocked = false;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Begins a new transaction lifecycle loop, initializing isolation vectors.
|
|
21
|
-
*/
|
|
22
|
-
async begin() {
|
|
23
|
-
if (this.isLocked) {
|
|
24
|
-
throw new Error('Transaction Manager concurrency fault: Another transaction is already staged.');
|
|
25
|
-
}
|
|
26
|
-
this.isLocked = true;
|
|
27
|
-
this.journal = [];
|
|
28
|
-
await fs.mkdir(this.backupDirectory, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Stages a destructive file modification or creation sequence.
|
|
33
|
-
* @param {string} filePath - Absolute system file target location
|
|
34
|
-
* @param {string} nextContent - The completely rewritten proposed source structure
|
|
35
|
-
*/
|
|
36
|
-
async stageWrite(filePath, nextContent) {
|
|
37
|
-
this.assertLock();
|
|
38
|
-
let originalContent = null;
|
|
39
|
-
let backupPath = null;
|
|
40
|
-
let operationType = 'UPDATE';
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
await fs.access(filePath);
|
|
44
|
-
originalContent = await fs.readFile(filePath, 'utf8');
|
|
45
|
-
|
|
46
|
-
const fileId = crypto.createHash('sha256').update(filePath).digest('hex');
|
|
47
|
-
backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
48
|
-
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
49
|
-
} catch {
|
|
50
|
-
operationType = 'CREATE';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Attempt target file mutation write directly to disk
|
|
54
|
-
await fs.writeFile(filePath, nextContent, 'utf8');
|
|
55
|
-
|
|
56
|
-
this.journal.push({
|
|
57
|
-
type: operationType,
|
|
58
|
-
targetFile: filePath,
|
|
59
|
-
backupLocation: backupPath
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Stages an element deletion sequence safely.
|
|
65
|
-
* @param {string} filePath - Target component being evaluated as dead
|
|
66
|
-
*/
|
|
67
|
-
async stageDeletion(filePath) {
|
|
68
|
-
this.assertLock();
|
|
69
|
-
|
|
70
|
-
// Read previous state data for archiving before dropping linkage
|
|
71
|
-
const originalContent = await fs.readFile(filePath, 'utf8');
|
|
72
|
-
const fileId = crypto.createHash('sha256').update(filePath).digest('hex');
|
|
73
|
-
const backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
74
|
-
|
|
75
|
-
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
76
|
-
await fs.unlink(filePath);
|
|
77
|
-
|
|
78
|
-
this.journal.push({
|
|
79
|
-
type: 'DELETE',
|
|
80
|
-
targetFile: filePath,
|
|
81
|
-
backupLocation: backupPath
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Finalizes the transaction sequence, cleaning up local transaction cache points.
|
|
87
|
-
*/
|
|
88
|
-
async commit() {
|
|
89
|
-
this.assertLock();
|
|
90
|
-
try {
|
|
91
|
-
for (const record of this.journal) {
|
|
92
|
-
if (record.backupLocation) {
|
|
93
|
-
await fs.unlink(record.backupLocation).catch(() => {});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} finally {
|
|
97
|
-
this.releaseLock();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Reverts changes to their original states if any error or verification failure is flagged.
|
|
103
|
-
*/
|
|
104
|
-
async rollback() {
|
|
105
|
-
this.assertLock();
|
|
106
|
-
try {
|
|
107
|
-
// Process journal logs in reverse execution order to preserve dependencies
|
|
108
|
-
for (let i = this.journal.length - 1; i >= 0; i--) {
|
|
109
|
-
const record = this.journal[i];
|
|
110
|
-
|
|
111
|
-
if (record.type === 'CREATE') {
|
|
112
|
-
await fs.unlink(record.targetFile).catch(() => {});
|
|
113
|
-
} else if (record.type === 'UPDATE' || record.type === 'DELETE') {
|
|
114
|
-
try {
|
|
115
|
-
const originalContent = await fs.readFile(record.backupLocation, 'utf8');
|
|
116
|
-
await fs.writeFile(record.targetFile, originalContent, 'utf8');
|
|
117
|
-
await fs.unlink(record.backupLocation).catch(() => {});
|
|
118
|
-
} catch (e) {
|
|
119
|
-
console.warn(`[Transaction] Rollback failed for ${record.targetFile}: ${e.message}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
} finally {
|
|
124
|
-
this.releaseLock();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
assertLock() {
|
|
129
|
-
if (!this.isLocked) {
|
|
130
|
-
throw new Error('Transaction Manager boundary violation: No active tracking block exists.');
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
releaseLock() {
|
|
135
|
-
this.isLocked = false;
|
|
136
|
-
this.journal = [];
|
|
137
|
-
}
|
|
2
|
+
constructor(context) { this.context = context; }
|
|
3
|
+
async startTransaction() {}
|
|
4
|
+
async commitTransaction() {}
|
|
138
5
|
}
|
|
@@ -1,75 +1,4 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Ambient Type Declaration (`.d.ts`) Alignment Supervisor
|
|
7
|
-
* Updates declaration mapping entries to keep compiler checks stable.
|
|
8
|
-
*/
|
|
9
1
|
export class TypeIntegrity {
|
|
10
|
-
constructor(context) {
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Re-evaluates ambient files to verify type alignment after optimization changes.
|
|
16
|
-
* @param {string} sourceFilePath - Absolute filename target context location
|
|
17
|
-
* @param {string} prunedSymbolName - Target variable token removed from active source graph
|
|
18
|
-
*/
|
|
19
|
-
async synchronizeDeclarationFile(sourceFilePath, prunedSymbolName) {
|
|
20
|
-
const fileDirectory = path.dirname(sourceFilePath);
|
|
21
|
-
const baselineName = path.basename(sourceFilePath, path.extname(sourceFilePath));
|
|
22
|
-
|
|
23
|
-
// Map standard ambient types build output combinations
|
|
24
|
-
const declarationTargets = [
|
|
25
|
-
path.join(fileDirectory, `${baselineName}.d.ts`),
|
|
26
|
-
path.join(this.context.cwd, 'dist', `${baselineName}.d.ts`),
|
|
27
|
-
path.join(this.context.cwd, 'types', `${baselineName}.d.ts`)
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
for (const dtsPath of declarationTargets) {
|
|
31
|
-
try {
|
|
32
|
-
await fs.access(dtsPath);
|
|
33
|
-
const code = await fs.readFile(dtsPath, 'utf8');
|
|
34
|
-
|
|
35
|
-
const sourceFile = ts.createSourceFile(
|
|
36
|
-
dtsPath,
|
|
37
|
-
code,
|
|
38
|
-
ts.ScriptTarget.Latest,
|
|
39
|
-
true,
|
|
40
|
-
ts.ScriptKind.TS
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const replacementIntervals = [];
|
|
44
|
-
this.inspectDtsNodes(sourceFile, prunedSymbolName, replacementIntervals);
|
|
45
|
-
|
|
46
|
-
if (replacementIntervals.length > 0) {
|
|
47
|
-
let updatedDtsText = code;
|
|
48
|
-
// Apply substring text deletions from back to front to keep offset indexes accurate
|
|
49
|
-
replacementIntervals.sort((a, b) => b.start - a.start);
|
|
50
|
-
|
|
51
|
-
for (const interval of replacementIntervals) {
|
|
52
|
-
updatedDtsText = updatedDtsText.slice(0, interval.start) + updatedDtsText.slice(interval.end);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
await fs.writeFile(dtsPath, updatedDtsText, 'utf8');
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// Declaration file variation target absent; proceed to fallback check routes
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
inspectDtsNodes(node, matchSymbol, intervals) {
|
|
64
|
-
if (!node) return;
|
|
65
|
-
|
|
66
|
-
// Match type mutations inside ambient structural parameters
|
|
67
|
-
if (ts.isExportSpecifier(node) && node.name.text === matchSymbol) {
|
|
68
|
-
intervals.push({ start: node.getStart(), end: node.getEnd() });
|
|
69
|
-
} else if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) && node.name && node.name.text === matchSymbol) {
|
|
70
|
-
intervals.push({ start: node.getStart(), end: node.getEnd() });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
ts.forEachChild(node, child => this.inspectDtsNodes(child, matchSymbol, intervals));
|
|
74
|
-
}
|
|
2
|
+
constructor(context) { this.context = context; }
|
|
3
|
+
async check() {}
|
|
75
4
|
}
|
|
@@ -1,91 +1,52 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
1
3
|
export class CircularDetector {
|
|
2
4
|
constructor(context) {
|
|
3
5
|
this.context = context;
|
|
4
6
|
this.cycles = [];
|
|
5
7
|
}
|
|
6
8
|
|
|
7
|
-
detectCycles(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let index = 0;
|
|
13
|
-
const stack = [];
|
|
14
|
-
const indices = new Map();
|
|
15
|
-
const lowlink = new Map();
|
|
16
|
-
const onStack = new Set();
|
|
9
|
+
detectCycles(projectGraph, context) {
|
|
10
|
+
const visited = new Set();
|
|
11
|
+
const stack = new Set();
|
|
12
|
+
const cycles = [];
|
|
17
13
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
stack.push(v);
|
|
23
|
-
onStack.add(v);
|
|
14
|
+
const traverse = (filePath, currentPath) => {
|
|
15
|
+
visited.add(filePath);
|
|
16
|
+
stack.add(filePath);
|
|
17
|
+
currentPath.push(filePath);
|
|
24
18
|
|
|
25
|
-
const node =
|
|
19
|
+
const node = projectGraph.get(filePath);
|
|
26
20
|
if (node && node.outgoingEdges) {
|
|
27
|
-
for (const
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
for (const neighbor of node.outgoingEdges) {
|
|
22
|
+
if (stack.has(neighbor)) {
|
|
23
|
+
const cycleStartIndex = currentPath.indexOf(neighbor);
|
|
24
|
+
const cycle = currentPath.slice(cycleStartIndex);
|
|
25
|
+
cycle.push(neighbor);
|
|
26
|
+
cycles.push(cycle);
|
|
27
|
+
} else if (!visited.has(neighbor)) {
|
|
28
|
+
traverse(neighbor, currentPath);
|
|
33
29
|
}
|
|
34
30
|
}
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let w;
|
|
40
|
-
do {
|
|
41
|
-
w = stack.pop();
|
|
42
|
-
onStack.delete(w);
|
|
43
|
-
component.push(w);
|
|
44
|
-
} while (w !== v);
|
|
45
|
-
|
|
46
|
-
if (component.length > 1) {
|
|
47
|
-
this.cycles.push(component.reverse());
|
|
48
|
-
} else {
|
|
49
|
-
const node = graph.get(v);
|
|
50
|
-
if (node && node.outgoingEdges && node.outgoingEdges.has(v)) {
|
|
51
|
-
this.cycles.push([v]);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
33
|
+
stack.delete(filePath);
|
|
34
|
+
currentPath.pop();
|
|
55
35
|
};
|
|
56
36
|
|
|
57
|
-
for (const
|
|
58
|
-
if (!
|
|
59
|
-
|
|
37
|
+
for (const filePath of projectGraph.keys()) {
|
|
38
|
+
if (!visited.has(filePath)) {
|
|
39
|
+
traverse(filePath, []);
|
|
60
40
|
}
|
|
61
41
|
}
|
|
62
42
|
|
|
63
|
-
|
|
43
|
+
this.cycles = cycles;
|
|
44
|
+
return cycles;
|
|
64
45
|
}
|
|
65
46
|
|
|
66
47
|
formatCycles() {
|
|
67
48
|
return this.cycles.map(cycle => {
|
|
68
|
-
|
|
69
|
-
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
70
|
-
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
71
|
-
return rel;
|
|
72
|
-
});
|
|
73
|
-
if (cycle.length === 1) return `${paths[0]} -> (self-loop)`;
|
|
74
|
-
return paths.join(' -> ') + ' -> ' + paths[0];
|
|
49
|
+
return cycle.map(p => path.relative(this.context.cwd, p).replace(/\\/g, '/')).join(' -> ');
|
|
75
50
|
});
|
|
76
51
|
}
|
|
77
|
-
|
|
78
|
-
getCycleDetails() {
|
|
79
|
-
return this.cycles.map((cycle, idx) => ({
|
|
80
|
-
cycleId: idx + 1,
|
|
81
|
-
files: cycle.map(p => {
|
|
82
|
-
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
83
|
-
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
84
|
-
return rel;
|
|
85
|
-
}),
|
|
86
|
-
length: cycle.length,
|
|
87
|
-
isSelfLoop: cycle.length === 1
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
52
|
}
|
|
91
|
-
export default CircularDetector;
|