devkit-quality 0.1.0 → 0.1.1

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/dist/discovery.js CHANGED
@@ -3,58 +3,205 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.GENERATED_HINTS = exports.IGNORE_DIRS = exports.SOURCE_EXTENSIONS = void 0;
6
+ exports.IGNORE_DIRS = exports.SOURCE_EXTENSIONS = void 0;
7
7
  exports.isRelevantFile = isRelevantFile;
8
8
  exports.isTestFile = isTestFile;
9
9
  exports.isGeneratedFile = isGeneratedFile;
10
10
  exports.isGeneratedByContent = isGeneratedByContent;
11
11
  exports.filterScannedFiles = filterScannedFiles;
12
+ exports.discoverFiles = discoverFiles;
12
13
  exports.walkDirectory = walkDirectory;
14
+ exports.toRelative = toRelative;
13
15
  exports.readFileSafe = readFileSafe;
14
16
  exports.countLines = countLines;
17
+ const node_child_process_1 = require("node:child_process");
15
18
  const node_fs_1 = __importDefault(require("node:fs"));
16
19
  const node_path_1 = __importDefault(require("node:path"));
17
20
  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
+ const ignore_1 = require("./ignore");
22
+ exports.SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts']);
23
+ // Tool caches and framework build output: never source, wherever they appear.
24
+ exports.IGNORE_DIRS = new Set([
25
+ 'node_modules', '.git', '.devkit', '.next', '.nuxt', '.svelte-kit', '.angular', '.turbo', '.cache',
26
+ '.vercel', '.output', '.parcel-cache', '.yarn', '.pnpm-store', '__pycache__', '.venv', '.idea'
27
+ ]);
28
+ // Build-output names that are only build output when they sit directly in a package root
29
+ // (next to a package.json). `src/build/` or `src/commands/out/` are ordinary source folders.
30
+ // Deliberately excludes `vendor`: vendored/checked-in third-party code is sometimes kept out of
31
+ // lint configs, but it is still real, committed content that must stay visible to a security scan.
32
+ const PACKAGE_OUTPUT_DIRS = new Set(['dist', 'build', 'out', 'coverage']);
33
+ const GENERATED_DIRS = new Set(['generated', '__generated__']);
34
+ const MAX_SOURCE_FILE_BYTES = 1_500_000;
35
+ const MAX_TEXT_FILE_BYTES = 1_000_000;
36
+ // Files whose contents are machine-produced noise for a secret scan.
37
+ const NON_SCANNABLE_TEXT = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|go\.sum)$|\.(?:map|svg|snap|lock)$/i;
38
+ const BINARY_EXTENSIONS = /\.(?:png|jpe?g|gif|webp|ico|bmp|tiff?|avif|pdf|zip|gz|tgz|bz2|xz|7z|rar|jar|war|class|exe|dll|so|dylib|o|a|wasm|woff2?|ttf|otf|eot|mp[34]|mov|avi|webm|wav|ogg|flac|psd|sketch|fig|db|sqlite3?|bin|dat|pyc)$/i;
21
39
  function isRelevantFile(filePath) {
22
- const ext = node_path_1.default.extname(filePath).toLowerCase();
23
- return exports.SOURCE_EXTENSIONS.has(ext);
40
+ return exports.SOURCE_EXTENSIONS.has(node_path_1.default.extname(filePath).toLowerCase());
24
41
  }
