@codometer/measurement 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +553 -2
- package/dist/src/index.d.ts +1280 -0
- package/dist/src/index.js +1150 -0
- package/package.json +63 -3
|
@@ -0,0 +1,1280 @@
|
|
|
1
|
+
import { CodeStatisticsResult } from '@codometer/core';
|
|
2
|
+
import { CodometerCompression } from '@codometer/configuration';
|
|
3
|
+
import { CodometerSeverity } from '@codometer/core';
|
|
4
|
+
import { CommentCounter } from '@codometer/languages';
|
|
5
|
+
import { CommentMeasurement } from '@codometer/languages';
|
|
6
|
+
import { ConsoleLogger } from '@nestjs/common';
|
|
7
|
+
import { CustomStatisticResult } from '@codometer/core';
|
|
8
|
+
import { CustomStatisticResultInstance } from '@codometer/core';
|
|
9
|
+
import { Ignore } from 'ignore';
|
|
10
|
+
import { LanguagesService } from '@codometer/languages';
|
|
11
|
+
import pino from 'pino';
|
|
12
|
+
import { ReportFailure } from '@codometer/core';
|
|
13
|
+
import { ResolvedCodometerConfiguration } from '@codometer/configuration';
|
|
14
|
+
import { ResolvedCodometerCustomStatistic } from '@codometer/configuration';
|
|
15
|
+
import { ResolvedCodometerInput } from '@codometer/configuration';
|
|
16
|
+
import { TypescriptSymbolCounter } from '@codometer/languages';
|
|
17
|
+
|
|
18
|
+
/** Arguments accepted when measuring the size of a target's files. */
|
|
19
|
+
export declare interface AnalyzeSizeArguments {
|
|
20
|
+
compression: CodometerCompression;
|
|
21
|
+
/** Paths relative to the working directory, as the target matched them. */
|
|
22
|
+
files: string[];
|
|
23
|
+
workingDirectory: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Arguments accepted when building a rule set from patterns already in hand. */
|
|
27
|
+
declare interface CreateIgnoreScopeArguments {
|
|
28
|
+
directory: string;
|
|
29
|
+
patterns: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Input to the custom statistics step. */
|
|
33
|
+
export declare interface CustomizationInput {
|
|
34
|
+
/**
|
|
35
|
+
* Every `comment`-selector counter's own measurements, keyed by its
|
|
36
|
+
* statistic's label.
|
|
37
|
+
*
|
|
38
|
+
* Built by measuring the counters `buildCommentCounters` returns, then
|
|
39
|
+
* merging the two results back together by label — the languages package
|
|
40
|
+
* measures a counter naming a declaration kind and one naming a language
|
|
41
|
+
* through two different calls, but a label belongs to exactly one custom
|
|
42
|
+
* statistic either way.
|
|
43
|
+
*/
|
|
44
|
+
commentCounts: Record<string, CommentMeasurement[]>;
|
|
45
|
+
/** Every file of the target being counted over. */
|
|
46
|
+
files: string[];
|
|
47
|
+
statistics: ResolvedCodometerCustomStatistic[];
|
|
48
|
+
/** What the TypeScript analyzer tallied, keyed by counter label. */
|
|
49
|
+
symbolCounts: Record<string, number>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* NestJS module that provides the configured file-name counters.
|
|
54
|
+
*/
|
|
55
|
+
export declare class CustomizationModule {
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Counts the conventions a repository holds itself to.
|
|
60
|
+
*
|
|
61
|
+
* The languages a repository is written in are the same everywhere; what a
|
|
62
|
+
* `*.service.ts` means, or whether a static method is something to keep an
|
|
63
|
+
* eye on, is not — which is why these counters come from the configuration
|
|
64
|
+
* rather than from this package.
|
|
65
|
+
*
|
|
66
|
+
* A counter measures files by path, declarations by shape, or a comment
|
|
67
|
+
* budget by length. The file half is done here; the declaration half is
|
|
68
|
+
* tallied by the TypeScript analyzer during the walk it already makes, and
|
|
69
|
+
* the comment half by `@codometer/languages`' comment services — both arrive
|
|
70
|
+
* here as counts to be labelled.
|
|
71
|
+
*/
|
|
72
|
+
export declare class CustomizationService {
|
|
73
|
+
constructor();
|
|
74
|
+
/** Turn a `comment`-selector statistic's own breaches into its result. */
|
|
75
|
+
private buildCommentResult;
|
|
76
|
+
/**
|
|
77
|
+
* Counts the target's files that at least one of the globs claims.
|
|
78
|
+
*
|
|
79
|
+
* A file matching several globs of the same counter is one file, not
|
|
80
|
+
* several: the counter asks how many files there are, not how many times
|
|
81
|
+
* they matched.
|
|
82
|
+
*/
|
|
83
|
+
private countMatches;
|
|
84
|
+
/** Count every configured statistic over the discovered files. */
|
|
85
|
+
analyze({ commentCounts, files, statistics, symbolCounts, }: CustomizationInput): CustomStatisticResult[];
|
|
86
|
+
/**
|
|
87
|
+
* Pick out the counters `@codometer/languages`' comment services have to
|
|
88
|
+
* measure.
|
|
89
|
+
*
|
|
90
|
+
* Handed to those services rather than measured again here: a `comment`
|
|
91
|
+
* selector names a language or a documentable kind, and turning that into a
|
|
92
|
+
* budget is this package's business, not the tokenizer's. One list, mirroring
|
|
93
|
+
* the selector field for field — which counter a given measurer takes is
|
|
94
|
+
* decided by each measurer, which selects from the list by reading `kind`.
|
|
95
|
+
*/
|
|
96
|
+
buildCommentCounters(statistics: CustomizationInput["statistics"]): CommentCounter[];
|
|
97
|
+
/**
|
|
98
|
+
* Pick out the counters the TypeScript analyzer has to tally.
|
|
99
|
+
*
|
|
100
|
+
* Handed to that analyzer rather than parsed again here: it already walks
|
|
101
|
+
* every source file, and a second walk would double the slowest part of a
|
|
102
|
+
* run to learn what the first one passed straight over.
|
|
103
|
+
*/
|
|
104
|
+
buildSymbolCounters(statistics: CustomizationInput["statistics"]): TypescriptSymbolCounter[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Arguments accepted when discovering the files to measure. */
|
|
108
|
+
export declare interface DiscoverFilesArguments {
|
|
109
|
+
exclude: string[];
|
|
110
|
+
excludeFrom: string[];
|
|
111
|
+
workingDirectory: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* NestJS module that discovers and categorizes the files of a codebase.
|
|
116
|
+
*/
|
|
117
|
+
export declare class DiscoveryModule {
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Categorized lists of file paths, relative to the working directory. */
|
|
121
|
+
export declare interface DiscoveryResult {
|
|
122
|
+
cssFiles: string[];
|
|
123
|
+
/** Every file the target holds, before any category claims it. */
|
|
124
|
+
files: string[];
|
|
125
|
+
hclFiles: string[];
|
|
126
|
+
jsFiles: string[];
|
|
127
|
+
jsonFiles: string[];
|
|
128
|
+
markdownFiles: string[];
|
|
129
|
+
notebookFiles: string[];
|
|
130
|
+
pyFiles: string[];
|
|
131
|
+
shellFiles: string[];
|
|
132
|
+
sourceFiles: string[];
|
|
133
|
+
sqlFiles: string[];
|
|
134
|
+
testFiles: string[];
|
|
135
|
+
tomlFiles: string[];
|
|
136
|
+
tsFiles: string[];
|
|
137
|
+
yamlFiles: string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Discovers and categorizes the files of a codebase directory. */
|
|
141
|
+
export declare class DiscoveryService {
|
|
142
|
+
private readonly ignoreRulesService;
|
|
143
|
+
private readonly logger;
|
|
144
|
+
constructor(ignoreRulesService: IgnoreRulesService, logger: LoggerService);
|
|
145
|
+
/**
|
|
146
|
+
* Folds a directory's own `.gitignore` into the rule sets already in force.
|
|
147
|
+
*
|
|
148
|
+
* Appended last so its patterns outrank the ones above it, which is how git
|
|
149
|
+
* resolves a nested ignore file: the closest one to the file wins.
|
|
150
|
+
*/
|
|
151
|
+
private applyDirectoryIgnoreFile;
|
|
152
|
+
/** Selects the discovered files whose extension belongs to a category. */
|
|
153
|
+
private filterByExtension;
|
|
154
|
+
/**
|
|
155
|
+
* Whether any exclusion glob claims the given repository-relative path.
|
|
156
|
+
*
|
|
157
|
+
* Matched with `path.matchesGlob` rather than by substring, so a glob naming
|
|
158
|
+
* a `dist` directory removes build output and leaves a `redistribute`
|
|
159
|
+
* directory alone.
|
|
160
|
+
*/
|
|
161
|
+
private isExcluded;
|
|
162
|
+
/**
|
|
163
|
+
* Whether every file beneath a directory is excluded by a glob already.
|
|
164
|
+
*
|
|
165
|
+
* A pattern ending in `/**` claims every descendant without exception, so a
|
|
166
|
+
* directory matching the pattern with that suffix removed cannot contribute
|
|
167
|
+
* a single file and never has to be read. This is a shortcut and not a rule
|
|
168
|
+
* of its own: the same files would be dropped one by one afterwards either
|
|
169
|
+
* way. It matters in a directory with no `.gitignore` to prune
|
|
170
|
+
* `node_modules`, where reading it dwarfs reading the codebase.
|
|
171
|
+
*/
|
|
172
|
+
private isExhaustivelyExcluded;
|
|
173
|
+
/**
|
|
174
|
+
* Whether either set of ignore rules claims a path.
|
|
175
|
+
*
|
|
176
|
+
* The two sets are answered independently and the answers combined, rather
|
|
177
|
+
* than merged into one set. A configured ignore file subtracts from what the
|
|
178
|
+
* repository's own `.gitignore` files leave behind, exactly as the two git
|
|
179
|
+
* invocations this replaced did; a negation in one cannot resurrect a file
|
|
180
|
+
* the other removed.
|
|
181
|
+
*/
|
|
182
|
+
private isIgnoredPath;
|
|
183
|
+
/**
|
|
184
|
+
* Lists every measurable file in the given directory, in sorted order.
|
|
185
|
+
*
|
|
186
|
+
* Sorted because the walk visits directories in whatever order the
|
|
187
|
+
* filesystem reports them, and every consumer downstream deserves the same
|
|
188
|
+
* list from the same tree on any machine.
|
|
189
|
+
*/
|
|
190
|
+
private listDiscoveredFiles;
|
|
191
|
+
/**
|
|
192
|
+
* Reads a directory's entries, or none when the directory cannot be read.
|
|
193
|
+
*
|
|
194
|
+
* A directory can vanish or refuse to open partway through a walk — one
|
|
195
|
+
* being cleaned up by another process, a mount the caller has no permission
|
|
196
|
+
* on. Shelling out to git never failed for either reason, so letting one
|
|
197
|
+
* unreadable directory abort the whole measurement would be a regression:
|
|
198
|
+
* warn, skip it, and keep counting the rest.
|
|
199
|
+
*/
|
|
200
|
+
private readDirectoryEntries;
|
|
201
|
+
/**
|
|
202
|
+
* Reads the configured ignore files into rule sets anchored at the root.
|
|
203
|
+
*
|
|
204
|
+
* A missing file is a warning rather than a failure: a repository that has
|
|
205
|
+
* renamed its ignore file should hear about it, but a report is still worth
|
|
206
|
+
* more than a crash.
|
|
207
|
+
*/
|
|
208
|
+
private readExcludeFromScopes;
|
|
209
|
+
/**
|
|
210
|
+
* Collects every measurable file under one directory.
|
|
211
|
+
*
|
|
212
|
+
* Walked directory by directory rather than matched by a single recursive
|
|
213
|
+
* glob, because an ignored directory then costs one decision instead of an
|
|
214
|
+
* enumeration: `node_modules/` is pruned where it is named, not discovered
|
|
215
|
+
* in full and thrown away.
|
|
216
|
+
*/
|
|
217
|
+
private walkDirectory;
|
|
218
|
+
/**
|
|
219
|
+
* Descends into one subdirectory, or skips it when nothing there counts.
|
|
220
|
+
*
|
|
221
|
+
* The trailing slash is what makes a `coverage/` pattern claim the directory
|
|
222
|
+
* itself: without it the pattern only ever matches a file of that name.
|
|
223
|
+
*/
|
|
224
|
+
private walkSubdirectory;
|
|
225
|
+
/**
|
|
226
|
+
* Sorts a list of file paths into the categories the analyzers ask for.
|
|
227
|
+
*
|
|
228
|
+
* Separate from the walk so that any target's files can be categorized, not
|
|
229
|
+
* only the ones this service found itself: a target naming its files by glob
|
|
230
|
+
* is analyzed by the same language analyzers as the codebase around it.
|
|
231
|
+
*/
|
|
232
|
+
categorize(files: string[]): DiscoveryResult;
|
|
233
|
+
/** Returns categorized file path lists for the given codebase root. */
|
|
234
|
+
discoverFiles(args: DiscoverFilesArguments): DiscoveryResult;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A target name that answered to more than one measured target. */
|
|
238
|
+
declare interface DuplicateTargetFinding {
|
|
239
|
+
reason: string;
|
|
240
|
+
target: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Raised when a target carrying a limit matched no files at all.
|
|
245
|
+
*
|
|
246
|
+
* Writing a limit asserts the files exist, so nothing to measure means a glob
|
|
247
|
+
* that no longer matches or a build that never ran — either way a number that
|
|
248
|
+
* would pass every limit written against it. A target nobody limited is left
|
|
249
|
+
* alone: there it is simply zero, and unremarkable.
|
|
250
|
+
*/
|
|
251
|
+
export declare class EmptyTargetError extends Error {
|
|
252
|
+
constructor(target: string, metric: string);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* What one limit found once it was pointed at its metric.
|
|
257
|
+
*
|
|
258
|
+
* A breach is one of these carrying `breached`, and it reports everything a
|
|
259
|
+
* breach has to: which metric, held to what, and what the metric actually
|
|
260
|
+
* measured. A limit that held is reported the same way, so a report can show
|
|
261
|
+
* the headroom rather than only the failures.
|
|
262
|
+
*/
|
|
263
|
+
export declare interface EvaluatedLimit {
|
|
264
|
+
/** Whether the measured value came out above the limit. */
|
|
265
|
+
breached: boolean;
|
|
266
|
+
/** Stays `undefined` when none was written; a report falls back to the path. */
|
|
267
|
+
label: string | undefined;
|
|
268
|
+
limit: number;
|
|
269
|
+
measured: number;
|
|
270
|
+
/** The metric's path within its target, with no target name on the front. */
|
|
271
|
+
metric: string;
|
|
272
|
+
severity: CodometerSeverity;
|
|
273
|
+
target: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Arguments accepted when evaluating every declared limit. */
|
|
277
|
+
declare interface EvaluateLimitsArguments {
|
|
278
|
+
configuration: ResolvedCodometerConfiguration;
|
|
279
|
+
indexes: ReadonlyMap<string, TargetMetricIndex>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Reads and applies gitignore-syntax rule sets without invoking git.
|
|
284
|
+
*
|
|
285
|
+
* Codometer used to hand this problem to `git ls-files`, which meant it could
|
|
286
|
+
* only measure a directory that was a git repository. Reading the syntax here
|
|
287
|
+
* is what lets it measure any directory at all, and it is the only reading of
|
|
288
|
+
* that syntax in the tool — there is no git fast path to disagree with.
|
|
289
|
+
*/
|
|
290
|
+
declare class IgnoreRulesService {
|
|
291
|
+
constructor();
|
|
292
|
+
/**
|
|
293
|
+
* The path a rule set sees, or nothing when the path lies outside it.
|
|
294
|
+
*
|
|
295
|
+
* A rule set anchored at `applications/affirmations` matches its patterns
|
|
296
|
+
* against `output/one.md`, not against the full path, because that is what
|
|
297
|
+
* the patterns in that directory's ignore file were written against.
|
|
298
|
+
*/
|
|
299
|
+
private toScopedPath;
|
|
300
|
+
/**
|
|
301
|
+
* Builds a rule set from patterns already in hand.
|
|
302
|
+
*
|
|
303
|
+
* Case-sensitive on every platform, deliberately. Git decides that from the
|
|
304
|
+
* filesystem it happens to be on, so the same ignore file can claim a
|
|
305
|
+
* different set of files on a developer's Mac and on Linux CI. A measurement
|
|
306
|
+
* that disagrees with itself between machines is worse than a strict one.
|
|
307
|
+
*/
|
|
308
|
+
createScope(args: CreateIgnoreScopeArguments): IgnoreScope;
|
|
309
|
+
/**
|
|
310
|
+
* Whether the rule sets ignore a path, the innermost one deciding.
|
|
311
|
+
*
|
|
312
|
+
* gitignore resolution is nested rather than additive: a rule set in a
|
|
313
|
+
* subdirectory overrides the one above it, which is what lets a `!pattern`
|
|
314
|
+
* re-include a file its parent excluded. Reading the scopes outermost first
|
|
315
|
+
* and keeping the last decision reproduces that ordering.
|
|
316
|
+
*
|
|
317
|
+
* A directory is passed with a trailing slash, so a `build/` pattern claims
|
|
318
|
+
* the directory rather than only a file that happens to be named `build`.
|
|
319
|
+
*/
|
|
320
|
+
isIgnored(scopes: readonly IgnoreScope[], relativePath: string): boolean;
|
|
321
|
+
/**
|
|
322
|
+
* Reads a rule set out of a gitignore-syntax file.
|
|
323
|
+
*
|
|
324
|
+
* Returns nothing when the file is absent, so a configured ignore file that
|
|
325
|
+
* was renamed is reported by the caller rather than silently behaving as an
|
|
326
|
+
* empty one.
|
|
327
|
+
*/
|
|
328
|
+
readScope(args: ReadIgnoreScopeArguments): IgnoreScope | undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* A gitignore-syntax rule set together with the directory it is anchored to.
|
|
333
|
+
*
|
|
334
|
+
* The directory matters because gitignore patterns are relative to the file
|
|
335
|
+
* they were written in: `/output` in a project's own ignore file claims that
|
|
336
|
+
* project's `output`, not the one at the repository root.
|
|
337
|
+
*/
|
|
338
|
+
declare interface IgnoreScope {
|
|
339
|
+
/** Directory the patterns are relative to, `/`-separated from the walk root. `""` is the root itself. */
|
|
340
|
+
directory: string;
|
|
341
|
+
matcher: Ignore;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** What a directory entry counts as while an input's tree is walked. */
|
|
345
|
+
export declare type InputEntryKind = "directory" | "file" | "other";
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* What every analysis declared for one input reported over its files.
|
|
349
|
+
*
|
|
350
|
+
* An analysis an input did not ask for reports `undefined` rather than a zero,
|
|
351
|
+
* so an input nobody measured the size of is never mistaken for an empty one.
|
|
352
|
+
*/
|
|
353
|
+
export declare interface InputMeasurement {
|
|
354
|
+
/** How many files the input's globs claimed. */
|
|
355
|
+
files: number;
|
|
356
|
+
language: CodeStatisticsResult | undefined;
|
|
357
|
+
name: string;
|
|
358
|
+
size: SizeResult | undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Raised when an input's directory reaches outside the repository.
|
|
363
|
+
*
|
|
364
|
+
* An input may name its way out of the folder being measured — that is how a
|
|
365
|
+
* project reaches build output written above it — but not out of the
|
|
366
|
+
* repository holding both. Beyond that boundary a configuration file could
|
|
367
|
+
* read any file on the machine and report its size, which is not a measurement
|
|
368
|
+
* anybody asked for and is exactly what a tool published as a shared action
|
|
369
|
+
* must not be able to do.
|
|
370
|
+
*/
|
|
371
|
+
export declare class InputOutsideRepositoryError extends Error {
|
|
372
|
+
constructor(input: string, directory: string, boundary: string);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* NestJS module that lists the files each declared input holds.
|
|
377
|
+
*/
|
|
378
|
+
export declare class InputsModule {
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Lists the files an input holds.
|
|
383
|
+
*
|
|
384
|
+
* Globs alone decide, with no ignore file consulted: an input exists to name a
|
|
385
|
+
* part of the tree outright, and the most useful part to name is compiled
|
|
386
|
+
* output, which every repository's ignore files claim.
|
|
387
|
+
*/
|
|
388
|
+
export declare class InputsService {
|
|
389
|
+
private readonly logger;
|
|
390
|
+
constructor(logger: LoggerService);
|
|
391
|
+
/**
|
|
392
|
+
* Whether a directory can hold anything the input's globs claim.
|
|
393
|
+
*
|
|
394
|
+
* A hidden directory is only entered when a glob spells it out. That is what
|
|
395
|
+
* every glob library means by excluding dot files, and it is also what stops
|
|
396
|
+
* an input over the whole tree from reading the repository's git database.
|
|
397
|
+
*/
|
|
398
|
+
private canDescend;
|
|
399
|
+
/**
|
|
400
|
+
* The furthest out an input is allowed to reach, from where the run started.
|
|
401
|
+
*
|
|
402
|
+
* The repository holding the measured directory, or that directory itself
|
|
403
|
+
* when nothing above it looks like one. Found by marker rather than by
|
|
404
|
+
* asking git, which is never invoked here, and generic to every repository —
|
|
405
|
+
* it says where measuring stops, not how any tree beneath it is arranged.
|
|
406
|
+
*/
|
|
407
|
+
private findBoundary;
|
|
408
|
+
/** Whether the last segment of a path starts with a dot. */
|
|
409
|
+
private isHidden;
|
|
410
|
+
/**
|
|
411
|
+
* Whether a symbolic link points at a file.
|
|
412
|
+
*
|
|
413
|
+
* Links are followed, as every glob library does. Only to files, though: a
|
|
414
|
+
* link pointing back at one of its own ancestors would otherwise be walked
|
|
415
|
+
* until the stack ran out.
|
|
416
|
+
*/
|
|
417
|
+
private isLinkedFile;
|
|
418
|
+
/**
|
|
419
|
+
* Whether the input claims a file.
|
|
420
|
+
*
|
|
421
|
+
* Both lists are answered in full rather than in order, so where a pattern
|
|
422
|
+
* sits within either of them cannot change the answer.
|
|
423
|
+
*/
|
|
424
|
+
private isMatched;
|
|
425
|
+
/** Whether the directory is on the way down to a glob's literal prefix. */
|
|
426
|
+
private leadsToBase;
|
|
427
|
+
/**
|
|
428
|
+
* Reads a directory's entries, or none when the directory cannot be read.
|
|
429
|
+
*
|
|
430
|
+
* An input naming a directory that was never built is an ordinary state
|
|
431
|
+
* rather than a failure — it holds no files, and what that means is decided
|
|
432
|
+
* by whoever asked for the measurement.
|
|
433
|
+
*/
|
|
434
|
+
private readEntries;
|
|
435
|
+
/**
|
|
436
|
+
* The measured-directory-relative prefix every matched path carries.
|
|
437
|
+
*
|
|
438
|
+
* Empty when the input starts where the run does. Otherwise it is the walk
|
|
439
|
+
* root written relative to the measured directory — `../../dist` and the
|
|
440
|
+
* like — so that every path leaving this service is relative to the same
|
|
441
|
+
* directory whether or not the input reached outside it.
|
|
442
|
+
*/
|
|
443
|
+
private readInputPrefix;
|
|
444
|
+
/** What a directory entry counts as, once any link has been followed. */
|
|
445
|
+
private resolveEntryKind;
|
|
446
|
+
/** Whether the directory sits inside a glob's literal prefix. */
|
|
447
|
+
private sitsInsideBase;
|
|
448
|
+
/** Whether a directory is the boundary or sits somewhere beneath it. */
|
|
449
|
+
private sitsInsideBoundary;
|
|
450
|
+
/**
|
|
451
|
+
* The literal path prefix of a glob, up to its first magic character.
|
|
452
|
+
*
|
|
453
|
+
* `dist/packages/logger/**` can only match inside `dist/packages/logger`, so
|
|
454
|
+
* that is the only branch of the tree worth reading — the difference between
|
|
455
|
+
* measuring one build directory and enumerating every dependency to find it.
|
|
456
|
+
*/
|
|
457
|
+
private toIncludeBase;
|
|
458
|
+
/** Collects every file one directory of the input's tree contributes. */
|
|
459
|
+
private walk;
|
|
460
|
+
/**
|
|
461
|
+
* Lists the files an input holds, sorted, relative to the measured directory.
|
|
462
|
+
*
|
|
463
|
+
* The walk starts at the input's own directory, which is the measured one
|
|
464
|
+
* unless the input named a way out of it. Where a repository builds is a
|
|
465
|
+
* convention its configuration states and this service is told, never one
|
|
466
|
+
* inferred here from a project's position — but the reach is bounded: a
|
|
467
|
+
* directory landing outside the repository fails the input by name rather
|
|
468
|
+
* than measuring whatever it found there.
|
|
469
|
+
*
|
|
470
|
+
* Sorted because the walk visits directories in whatever order the
|
|
471
|
+
* filesystem reports them, and a size is a sum of every file either way —
|
|
472
|
+
* but a list nobody can predict is one nobody can compare.
|
|
473
|
+
*/
|
|
474
|
+
matchFiles(args: MatchInputFilesArguments): string[];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* One limit that could not be held against anything.
|
|
479
|
+
*
|
|
480
|
+
* Collected rather than thrown. A configuration carrying three limits that
|
|
481
|
+
* bind to nothing is three mistakes to fix, and reporting only the first turns
|
|
482
|
+
* one repair into three runs.
|
|
483
|
+
*/
|
|
484
|
+
declare interface LimitFailure {
|
|
485
|
+
/** The dotted path exactly as the limit was written. */
|
|
486
|
+
metric: string;
|
|
487
|
+
reason: string;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* What every declared limit found, alongside the ones that bound to nothing.
|
|
492
|
+
*
|
|
493
|
+
* Both lists are always present. A limit that could not be bound is neither a
|
|
494
|
+
* breach nor a pass, and reporting it as either would be a gate whose verdict
|
|
495
|
+
* nobody could trust.
|
|
496
|
+
*/
|
|
497
|
+
declare interface LimitsEvaluation {
|
|
498
|
+
failures: LimitFailure[];
|
|
499
|
+
limits: EvaluatedLimit[];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* NestJS module that holds measured metrics to their declared limits.
|
|
504
|
+
*/
|
|
505
|
+
export declare class LimitsModule {
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Holds measured metrics to the limits declared against them.
|
|
510
|
+
*
|
|
511
|
+
* Every metric is addressable, whichever analysis produced it, by a dotted
|
|
512
|
+
* path of target name and metric path. Nothing here decides what a breach
|
|
513
|
+
* costs: it reports which limits were exceeded and how badly, and what that is
|
|
514
|
+
* worth is the caller's to say.
|
|
515
|
+
*/
|
|
516
|
+
export declare class LimitsService {
|
|
517
|
+
constructor();
|
|
518
|
+
/** Binds one metric path within one target, if that target measured it. */
|
|
519
|
+
private bind;
|
|
520
|
+
/** Names one binding the way an error message reads it out. */
|
|
521
|
+
private describeBinding;
|
|
522
|
+
/** Reads whatever a failed binding threw as the sentence a report prints. */
|
|
523
|
+
private describeFailure;
|
|
524
|
+
/** Names every measured target, for an error that has to list them. */
|
|
525
|
+
private describeTargets;
|
|
526
|
+
/**
|
|
527
|
+
* Every metric a written path could mean.
|
|
528
|
+
*
|
|
529
|
+
* Each target is asked whether the path starts with its name, rather than
|
|
530
|
+
* the path being split at its first dot: a target's name may hold a dot of
|
|
531
|
+
* its own, and so may the metric path that follows it. Whatever the shapes
|
|
532
|
+
* involved, every reading is collected and none of them is preferred.
|
|
533
|
+
*/
|
|
534
|
+
private findCandidates;
|
|
535
|
+
/**
|
|
536
|
+
* What the path would mean read as the default target's, if one is set.
|
|
537
|
+
*
|
|
538
|
+
* A default target that was never measured is refused rather than ignored:
|
|
539
|
+
* ignoring it would turn every unqualified path in the configuration into an
|
|
540
|
+
* unresolvable one, and report the paths instead of the reason.
|
|
541
|
+
*/
|
|
542
|
+
private findDefaultCandidate;
|
|
543
|
+
/**
|
|
544
|
+
* Points one written path at exactly one measured metric.
|
|
545
|
+
*
|
|
546
|
+
* Ambiguity is refused rather than settled by a rule about which reading
|
|
547
|
+
* wins. Any such rule would be invisible in the configuration file, and a
|
|
548
|
+
* limit holding a metric nobody meant to limit reads exactly like one that
|
|
549
|
+
* works.
|
|
550
|
+
*/
|
|
551
|
+
private resolve;
|
|
552
|
+
/**
|
|
553
|
+
* Holds every declared limit against the metric it addresses.
|
|
554
|
+
*
|
|
555
|
+
* Returns what each limit found rather than a verdict. Severity is carried
|
|
556
|
+
* through untouched: whether a breach stops the run is a decision about the
|
|
557
|
+
* run, not about the measurement.
|
|
558
|
+
*
|
|
559
|
+
* A limit that binds to nothing joins `failures` and the rest are evaluated
|
|
560
|
+
* anyway, so one run names every path in a configuration that binds to
|
|
561
|
+
* nothing instead of the first one and nothing after it.
|
|
562
|
+
*/
|
|
563
|
+
evaluate(args: EvaluateLimitsArguments): LimitsEvaluation;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Structured values that belong beside a log line rather than inside it.
|
|
568
|
+
*
|
|
569
|
+
* Counts, percentages, and durations are the values that change on every
|
|
570
|
+
* occurrence, so they are carried as fields: the message stays constant and
|
|
571
|
+
* groupable in telemetry, and the numbers stay queryable instead of having to
|
|
572
|
+
* be parsed back out of prose.
|
|
573
|
+
*
|
|
574
|
+
* The named members are the recurring ones; the index signature keeps the
|
|
575
|
+
* argument open for whatever a given call site needs to attach.
|
|
576
|
+
*/
|
|
577
|
+
declare interface LogData {
|
|
578
|
+
[key: string]: unknown;
|
|
579
|
+
/** How many things the operation handled. */
|
|
580
|
+
count?: number;
|
|
581
|
+
/** Wall-clock milliseconds the operation took. */
|
|
582
|
+
durationMs?: number;
|
|
583
|
+
/** Completion between 0 and 100. */
|
|
584
|
+
percent?: number;
|
|
585
|
+
/** How many things the operation set out to handle. */
|
|
586
|
+
total?: number;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Transient-scoped logger so each injecting class gets its own instance.
|
|
591
|
+
* Each consumer calls `setContext(ClassName.name)` to tag every log line
|
|
592
|
+
* with the originating class. Backed by pino for structured JSON output in
|
|
593
|
+
* production and human-readable pretty-print in development.
|
|
594
|
+
*
|
|
595
|
+
* Messages follow one grammar: an emoji naming the subject, a verb in present
|
|
596
|
+
* progressive or past tense, then the object. Values that vary per call —
|
|
597
|
+
* counts, percentages, durations — go in the `data` argument rather than the
|
|
598
|
+
* message, so the message stays constant enough for telemetry to group on.
|
|
599
|
+
*
|
|
600
|
+
* ```ts
|
|
601
|
+
* this.logger.info("📥 Downloading CSEL sources", undefined, { total: 428 });
|
|
602
|
+
* this.logger.info("📥 Downloaded CSEL sources", undefined, { count: 412 });
|
|
603
|
+
* ```
|
|
604
|
+
*/
|
|
605
|
+
declare @Injectable({ scope: Scope.TRANSIENT })
|
|
606
|
+
class LoggerService extends ConsoleLogger {
|
|
607
|
+
// 🏗 Dependency Injection
|
|
608
|
+
|
|
609
|
+
constructor() {
|
|
610
|
+
super();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 🔐 Private Fields
|
|
614
|
+
|
|
615
|
+
private static readonly isProduction =
|
|
616
|
+
process.env["NODE_ENV"] === "production";
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Built on first use, not when this file is evaluated.
|
|
620
|
+
*
|
|
621
|
+
* A destination fixed at import time could only ever be chosen by this
|
|
622
|
+
* package, since every consumer's own code runs after its imports.
|
|
623
|
+
*/
|
|
624
|
+
private static rootLogger: pino.Logger | undefined;
|
|
625
|
+
|
|
626
|
+
/** Whether lines go to standard error instead of standard output. */
|
|
627
|
+
private static writesToStandardError = false;
|
|
628
|
+
|
|
629
|
+
private child: pino.Logger = LoggerService.root;
|
|
630
|
+
|
|
631
|
+
// 🔑 Public Fields
|
|
632
|
+
|
|
633
|
+
// 🔏 Private Methods
|
|
634
|
+
|
|
635
|
+
/** Build the pino instance for production or local development output. */
|
|
636
|
+
private static createRootLogger(): pino.Logger {
|
|
637
|
+
const level = process.env["LOG_LEVEL"] ?? "info";
|
|
638
|
+
|
|
639
|
+
if (LoggerService.isProduction) {
|
|
640
|
+
return LoggerService.writesToStandardError
|
|
641
|
+
? pino({ level }, pino.destination(STANDARD_ERROR_DESCRIPTOR))
|
|
642
|
+
: pino({ level });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return pino({
|
|
646
|
+
level,
|
|
647
|
+
transport: {
|
|
648
|
+
options: {
|
|
649
|
+
colorize: true,
|
|
650
|
+
destination: LoggerService.writesToStandardError
|
|
651
|
+
? STANDARD_ERROR_DESCRIPTOR
|
|
652
|
+
: STANDARD_OUTPUT_DESCRIPTOR,
|
|
653
|
+
// The emoji is a field, not part of the message, so the console can
|
|
654
|
+
// show it while telemetry stores unadorned prose. `ignore` then keeps
|
|
655
|
+
// it from being printed a second time in the trailing object.
|
|
656
|
+
ignore: "pid,hostname,emoji",
|
|
657
|
+
messageFormat: "{emoji} {msg}",
|
|
658
|
+
singleLine: true,
|
|
659
|
+
},
|
|
660
|
+
target: "pino-pretty",
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Sends every subsequent line to standard error instead of standard output.
|
|
667
|
+
*
|
|
668
|
+
* For a command-line application whose standard output *is* its result. A log
|
|
669
|
+
* line sharing that stream is not a diagnostic beside the data, it is a
|
|
670
|
+
* corruption of it. Call it before anything logs — the first statement of the
|
|
671
|
+
* application's bootstrap.
|
|
672
|
+
*
|
|
673
|
+
* A call after the first line warns and changes nothing: the destination is
|
|
674
|
+
* fixed when the pino instance is built, and tearing down a transport
|
|
675
|
+
* somebody is writing through would be worse than refusing. The warning is
|
|
676
|
+
* the point — silently leaving the lines on standard output is how a caller
|
|
677
|
+
* would ship a corrupted pipe without ever being told.
|
|
678
|
+
*/
|
|
679
|
+
static logToStandardError(): void {
|
|
680
|
+
if (LoggerService.rootLogger !== undefined) {
|
|
681
|
+
process.emitWarning(
|
|
682
|
+
"LoggerService.logToStandardError() was called after the first log line, so log lines still go to standard output and anything piping that stream will read them as data. Call it as the first statement of the application's bootstrap.",
|
|
683
|
+
);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
LoggerService.writesToStandardError = true;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Fails a malformed message in development, and never in production.
|
|
692
|
+
*
|
|
693
|
+
* A logger that throws in production turns an observability call into an
|
|
694
|
+
* outage, so the check runs only where a developer is present to fix it.
|
|
695
|
+
*/
|
|
696
|
+
private assertConventionalMessage(args: {
|
|
697
|
+
context: string | undefined;
|
|
698
|
+
parsed: ParsedLogMessage;
|
|
699
|
+
}): void {
|
|
700
|
+
if (
|
|
701
|
+
LoggerService.isProduction ||
|
|
702
|
+
this.shouldSkipConventionalMessageValidation(args.context)
|
|
703
|
+
) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const violation = this.getConventionalMessageViolation(args.parsed);
|
|
708
|
+
|
|
709
|
+
if (violation !== undefined) {
|
|
710
|
+
throw new Error(violation);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Assembles the object pino merges into the line. */
|
|
715
|
+
private buildBindings(args: {
|
|
716
|
+
context: string | undefined;
|
|
717
|
+
data: LogData | undefined;
|
|
718
|
+
parsed: ParsedLogMessage;
|
|
719
|
+
}): Record<string, unknown> {
|
|
720
|
+
this.assertConventionalMessage({
|
|
721
|
+
context: args.context,
|
|
722
|
+
parsed: args.parsed,
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
...args.data,
|
|
727
|
+
context: args.context,
|
|
728
|
+
// Telemetry gets prose; only the console-bound transport reads this.
|
|
729
|
+
...(LoggerService.isProduction ? {} : { emoji: args.parsed.emoji }),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Returns a human-readable explanation when the message format is invalid. */
|
|
734
|
+
private getConventionalMessageViolation(
|
|
735
|
+
parsed: ParsedLogMessage,
|
|
736
|
+
): string | undefined {
|
|
737
|
+
const emoji = parsed.emoji;
|
|
738
|
+
const text = parsed.text;
|
|
739
|
+
|
|
740
|
+
if (emoji === undefined) {
|
|
741
|
+
return `Log message must start with an emoji naming its subject, then a verb: "${text}"`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const firstWord = FIRST_WORD_PATTERN.exec(text)?.[1];
|
|
745
|
+
|
|
746
|
+
if (firstWord === undefined || !this.isConventionalVerb(firstWord)) {
|
|
747
|
+
return `Log message must begin with a verb in present progressive or past tense, got "${firstWord ?? ""}": "${emoji} ${text}"`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Whether a word is a verb in one of the two tenses the convention allows.
|
|
755
|
+
*
|
|
756
|
+
* Present progressive means the operation is under way; past means it
|
|
757
|
+
* finished. Regular morphology covers both, so a new verb needs no
|
|
758
|
+
* registration anywhere — only irregular pasts are enumerated.
|
|
759
|
+
*/
|
|
760
|
+
private isConventionalVerb(word: string): boolean {
|
|
761
|
+
const lowercased = word.toLowerCase();
|
|
762
|
+
|
|
763
|
+
return (
|
|
764
|
+
lowercased.endsWith("ing") ||
|
|
765
|
+
lowercased.endsWith("ed") ||
|
|
766
|
+
IRREGULAR_PAST_VERBS.has(lowercased)
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Splits a leading emoji off a message, leaving prose behind. */
|
|
771
|
+
private parseMessage(message: unknown): ParsedLogMessage {
|
|
772
|
+
const text = String(message);
|
|
773
|
+
const match = LEADING_EMOJI_PATTERN.exec(text);
|
|
774
|
+
const emoji = match?.[1];
|
|
775
|
+
|
|
776
|
+
return emoji === undefined
|
|
777
|
+
? { emoji: undefined, text }
|
|
778
|
+
: { emoji, text: text.slice(match?.[0].length) };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** Whether a context is intentionally exempt from the validation rule. */
|
|
782
|
+
private shouldSkipConventionalMessageValidation(
|
|
783
|
+
context: string | undefined,
|
|
784
|
+
): boolean {
|
|
785
|
+
return context !== undefined && UNVALIDATED_LOG_CONTEXTS.has(context);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// 🌎 Public Methods
|
|
789
|
+
|
|
790
|
+
/** The pino instance every logger's child is taken from. */
|
|
791
|
+
private static get root(): pino.Logger {
|
|
792
|
+
LoggerService.rootLogger ??= LoggerService.createRootLogger();
|
|
793
|
+
|
|
794
|
+
return LoggerService.rootLogger;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Normalizes unknown errors into a stable message and timestamped log line. */
|
|
798
|
+
buildErrorLogEntry(
|
|
799
|
+
context: string,
|
|
800
|
+
error: unknown,
|
|
801
|
+
): { errorMessage: string; logLine: string } {
|
|
802
|
+
const errorMessage =
|
|
803
|
+
error instanceof Error ? error.stack || error.message : String(error);
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
errorMessage,
|
|
807
|
+
logLine: `[${new Date().toISOString()}] ${context}: ${errorMessage}\n`,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Builds a timestamped output log file path and ensures the output directory exists. */
|
|
812
|
+
createTimestampedOutputLogFilePath(filePrefix: string): string {
|
|
813
|
+
const outputDirectory = path.join(process.cwd(), "output");
|
|
814
|
+
if (!existsSync(outputDirectory)) {
|
|
815
|
+
mkdirSync(outputDirectory, { recursive: true });
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return path.join(
|
|
819
|
+
outputDirectory,
|
|
820
|
+
`${filePrefix}-${new Date().toISOString().replaceAll(/[:.]/g, "-")}.log`,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Logs a debug message at the `debug` level. */
|
|
825
|
+
override debug(message: unknown, context?: string, data?: LogData): void {
|
|
826
|
+
const parsed = this.parseMessage(message);
|
|
827
|
+
this.child.debug(
|
|
828
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
829
|
+
parsed.text,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Logs an error message at the `error` level, optionally including a stack trace.
|
|
835
|
+
*
|
|
836
|
+
* `ConsoleLogger.error` spends a third slot on a context string that the
|
|
837
|
+
* other levels do not have, so this one accepts either: a string keeps
|
|
838
|
+
* NestJS's meaning, an object is structured data like everywhere else.
|
|
839
|
+
*/
|
|
840
|
+
override error(
|
|
841
|
+
message: unknown,
|
|
842
|
+
stackOrContext?: string,
|
|
843
|
+
contextOrData?: LogData | string,
|
|
844
|
+
): void {
|
|
845
|
+
const parsed = this.parseMessage(message);
|
|
846
|
+
const data = typeof contextOrData === "object" ? contextOrData : undefined;
|
|
847
|
+
const context =
|
|
848
|
+
typeof contextOrData === "string" ? contextOrData : this.context;
|
|
849
|
+
|
|
850
|
+
this.child.error(
|
|
851
|
+
{
|
|
852
|
+
...this.buildBindings({ context, data, parsed }),
|
|
853
|
+
stack: stackOrContext,
|
|
854
|
+
},
|
|
855
|
+
parsed.text,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** Logs an informational message at the `info` level. */
|
|
860
|
+
info(message: unknown, context?: string, data?: LogData): void {
|
|
861
|
+
const parsed = this.parseMessage(message);
|
|
862
|
+
this.child.info(
|
|
863
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
864
|
+
parsed.text,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Logs an informational message at the `info` level.
|
|
870
|
+
*
|
|
871
|
+
* NestJS and `nest-commander` call this method directly as part of the
|
|
872
|
+
* framework's own `LoggerService` contract, so it must keep working
|
|
873
|
+
* exactly as before. Application code should call `info` instead — the
|
|
874
|
+
* same behavior under a name that says what level it logs at.
|
|
875
|
+
*/
|
|
876
|
+
override log(message: unknown, context?: string, data?: LogData): void {
|
|
877
|
+
this.info(message, context, data);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Sets the context label included in every subsequent log line. */
|
|
881
|
+
override setContext(context: string): void {
|
|
882
|
+
super.setContext(context);
|
|
883
|
+
this.child = LoggerService.root.child({ context });
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** Logs a verbose message at the `trace` level. */
|
|
887
|
+
override verbose(message: unknown, context?: string, data?: LogData): void {
|
|
888
|
+
const parsed = this.parseMessage(message);
|
|
889
|
+
this.child.trace(
|
|
890
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
891
|
+
parsed.text,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** Logs a warning message at the `warn` level. */
|
|
896
|
+
override warn(message: unknown, context?: string, data?: LogData): void {
|
|
897
|
+
const parsed = this.parseMessage(message);
|
|
898
|
+
this.child.warn(
|
|
899
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
900
|
+
parsed.text,
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Arguments accepted when listing the files an input holds. */
|
|
906
|
+
export declare interface MatchInputFilesArguments {
|
|
907
|
+
input: ResolvedCodometerInput;
|
|
908
|
+
workingDirectory: string;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Arguments accepted by the measurement pipeline.
|
|
913
|
+
*/
|
|
914
|
+
export declare interface MeasureArguments {
|
|
915
|
+
configuration: ResolvedCodometerConfiguration;
|
|
916
|
+
/**
|
|
917
|
+
* Files codometer writes itself, relative to the measured directory.
|
|
918
|
+
*
|
|
919
|
+
* Never measured, whether or not this particular run writes them: a run that
|
|
920
|
+
* measured a different tree depending on its flags could not tell a stale
|
|
921
|
+
* report from a report written by a differently-flagged run.
|
|
922
|
+
*/
|
|
923
|
+
outputPaths: readonly string[];
|
|
924
|
+
workingDirectory: string;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* What one target measured, as the limits layer reads it.
|
|
929
|
+
*
|
|
930
|
+
* Declared here rather than imported from the measurement pipeline: gating a
|
|
931
|
+
* number needs the number and its target's name, and nothing about how either
|
|
932
|
+
* was produced.
|
|
933
|
+
*/
|
|
934
|
+
declare interface MeasuredTarget {
|
|
935
|
+
files: number;
|
|
936
|
+
language: CodeStatisticsResult | undefined;
|
|
937
|
+
name: string;
|
|
938
|
+
size: SizeResult | undefined;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Everything one run measured, input by input.
|
|
943
|
+
*
|
|
944
|
+
* `statistics` is the `codebase` input's own language metrics, which is the
|
|
945
|
+
* report every consumer renders today. It is the same object that input
|
|
946
|
+
* carries, held out separately so nothing downstream has to know which input
|
|
947
|
+
* it came from.
|
|
948
|
+
*/
|
|
949
|
+
export declare interface MeasurementResult {
|
|
950
|
+
/**
|
|
951
|
+
* Whatever the run could not do, collected rather than thrown.
|
|
952
|
+
*
|
|
953
|
+
* An input that will not measure and a limit that binds to nothing are both
|
|
954
|
+
* recorded here and stepped over, so one run names every one of them instead
|
|
955
|
+
* of stopping at the first and hiding the rest behind it.
|
|
956
|
+
*/
|
|
957
|
+
failures: ReportFailure[];
|
|
958
|
+
/** Every metric each measured input counted, addressable by dotted path. */
|
|
959
|
+
indexes: Map<string, TargetMetricIndex>;
|
|
960
|
+
/** Every input measured, in the order `inputs` declared them. */
|
|
961
|
+
inputs: InputMeasurement[];
|
|
962
|
+
/**
|
|
963
|
+
* What every declared limit found, in the order they were declared.
|
|
964
|
+
*
|
|
965
|
+
* Empty when nothing declared one, which is the ordinary case: a metric with
|
|
966
|
+
* no limit is measured and reported like every other, and gated by nothing.
|
|
967
|
+
*/
|
|
968
|
+
limits: EvaluatedLimit[];
|
|
969
|
+
statistics: CodeStatisticsResult;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Wires every analyzer a measurement run joins, and the service that joins
|
|
974
|
+
* them.
|
|
975
|
+
*
|
|
976
|
+
* The one module a host has to import to measure anything: discovery finds the
|
|
977
|
+
* files, the language and size analyzers count them, the custom counters add
|
|
978
|
+
* whatever a configuration declared, and the limits layer holds the result to
|
|
979
|
+
* what the configuration gates. None of the five imports another, which is why
|
|
980
|
+
* the join lives here rather than inside one of them.
|
|
981
|
+
*/
|
|
982
|
+
export declare class MeasureModule {
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Aggregates every analyzer's report into a single set of statistics.
|
|
987
|
+
*
|
|
988
|
+
* The one place the discovery, language, size, and customization analyzers
|
|
989
|
+
* meet. None of the four imports another, so joining them has to happen
|
|
990
|
+
* somewhere, and a call-stack trace reports that join as module spread against
|
|
991
|
+
* `measureInput` and `analyzeFiles` — the two methods that personally name
|
|
992
|
+
* three of the four. That is the arrangement working, not drifting: pushing the
|
|
993
|
+
* join down into one of the analyzers is what would couple them to each other.
|
|
994
|
+
*/
|
|
995
|
+
export declare class MeasureService {
|
|
996
|
+
private readonly discoveryService;
|
|
997
|
+
private readonly languagesService;
|
|
998
|
+
private readonly customizationService;
|
|
999
|
+
private readonly inputsService;
|
|
1000
|
+
private readonly sizeService;
|
|
1001
|
+
private readonly limitsService;
|
|
1002
|
+
private readonly metricIndexService;
|
|
1003
|
+
constructor(discoveryService: DiscoveryService, languagesService: LanguagesService, customizationService: CustomizationService, inputsService: InputsService, sizeService: SizeService, limitsService: LimitsService, metricIndexService: MetricIndexService);
|
|
1004
|
+
/**
|
|
1005
|
+
* Run every analyzer over one set of files and shape the result.
|
|
1006
|
+
*
|
|
1007
|
+
* Three analyzers, not one: the language analyzers, the size analyzer that
|
|
1008
|
+
* produces the headline byte total, and the custom counters a configuration
|
|
1009
|
+
* declares. Named for the file set rather than for any of the three, because
|
|
1010
|
+
* this sits directly above `LanguagesService.analyze` in every measurement
|
|
1011
|
+
* stack — named for a language it reads there as a forwarding layer instead
|
|
1012
|
+
* of as the place all three are joined.
|
|
1013
|
+
*
|
|
1014
|
+
* Takes the files it is given rather than finding them, so the codebase and
|
|
1015
|
+
* an input naming compiled output are counted by exactly the same analyzers.
|
|
1016
|
+
*/
|
|
1017
|
+
private analyzeFiles;
|
|
1018
|
+
/** Project the TypeScript analyzer's counters onto the JavaScript group. */
|
|
1019
|
+
private buildJavascriptStatistics;
|
|
1020
|
+
/** Project the TypeScript analyzer's counters onto the TypeScript group. */
|
|
1021
|
+
private buildTypescriptStatistics;
|
|
1022
|
+
/** Reads whatever an input's measurement threw as a printable sentence. */
|
|
1023
|
+
private describeFailure;
|
|
1024
|
+
/**
|
|
1025
|
+
* Discovers one input's files, minus the ones codometer writes itself.
|
|
1026
|
+
*
|
|
1027
|
+
* The built-in `codebase` input is discovered by whatever its ignore files
|
|
1028
|
+
* leave behind rather than by matching its own `include`/`exclude` globs —
|
|
1029
|
+
* those stay placeholders for that one input, exactly as
|
|
1030
|
+
* `@codometer/configuration` documents. Every other input's files are
|
|
1031
|
+
* whatever its globs claim.
|
|
1032
|
+
*/
|
|
1033
|
+
private discoverInputFiles;
|
|
1034
|
+
/**
|
|
1035
|
+
* Drops the files codometer writes from a list of measured ones.
|
|
1036
|
+
*
|
|
1037
|
+
* Codometer's reports are made of what it measured, so measuring them makes
|
|
1038
|
+
* every report an input to the next one: a badge block changes the markdown
|
|
1039
|
+
* counters, which changes the badges. Removing them is what makes a second
|
|
1040
|
+
* run over an untouched tree produce the same bytes as the first.
|
|
1041
|
+
*/
|
|
1042
|
+
private excludeOutputPaths;
|
|
1043
|
+
/**
|
|
1044
|
+
* Count the unique folders the input's files sit in.
|
|
1045
|
+
*/
|
|
1046
|
+
private getFolderCount;
|
|
1047
|
+
/**
|
|
1048
|
+
* Measure one declared input with whichever analyses it asked for.
|
|
1049
|
+
*
|
|
1050
|
+
* An analysis nobody asked for is not run at all. Compressing a source tree
|
|
1051
|
+
* to answer a question nobody put costs more than every other analysis put
|
|
1052
|
+
* together.
|
|
1053
|
+
*/
|
|
1054
|
+
private measureInput;
|
|
1055
|
+
/** Restates the limits layer's failures in the report's own vocabulary. */
|
|
1056
|
+
private readLimitFailures;
|
|
1057
|
+
/** Whether an input asked for one of the analyses. */
|
|
1058
|
+
private runsAnalysis;
|
|
1059
|
+
/**
|
|
1060
|
+
* Measure every input the configuration declares.
|
|
1061
|
+
*
|
|
1062
|
+
* The built-in `codebase` input is not special-cased here — resolution
|
|
1063
|
+
* already prepends it unless a configuration replaces it by name, so this
|
|
1064
|
+
* simply measures whichever inputs it was handed, in the order given. An
|
|
1065
|
+
* input that cannot be measured — a glob pointing at a directory that
|
|
1066
|
+
* vanished, a file that will not open — is recorded and stepped over, so one
|
|
1067
|
+
* unreadable file never takes the whole run with it.
|
|
1068
|
+
*/
|
|
1069
|
+
measure(args: MeasureArguments): MeasurementResult;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** Every measured target's metrics, and the names two targets fought over. */
|
|
1073
|
+
declare interface MetricIndexResult {
|
|
1074
|
+
/** Collisions found while indexing, in the order they were found. */
|
|
1075
|
+
duplicates: DuplicateTargetFinding[];
|
|
1076
|
+
indexes: Map<string, TargetMetricIndex>;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Makes every measured number addressable by a dotted path.
|
|
1081
|
+
*
|
|
1082
|
+
* The same index answers two questions that used to be one layer's private
|
|
1083
|
+
* business: which metric a limit is written against, and which metrics the
|
|
1084
|
+
* report has to list. Both need every number a target measured, named the same
|
|
1085
|
+
* way, so the naming lives here rather than in either caller.
|
|
1086
|
+
*/
|
|
1087
|
+
export declare class MetricIndexService {
|
|
1088
|
+
constructor();
|
|
1089
|
+
/**
|
|
1090
|
+
* Records one metric, or marks its path as answered by more than one.
|
|
1091
|
+
*
|
|
1092
|
+
* Two counters sharing a path is a configuration mistake rather than a
|
|
1093
|
+
* measurement one — two statistics with the same label — and it is caught
|
|
1094
|
+
* here so that a limit addressing that path is refused instead of being
|
|
1095
|
+
* given whichever counter was indexed first.
|
|
1096
|
+
*/
|
|
1097
|
+
private addMetric;
|
|
1098
|
+
/**
|
|
1099
|
+
* Indexes every metric one target measured, by its path within that target.
|
|
1100
|
+
*
|
|
1101
|
+
* An analysis the target never ran contributes nothing, so a limit written
|
|
1102
|
+
* against it is refused rather than being compared against a zero the target
|
|
1103
|
+
* never reported.
|
|
1104
|
+
*/
|
|
1105
|
+
private buildTargetIndex;
|
|
1106
|
+
/**
|
|
1107
|
+
* Explains a name two measured targets both answered to.
|
|
1108
|
+
*
|
|
1109
|
+
* Written where the collision is found rather than raised as an error: the
|
|
1110
|
+
* run carries on with the first target of that name, and this is what the
|
|
1111
|
+
* report says about the one it had to drop.
|
|
1112
|
+
*/
|
|
1113
|
+
private describeDuplicate;
|
|
1114
|
+
/** Indexes a group of counters, descending into the nested ones. */
|
|
1115
|
+
private indexCounters;
|
|
1116
|
+
/**
|
|
1117
|
+
* Indexes everything language analysis counted.
|
|
1118
|
+
*
|
|
1119
|
+
* Configured counters are indexed under a prefix rather than beside the
|
|
1120
|
+
* built-in ones, so a counter labelled `files` cannot take the path the file
|
|
1121
|
+
* count already answers to.
|
|
1122
|
+
*/
|
|
1123
|
+
private indexLanguage;
|
|
1124
|
+
/** Whether a statistics entry holds counters of its own. */
|
|
1125
|
+
private isCounterGroup;
|
|
1126
|
+
/**
|
|
1127
|
+
* Indexes every measured target, by name.
|
|
1128
|
+
*
|
|
1129
|
+
* A repeated name is collected rather than thrown, so a run reports every
|
|
1130
|
+
* naming collision it found instead of the first one. The later target is
|
|
1131
|
+
* dropped: a metric is addressed by its target's name, so a name answering
|
|
1132
|
+
* to two targets can address neither.
|
|
1133
|
+
*/
|
|
1134
|
+
index(targets: readonly MeasuredTarget[]): MetricIndexResult;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** A message split into the emoji the console shows and the prose telemetry stores. */
|
|
1138
|
+
declare interface ParsedLogMessage {
|
|
1139
|
+
emoji: string | undefined;
|
|
1140
|
+
text: string;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/** Arguments accepted when reading a rule set out of a gitignore-syntax file. */
|
|
1144
|
+
declare interface ReadIgnoreScopeArguments {
|
|
1145
|
+
directory: string;
|
|
1146
|
+
filePath: string;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* NestJS module that measures the compressed size of a target's files.
|
|
1151
|
+
*/
|
|
1152
|
+
export declare class SizeModule {
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** What size analysis reported over one target. */
|
|
1156
|
+
export declare interface SizeResult {
|
|
1157
|
+
/**
|
|
1158
|
+
* Total bytes the target's files occupy under the chosen compression.
|
|
1159
|
+
*
|
|
1160
|
+
* A sum of separately compressed files rather than one compression of all of
|
|
1161
|
+
* them: a browser fetches each file on its own, so compressing them together
|
|
1162
|
+
* would report a number no client ever receives.
|
|
1163
|
+
*/
|
|
1164
|
+
bytes: number;
|
|
1165
|
+
compression: CodometerCompression;
|
|
1166
|
+
files: number;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Measures how many bytes a target's files occupy once compressed. */
|
|
1170
|
+
export declare class SizeService {
|
|
1171
|
+
private readonly logger;
|
|
1172
|
+
constructor(logger: LoggerService);
|
|
1173
|
+
/**
|
|
1174
|
+
* Compresses one file's contents and reports the resulting byte count.
|
|
1175
|
+
*
|
|
1176
|
+
* Written as a lookup rather than a switch so that a compression added to
|
|
1177
|
+
* the configuration without an implementation here fails to compile, instead
|
|
1178
|
+
* of falling through to whichever branch happened to be last.
|
|
1179
|
+
*/
|
|
1180
|
+
private compress;
|
|
1181
|
+
/**
|
|
1182
|
+
* Measures one file, or fails the run.
|
|
1183
|
+
*
|
|
1184
|
+
* A file can vanish between being matched and being read — a build running
|
|
1185
|
+
* beside the measurement is enough. Skipping it would report a total short
|
|
1186
|
+
* by that file while still counting it, which is a number that looks
|
|
1187
|
+
* consistent and lets a real breach through. Failing is the lesser harm.
|
|
1188
|
+
*/
|
|
1189
|
+
private measureFile;
|
|
1190
|
+
/**
|
|
1191
|
+
* Measures every file of a target and sums the results.
|
|
1192
|
+
*
|
|
1193
|
+
* Each file is compressed on its own. Compressing them together would let
|
|
1194
|
+
* one file's dictionary shrink the next, reporting a total smaller than
|
|
1195
|
+
* anything that will ever be transferred.
|
|
1196
|
+
*/
|
|
1197
|
+
analyze(args: AnalyzeSizeArguments): SizeResult;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Every metric one target measured, addressable by dotted path. */
|
|
1201
|
+
export declare interface TargetMetricIndex {
|
|
1202
|
+
/**
|
|
1203
|
+
* Paths more than one metric answers to, which no limit may address.
|
|
1204
|
+
*
|
|
1205
|
+
* Two configured counters sharing a label is the way this happens. Either
|
|
1206
|
+
* metric would be a defensible binding, which is exactly why neither is.
|
|
1207
|
+
*/
|
|
1208
|
+
ambiguous: Set<string>;
|
|
1209
|
+
files: number;
|
|
1210
|
+
/**
|
|
1211
|
+
* Where each metric's per-instance measurements were found, keyed by the
|
|
1212
|
+
* same dotted path `metrics` uses.
|
|
1213
|
+
*
|
|
1214
|
+
* Only a metric a per-instance selector produced has an entry here — a
|
|
1215
|
+
* `comment` selector today, and any future selector that measures more
|
|
1216
|
+
* than a count. A metric with no entry counted matches and nothing more,
|
|
1217
|
+
* so a consumer reading this map can tell the two apart without reading
|
|
1218
|
+
* the configuration that produced either.
|
|
1219
|
+
*/
|
|
1220
|
+
instances: Map<string, CustomStatisticResultInstance[]>;
|
|
1221
|
+
metrics: Map<string, number>;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Raised when a limit's dotted path does not name exactly one metric.
|
|
1226
|
+
*
|
|
1227
|
+
* Both halves of that are failures worth stopping for. A path naming nothing
|
|
1228
|
+
* gates nothing while looking like a gate, and a path naming several would
|
|
1229
|
+
* have to pick one — and a limit quietly holding the wrong metric is a limit
|
|
1230
|
+
* nobody would ever discover was wrong.
|
|
1231
|
+
*/
|
|
1232
|
+
export declare class UnboundMetricError extends Error {
|
|
1233
|
+
constructor(path: string, reason: string);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Raised when a file a target matched cannot be read.
|
|
1238
|
+
*
|
|
1239
|
+
* Louder than the unreadable *directory* discovery tolerates, and deliberately
|
|
1240
|
+
* so: a directory nobody can read narrows what was measured, while a matched
|
|
1241
|
+
* file nobody can read makes the reported total wrong by definition. Size
|
|
1242
|
+
* analysis exists to gate, and a number quietly short by one file lets a real
|
|
1243
|
+
* breach pass.
|
|
1244
|
+
*/
|
|
1245
|
+
export declare class UnreadableTargetFileError extends Error {
|
|
1246
|
+
constructor(filePath: string, reason: string);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** Arguments accepted when walking one directory of the measured tree. */
|
|
1250
|
+
export declare interface WalkDirectoryArguments {
|
|
1251
|
+
absoluteDirectory: string;
|
|
1252
|
+
exclude: string[];
|
|
1253
|
+
/** Rule sets from the configured ignore files, all anchored at the walk root. */
|
|
1254
|
+
excludeFromScopes: readonly IgnoreScope[];
|
|
1255
|
+
/** Rule sets from the `.gitignore` files seen so far, outermost first. */
|
|
1256
|
+
ignoreScopes: readonly IgnoreScope[];
|
|
1257
|
+
relativeDirectory: string;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/** Arguments accepted when walking one directory of an input's tree. */
|
|
1261
|
+
export declare interface WalkInputArguments {
|
|
1262
|
+
absoluteDirectory: string;
|
|
1263
|
+
/**
|
|
1264
|
+
* The literal path prefix of each include glob.
|
|
1265
|
+
*
|
|
1266
|
+
* A glob's prefix is where its matches can begin, so a directory neither
|
|
1267
|
+
* leading to a prefix nor sitting inside one holds nothing the input wants.
|
|
1268
|
+
*/
|
|
1269
|
+
includeBases: readonly string[];
|
|
1270
|
+
input: ResolvedCodometerInput;
|
|
1271
|
+
relativeDirectory: string;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** Arguments accepted when descending into one subdirectory. */
|
|
1275
|
+
export declare interface WalkSubdirectoryArguments extends WalkDirectoryArguments {
|
|
1276
|
+
absolutePath: string;
|
|
1277
|
+
relativePath: string;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
export { }
|