devkit-quality 0.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/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/devkit.js +3 -0
- package/dist/ast/comments.js +28 -0
- package/dist/ast/parse.js +56 -0
- package/dist/ast/walk.js +69 -0
- package/dist/cli.js +167 -0
- package/dist/cliLogic.js +36 -0
- package/dist/config.js +57 -0
- package/dist/context.js +2 -0
- package/dist/discovery.js +99 -0
- package/dist/finding.js +20 -0
- package/dist/glob.js +35 -0
- package/dist/ignore.js +28 -0
- package/dist/moduleGraph.js +311 -0
- package/dist/moduleResolution.js +64 -0
- package/dist/reporters.js +258 -0
- package/dist/rules/architecture.js +57 -0
- package/dist/rules/complexity.js +230 -0
- package/dist/rules/deadCode.js +252 -0
- package/dist/rules/dependencies.js +137 -0
- package/dist/rules/duplication.js +138 -0
- package/dist/rules/errorHandling.js +120 -0
- package/dist/rules/hygiene.js +102 -0
- package/dist/rules/javascript.js +69 -0
- package/dist/rules/redundancy.js +75 -0
- package/dist/rules/secretPatterns.js +24 -0
- package/dist/rules/security.js +318 -0
- package/dist/rules/typescript.js +124 -0
- package/dist/rules.js +546 -0
- package/dist/scanner.js +150 -0
- package/dist/scoring.js +68 -0
- package/dist/suppressions.js +21 -0
- package/dist/terminal.js +135 -0
- package/dist/types.js +2 -0
- package/package.json +46 -0
package/dist/cliLogic.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterFindings = filterFindings;
|
|
4
|
+
exports.isKnownSeverity = isKnownSeverity;
|
|
5
|
+
exports.determineExitFailure = determineExitFailure;
|
|
6
|
+
const SEVERITY_RANK = {
|
|
7
|
+
INFO: 0,
|
|
8
|
+
LOW: 1,
|
|
9
|
+
MEDIUM: 2,
|
|
10
|
+
HIGH: 3,
|
|
11
|
+
CRITICAL: 4
|
|
12
|
+
};
|
|
13
|
+
function filterFindings(findings, filters) {
|
|
14
|
+
return findings.filter((finding) => {
|
|
15
|
+
if (filters.category && finding.category.toLowerCase() !== filters.category.toLowerCase())
|
|
16
|
+
return false;
|
|
17
|
+
if (filters.severity && finding.severity.toLowerCase() !== filters.severity.toLowerCase())
|
|
18
|
+
return false;
|
|
19
|
+
return true;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isKnownSeverity(value) {
|
|
23
|
+
return Object.prototype.hasOwnProperty.call(SEVERITY_RANK, value.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
function determineExitFailure(summary, gates) {
|
|
26
|
+
if (gates.minScore !== undefined && summary.score < Number(gates.minScore))
|
|
27
|
+
return true;
|
|
28
|
+
if (gates.failOn !== undefined) {
|
|
29
|
+
const threshold = gates.failOn.toUpperCase();
|
|
30
|
+
if (!isKnownSeverity(gates.failOn))
|
|
31
|
+
return true;
|
|
32
|
+
if (summary.findings.some((finding) => SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[threshold]))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.defaultConfig = void 0;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
8
|
+
exports.writeConfig = writeConfig;
|
|
9
|
+
exports.isRuleEnabled = isRuleEnabled;
|
|
10
|
+
exports.getRuleThreshold = getRuleThreshold;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
exports.defaultConfig = {
|
|
14
|
+
project: {
|
|
15
|
+
name: 'my-project'
|
|
16
|
+
},
|
|
17
|
+
scan: {
|
|
18
|
+
include: ['**/*'],
|
|
19
|
+
exclude: ['node_modules/**', 'dist/**', 'coverage/**', '.git/**', '.devkit/**']
|
|
20
|
+
},
|
|
21
|
+
rules: {}
|
|
22
|
+
};
|
|
23
|
+
function loadConfig(projectRoot) {
|
|
24
|
+
const configPath = node_path_1.default.join(projectRoot, '.devkitrc.json');
|
|
25
|
+
if (!node_fs_1.default.existsSync(configPath)) {
|
|
26
|
+
return exports.defaultConfig;
|
|
27
|
+
}
|
|
28
|
+
const raw = node_fs_1.default.readFileSync(configPath, 'utf8');
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
return {
|
|
32
|
+
...exports.defaultConfig,
|
|
33
|
+
...parsed,
|
|
34
|
+
project: { ...exports.defaultConfig.project, ...(parsed.project ?? {}) },
|
|
35
|
+
scan: { ...exports.defaultConfig.scan, ...(parsed.scan ?? {}) },
|
|
36
|
+
architecture: parsed.architecture
|
|
37
|
+
? { ...parsed.architecture, layers: { ...(exports.defaultConfig.architecture?.layers ?? {}), ...(parsed.architecture.layers ?? {}) } }
|
|
38
|
+
: exports.defaultConfig.architecture,
|
|
39
|
+
rules: { ...exports.defaultConfig.rules, ...(parsed.rules ?? {}) }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return exports.defaultConfig;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function writeConfig(projectRoot) {
|
|
47
|
+
const configPath = node_path_1.default.join(projectRoot, '.devkitrc.json');
|
|
48
|
+
node_fs_1.default.writeFileSync(configPath, `${JSON.stringify(exports.defaultConfig, null, 2)}\n`);
|
|
49
|
+
return configPath;
|
|
50
|
+
}
|
|
51
|
+
function isRuleEnabled(config, ruleId) {
|
|
52
|
+
return config.rules[ruleId]?.enabled !== false;
|
|
53
|
+
}
|
|
54
|
+
function getRuleThreshold(config, ruleId, fallback) {
|
|
55
|
+
const override = config.rules[ruleId]?.max;
|
|
56
|
+
return typeof override === 'number' ? override : fallback;
|
|
57
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GENERATED_HINTS = exports.IGNORE_DIRS = exports.SOURCE_EXTENSIONS = void 0;
|
|
7
|
+
exports.isRelevantFile = isRelevantFile;
|
|
8
|
+
exports.isTestFile = isTestFile;
|
|
9
|
+
exports.isGeneratedFile = isGeneratedFile;
|
|
10
|
+
exports.isGeneratedByContent = isGeneratedByContent;
|
|
11
|
+
exports.filterScannedFiles = filterScannedFiles;
|
|
12
|
+
exports.walkDirectory = walkDirectory;
|
|
13
|
+
exports.readFileSafe = readFileSafe;
|
|
14
|
+
exports.countLines = countLines;
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const glob_1 = require("./glob");
|
|
18
|
+
exports.SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
19
|
+
exports.IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.devkit', 'generated', 'vendor', '.next', '.nuxt', '.svelte-kit', '.angular', 'out']);
|
|
20
|
+
exports.GENERATED_HINTS = ['generated', 'dist', 'build', '.d.ts', '.min.', 'coverage', '.next', '.nuxt', '.svelte-kit', '.angular', 'out'];
|
|
21
|
+
function isRelevantFile(filePath) {
|
|
22
|
+
const ext = node_path_1.default.extname(filePath).toLowerCase();
|
|
23
|
+
return exports.SOURCE_EXTENSIONS.has(ext);
|
|
24
|
+
}
|
|
25
|
+
function isTestFile(filePath) {
|
|
26
|
+
const lower = filePath.toLowerCase();
|
|
27
|
+
return lower.includes('.test.') || lower.includes('.spec.') || lower.includes('__tests__');
|
|
28
|
+
}
|
|
29
|
+
function isGeneratedFile(filePath) {
|
|
30
|
+
const lower = filePath.replace(/\\/g, '/').toLowerCase();
|
|
31
|
+
const segments = lower.split('/');
|
|
32
|
+
const generatedDirectories = new Set(['generated', 'dist', 'build', 'coverage', '.next', '.nuxt', '.svelte-kit', '.angular', 'out']);
|
|
33
|
+
const basename = segments[segments.length - 1] ?? '';
|
|
34
|
+
return segments.some((segment) => generatedDirectories.has(segment)) || basename.endsWith('.d.ts') || basename.includes('.min.');
|
|
35
|
+
}
|
|
36
|
+
function isGeneratedByContent(text) {
|
|
37
|
+
return /(?:@generated|\bDO NOT EDIT\b|\bGENERATED FILE\b)/i.test(text) || /^\/\/[#@]\s*sourceMappingURL=/m.test(text);
|
|
38
|
+
}
|
|
39
|
+
function filterScannedFiles(allFiles, projectRoot, config, ignorePatterns) {
|
|
40
|
+
const files = [];
|
|
41
|
+
const generatedFiles = [];
|
|
42
|
+
for (const file of allFiles) {
|
|
43
|
+
if (!isRelevantFile(file))
|
|
44
|
+
continue;
|
|
45
|
+
const relative = node_path_1.default.relative(projectRoot, file).replace(/\\/g, '/');
|
|
46
|
+
if (!(0, glob_1.matchesAnyGlob)(relative, config.scan.include))
|
|
47
|
+
continue;
|
|
48
|
+
if ((0, glob_1.matchesAnyGlob)(relative, config.scan.exclude) || (0, glob_1.matchesAnyGlob)(relative, ignorePatterns))
|
|
49
|
+
continue;
|
|
50
|
+
const text = readFileSafe(file);
|
|
51
|
+
if (isGeneratedFile(relative) || isGeneratedByContent(text)) {
|
|
52
|
+
generatedFiles.push(relative);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
files.push(file);
|
|
56
|
+
}
|
|
57
|
+
return { files, generatedFiles };
|
|
58
|
+
}
|
|
59
|
+
function walkDirectory(rootDir) {
|
|
60
|
+
const allFiles = [];
|
|
61
|
+
const visit = (currentDir) => {
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = node_fs_1.default.readdirSync(currentDir, { withFileTypes: true });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const absolutePath = node_path_1.default.join(currentDir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
if (exports.IGNORE_DIRS.has(entry.name)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
visit(absolutePath);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (entry.isFile()) {
|
|
79
|
+
allFiles.push(absolutePath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
visit(rootDir);
|
|
84
|
+
return allFiles;
|
|
85
|
+
}
|
|
86
|
+
function readFileSafe(filePath) {
|
|
87
|
+
try {
|
|
88
|
+
return node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function countLines(content) {
|
|
95
|
+
if (!content.trim()) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
return content.split(/\r\n|\r|\n/).length;
|
|
99
|
+
}
|
package/dist/finding.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildFinding = buildFinding;
|
|
4
|
+
function buildFinding(params) {
|
|
5
|
+
return {
|
|
6
|
+
id: `${params.ruleId}-${params.file}-${params.line}-${params.column}`,
|
|
7
|
+
ruleId: params.ruleId,
|
|
8
|
+
category: params.category,
|
|
9
|
+
severity: params.severity,
|
|
10
|
+
confidence: params.confidence,
|
|
11
|
+
file: params.file,
|
|
12
|
+
line: params.line,
|
|
13
|
+
column: params.column,
|
|
14
|
+
message: params.message,
|
|
15
|
+
description: params.description,
|
|
16
|
+
evidence: params.evidence,
|
|
17
|
+
suggestion: params.suggestion,
|
|
18
|
+
fixAvailable: params.fixAvailable ?? false
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/glob.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.globToRegExp = globToRegExp;
|
|
4
|
+
exports.matchesAnyGlob = matchesAnyGlob;
|
|
5
|
+
function globToRegExp(pattern) {
|
|
6
|
+
let regex = '';
|
|
7
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
8
|
+
const char = pattern[i];
|
|
9
|
+
if (char === '*' && pattern[i + 1] === '*') {
|
|
10
|
+
regex += '.*';
|
|
11
|
+
i += 1;
|
|
12
|
+
if (pattern[i + 1] === '/') {
|
|
13
|
+
i += 1;
|
|
14
|
+
}
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (char === '*') {
|
|
18
|
+
regex += '[^/]*';
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === '?') {
|
|
22
|
+
regex += '[^/]';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if ('.+^${}()|[]\\'.includes(char)) {
|
|
26
|
+
regex += `\\${char}`;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
regex += char;
|
|
30
|
+
}
|
|
31
|
+
return new RegExp(`^${regex}$`);
|
|
32
|
+
}
|
|
33
|
+
function matchesAnyGlob(value, patterns) {
|
|
34
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(value));
|
|
35
|
+
}
|
package/dist/ignore.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadIgnorePatterns = loadIgnorePatterns;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
function loadIgnorePatterns(projectRoot) {
|
|
10
|
+
const patterns = new Set();
|
|
11
|
+
for (const name of ['.gitignore', '.devkitignore']) {
|
|
12
|
+
const file = node_path_1.default.join(projectRoot, name);
|
|
13
|
+
if (!node_fs_1.default.existsSync(file))
|
|
14
|
+
continue;
|
|
15
|
+
for (const rawLine of node_fs_1.default.readFileSync(file, 'utf8').split(/\r\n|\r|\n/)) {
|
|
16
|
+
const line = rawLine.trim();
|
|
17
|
+
if (!line || line.startsWith('#') || line.startsWith('!'))
|
|
18
|
+
continue;
|
|
19
|
+
const normalized = line.replace(/^\//, '').replace(/\/$/, '');
|
|
20
|
+
if (!normalized)
|
|
21
|
+
continue;
|
|
22
|
+
patterns.add(normalized);
|
|
23
|
+
patterns.add(`**/${normalized}`);
|
|
24
|
+
patterns.add(`**/${normalized}/**`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...patterns];
|
|
28
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildModuleGraph = buildModuleGraph;
|
|
7
|
+
exports.findImportCycles = findImportCycles;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
10
|
+
const discovery_1 = require("./discovery");
|
|
11
|
+
const parse_1 = require("./ast/parse");
|
|
12
|
+
const walk_1 = require("./ast/walk");
|
|
13
|
+
const glob_1 = require("./glob");
|
|
14
|
+
const moduleResolution_1 = require("./moduleResolution");
|
|
15
|
+
function extractImportsAndExports(file, projectRoot, sourceFile, fileSet, resolutionContext) {
|
|
16
|
+
const imports = [];
|
|
17
|
+
const exports = [];
|
|
18
|
+
const reExportAllTargets = [];
|
|
19
|
+
const dynamicRequireHints = [];
|
|
20
|
+
const addImport = (specifier, kind, line, details = {}) => {
|
|
21
|
+
const info = {
|
|
22
|
+
specifier,
|
|
23
|
+
resolved: (0, moduleResolution_1.resolveSpecifier)(file, specifier, kind, fileSet, resolutionContext),
|
|
24
|
+
isRelative: specifier.startsWith('.'),
|
|
25
|
+
namedImports: details.namedImports ?? [],
|
|
26
|
+
hasDefaultImport: details.hasDefaultImport ?? false,
|
|
27
|
+
hasNamespaceImport: details.hasNamespaceImport ?? false,
|
|
28
|
+
isSideEffectOnly: details.isSideEffectOnly ?? false,
|
|
29
|
+
line,
|
|
30
|
+
kind
|
|
31
|
+
};
|
|
32
|
+
imports.push(info);
|
|
33
|
+
return info;
|
|
34
|
+
};
|
|
35
|
+
for (const statement of sourceFile.statements) {
|
|
36
|
+
if (typescript_1.default.isImportDeclaration(statement) && typescript_1.default.isStringLiteral(statement.moduleSpecifier)) {
|
|
37
|
+
const clause = statement.importClause;
|
|
38
|
+
const namedImports = [];
|
|
39
|
+
let hasDefaultImport = false;
|
|
40
|
+
let hasNamespaceImport = false;
|
|
41
|
+
if (clause) {
|
|
42
|
+
hasDefaultImport = !!clause.name;
|
|
43
|
+
if (clause.namedBindings && typescript_1.default.isNamespaceImport(clause.namedBindings))
|
|
44
|
+
hasNamespaceImport = true;
|
|
45
|
+
else if (clause?.namedBindings && typescript_1.default.isNamedImports(clause.namedBindings)) {
|
|
46
|
+
for (const element of clause.namedBindings.elements)
|
|
47
|
+
namedImports.push((element.propertyName ?? element.name).text);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
addImport(statement.moduleSpecifier.text, 'import', (0, walk_1.lineAndColumn)(sourceFile, statement.getStart()).line, {
|
|
51
|
+
namedImports, hasDefaultImport, hasNamespaceImport, isSideEffectOnly: !clause
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (typescript_1.default.isImportEqualsDeclaration(statement) && typescript_1.default.isExternalModuleReference(statement.moduleReference)) {
|
|
56
|
+
const expr = statement.moduleReference.expression;
|
|
57
|
+
if (typescript_1.default.isStringLiteral(expr))
|
|
58
|
+
addImport(expr.text, 'importEquals', (0, walk_1.lineAndColumn)(sourceFile, statement.getStart()).line, { hasNamespaceImport: true });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (typescript_1.default.isExportDeclaration(statement)) {
|
|
62
|
+
const line = (0, walk_1.lineAndColumn)(sourceFile, statement.getStart()).line;
|
|
63
|
+
if (statement.moduleSpecifier && typescript_1.default.isStringLiteral(statement.moduleSpecifier)) {
|
|
64
|
+
const specifier = statement.moduleSpecifier.text;
|
|
65
|
+
if (!statement.exportClause) {
|
|
66
|
+
const info = addImport(specifier, 'exportFrom', line, { hasNamespaceImport: true });
|
|
67
|
+
if (info.resolved)
|
|
68
|
+
reExportAllTargets.push(info.resolved);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (typescript_1.default.isNamedExports(statement.exportClause)) {
|
|
72
|
+
for (const element of statement.exportClause.elements)
|
|
73
|
+
exports.push({ name: element.name.text, kind: 'named', line });
|
|
74
|
+
addImport(specifier, 'exportFrom', line, {
|
|
75
|
+
namedImports: statement.exportClause.elements.map((element) => (element.propertyName ?? element.name).text)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (statement.exportClause && typescript_1.default.isNamedExports(statement.exportClause)) {
|
|
81
|
+
for (const element of statement.exportClause.elements)
|
|
82
|
+
exports.push({ name: element.name.text, kind: 'named', line });
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (typescript_1.default.isExportAssignment(statement)) {
|
|
87
|
+
exports.push({ name: 'default', kind: 'default', line: (0, walk_1.lineAndColumn)(sourceFile, statement.getStart()).line });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const modifiers = typescript_1.default.canHaveModifiers(statement) ? typescript_1.default.getModifiers(statement) : undefined;
|
|
91
|
+
const isExported = modifiers?.some((modifier) => modifier.kind === typescript_1.default.SyntaxKind.ExportKeyword) ?? false;
|
|
92
|
+
if (!isExported)
|
|
93
|
+
continue;
|
|
94
|
+
const isDefault = modifiers?.some((modifier) => modifier.kind === typescript_1.default.SyntaxKind.DefaultKeyword) ?? false;
|
|
95
|
+
const line = (0, walk_1.lineAndColumn)(sourceFile, statement.getStart()).line;
|
|
96
|
+
if (isDefault)
|
|
97
|
+
exports.push({ name: 'default', kind: 'default', line });
|
|
98
|
+
else if (typescript_1.default.isFunctionDeclaration(statement) && statement.name)
|
|
99
|
+
exports.push({ name: statement.name.text, kind: 'function', line });
|
|
100
|
+
else if (typescript_1.default.isClassDeclaration(statement) && statement.name)
|
|
101
|
+
exports.push({ name: statement.name.text, kind: 'class', line });
|
|
102
|
+
else if (typescript_1.default.isInterfaceDeclaration(statement))
|
|
103
|
+
exports.push({ name: statement.name.text, kind: 'interface', line });
|
|
104
|
+
else if (typescript_1.default.isTypeAliasDeclaration(statement))
|
|
105
|
+
exports.push({ name: statement.name.text, kind: 'typeAlias', line });
|
|
106
|
+
else if (typescript_1.default.isEnumDeclaration(statement))
|
|
107
|
+
exports.push({ name: statement.name.text, kind: 'enum', line });
|
|
108
|
+
else if (typescript_1.default.isVariableStatement(statement)) {
|
|
109
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
110
|
+
if (typescript_1.default.isIdentifier(declaration.name))
|
|
111
|
+
exports.push({ name: declaration.name.text, kind: 'variable', line });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const dynamicDir = node_path_1.default.relative(projectRoot, node_path_1.default.dirname(file)).replace(/\\/g, '/') || '.';
|
|
116
|
+
(0, walk_1.forEachNode)(sourceFile, (node) => {
|
|
117
|
+
if (!typescript_1.default.isCallExpression(node))
|
|
118
|
+
return;
|
|
119
|
+
const isRequire = typescript_1.default.isIdentifier(node.expression) && node.expression.text === 'require';
|
|
120
|
+
const isDynamicImport = node.expression.kind === typescript_1.default.SyntaxKind.ImportKeyword;
|
|
121
|
+
if (!isRequire && !isDynamicImport)
|
|
122
|
+
return;
|
|
123
|
+
const kind = isRequire ? 'require' : 'dynamicImport';
|
|
124
|
+
const argument = node.arguments[0];
|
|
125
|
+
if (!argument || !(typescript_1.default.isStringLiteral(argument) || typescript_1.default.isNoSubstitutionTemplateLiteral(argument))) {
|
|
126
|
+
dynamicRequireHints.push(dynamicDir);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let namedImports = [];
|
|
130
|
+
let hasNamespaceImport = true;
|
|
131
|
+
let isSideEffectOnly = false;
|
|
132
|
+
let parent = node.parent;
|
|
133
|
+
if (typescript_1.default.isAwaitExpression(parent))
|
|
134
|
+
parent = parent.parent;
|
|
135
|
+
if (typescript_1.default.isVariableDeclaration(parent) && parent.initializer && (parent.initializer === node || (typescript_1.default.isAwaitExpression(parent.initializer) && parent.initializer.expression === node))) {
|
|
136
|
+
if (typescript_1.default.isObjectBindingPattern(parent.name)) {
|
|
137
|
+
const hasStaticProperties = parent.name.elements.every((element) => {
|
|
138
|
+
if (element.dotDotDotToken)
|
|
139
|
+
return false;
|
|
140
|
+
const property = element.propertyName ?? element.name;
|
|
141
|
+
return typescript_1.default.isIdentifier(property) || typescript_1.default.isStringLiteral(property) || typescript_1.default.isNumericLiteral(property);
|
|
142
|
+
});
|
|
143
|
+
if (hasStaticProperties) {
|
|
144
|
+
namedImports = parent.name.elements.map((element) => (element.propertyName ?? element.name).getText(sourceFile).replace(/^['"]|['"]$/g, ''));
|
|
145
|
+
hasNamespaceImport = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (isRequire && typescript_1.default.isExpressionStatement(node.parent)) {
|
|
150
|
+
isSideEffectOnly = true;
|
|
151
|
+
}
|
|
152
|
+
addImport(argument.text, kind, (0, walk_1.lineAndColumn)(sourceFile, node.getStart()).line, { namedImports, hasNamespaceImport, isSideEffectOnly });
|
|
153
|
+
});
|
|
154
|
+
return { imports, exports, reExportAllTargets, dynamicRequireHints };
|
|
155
|
+
}
|
|
156
|
+
function resolveEntryPoints(projectRoot, files, packageJson, config) {
|
|
157
|
+
const entryPoints = new Set();
|
|
158
|
+
const fileSet = new Set(files);
|
|
159
|
+
const addFromRelativePath = (relativePath) => {
|
|
160
|
+
const resolved = node_path_1.default.resolve(projectRoot, relativePath);
|
|
161
|
+
if (fileSet.has(resolved)) {
|
|
162
|
+
entryPoints.add(resolved);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const ext of discovery_1.SOURCE_EXTENSIONS) {
|
|
166
|
+
if (fileSet.has(resolved + ext)) {
|
|
167
|
+
entryPoints.add(resolved + ext);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const scanStringForPaths = (value) => {
|
|
172
|
+
const tokens = value.split(/\s+/);
|
|
173
|
+
for (const token of tokens) {
|
|
174
|
+
const cleaned = token.replace(/^["']|["']$/g, '');
|
|
175
|
+
if ([...discovery_1.SOURCE_EXTENSIONS].some((ext) => cleaned.endsWith(ext))) {
|
|
176
|
+
addFromRelativePath(cleaned);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
if (packageJson) {
|
|
181
|
+
if (typeof packageJson.main === 'string') {
|
|
182
|
+
addFromRelativePath(packageJson.main);
|
|
183
|
+
}
|
|
184
|
+
if (typeof packageJson.bin === 'string') {
|
|
185
|
+
addFromRelativePath(packageJson.bin);
|
|
186
|
+
}
|
|
187
|
+
else if (packageJson.bin && typeof packageJson.bin === 'object') {
|
|
188
|
+
for (const value of Object.values(packageJson.bin)) {
|
|
189
|
+
addFromRelativePath(value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const scripts = packageJson.scripts;
|
|
193
|
+
if (scripts) {
|
|
194
|
+
for (const command of Object.values(scripts)) {
|
|
195
|
+
scanStringForPaths(command);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const addExportTargets = (value) => {
|
|
199
|
+
if (typeof value === 'string') {
|
|
200
|
+
if (value.startsWith('./'))
|
|
201
|
+
addFromRelativePath(value.slice(2));
|
|
202
|
+
}
|
|
203
|
+
else if (value && typeof value === 'object') {
|
|
204
|
+
for (const nested of Object.values(value))
|
|
205
|
+
addExportTargets(nested);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
addExportTargets(packageJson.exports);
|
|
209
|
+
const dependencies = {
|
|
210
|
+
...(packageJson.dependencies ?? {}),
|
|
211
|
+
...(packageJson.devDependencies ?? {})
|
|
212
|
+
};
|
|
213
|
+
if (Object.prototype.hasOwnProperty.call(dependencies, 'next')) {
|
|
214
|
+
const patterns = [
|
|
215
|
+
'pages/**/*.js', 'pages/**/*.jsx', 'pages/**/*.ts', 'pages/**/*.tsx',
|
|
216
|
+
'app/**/page.js', 'app/**/page.jsx', 'app/**/page.ts', 'app/**/page.tsx',
|
|
217
|
+
'app/**/layout.js', 'app/**/layout.jsx', 'app/**/layout.ts', 'app/**/layout.tsx',
|
|
218
|
+
'app/**/route.js', 'app/**/route.jsx', 'app/**/route.ts', 'app/**/route.tsx',
|
|
219
|
+
'app/**/loading.js', 'app/**/loading.jsx', 'app/**/loading.ts', 'app/**/loading.tsx',
|
|
220
|
+
'app/**/error.js', 'app/**/error.jsx', 'app/**/error.ts', 'app/**/error.tsx',
|
|
221
|
+
'app/**/not-found.js', 'app/**/not-found.jsx', 'app/**/not-found.ts', 'app/**/not-found.tsx',
|
|
222
|
+
'app/**/middleware.js', 'app/**/middleware.jsx', 'app/**/middleware.ts', 'app/**/middleware.tsx'
|
|
223
|
+
];
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
const relative = node_path_1.default.relative(projectRoot, file).replace(/\\/g, '/');
|
|
226
|
+
if ((0, glob_1.matchesAnyGlob)(relative, patterns))
|
|
227
|
+
entryPoints.add(file);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Keep the parameter explicit: framework and custom entry conventions are config-dependent.
|
|
232
|
+
void config;
|
|
233
|
+
for (const file of files) {
|
|
234
|
+
if ((0, discovery_1.isTestFile)(file)) {
|
|
235
|
+
entryPoints.add(file);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return entryPoints;
|
|
239
|
+
}
|
|
240
|
+
function buildModuleGraph(projectRoot, files, packageJson, config) {
|
|
241
|
+
const fileSet = new Set(files);
|
|
242
|
+
const importsByFile = new Map();
|
|
243
|
+
const exportsByFile = new Map();
|
|
244
|
+
const edges = new Map();
|
|
245
|
+
const reverseEdges = new Map();
|
|
246
|
+
const reExportAllTargets = new Map();
|
|
247
|
+
const dynamicRequireHints = new Set();
|
|
248
|
+
const resolutionContext = (0, moduleResolution_1.createResolutionContext)(projectRoot);
|
|
249
|
+
for (const file of files) {
|
|
250
|
+
const text = (0, discovery_1.readFileSafe)(file);
|
|
251
|
+
const sourceFile = (0, parse_1.parseSourceFile)(file, text);
|
|
252
|
+
const { imports, exports, reExportAllTargets: reExportTargets, dynamicRequireHints: hints } = extractImportsAndExports(file, projectRoot, sourceFile, fileSet, resolutionContext);
|
|
253
|
+
hints.forEach((hint) => dynamicRequireHints.add(hint));
|
|
254
|
+
importsByFile.set(file, imports);
|
|
255
|
+
exportsByFile.set(file, exports);
|
|
256
|
+
reExportAllTargets.set(file, new Set(reExportTargets));
|
|
257
|
+
const edgeSet = new Set();
|
|
258
|
+
for (const importInfo of imports) {
|
|
259
|
+
if (importInfo.resolved) {
|
|
260
|
+
edgeSet.add(importInfo.resolved);
|
|
261
|
+
if (!reverseEdges.has(importInfo.resolved)) {
|
|
262
|
+
reverseEdges.set(importInfo.resolved, new Set());
|
|
263
|
+
}
|
|
264
|
+
reverseEdges.get(importInfo.resolved).add(file);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const target of reExportTargets) {
|
|
268
|
+
edgeSet.add(target);
|
|
269
|
+
if (!reverseEdges.has(target)) {
|
|
270
|
+
reverseEdges.set(target, new Set());
|
|
271
|
+
}
|
|
272
|
+
reverseEdges.get(target).add(file);
|
|
273
|
+
}
|
|
274
|
+
edges.set(file, edgeSet);
|
|
275
|
+
}
|
|
276
|
+
const entryPoints = resolveEntryPoints(projectRoot, files, packageJson, config);
|
|
277
|
+
return { importsByFile, exportsByFile, edges, reverseEdges, reExportAllTargets, entryPoints, fileSet, dynamicRequireHints };
|
|
278
|
+
}
|
|
279
|
+
function findImportCycles(graph) {
|
|
280
|
+
const cycles = [];
|
|
281
|
+
const visitedGlobal = new Set();
|
|
282
|
+
for (const start of graph.edges.keys()) {
|
|
283
|
+
if (visitedGlobal.has(start)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const stack = [];
|
|
287
|
+
const onStack = new Set();
|
|
288
|
+
const visit = (node) => {
|
|
289
|
+
if (onStack.has(node)) {
|
|
290
|
+
const cycleStart = stack.indexOf(node);
|
|
291
|
+
if (cycleStart !== -1) {
|
|
292
|
+
cycles.push([...stack.slice(cycleStart), node]);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (visitedGlobal.has(node)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
visitedGlobal.add(node);
|
|
300
|
+
stack.push(node);
|
|
301
|
+
onStack.add(node);
|
|
302
|
+
for (const next of graph.edges.get(node) ?? []) {
|
|
303
|
+
visit(next);
|
|
304
|
+
}
|
|
305
|
+
stack.pop();
|
|
306
|
+
onStack.delete(node);
|
|
307
|
+
};
|
|
308
|
+
visit(start);
|
|
309
|
+
}
|
|
310
|
+
return cycles;
|
|
311
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadScannedTsConfig = loadScannedTsConfig;
|
|
7
|
+
exports.createResolutionContext = createResolutionContext;
|
|
8
|
+
exports.resolveSpecifier = resolveSpecifier;
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
11
|
+
const discovery_1 = require("./discovery");
|
|
12
|
+
const FALLBACK_RESOLUTION_OPTIONS = {
|
|
13
|
+
moduleResolution: typescript_1.default.ModuleResolutionKind.NodeNext,
|
|
14
|
+
module: typescript_1.default.ModuleKind.NodeNext,
|
|
15
|
+
target: typescript_1.default.ScriptTarget.ES2022,
|
|
16
|
+
allowJs: true,
|
|
17
|
+
resolveJsonModule: true,
|
|
18
|
+
esModuleInterop: true
|
|
19
|
+
};
|
|
20
|
+
function loadScannedTsConfig(projectRoot) {
|
|
21
|
+
const configPath = typescript_1.default.findConfigFile(projectRoot, typescript_1.default.sys.fileExists, 'tsconfig.json');
|
|
22
|
+
if (!configPath)
|
|
23
|
+
return { ...FALLBACK_RESOLUTION_OPTIONS };
|
|
24
|
+
const configFile = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
|
|
25
|
+
if (configFile.error)
|
|
26
|
+
return { ...FALLBACK_RESOLUTION_OPTIONS };
|
|
27
|
+
const parsed = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, node_path_1.default.dirname(configPath), FALLBACK_RESOLUTION_OPTIONS, configPath);
|
|
28
|
+
return { ...FALLBACK_RESOLUTION_OPTIONS, ...parsed.options };
|
|
29
|
+
}
|
|
30
|
+
function createResolutionContext(projectRoot) {
|
|
31
|
+
const compilerOptions = loadScannedTsConfig(projectRoot);
|
|
32
|
+
const cache = typescript_1.default.createModuleResolutionCache(projectRoot, typescript_1.default.sys.useCaseSensitiveFileNames ? (fileName) => fileName : (fileName) => fileName.toLowerCase(), compilerOptions);
|
|
33
|
+
return { compilerOptions, cache, host: typescript_1.default.sys };
|
|
34
|
+
}
|
|
35
|
+
function resolveRelativeCheap(fromFile, specifier, fileSet) {
|
|
36
|
+
const base = node_path_1.default.resolve(node_path_1.default.dirname(fromFile), specifier);
|
|
37
|
+
const direct = [base, ...[...discovery_1.SOURCE_EXTENSIONS].map((extension) => base + extension)];
|
|
38
|
+
for (const candidate of direct)
|
|
39
|
+
if (fileSet.has(candidate))
|
|
40
|
+
return candidate;
|
|
41
|
+
for (const extension of discovery_1.SOURCE_EXTENSIONS) {
|
|
42
|
+
const candidate = node_path_1.default.join(base, `index${extension}`);
|
|
43
|
+
if (fileSet.has(candidate))
|
|
44
|
+
return candidate;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function resolveSpecifier(fromFile, specifier, kind, fileSet, context) {
|
|
49
|
+
const normalizedFiles = fileSet;
|
|
50
|
+
if (specifier.startsWith('.')) {
|
|
51
|
+
const cheap = resolveRelativeCheap(fromFile, specifier, normalizedFiles);
|
|
52
|
+
if (cheap)
|
|
53
|
+
return cheap;
|
|
54
|
+
}
|
|
55
|
+
// TypeScript treats ESNext as an explicit package-condition mode and can skip `paths`
|
|
56
|
+
// mappings under NodeNext. The default is correct for ESM imports and path aliases;
|
|
57
|
+
// only CommonJS specifiers need an explicit condition mode.
|
|
58
|
+
const mode = kind === 'require' || kind === 'importEquals' ? typescript_1.default.ModuleKind.CommonJS : undefined;
|
|
59
|
+
const result = typescript_1.default.resolveModuleName(specifier, fromFile, context.compilerOptions, context.host, context.cache, undefined, mode).resolvedModule;
|
|
60
|
+
if (!result)
|
|
61
|
+
return null;
|
|
62
|
+
const resolved = node_path_1.default.resolve(result.resolvedFileName);
|
|
63
|
+
return normalizedFiles.has(resolved) ? resolved : null;
|
|
64
|
+
}
|