25
42
  function isTestFile(filePath) {
26
- const lower = filePath.toLowerCase();
27
- return lower.includes('.test.') || lower.includes('.spec.') || lower.includes('__tests__');
43
+ const lower = filePath.replace(/\\/g, '/').toLowerCase();
44
+ return (/\.(?:test|spec|e2e|stories)\.[cm]?[jt]sx?$/.test(lower) ||
45
+ /(?:^|\/)(?:__tests__|__mocks__|__fixtures__|tests?|e2e|cypress|playwright|fixtures?)\//.test(lower));
28
46
  }
29
47
  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']);
48
+ const segments = filePath.replace(/\\/g, '/').toLowerCase().split('/');
33
49
  const basename = segments[segments.length - 1] ?? '';
34
- return segments.some((segment) => generatedDirectories.has(segment)) || basename.endsWith('.d.ts') || basename.includes('.min.');
50
+ return (segments.some((segment) => exports.IGNORE_DIRS.has(segment) || GENERATED_DIRS.has(segment)) ||
51
+ /\.d\.[cm]?ts$/.test(basename) ||
52
+ /[.-]min\.[cm]?js$/.test(basename) ||
53
+ /\.bundle\.[cm]?js$/.test(basename));
54
+ }
55
+ /** The comment block (and blank lines) at the very top of a file, before any code. */
56
+ function leadingCommentBlock(text) {
57
+ const header = [];
58
+ for (const line of text.split(/\r\n|\r|\n/, 40)) {
59
+ const trimmed = line.trim();
60
+ if (trimmed && !/^(?:\/\/|\/\*|\*|#!|'use |"use )/.test(trimmed))
61
+ break;
62
+ header.push(line);
63
+ }
64
+ return header.join('\n');
35
65
  }
66
+ /**
67
+ * Only the leading comment block is inspected: a "do not edit" note inside a hand-written
68
+ * file must not hide that file from the scan.
69
+ */
36
70
  function isGeneratedByContent(text) {
37
- return /(?:@generated|\bDO NOT EDIT\b|\bGENERATED FILE\b)/i.test(text) || /^\/\/[#@]\s*sourceMappingURL=/m.test(text);
71
+ const header = leadingCommentBlock(text);
72
+ // `DO NOT EDIT` is matched case-sensitively: generators emit it uppercase, while a human's
73
+ // "do not edit this by hand" note is not a generated-file marker.
74
+ if (/\bDO NOT EDIT\b/.test(header) || /@generated\b|\bauto-?generated\b|\bGENERATED FILE\b|\bThis file (?:is|was) (?:automatically )?generated\b/i.test(header)) {
75
+ return true;
76
+ }
77
+ if (/^\/\/[#@]\s*sourceMappingURL=/m.test(text.slice(-400)))
78
+ return true;
79
+ return looksMinified(text);
80
+ }
81
+ function looksMinified(text) {
82
+ if (text.length < 5000)
83
+ return false;
84
+ const lines = text.split('\n');
85
+ return text.length / lines.length > 300;
86
+ }
87
+ function isBinaryContent(buffer) {
88
+ const sample = buffer.subarray(0, 8000);
89
+ return sample.includes(0);
90
+ }
91
+ function listGitFiles(projectRoot) {
92
+ try {
93
+ const output = (0, node_child_process_1.execFileSync)('git', ['-C', projectRoot, 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
94
+ encoding: 'utf8',
95
+ stdio: ['ignore', 'pipe', 'ignore'],
96
+ maxBuffer: 256 * 1024 * 1024
97
+ });
98
+ const listed = output.split('\0').filter(Boolean);
99
+ // An empty listing usually means the folder is itself ignored by an enclosing repository.
100
+ if (listed.length === 0)
101
+ return null;
102
+ return listed
103
+ .map((relative) => node_path_1.default.join(projectRoot, relative))
104
+ .filter((file) => {
105
+ try {
106
+ return node_fs_1.default.statSync(file).isFile();
107
+ }
108
+ catch {
109
+ return false; // tracked but deleted in the working tree
110
+ }
111
+ });
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /** Drops files inside ignored directories, including build output located in a package root. */
118
+ function isInIgnoredDirectory(relativePath, packageRoots) {
119
+ const segments = relativePath.split('/');
120
+ for (let index = 0; index < segments.length - 1; index += 1) {
121
+ const segment = segments[index];
122
+ if (exports.IGNORE_DIRS.has(segment))
123
+ return true;
124
+ if (PACKAGE_OUTPUT_DIRS.has(segment) && packageRoots.has(segments.slice(0, index).join('/')))
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+ function collectPackageRoots(relativeFiles) {
130
+ const roots = new Set(['']);
131
+ for (const relative of relativeFiles) {
132
+ if (relative === 'package.json' || relative.endsWith('/package.json')) {
133
+ roots.add(relative.slice(0, -'package.json'.length).replace(/\/$/, ''));
134
+ }
135
+ }
136
+ return roots;
38
137
  }
39
138
  function filterScannedFiles(allFiles, projectRoot, config, ignorePatterns) {
40
139
  const files = [];
41
140
  const generatedFiles = [];
141
+ const tooLarge = [];
42
142
  for (const file of allFiles) {
43
143
  if (!isRelevantFile(file))
44
144
  continue;
45
- const relative = node_path_1.default.relative(projectRoot, file).replace(/\\/g, '/');
145
+ const relative = toRelative(projectRoot, file);
46
146
  if (!(0, glob_1.matchesAnyGlob)(relative, config.scan.include))
47
147
  continue;
48
148
  if ((0, glob_1.matchesAnyGlob)(relative, config.scan.exclude) || (0, glob_1.matchesAnyGlob)(relative, ignorePatterns))
49
149
  continue;
50
- const text = readFileSafe(file);
51
- if (isGeneratedFile(relative) || isGeneratedByContent(text)) {
150
+ if (fileSize(file) > MAX_SOURCE_FILE_BYTES) {
151
+ tooLarge.push(relative);
152
+ continue;
153
+ }
154
+ if (isGeneratedFile(relative) || isGeneratedByContent(readFileSafe(file))) {
52
155
  generatedFiles.push(relative);
53
156
  continue;
54
157
  }
55
158
  files.push(file);
56
159
  }
57
- return { files, generatedFiles };
160
+ return { files, generatedFiles, tooLarge };
161
+ }
162
+ function discoverFiles(projectRoot, config) {
163
+ const gitFiles = listGitFiles(projectRoot);
164
+ const ignorePatterns = (0, ignore_1.loadIgnorePatterns)(projectRoot, { includeGitignore: gitFiles === null });
165
+ const candidates = gitFiles ?? walkDirectory(projectRoot);
166
+ const relativeCandidates = candidates.map((file) => toRelative(projectRoot, file));
167
+ const packageRoots = collectPackageRoots(relativeCandidates);
168
+ const allFiles = candidates
169
+ .filter((_, index) => !isInIgnoredDirectory(relativeCandidates[index], packageRoots))
170
+ .filter((file) => !(0, glob_1.matchesAnyGlob)(toRelative(projectRoot, file), ignorePatterns))
171
+ .sort();
172
+ const { files, generatedFiles, tooLarge } = filterScannedFiles(allFiles, projectRoot, config, ignorePatterns);
173
+ const textFiles = [];
174
+ let binary = 0;
175
+ for (const file of allFiles) {
176
+ if (isRelevantFile(file))
177
+ continue;
178
+ const relative = toRelative(projectRoot, file);
179
+ if ((0, glob_1.matchesAnyGlob)(relative, config.scan.exclude))
180
+ continue;
181
+ if (BINARY_EXTENSIONS.test(file)) {
182
+ binary += 1;
183
+ continue;
184
+ }
185
+ if (NON_SCANNABLE_TEXT.test(relative))
186
+ continue;
187
+ if (fileSize(file) > MAX_TEXT_FILE_BYTES) {
188
+ tooLarge.push(relative);
189
+ continue;
190
+ }
191
+ let buffer;
192
+ try {
193
+ buffer = node_fs_1.default.readFileSync(file);
194
+ }
195
+ catch {
196
+ continue;
197
+ }
198
+ if (isBinaryContent(buffer)) {
199
+ binary += 1;
200
+ continue;
201
+ }
202
+ textFiles.push(file);
203
+ }
204
+ return { allFiles, files, textFiles, generatedFiles, skipped: { tooLarge, binary }, method: gitFiles ? 'git' : 'filesystem' };
58
205
  }
59
206
  function walkDirectory(rootDir) {
60
207
  const allFiles = [];
@@ -83,6 +230,17 @@ function walkDirectory(rootDir) {
83
230
  visit(rootDir);
84
231
  return allFiles;
85
232
  }
233
+ function toRelative(projectRoot, file) {
234
+ return node_path_1.default.relative(projectRoot, file).replace(/\\/g, '/');
235
+ }
236
+ function fileSize(filePath) {
237
+ try {
238
+ return node_fs_1.default.statSync(filePath).size;
239
+ }
240
+ catch {
241
+ return 0;
242
+ }
243
+ }
86
244
  function readFileSafe(filePath) {
87
245
  try {
88
246
  return node_fs_1.default.readFileSync(filePath, 'utf8');
package/dist/glob.js CHANGED
@@ -7,11 +7,15 @@ function globToRegExp(pattern) {
7
7
  for (let i = 0; i < pattern.length; i += 1) {
8
8
  const char = pattern[i];
9
9
  if (char === '*' && pattern[i + 1] === '*') {
10
- regex += '.*';
11
10
  i += 1;
12
11
  if (pattern[i + 1] === '/') {
12
+ // `**/` spans zero or more whole directories, so `**/lib` must not match `mylib`.
13
+ regex += '(?:.*/)?';
13
14
  i += 1;
14
15
  }
16
+ else {
17
+ regex += '.*';
18
+ }
15
19
  continue;
16
20
  }
17
21
  if (char === '*') {
package/dist/ignore.js CHANGED
@@ -6,9 +6,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadIgnorePatterns = loadIgnorePatterns;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- function loadIgnorePatterns(projectRoot) {
9
+ /**
10
+ * Converts .gitignore-style lines into globs. A pattern with a leading or inner slash is
11
+ * anchored to the project root (`/lib` ignores `lib/`, not `src/lib/`); a bare name matches
12
+ * at any depth. Negations (`!pattern`) are not supported here — when the project is a git
13
+ * repository, git itself applies .gitignore and this is only used for .devkitignore.
14
+ */
15
+ function loadIgnorePatterns(projectRoot, options = {}) {
16
+ const includeGitignore = options.includeGitignore ?? true;
10
17
  const patterns = new Set();
11
- for (const name of ['.gitignore', '.devkitignore']) {
18
+ const sources = includeGitignore ? ['.gitignore', '.devkitignore'] : ['.devkitignore'];
19
+ for (const name of sources) {
12
20
  const file = node_path_1.default.join(projectRoot, name);
13
21
  if (!node_fs_1.default.existsSync(file))
14
22
  continue;
@@ -16,12 +24,13 @@ function loadIgnorePatterns(projectRoot) {
16
24
  const line = rawLine.trim();
17
25
  if (!line || line.startsWith('#') || line.startsWith('!'))
18
26
  continue;
27
+ const anchored = line.startsWith('/') || line.replace(/\/$/, '').includes('/');
19
28
  const normalized = line.replace(/^\//, '').replace(/\/$/, '');
20
29
  if (!normalized)
21
30
  continue;
22
- patterns.add(normalized);
23
- patterns.add(`**/${normalized}`);
24
- patterns.add(`**/${normalized}/**`);
31
+ const prefix = anchored || normalized.startsWith('**/') ? '' : '**/';
32
+ patterns.add(`${prefix}${normalized}`);
33
+ patterns.add(`${prefix}${normalized}/**`);
25
34
  }
26
35
  }
27
36
  return [...patterns];
@@ -12,6 +12,7 @@ const parse_1 = require("./ast/parse");
12
12
  const walk_1 = require("./ast/walk");
13
13
  const glob_1 = require("./glob");
14
14
  const moduleResolution_1 = require("./moduleResolution");
15
+ const TOOL_CONFIG_FILE = /(?:^|\/)(?:[\w.-]+\.config|\.[\w-]+rc)\.[cm]?[jt]sx?$/;
15
16
  function extractImportsAndExports(file, projectRoot, sourceFile, fileSet, resolutionContext) {
16
17
  const imports = [];
17
18
  const exports = [];
@@ -231,7 +232,9 @@ function resolveEntryPoints(projectRoot, files, packageJson, config) {
231
232
  // Keep the parameter explicit: framework and custom entry conventions are config-dependent.
232
233
  void config;
233
234
  for (const file of files) {
234
- if ((0, discovery_1.isTestFile)(file)) {
235
+ const relative = node_path_1.default.relative(projectRoot, file).replace(/\\/g, '/');
236
+ // Tests and tool config files (vite.config.ts, eslint.config.mjs, ...) are loaded by tools, not imported.
237
+ if ((0, discovery_1.isTestFile)(relative) || TOOL_CONFIG_FILE.test(relative)) {
235
238
  entryPoints.add(file);
236
239
  }
237
240
  }