@codapult/guard 0.2.0 → 0.4.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/README.md +73 -1
- package/dist/adapters/command.d.ts +10 -8
- package/dist/adapters/command.js +23 -2
- package/dist/adapters/project-checks.d.ts +23 -20
- package/dist/cli/commands/guard.d.ts +53 -54
- package/dist/cli/commands/guard.js +155 -32
- package/dist/cli/index.js +41 -5
- package/dist/core/analysis/doctor.d.ts +1 -1
- package/dist/core/analysis/doctor.js +19 -6
- package/dist/core/discovery/discovery.d.ts +27 -13
- package/dist/core/discovery/discovery.js +182 -29
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/guard.d.ts +134 -69
- package/dist/core/guard.js +767 -145
- package/dist/core/history/runs.d.ts +28 -0
- package/dist/core/history/runs.js +59 -0
- package/dist/core/model/types.d.ts +10 -0
- package/dist/core/model/types.js +1 -0
- package/dist/core/policy/schemas.d.ts +81 -0
- package/dist/core/policy/schemas.js +46 -17
- package/dist/core/verification/verify.d.ts +21 -12
- package/dist/core/verification/verify.js +81 -8
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp/resources.js +64 -29
- package/dist/mcp/tools/guard.js +300 -52
- package/package.json +12 -7
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, extname, join, normalize, relative, resolve } from 'node:path';
|
|
5
5
|
import { Project, SyntaxKind } from 'ts-morph';
|
|
6
6
|
import { config } from '../config.js';
|
|
7
|
+
export class DiscoveryLimitError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'DiscoveryLimitError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class DiscoveryStaleError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('Project changed while Guard was discovering it.');
|
|
16
|
+
this.name = 'DiscoveryStaleError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const DEFAULT_MAX_FILES = 100_000;
|
|
20
|
+
const DEFAULT_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
7
21
|
const IGNORED_DIRECTORIES = new Set([
|
|
8
22
|
'.git',
|
|
9
23
|
'.next',
|
|
@@ -40,12 +54,16 @@ const CONFIG_NAMES = new Set([
|
|
|
40
54
|
]);
|
|
41
55
|
const discoveryCache = new Map();
|
|
42
56
|
const discoveryReuse = new Map();
|
|
57
|
+
const discoveryCacheHits = new Map();
|
|
43
58
|
// Bump when the persisted model shape changes; old cache entries must never
|
|
44
59
|
// bypass discovery and return an incomplete ProjectModel.
|
|
45
60
|
const DISCOVERY_CACHE_VERSION = 3;
|
|
46
61
|
function discoveryCachePath(root) {
|
|
47
62
|
return resolve(root, `.${config.appName}/guard/cache.json`);
|
|
48
63
|
}
|
|
64
|
+
export function clearDiscoveryCache(root) {
|
|
65
|
+
rmSync(discoveryCachePath(root), { force: true });
|
|
66
|
+
}
|
|
49
67
|
function contentHash(root, path) {
|
|
50
68
|
return createHash('sha256')
|
|
51
69
|
.update(readFileSync(resolve(root, path)))
|
|
@@ -121,20 +139,45 @@ function workspacePackages(root, files) {
|
|
|
121
139
|
...asStringRecord(manifest.devDependencies),
|
|
122
140
|
...asStringRecord(manifest.peerDependencies),
|
|
123
141
|
}).sort(),
|
|
142
|
+
...(manifest.exports !== undefined
|
|
143
|
+
? {
|
|
144
|
+
exports: Array.isArray(manifest.exports)
|
|
145
|
+
? manifest.exports.filter((item) => typeof item === 'string')
|
|
146
|
+
: manifest.exports !== null && typeof manifest.exports === 'object'
|
|
147
|
+
? Object.keys(manifest.exports)
|
|
148
|
+
: [],
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
151
|
+
...(Array.isArray(readJsonObject(resolve(root, dirname(path), 'tsconfig.json')).references)
|
|
152
|
+
? {
|
|
153
|
+
projectReferences: readJsonObject(resolve(root, dirname(path), 'tsconfig.json'))
|
|
154
|
+
.references
|
|
155
|
+
.filter((reference) => reference !== null &&
|
|
156
|
+
typeof reference === 'object' &&
|
|
157
|
+
typeof reference.path === 'string')
|
|
158
|
+
.map((reference) => reference.path)
|
|
159
|
+
.sort(),
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
124
162
|
};
|
|
125
163
|
})
|
|
126
164
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
127
165
|
}
|
|
128
|
-
function listFiles(root, directory = root) {
|
|
166
|
+
function listFiles(root, directory = root, maxFiles = DEFAULT_MAX_FILES) {
|
|
129
167
|
const result = [];
|
|
130
168
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
131
169
|
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name))
|
|
132
170
|
continue;
|
|
133
171
|
const path = resolve(directory, entry.name);
|
|
134
172
|
if (entry.isDirectory())
|
|
135
|
-
result.push(...listFiles(root, path));
|
|
136
|
-
else if (entry.isFile() && !ignoredFile(path))
|
|
173
|
+
result.push(...listFiles(root, path, maxFiles - result.length));
|
|
174
|
+
else if (entry.isFile() && !ignoredFile(path)) {
|
|
137
175
|
result.push(relative(root, path));
|
|
176
|
+
if (result.length > maxFiles) {
|
|
177
|
+
throw new DiscoveryLimitError(`Project contains more than ${maxFiles} files; discovery limit was reached. ` +
|
|
178
|
+
'Use an ignore file or raise maxFiles deliberately.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
138
181
|
}
|
|
139
182
|
return result.sort();
|
|
140
183
|
}
|
|
@@ -169,7 +212,54 @@ function resolveInternalImport(from, importPath, modules) {
|
|
|
169
212
|
];
|
|
170
213
|
return candidates.find((candidate) => modules.has(candidate));
|
|
171
214
|
}
|
|
172
|
-
function
|
|
215
|
+
function pathAliases(root) {
|
|
216
|
+
return listFiles(root)
|
|
217
|
+
.filter((path) => basename(path) === 'tsconfig.json' || basename(path) === 'jsconfig.json')
|
|
218
|
+
.flatMap((path) => {
|
|
219
|
+
const tsconfigJson = readJsonObject(resolve(root, path));
|
|
220
|
+
const compilerOptions = tsconfigJson.compilerOptions !== null && typeof tsconfigJson.compilerOptions === 'object'
|
|
221
|
+
? tsconfigJson.compilerOptions
|
|
222
|
+
: {};
|
|
223
|
+
const paths = compilerOptions.paths;
|
|
224
|
+
if (paths === null || typeof paths !== 'object')
|
|
225
|
+
return [];
|
|
226
|
+
const baseUrl = typeof compilerOptions.baseUrl === 'string' ? compilerOptions.baseUrl : '.';
|
|
227
|
+
return Object.entries(paths).flatMap(([pattern, value]) => {
|
|
228
|
+
if (!Array.isArray(value))
|
|
229
|
+
return [];
|
|
230
|
+
const targets = value.filter((item) => typeof item === 'string');
|
|
231
|
+
return targets.length > 0
|
|
232
|
+
? [{ pattern, baseDirectory: resolve(root, dirname(path), baseUrl), targets }]
|
|
233
|
+
: [];
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function resolveAliasedImport(root, importPath, aliases, modules) {
|
|
238
|
+
for (const alias of aliases) {
|
|
239
|
+
const wildcard = alias.pattern.indexOf('*');
|
|
240
|
+
const prefix = wildcard >= 0 ? alias.pattern.slice(0, wildcard) : alias.pattern;
|
|
241
|
+
const suffix = wildcard >= 0 ? alias.pattern.slice(wildcard + 1) : '';
|
|
242
|
+
if (!importPath.startsWith(prefix) || (suffix && !importPath.endsWith(suffix)))
|
|
243
|
+
continue;
|
|
244
|
+
const replacement = importPath.slice(prefix.length, importPath.length - suffix.length);
|
|
245
|
+
for (const target of alias.targets) {
|
|
246
|
+
const targetPath = target.replace('*', replacement);
|
|
247
|
+
const absoluteBase = resolve(alias.baseDirectory, targetPath);
|
|
248
|
+
const relativeBase = relative(root, absoluteBase);
|
|
249
|
+
const normalizedBase = relativeBase.replace(/^\.\//, '').replaceAll('\\', '/');
|
|
250
|
+
const candidates = [
|
|
251
|
+
normalizedBase,
|
|
252
|
+
...[...SOURCE_EXTENSIONS].map((extension) => `${normalizedBase}${extension}`),
|
|
253
|
+
...[...SOURCE_EXTENSIONS].map((extension) => `${normalizedBase}/index${extension}`),
|
|
254
|
+
];
|
|
255
|
+
const projectCandidate = candidates.find((candidate) => modules.has(candidate));
|
|
256
|
+
if (projectCandidate)
|
|
257
|
+
return projectCandidate;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function parseModule(project, root, path, projectFiles, aliases) {
|
|
173
263
|
if (!SOURCE_EXTENSIONS.has(extname(path)))
|
|
174
264
|
return undefined;
|
|
175
265
|
let sourceFile;
|
|
@@ -187,6 +277,7 @@ function parseModule(project, root, path, projectFiles) {
|
|
|
187
277
|
const dynamicImports = [];
|
|
188
278
|
const declarations = { classes: 0, functions: 0, interfaces: 0, types: 0, variables: 0 };
|
|
189
279
|
const resolvedImports = new Set();
|
|
280
|
+
const resolvedImportMap = {};
|
|
190
281
|
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
191
282
|
const importPath = declaration.getModuleSpecifierValue();
|
|
192
283
|
imports.push(importPath);
|
|
@@ -200,9 +291,12 @@ function parseModule(project, root, path, projectFiles) {
|
|
|
200
291
|
importedSymbols.push(namedImport.getAliasNode()?.getText() ?? namedImport.getName());
|
|
201
292
|
}
|
|
202
293
|
const resolved = declaration.getModuleSpecifierSourceFile();
|
|
203
|
-
const resolvedPath = resolved && sourceFilePath(root, resolved, projectFiles)
|
|
204
|
-
|
|
294
|
+
const resolvedPath = (resolved && sourceFilePath(root, resolved, projectFiles)) ??
|
|
295
|
+
resolveAliasedImport(root, importPath, aliases, projectFiles);
|
|
296
|
+
if (resolvedPath) {
|
|
205
297
|
resolvedImports.add(resolvedPath);
|
|
298
|
+
resolvedImportMap[importPath] = resolvedPath;
|
|
299
|
+
}
|
|
206
300
|
}
|
|
207
301
|
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
208
302
|
const moduleSpecifier = declaration.getModuleSpecifierValue();
|
|
@@ -210,9 +304,12 @@ function parseModule(project, root, path, projectFiles) {
|
|
|
210
304
|
continue;
|
|
211
305
|
exports.push(moduleSpecifier);
|
|
212
306
|
const resolved = declaration.getModuleSpecifierSourceFile();
|
|
213
|
-
const resolvedPath = resolved && sourceFilePath(root, resolved, projectFiles)
|
|
214
|
-
|
|
307
|
+
const resolvedPath = (resolved && sourceFilePath(root, resolved, projectFiles)) ??
|
|
308
|
+
resolveAliasedImport(root, moduleSpecifier, aliases, projectFiles);
|
|
309
|
+
if (resolvedPath) {
|
|
215
310
|
resolvedImports.add(resolvedPath);
|
|
311
|
+
resolvedImportMap[moduleSpecifier] = resolvedPath;
|
|
312
|
+
}
|
|
216
313
|
}
|
|
217
314
|
declarations.classes = sourceFile.getClasses().length;
|
|
218
315
|
declarations.functions = sourceFile.getFunctions().length;
|
|
@@ -234,9 +331,12 @@ function parseModule(project, root, path, projectFiles) {
|
|
|
234
331
|
if (!importPath)
|
|
235
332
|
continue;
|
|
236
333
|
dynamicImports.push(importPath);
|
|
237
|
-
const resolvedPath = resolveInternalImport(path, importPath, projectFiles)
|
|
238
|
-
|
|
334
|
+
const resolvedPath = resolveInternalImport(path, importPath, projectFiles) ??
|
|
335
|
+
resolveAliasedImport(root, importPath, aliases, projectFiles);
|
|
336
|
+
if (resolvedPath) {
|
|
239
337
|
resolvedImports.add(resolvedPath);
|
|
338
|
+
resolvedImportMap[importPath] = resolvedPath;
|
|
339
|
+
}
|
|
240
340
|
}
|
|
241
341
|
const directives = sourceFile
|
|
242
342
|
.getStatements()
|
|
@@ -257,6 +357,7 @@ function parseModule(project, root, path, projectFiles) {
|
|
|
257
357
|
exports: [...new Set(exports)].sort(),
|
|
258
358
|
calls: [...new Set(calls)].sort(),
|
|
259
359
|
resolvedImports: [...resolvedImports].sort(),
|
|
360
|
+
resolvedImportMap,
|
|
260
361
|
dynamicImports: [...new Set(dynamicImports)].sort(),
|
|
261
362
|
declarations,
|
|
262
363
|
directives,
|
|
@@ -271,6 +372,17 @@ function runGit(root, args) {
|
|
|
271
372
|
return stdout?.toString().trim() || undefined;
|
|
272
373
|
}
|
|
273
374
|
}
|
|
375
|
+
function discoverySignature(root, packageJson, allFiles) {
|
|
376
|
+
return JSON.stringify([
|
|
377
|
+
JSON.stringify(packageJson),
|
|
378
|
+
allFiles.map((path) => {
|
|
379
|
+
const stat = statSync(resolve(root, path));
|
|
380
|
+
return [path, stat.size, stat.mtimeMs, contentHash(root, path)];
|
|
381
|
+
}),
|
|
382
|
+
runGit(root, ['rev-parse', 'HEAD']),
|
|
383
|
+
runGit(root, ['status', '--porcelain']),
|
|
384
|
+
]);
|
|
385
|
+
}
|
|
274
386
|
function gitModel(root) {
|
|
275
387
|
const branch = runGit(root, ['branch', '--show-current']);
|
|
276
388
|
const status = runGit(root, ['status', '--porcelain'])?.split('\n').filter(Boolean) ?? [];
|
|
@@ -288,7 +400,7 @@ function gitModel(root) {
|
|
|
288
400
|
});
|
|
289
401
|
return {
|
|
290
402
|
repository: runGit(root, ['rev-parse', '--is-inside-work-tree']) === 'true',
|
|
291
|
-
branch
|
|
403
|
+
...(branch ? { branch } : {}),
|
|
292
404
|
dirty: status.length > 0,
|
|
293
405
|
changedFiles: [...changed].sort(),
|
|
294
406
|
status,
|
|
@@ -645,32 +757,59 @@ function buildInsights(root, files, modules, sourceFiles, dependencies, astProje
|
|
|
645
757
|
function createAstProject(root) {
|
|
646
758
|
const tsConfigPath = resolve(root, 'tsconfig.json');
|
|
647
759
|
try {
|
|
648
|
-
|
|
760
|
+
const project = existsSync(tsConfigPath)
|
|
649
761
|
? new Project({ tsConfigFilePath: tsConfigPath })
|
|
650
762
|
: new Project({ skipAddingFilesFromTsConfig: true });
|
|
763
|
+
// Monorepos commonly keep compiler options and path aliases per package.
|
|
764
|
+
// Load those projects as well; files still remain bounded by listFiles().
|
|
765
|
+
for (const configPath of listFiles(root).filter((path) => basename(path) === 'tsconfig.json')) {
|
|
766
|
+
const absolutePath = resolve(root, configPath);
|
|
767
|
+
if (absolutePath === tsConfigPath)
|
|
768
|
+
continue;
|
|
769
|
+
try {
|
|
770
|
+
project.addSourceFilesFromTsConfig(absolutePath);
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
// A package config can be partial or reference an unavailable project.
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return project;
|
|
651
777
|
}
|
|
652
778
|
catch {
|
|
653
779
|
return new Project({ skipAddingFilesFromTsConfig: true });
|
|
654
780
|
}
|
|
655
781
|
}
|
|
656
|
-
|
|
782
|
+
function discoverProjectOnce(root, options = {}, attempt = 0) {
|
|
657
783
|
const packageJson = readJsonObject(resolve(root, 'package.json'));
|
|
658
784
|
const dependencies = asStringRecord(packageJson.dependencies);
|
|
659
785
|
const devDependencies = asStringRecord(packageJson.devDependencies);
|
|
660
786
|
const scripts = asStringRecord(packageJson.scripts);
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
787
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
788
|
+
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
789
|
+
if (!Number.isInteger(maxFiles) || maxFiles < 1) {
|
|
790
|
+
throw new DiscoveryLimitError('Discovery maxFiles must be a positive integer.');
|
|
791
|
+
}
|
|
792
|
+
if (!Number.isInteger(maxFileBytes) || maxFileBytes < 1) {
|
|
793
|
+
throw new DiscoveryLimitError('Discovery maxFileBytes must be a positive integer.');
|
|
794
|
+
}
|
|
795
|
+
const allFiles = listFiles(root, root, maxFiles);
|
|
796
|
+
if (allFiles.length > maxFiles) {
|
|
797
|
+
throw new DiscoveryLimitError(`Project contains ${allFiles.length} files; discovery limit is ${maxFiles}. ` +
|
|
798
|
+
'Use an ignore file or raise maxFiles deliberately.');
|
|
799
|
+
}
|
|
800
|
+
for (const path of allFiles) {
|
|
801
|
+
const bytes = statSync(resolve(root, path)).size;
|
|
802
|
+
if (bytes > maxFileBytes) {
|
|
803
|
+
throw new DiscoveryLimitError(`File ${path} is ${bytes} bytes; discovery limit is ${maxFileBytes}. ` +
|
|
804
|
+
'Ignore the file or raise maxFileBytes deliberately.');
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const signature = discoverySignature(root, packageJson, allFiles);
|
|
671
808
|
const cacheKey = resolve(root);
|
|
809
|
+
discoveryCacheHits.set(cacheKey, false);
|
|
672
810
|
const cached = discoveryCache.get(cacheKey);
|
|
673
811
|
if (cached?.signature === signature) {
|
|
812
|
+
discoveryCacheHits.set(cacheKey, true);
|
|
674
813
|
discoveryReuse.set(cacheKey, cached.model.modules.length);
|
|
675
814
|
return cached.model;
|
|
676
815
|
}
|
|
@@ -683,6 +822,7 @@ export function discoverProject(root, options = {}) {
|
|
|
683
822
|
persisted.model.version === 1) {
|
|
684
823
|
previousModel = persisted.model;
|
|
685
824
|
if (persisted.signature === signature) {
|
|
825
|
+
discoveryCacheHits.set(cacheKey, true);
|
|
686
826
|
discoveryCache.set(cacheKey, { signature, model: previousModel });
|
|
687
827
|
discoveryReuse.set(cacheKey, previousModel.modules.length);
|
|
688
828
|
return previousModel;
|
|
@@ -699,6 +839,7 @@ export function discoverProject(root, options = {}) {
|
|
|
699
839
|
}));
|
|
700
840
|
const projectFiles = new Set(files.map((file) => file.path));
|
|
701
841
|
const astProject = createAstProject(root);
|
|
842
|
+
const aliases = pathAliases(root);
|
|
702
843
|
const previousModules = new Map(previousModel?.modules.map((module) => [module.path, module]));
|
|
703
844
|
const currentShape = files
|
|
704
845
|
.filter((file) => file.kind === 'source' || file.kind === 'test')
|
|
@@ -723,7 +864,7 @@ export function discoverProject(root, options = {}) {
|
|
|
723
864
|
reusedModules += 1;
|
|
724
865
|
return previous;
|
|
725
866
|
}
|
|
726
|
-
return parseModule(astProject, root, file.path, projectFiles);
|
|
867
|
+
return parseModule(astProject, root, file.path, projectFiles, aliases);
|
|
727
868
|
})
|
|
728
869
|
.filter((module) => module !== undefined);
|
|
729
870
|
const sourceFiles = files.filter((file) => file.kind === 'source').map((file) => file.path);
|
|
@@ -756,11 +897,12 @@ export function discoverProject(root, options = {}) {
|
|
|
756
897
|
}
|
|
757
898
|
return { path, methods: [...methods].sort() };
|
|
758
899
|
});
|
|
900
|
+
const detectedPackageManager = packageManager(root);
|
|
759
901
|
const model = {
|
|
760
902
|
version: 1,
|
|
761
903
|
project: {
|
|
762
|
-
|
|
763
|
-
packageManager:
|
|
904
|
+
...(typeof packageJson.name === 'string' ? { name: packageJson.name } : {}),
|
|
905
|
+
...(detectedPackageManager ? { packageManager: detectedPackageManager } : {}),
|
|
764
906
|
frameworks: inferFrameworks(dependencies, devDependencies),
|
|
765
907
|
scripts,
|
|
766
908
|
dependencies,
|
|
@@ -788,6 +930,15 @@ export function discoverProject(root, options = {}) {
|
|
|
788
930
|
},
|
|
789
931
|
insights: buildInsights(root, files, modules, sourceFiles, dependencies, astProject),
|
|
790
932
|
};
|
|
933
|
+
const finalFiles = listFiles(root, root, maxFiles);
|
|
934
|
+
const finalSignature = finalFiles.length <= maxFiles
|
|
935
|
+
? discoverySignature(root, readJsonObject(resolve(root, 'package.json')), finalFiles)
|
|
936
|
+
: undefined;
|
|
937
|
+
if (finalSignature !== signature) {
|
|
938
|
+
if (attempt < 2)
|
|
939
|
+
return discoverProjectOnce(root, options, attempt + 1);
|
|
940
|
+
throw new DiscoveryStaleError();
|
|
941
|
+
}
|
|
791
942
|
discoveryCache.set(cacheKey, { signature, model });
|
|
792
943
|
discoveryReuse.set(cacheKey, reusedModules);
|
|
793
944
|
if (options.persistCache ?? existsSync(resolve(root, `.${config.appName}/guard`))) {
|
|
@@ -804,9 +955,11 @@ export function discoverProject(root, options = {}) {
|
|
|
804
955
|
}
|
|
805
956
|
return model;
|
|
806
957
|
}
|
|
958
|
+
export function discoverProject(root, options = {}) {
|
|
959
|
+
return discoverProjectOnce(root, options);
|
|
960
|
+
}
|
|
807
961
|
export function discoverProjectWithMetrics(root, options = {}) {
|
|
808
962
|
const startedAt = Date.now();
|
|
809
|
-
const cacheHit = discoveryCache.has(resolve(root));
|
|
810
963
|
const model = discoverProject(root, options);
|
|
811
964
|
return {
|
|
812
965
|
model,
|
|
@@ -815,7 +968,7 @@ export function discoverProjectWithMetrics(root, options = {}) {
|
|
|
815
968
|
files: model.files.length,
|
|
816
969
|
modules: model.modules.length,
|
|
817
970
|
cacheAvailable: existsSync(discoveryCachePath(root)),
|
|
818
|
-
cacheHit,
|
|
971
|
+
cacheHit: discoveryCacheHits.get(resolve(root)) ?? false,
|
|
819
972
|
changedFiles: model.git.changedFiles.length,
|
|
820
973
|
reusedModules: discoveryReuse.get(resolve(root)) ?? 0,
|
|
821
974
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type GuardErrorCode = 'GUARD_NOT_CONFIGURED' | 'GUARD_CONFIG_INVALID' | 'GUARD_STATE_BUSY' | 'GUARD_STATE_STALE' | 'GUARD_INVALID_INPUT';
|
|
2
|
+
export interface GuardErrorPayload {
|
|
3
|
+
status: 'error';
|
|
4
|
+
outcome: 'not-configured' | 'error';
|
|
5
|
+
configured: boolean;
|
|
6
|
+
errorCode: GuardErrorCode;
|
|
7
|
+
message: string;
|
|
8
|
+
recoverable: boolean;
|
|
9
|
+
hint?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function guardErrorPayload(errorCode: GuardErrorCode, message: string, options: Pick<GuardErrorPayload, 'configured' | 'recoverable' | 'outcome'> & Partial<Pick<GuardErrorPayload, 'hint'>>): GuardErrorPayload;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function guardErrorPayload(errorCode, message, options) {
|
|
2
|
+
return {
|
|
3
|
+
status: 'error',
|
|
4
|
+
outcome: options.outcome,
|
|
5
|
+
configured: options.configured,
|
|
6
|
+
errorCode,
|
|
7
|
+
message,
|
|
8
|
+
recoverable: options.recoverable,
|
|
9
|
+
...(options.hint ? { hint: options.hint } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|