@plugjs/plug 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.md +211 -0
- package/NOTICE.md +13 -0
- package/README.md +7 -0
- package/dist/assert.cjs +72 -0
- package/dist/assert.cjs.map +6 -0
- package/dist/assert.mjs +41 -0
- package/dist/assert.mjs.map +6 -0
- package/dist/async.cjs +58 -0
- package/dist/async.cjs.map +6 -0
- package/dist/async.mjs +30 -0
- package/dist/async.mjs.map +6 -0
- package/dist/build.cjs +136 -0
- package/dist/build.cjs.map +6 -0
- package/dist/build.mjs +110 -0
- package/dist/build.mjs.map +6 -0
- package/dist/files.cjs +113 -0
- package/dist/files.cjs.map +6 -0
- package/dist/files.mjs +88 -0
- package/dist/files.mjs.map +6 -0
- package/dist/fork.cjs +177 -0
- package/dist/fork.cjs.map +6 -0
- package/dist/fork.mjs +151 -0
- package/dist/fork.mjs.map +6 -0
- package/dist/helpers.cjs +129 -0
- package/dist/helpers.cjs.map +6 -0
- package/dist/helpers.mjs +97 -0
- package/dist/helpers.mjs.map +6 -0
- package/dist/index.cjs +25 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.mjs +7 -0
- package/dist/index.mjs.map +6 -0
- package/dist/log/colors.cjs +129 -0
- package/dist/log/colors.cjs.map +6 -0
- package/dist/log/colors.mjs +93 -0
- package/dist/log/colors.mjs.map +6 -0
- package/dist/log/emit.cjs +109 -0
- package/dist/log/emit.cjs.map +6 -0
- package/dist/log/emit.mjs +83 -0
- package/dist/log/emit.mjs.map +6 -0
- package/dist/log/levels.cjs +75 -0
- package/dist/log/levels.cjs.map +6 -0
- package/dist/log/levels.mjs +41 -0
- package/dist/log/levels.mjs.map +6 -0
- package/dist/log/logger.cjs +129 -0
- package/dist/log/logger.cjs.map +6 -0
- package/dist/log/logger.mjs +104 -0
- package/dist/log/logger.mjs.map +6 -0
- package/dist/log/options.cjs +149 -0
- package/dist/log/options.cjs.map +6 -0
- package/dist/log/options.mjs +124 -0
- package/dist/log/options.mjs.map +6 -0
- package/dist/log/report.cjs +284 -0
- package/dist/log/report.cjs.map +6 -0
- package/dist/log/report.mjs +259 -0
- package/dist/log/report.mjs.map +6 -0
- package/dist/log/spinner.cjs +83 -0
- package/dist/log/spinner.cjs.map +6 -0
- package/dist/log/spinner.mjs +57 -0
- package/dist/log/spinner.mjs.map +6 -0
- package/dist/log.cjs +71 -0
- package/dist/log.cjs.map +6 -0
- package/dist/log.mjs +45 -0
- package/dist/log.mjs.map +6 -0
- package/dist/paths.cjs +158 -0
- package/dist/paths.cjs.map +6 -0
- package/dist/paths.mjs +122 -0
- package/dist/paths.mjs.map +6 -0
- package/dist/pipe.cjs +71 -0
- package/dist/pipe.cjs.map +6 -0
- package/dist/pipe.mjs +44 -0
- package/dist/pipe.mjs.map +6 -0
- package/dist/plugs/copy.cjs +95 -0
- package/dist/plugs/copy.cjs.map +6 -0
- package/dist/plugs/copy.mjs +64 -0
- package/dist/plugs/copy.mjs.map +6 -0
- package/dist/plugs/coverage/analysis.cjs +229 -0
- package/dist/plugs/coverage/analysis.cjs.map +6 -0
- package/dist/plugs/coverage/analysis.mjs +202 -0
- package/dist/plugs/coverage/analysis.mjs.map +6 -0
- package/dist/plugs/coverage/report.cjs +215 -0
- package/dist/plugs/coverage/report.cjs.map +6 -0
- package/dist/plugs/coverage/report.mjs +200 -0
- package/dist/plugs/coverage/report.mjs.map +6 -0
- package/dist/plugs/coverage.cjs +142 -0
- package/dist/plugs/coverage.cjs.map +6 -0
- package/dist/plugs/coverage.mjs +117 -0
- package/dist/plugs/coverage.mjs.map +6 -0
- package/dist/plugs/debug.cjs +50 -0
- package/dist/plugs/debug.cjs.map +6 -0
- package/dist/plugs/debug.mjs +25 -0
- package/dist/plugs/debug.mjs.map +6 -0
- package/dist/plugs/esbuild/bundle-locals.cjs +51 -0
- package/dist/plugs/esbuild/bundle-locals.cjs.map +6 -0
- package/dist/plugs/esbuild/bundle-locals.mjs +26 -0
- package/dist/plugs/esbuild/bundle-locals.mjs.map +6 -0
- package/dist/plugs/esbuild/check-dependencies.cjs +140 -0
- package/dist/plugs/esbuild/check-dependencies.cjs.map +6 -0
- package/dist/plugs/esbuild/check-dependencies.mjs +115 -0
- package/dist/plugs/esbuild/check-dependencies.mjs.map +6 -0
- package/dist/plugs/esbuild/fix-extensions.cjs +91 -0
- package/dist/plugs/esbuild/fix-extensions.cjs.map +6 -0
- package/dist/plugs/esbuild/fix-extensions.mjs +60 -0
- package/dist/plugs/esbuild/fix-extensions.mjs.map +6 -0
- package/dist/plugs/esbuild.cjs +109 -0
- package/dist/plugs/esbuild.cjs.map +6 -0
- package/dist/plugs/esbuild.mjs +83 -0
- package/dist/plugs/esbuild.mjs.map +6 -0
- package/dist/plugs/eslint/runner.cjs +91 -0
- package/dist/plugs/eslint/runner.cjs.map +6 -0
- package/dist/plugs/eslint/runner.mjs +68 -0
- package/dist/plugs/eslint/runner.mjs.map +6 -0
- package/dist/plugs/exec.cjs +128 -0
- package/dist/plugs/exec.cjs.map +6 -0
- package/dist/plugs/exec.mjs +96 -0
- package/dist/plugs/exec.mjs.map +6 -0
- package/dist/plugs/filter.cjs +59 -0
- package/dist/plugs/filter.cjs.map +6 -0
- package/dist/plugs/filter.mjs +34 -0
- package/dist/plugs/filter.mjs.map +6 -0
- package/dist/plugs/mocha/reporter.cjs +140 -0
- package/dist/plugs/mocha/reporter.cjs.map +6 -0
- package/dist/plugs/mocha/reporter.mjs +107 -0
- package/dist/plugs/mocha/reporter.mjs.map +6 -0
- package/dist/plugs/mocha/runner.cjs +73 -0
- package/dist/plugs/mocha/runner.cjs.map +6 -0
- package/dist/plugs/mocha/runner.mjs +44 -0
- package/dist/plugs/mocha/runner.mjs.map +6 -0
- package/dist/plugs/tsc/compiler.cjs +74 -0
- package/dist/plugs/tsc/compiler.cjs.map +6 -0
- package/dist/plugs/tsc/compiler.mjs +43 -0
- package/dist/plugs/tsc/compiler.mjs.map +6 -0
- package/dist/plugs/tsc/options.cjs +82 -0
- package/dist/plugs/tsc/options.cjs.map +6 -0
- package/dist/plugs/tsc/options.mjs +51 -0
- package/dist/plugs/tsc/options.mjs.map +6 -0
- package/dist/plugs/tsc/report.cjs +90 -0
- package/dist/plugs/tsc/report.cjs.map +6 -0
- package/dist/plugs/tsc/report.mjs +59 -0
- package/dist/plugs/tsc/report.mjs.map +6 -0
- package/dist/plugs/tsc/runner.cjs +101 -0
- package/dist/plugs/tsc/runner.cjs.map +6 -0
- package/dist/plugs/tsc/runner.mjs +72 -0
- package/dist/plugs/tsc/runner.mjs.map +6 -0
- package/dist/plugs.cjs +31 -0
- package/dist/plugs.cjs.map +6 -0
- package/dist/plugs.mjs +13 -0
- package/dist/plugs.mjs.map +6 -0
- package/dist/run.cjs +95 -0
- package/dist/run.cjs.map +6 -0
- package/dist/run.mjs +70 -0
- package/dist/run.mjs.map +6 -0
- package/dist/task.cjs +39 -0
- package/dist/task.cjs.map +6 -0
- package/dist/task.mjs +14 -0
- package/dist/task.mjs.map +6 -0
- package/dist/utils/asyncfs.cjs +143 -0
- package/dist/utils/asyncfs.cjs.map +6 -0
- package/dist/utils/asyncfs.mjs +83 -0
- package/dist/utils/asyncfs.mjs.map +6 -0
- package/dist/utils/caller.cjs +59 -0
- package/dist/utils/caller.cjs.map +6 -0
- package/dist/utils/caller.mjs +34 -0
- package/dist/utils/caller.mjs.map +6 -0
- package/dist/utils/match.cjs +69 -0
- package/dist/utils/match.cjs.map +6 -0
- package/dist/utils/match.mjs +38 -0
- package/dist/utils/match.mjs.map +6 -0
- package/dist/utils/options.cjs +41 -0
- package/dist/utils/options.cjs.map +6 -0
- package/dist/utils/options.mjs +16 -0
- package/dist/utils/options.mjs.map +6 -0
- package/dist/utils/walk.cjs +104 -0
- package/dist/utils/walk.cjs.map +6 -0
- package/dist/utils/walk.mjs +79 -0
- package/dist/utils/walk.mjs.map +6 -0
- package/extra/cli.mjs +212 -0
- package/extra/ts-loader.mjs +214 -0
- package/extra/webassembly.d.ts +11 -0
- package/package.json +57 -0
- package/src/assert.ts +47 -0
- package/src/async.ts +50 -0
- package/src/files.ts +129 -0
- package/src/fork.ts +263 -0
- package/src/helpers.ts +145 -0
- package/src/index.ts +20 -0
- package/src/log/colors.ts +119 -0
- package/src/log/emit.ts +125 -0
- package/src/log/levels.ts +65 -0
- package/src/log/logger.ts +171 -0
- package/src/log/options.ts +199 -0
- package/src/log/report.ts +433 -0
- package/src/log/spinner.ts +70 -0
- package/src/log.ts +68 -0
- package/src/paths.ts +213 -0
- package/src/pipe.ts +231 -0
- package/src/plugs/copy.ts +113 -0
- package/src/plugs/coverage/analysis.ts +395 -0
- package/src/plugs/coverage/report.ts +337 -0
- package/src/plugs/coverage.ts +194 -0
- package/src/plugs/debug.ts +35 -0
- package/src/plugs/esbuild/bundle-locals.ts +33 -0
- package/src/plugs/esbuild/check-dependencies.ts +158 -0
- package/src/plugs/esbuild/fix-extensions.ts +108 -0
- package/src/plugs/esbuild.ts +128 -0
- package/src/plugs/eslint/runner.ts +112 -0
- package/src/plugs/exec.ts +215 -0
- package/src/plugs/filter.ts +56 -0
- package/src/plugs/mocha/reporter.ts +152 -0
- package/src/plugs/mocha/runner.ts +77 -0
- package/src/plugs/tsc/compiler.ts +66 -0
- package/src/plugs/tsc/options.ts +97 -0
- package/src/plugs/tsc/report.ts +74 -0
- package/src/plugs/tsc/runner.ts +100 -0
- package/src/plugs.ts +33 -0
- package/src/run.ts +160 -0
- package/src/task.ts +26 -0
- package/src/utils/asyncfs.ts +82 -0
- package/src/utils/caller.ts +45 -0
- package/src/utils/match.ts +286 -0
- package/src/utils/options.ts +22 -0
- package/src/utils/walk.ts +136 -0
- package/types/assert.d.ts +18 -0
- package/types/async.d.ts +20 -0
- package/types/build.d.ts +56 -0
- package/types/files.d.ts +44 -0
- package/types/fork.d.ts +57 -0
- package/types/helpers.d.ts +49 -0
- package/types/index.d.ts +14 -0
- package/types/log/colors.d.ts +25 -0
- package/types/log/emit.d.ts +14 -0
- package/types/log/levels.d.ts +52 -0
- package/types/log/logger.d.ts +31 -0
- package/types/log/options.d.ts +40 -0
- package/types/log/report.d.ts +64 -0
- package/types/log/spinner.d.ts +2 -0
- package/types/log.d.ts +10 -0
- package/types/paths.d.ts +76 -0
- package/types/pipe.d.ts +152 -0
- package/types/plugs/copy.d.ts +27 -0
- package/types/plugs/coverage/analysis.d.ts +104 -0
- package/types/plugs/coverage/report.d.ts +53 -0
- package/types/plugs/coverage.d.ts +46 -0
- package/types/plugs/debug.d.ts +14 -0
- package/types/plugs/esbuild/bundle-locals.d.ts +6 -0
- package/types/plugs/esbuild/check-dependencies.d.ts +12 -0
- package/types/plugs/esbuild/fix-extensions.d.ts +29 -0
- package/types/plugs/esbuild.d.ts +24 -0
- package/types/plugs/eslint/runner.d.ts +22 -0
- package/types/plugs/exec.d.ts +90 -0
- package/types/plugs/filter.d.ts +23 -0
- package/types/plugs/mocha/reporter.d.ts +8 -0
- package/types/plugs/mocha/runner.d.ts +34 -0
- package/types/plugs/tsc/compiler.d.ts +24 -0
- package/types/plugs/tsc/options.d.ts +8 -0
- package/types/plugs/tsc/report.d.ts +5 -0
- package/types/plugs/tsc/runner.d.ts +13 -0
- package/types/plugs.d.ts +16 -0
- package/types/run.d.ts +89 -0
- package/types/task.d.ts +15 -0
- package/types/utils/asyncfs.d.ts +37 -0
- package/types/utils/caller.d.ts +7 -0
- package/types/utils/match.d.ts +216 -0
- package/types/utils/options.d.ts +15 -0
- package/types/utils/walk.d.ts +28 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
|
|
2
|
+
import { parse } from '@babel/parser'
|
|
3
|
+
import {
|
|
4
|
+
Comment, isDeclaration,
|
|
5
|
+
isExportDeclaration,
|
|
6
|
+
isFile,
|
|
7
|
+
isIfStatement,
|
|
8
|
+
isImportDeclaration,
|
|
9
|
+
isProgram, isTryStatement,
|
|
10
|
+
isTSDeclareMethod,
|
|
11
|
+
isTSTypeReference,
|
|
12
|
+
isTypeScript,
|
|
13
|
+
Node,
|
|
14
|
+
VISITOR_KEYS
|
|
15
|
+
} from '@babel/types'
|
|
16
|
+
import { pathToFileURL } from 'node:url'
|
|
17
|
+
import { $p, Logger } from '../../log.js'
|
|
18
|
+
import { AbsolutePath } from '../../paths.js'
|
|
19
|
+
import { readFile } from '../../utils/asyncfs.js'
|
|
20
|
+
import { CoverageAnalyser } from './analysis.js'
|
|
21
|
+
|
|
22
|
+
/* ========================================================================== *
|
|
23
|
+
* EXPORTED CONSTANTS AND TYPES *
|
|
24
|
+
* ========================================================================== */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A constant indicating that coverage was skipped (is irrelevant, for e.g.
|
|
28
|
+
* comment or typescript definition nodes)
|
|
29
|
+
*/
|
|
30
|
+
export const COVERAGE_SKIPPED = -2
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A constant indicating that coverage was intentionally ignored because of a
|
|
34
|
+
* specific "coverage ignore ..." comment
|
|
35
|
+
*/
|
|
36
|
+
export const COVERAGE_IGNORED = -1
|
|
37
|
+
|
|
38
|
+
/** Node coverage summary */
|
|
39
|
+
export interface NodeCoverageResult {
|
|
40
|
+
/** Number of _covered_ nodes (good!) */
|
|
41
|
+
coveredNodes: number,
|
|
42
|
+
/** Number of nodes with _no coverage_ (bad!) */
|
|
43
|
+
missingNodes: number,
|
|
44
|
+
/** Number of nodes ignored by comments like `coverage ignore xxx` */
|
|
45
|
+
ignoredNodes: number,
|
|
46
|
+
/** Total number of nodes (sum of `covered`, `missing` and `ignored`) */
|
|
47
|
+
totalNodes: number,
|
|
48
|
+
/** Percentage of code coverage (covered as a % of total - ignored nodes)*/
|
|
49
|
+
coverage: number,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Per-file coverage result */
|
|
53
|
+
export interface CoverageResult {
|
|
54
|
+
/** The actual code this coverage is for */
|
|
55
|
+
code: string,
|
|
56
|
+
/**
|
|
57
|
+
* Per _character_ coverage report:
|
|
58
|
+
* - `-2`: coverage skipped (comments, typescript declarations, ...)
|
|
59
|
+
* - `-1`: coverage ignored (when using `coverage ignore xxx`)
|
|
60
|
+
* - `0`: no coverage collected for this character
|
|
61
|
+
* - _any number greater than zero_: number of times this was covered
|
|
62
|
+
*/
|
|
63
|
+
codeCoverage: number[],
|
|
64
|
+
/** Node coverage summary */
|
|
65
|
+
nodeCoverage: NodeCoverageResult,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Aggregation of {@link CoverageResult} over all files */
|
|
69
|
+
export type CoverageResults = Record<AbsolutePath, CoverageResult>
|
|
70
|
+
|
|
71
|
+
/** Our coverage report, per file */
|
|
72
|
+
export interface CoverageReport {
|
|
73
|
+
results: CoverageResults,
|
|
74
|
+
nodes: NodeCoverageResult,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ========================================================================== *
|
|
78
|
+
* EXPORTED CONSTANTS AND TYPES *
|
|
79
|
+
* ========================================================================== */
|
|
80
|
+
|
|
81
|
+
/** Tokens for `coverage ignore xxx`, this should be self-explanatory */
|
|
82
|
+
type IgnoreCoverage = 'test' | 'if' | 'else' | 'try' | 'catch' | 'finally' | 'next'
|
|
83
|
+
|
|
84
|
+
/** Regular expression matching strings like `coverage ignore xxx` */
|
|
85
|
+
const ignoreRegexp = /(coverage|istanbul)\s+ignore\s+(test|if|else|try|catch|finally|next)(\s|$)/g
|
|
86
|
+
|
|
87
|
+
/* ========================================================================== *
|
|
88
|
+
* EXPORTED CONSTANTS AND TYPES *
|
|
89
|
+
* ========================================================================== */
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Analyse coverage for the specified source files, using the data from the
|
|
93
|
+
* specified coverage files and produce a {@link CoverageReport}.
|
|
94
|
+
*/
|
|
95
|
+
export async function coverageReport(
|
|
96
|
+
analyser: CoverageAnalyser,
|
|
97
|
+
sourceFiles: AbsolutePath[],
|
|
98
|
+
log: Logger,
|
|
99
|
+
): Promise<CoverageReport> {
|
|
100
|
+
/* Some of our results */
|
|
101
|
+
const results: CoverageResults = {}
|
|
102
|
+
const nodes: NodeCoverageResult = {
|
|
103
|
+
coveredNodes: 0,
|
|
104
|
+
missingNodes: 0,
|
|
105
|
+
ignoredNodes: 0,
|
|
106
|
+
totalNodes: 0,
|
|
107
|
+
coverage: 0,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Here comes the interesting part: we need to parse the original soruces,
|
|
112
|
+
* walk their ASTs and see whether each node has been covered or not.
|
|
113
|
+
* We look up every node's position, (for sitemaps, map this to the position
|
|
114
|
+
* in the resulting file) then look at the coverage.
|
|
115
|
+
*/
|
|
116
|
+
for (const file of sourceFiles) {
|
|
117
|
+
/* Read up the file and parse the tree in the most liberal way possible */
|
|
118
|
+
const url = pathToFileURL(file).toString()
|
|
119
|
+
const code = await readFile(file, 'utf-8')
|
|
120
|
+
|
|
121
|
+
const tree = parse(code, {
|
|
122
|
+
allowImportExportEverywhere: true,
|
|
123
|
+
allowAwaitOutsideFunction: true,
|
|
124
|
+
allowReturnOutsideFunction: true,
|
|
125
|
+
allowSuperOutsideMethod: true,
|
|
126
|
+
allowUndeclaredExports: true,
|
|
127
|
+
attachComment: true,
|
|
128
|
+
errorRecovery: false,
|
|
129
|
+
sourceType: 'unambiguous',
|
|
130
|
+
sourceFilename: file,
|
|
131
|
+
startLine: 1,
|
|
132
|
+
startColumn: 0,
|
|
133
|
+
plugins: [ 'typescript' ],
|
|
134
|
+
strictMode: false,
|
|
135
|
+
ranges: false,
|
|
136
|
+
tokens: false,
|
|
137
|
+
createParenthesizedExpressions: true,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const codeCoverage: number[] = new Array(code.length).fill(0)
|
|
141
|
+
const nodeCoverage: NodeCoverageResult = {
|
|
142
|
+
coveredNodes: 0,
|
|
143
|
+
missingNodes: 0,
|
|
144
|
+
ignoredNodes: 0,
|
|
145
|
+
totalNodes: 0,
|
|
146
|
+
coverage: 0,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Set the code coverage for the specified node and (optionally) its children */
|
|
150
|
+
const setCodeCoverage = (
|
|
151
|
+
node: (Node | Comment)[] | Node | Comment | undefined | null,
|
|
152
|
+
coverage: number,
|
|
153
|
+
recursive: boolean,
|
|
154
|
+
): void => {
|
|
155
|
+
if (! node) return
|
|
156
|
+
|
|
157
|
+
if (Array.isArray(node)) {
|
|
158
|
+
for (const n of node) setCodeCoverage(n, coverage, recursive)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if ((node.start != null) && (node.end != null)) {
|
|
163
|
+
for (let i = node.start; i < node.end; i ++) {
|
|
164
|
+
codeCoverage[i] = coverage
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (coverage == COVERAGE_IGNORED) {
|
|
169
|
+
nodeCoverage.ignoredNodes++
|
|
170
|
+
} else if (coverage === 0) {
|
|
171
|
+
nodeCoverage.missingNodes++
|
|
172
|
+
} else if (coverage > 0) {
|
|
173
|
+
nodeCoverage.coveredNodes++
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (! recursive) return
|
|
177
|
+
|
|
178
|
+
const keys = VISITOR_KEYS[node.type] || []
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
const value: Node | Node[] = (<any> node)[key]
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
for (const child of value) {
|
|
183
|
+
setCodeCoverage(child, coverage, true)
|
|
184
|
+
}
|
|
185
|
+
} else if (value) {
|
|
186
|
+
setCodeCoverage(value, coverage, true)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Recursively invoke "visitNode" on the children of a given node */
|
|
192
|
+
const visitChildren = (node: Node, depth: number): void => {
|
|
193
|
+
const keys = VISITOR_KEYS[node.type] || []
|
|
194
|
+
for (const key of keys) {
|
|
195
|
+
const children: Node | null | (Node | null)[] = (<any> node)[key]
|
|
196
|
+
if (Array.isArray(children)) {
|
|
197
|
+
for (const child of children) {
|
|
198
|
+
if (child) visitNode(child, depth + 1)
|
|
199
|
+
}
|
|
200
|
+
} else if (children) {
|
|
201
|
+
visitNode(children, depth + 1)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Visit a node or ignore it depending on a condition */
|
|
207
|
+
const maybeIgnoreNode = (
|
|
208
|
+
condition: boolean,
|
|
209
|
+
node: Node | null | undefined,
|
|
210
|
+
depth: number,
|
|
211
|
+
): void => {
|
|
212
|
+
if (condition) {
|
|
213
|
+
setCodeCoverage(node, COVERAGE_IGNORED, true)
|
|
214
|
+
} else if (node) {
|
|
215
|
+
visitNode(node, depth)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Visit a node and evaluate its coverage */
|
|
220
|
+
const visitNode = (node: Node, depth: number): void => {
|
|
221
|
+
/* See what we're doing here... */
|
|
222
|
+
log.trace('-'.padStart((depth * 2) + 1, ' '), node.type, `${node.loc?.start.line}:${node.loc?.start.column}`)
|
|
223
|
+
|
|
224
|
+
/* Root nodes (file and program) simply go to their children */
|
|
225
|
+
if (isFile(node)) return visitChildren(node, depth)
|
|
226
|
+
if (isProgram(node)) return visitChildren(node, depth)
|
|
227
|
+
|
|
228
|
+
/* Figure out if we have some "coverage ignore xxxx" in comments */
|
|
229
|
+
const ignores: IgnoreCoverage[] = []
|
|
230
|
+
for (const comment of node.leadingComments || []) {
|
|
231
|
+
for (const match of comment.value.matchAll(ignoreRegexp)) {
|
|
232
|
+
ignores.push(match[2] as IgnoreCoverage)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* Skip this node if we have a "coverage ignore next" comment */
|
|
237
|
+
if (ignores.includes('next')) return setCodeCoverage(node, COVERAGE_IGNORED, true)
|
|
238
|
+
|
|
239
|
+
/* Typescript nodes are skipped, but children aren't in some cases */
|
|
240
|
+
if (isTypeScript(node)) {
|
|
241
|
+
if (isTSDeclareMethod(node)) return setCodeCoverage(node, COVERAGE_SKIPPED, true)
|
|
242
|
+
if (isTSTypeReference(node)) return setCodeCoverage(node, COVERAGE_SKIPPED, true)
|
|
243
|
+
if (isDeclaration(node)) return setCodeCoverage(node, COVERAGE_SKIPPED, true)
|
|
244
|
+
|
|
245
|
+
/* For things like "X as Y": the "as" node wraps the expression */
|
|
246
|
+
setCodeCoverage(node, COVERAGE_SKIPPED, false) // not recursive
|
|
247
|
+
return visitChildren(node, depth) // visit all children normally...
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Typescript "import type" or "export type" get skipped all together
|
|
251
|
+
if (isExportDeclaration(node) && (node.exportKind === 'type')) {
|
|
252
|
+
return setCodeCoverage(node, COVERAGE_SKIPPED, true)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (isImportDeclaration(node) && (node.importKind === 'type')) {
|
|
256
|
+
return setCodeCoverage(node, COVERAGE_SKIPPED, true)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Ok, from here we calculate the coverage */
|
|
260
|
+
let coverage = 0
|
|
261
|
+
if (node.loc) {
|
|
262
|
+
const { line, column } = node.loc.start
|
|
263
|
+
const c = analyser.coverage(url, line, column)
|
|
264
|
+
if (c == null) log.warn(`No coverage for ${node.type} at ${$p(file)}:${line}:${column}`)
|
|
265
|
+
else coverage = c
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Record the code coverage, and set up our variables */
|
|
269
|
+
setCodeCoverage(node, coverage, false)
|
|
270
|
+
|
|
271
|
+
/* The "if" node might have some ignores... */
|
|
272
|
+
if (isIfStatement(node)) {
|
|
273
|
+
maybeIgnoreNode(ignores.includes('test'), node.test, depth + 1)
|
|
274
|
+
maybeIgnoreNode(ignores.includes('if'), node.consequent, depth + 1)
|
|
275
|
+
maybeIgnoreNode(ignores.includes('else'), node.alternate, depth + 1)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* The "try" node might have some ignores... */
|
|
280
|
+
if (isTryStatement(node)) {
|
|
281
|
+
maybeIgnoreNode(ignores.includes('try'), node.block, depth + 1)
|
|
282
|
+
maybeIgnoreNode(ignores.includes('catch'), node.handler, depth + 1)
|
|
283
|
+
maybeIgnoreNode(ignores.includes('finally'), node.finalizer, depth + 1)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* All other nodes simply gets visited recursively*/
|
|
288
|
+
visitChildren(node, depth)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Start by setting the scope of our coverage in the code */
|
|
292
|
+
codeCoverage.fill(COVERAGE_SKIPPED) // by default, everything is skipped
|
|
293
|
+
setCodeCoverage(tree.program.directives, 0, true) // directives must be covered
|
|
294
|
+
setCodeCoverage(tree.program.body, 0, true) // program body must be covered
|
|
295
|
+
|
|
296
|
+
/* Cleanup our node statistics */
|
|
297
|
+
nodeCoverage.coveredNodes = 0
|
|
298
|
+
nodeCoverage.missingNodes = 0
|
|
299
|
+
nodeCoverage.ignoredNodes = 0
|
|
300
|
+
|
|
301
|
+
/* Visit all of the program nodes */
|
|
302
|
+
visitChildren(tree.program, -1)
|
|
303
|
+
|
|
304
|
+
/*
|
|
305
|
+
* As comments are mixed within codes (and do not get visited in the
|
|
306
|
+
* tree) we force-skip them _AFTER_ the tree is visited, otherwise (for
|
|
307
|
+
* example) a comment within a block will be shown with the coverage of
|
|
308
|
+
* the block itself.
|
|
309
|
+
*/
|
|
310
|
+
// setCodeCoverage(tree.comments, COVERAGE_SKIPPED, true)
|
|
311
|
+
|
|
312
|
+
/* Update nodes coverage results */
|
|
313
|
+
updateNodeCoverageResult(nodeCoverage)
|
|
314
|
+
|
|
315
|
+
nodes.coveredNodes += nodeCoverage.coveredNodes
|
|
316
|
+
nodes.missingNodes += nodeCoverage.missingNodes
|
|
317
|
+
nodes.ignoredNodes += nodeCoverage.ignoredNodes
|
|
318
|
+
nodes.totalNodes += nodeCoverage.totalNodes
|
|
319
|
+
|
|
320
|
+
/* This file is done, add it to the report */
|
|
321
|
+
results[file] = { code, codeCoverage, nodeCoverage }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* All done, return the report */
|
|
325
|
+
updateNodeCoverageResult(nodes)
|
|
326
|
+
return { results, nodes }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function updateNodeCoverageResult(result: NodeCoverageResult): void {
|
|
330
|
+
const { coveredNodes, missingNodes, ignoredNodes } = result
|
|
331
|
+
const totalNodes = result.totalNodes = coveredNodes + missingNodes + ignoredNodes
|
|
332
|
+
if (totalNodes - ignoredNodes) {
|
|
333
|
+
result.coverage = Math.floor((100 * coveredNodes) / (totalNodes - ignoredNodes))
|
|
334
|
+
} else {
|
|
335
|
+
result.coverage = 0 // No "infinity" on division by zero...
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { html, initFunction } from '@plugjs/cov8-html'
|
|
2
|
+
import { sep } from 'node:path'
|
|
3
|
+
import { fail } from '../assert.js'
|
|
4
|
+
import { Files } from '../files.js'
|
|
5
|
+
import { $gry, $ms, $p, $red, $ylw, ERROR, NOTICE, WARN } from '../log.js'
|
|
6
|
+
import { AbsolutePath } from '../paths.js'
|
|
7
|
+
import { install, Plug } from '../pipe.js'
|
|
8
|
+
import { Run } from '../run.js'
|
|
9
|
+
import { createAnalyser, SourceMapBias } from './coverage/analysis.js'
|
|
10
|
+
import { coverageReport, CoverageResult } from './coverage/report.js'
|
|
11
|
+
|
|
12
|
+
/** Options to analyse coverage reports */
|
|
13
|
+
export interface CoverageOptions {
|
|
14
|
+
/** The bias for source map analisys (defaults to `greatest_lower_bound`) */
|
|
15
|
+
sourceMapBias?: SourceMapBias
|
|
16
|
+
/** Minimum _overall_ coverage (as a percentage) */
|
|
17
|
+
minimumCoverage?: number,
|
|
18
|
+
/** Optimal _overall_ coverage (as a percentage) */
|
|
19
|
+
optimalCoverage?: number,
|
|
20
|
+
/** Minimum _per-file_ coverage (as a percentage) */
|
|
21
|
+
minimumFileCoverage?: number,
|
|
22
|
+
/** Optimal _per-file_ coverage (as a percentage) */
|
|
23
|
+
optimalFileCoverage?: number,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CoverageReportOptions extends CoverageOptions {
|
|
27
|
+
/** If specified, a JSON and HTML report will be written to this directory */
|
|
28
|
+
reportDir: string,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The {@link Coverage} plug type is inferred from the constructor, so this
|
|
33
|
+
* type helps to declare the correct types as we can't really infer them from
|
|
34
|
+
* `typeof Coverage`...
|
|
35
|
+
*/
|
|
36
|
+
type CoverageConstructor = {
|
|
37
|
+
new (coverageDir: string): Coverage<CoverageOptions>
|
|
38
|
+
new (coverageDir: string, options: CoverageOptions): Coverage<CoverageOptions>
|
|
39
|
+
new (coverageDir: string, options: CoverageReportOptions): Coverage<CoverageReportOptions>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Analyse coverage using files generated by V8/NodeJS. */
|
|
43
|
+
export class Coverage<
|
|
44
|
+
T extends CoverageOptions | CoverageReportOptions,
|
|
45
|
+
> implements Plug<T extends CoverageReportOptions ? Files : undefined> {
|
|
46
|
+
constructor(coverageDir: string)
|
|
47
|
+
constructor(coverageDir: string, options?: T)
|
|
48
|
+
constructor(
|
|
49
|
+
private readonly _coverageDir: string,
|
|
50
|
+
private readonly _options: Partial<CoverageReportOptions> = {},
|
|
51
|
+
) {}
|
|
52
|
+
|
|
53
|
+
async pipe(files: Files, run: Run): Promise<T extends CoverageReportOptions ? Files : undefined> {
|
|
54
|
+
const coverageFiles = await run.find('coverage-*.json', {
|
|
55
|
+
directory: this._coverageDir,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (coverageFiles.length === 0) {
|
|
59
|
+
fail(`No coverage files found in ${$p(coverageFiles.directory)}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sourceFiles = [ ...files.absolutePaths() ]
|
|
63
|
+
|
|
64
|
+
const ms1 = Date.now()
|
|
65
|
+
const analyser = await createAnalyser(
|
|
66
|
+
sourceFiles,
|
|
67
|
+
[ ...coverageFiles.absolutePaths() ],
|
|
68
|
+
this._options.sourceMapBias || 'least_upper_bound',
|
|
69
|
+
run.log,
|
|
70
|
+
)
|
|
71
|
+
run.log.info('Parsed', coverageFiles.length, 'coverage files', $ms(Date.now() - ms1))
|
|
72
|
+
|
|
73
|
+
const ms2 = Date.now()
|
|
74
|
+
const report = await coverageReport(analyser, sourceFiles, run.log)
|
|
75
|
+
run.log.info('Analysed', sourceFiles.length, 'source files', $ms(Date.now() - ms2))
|
|
76
|
+
|
|
77
|
+
analyser.destroy()
|
|
78
|
+
|
|
79
|
+
const {
|
|
80
|
+
minimumCoverage = 50,
|
|
81
|
+
minimumFileCoverage = minimumCoverage,
|
|
82
|
+
optimalCoverage = Math.round((100 + minimumCoverage) / 2),
|
|
83
|
+
optimalFileCoverage = Math.round((100 + minimumFileCoverage) / 2),
|
|
84
|
+
} = this._options
|
|
85
|
+
|
|
86
|
+
let max = 0
|
|
87
|
+
for (const file in report) {
|
|
88
|
+
if (file.length > max) max = file.length
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let maxLength = 0
|
|
92
|
+
for (const file in report.results) {
|
|
93
|
+
if (file.length > maxLength) maxLength = file.length
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let fileErrors = 0
|
|
97
|
+
let fileWarnings = 0
|
|
98
|
+
const _report = run.report('Coverage report')
|
|
99
|
+
|
|
100
|
+
for (const [ _file, result ] of Object.entries(report.results)) {
|
|
101
|
+
const { coverage } = result.nodeCoverage
|
|
102
|
+
const file = _file as AbsolutePath
|
|
103
|
+
|
|
104
|
+
if (coverage < minimumFileCoverage) {
|
|
105
|
+
_report.annotate(ERROR, file, `${coverage} %`)
|
|
106
|
+
fileErrors ++
|
|
107
|
+
} else if (coverage < optimalFileCoverage) {
|
|
108
|
+
_report.annotate(WARN, file, `${coverage} %`)
|
|
109
|
+
fileWarnings ++
|
|
110
|
+
} else {
|
|
111
|
+
_report.annotate(NOTICE, file, `${coverage} %`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (report.nodes.coverage < minimumCoverage) {
|
|
116
|
+
const message = `${$red(`${report.nodes.coverage}%`)} does not meet minimum coverage ${$gry(`(${minimumCoverage}%)`)}`
|
|
117
|
+
_report.add({ level: ERROR, message })
|
|
118
|
+
} else if (report.nodes.coverage < optimalCoverage) {
|
|
119
|
+
const message = `${$ylw(`${report.nodes.coverage}%`)} does not meet optimal coverage ${$gry(`(${optimalCoverage}%)`)}`
|
|
120
|
+
_report.add({ level: WARN, message })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (fileErrors) {
|
|
124
|
+
const message = `${$red(fileErrors)} files do not meet minimum file coverage ${$gry(`(${minimumFileCoverage}%)`)}`
|
|
125
|
+
_report.add({ level: ERROR, message })
|
|
126
|
+
}
|
|
127
|
+
if (fileWarnings) {
|
|
128
|
+
const message = `${$ylw(fileWarnings)} files do not meet optimal file coverage ${$gry(`(${optimalFileCoverage}%)`)}`
|
|
129
|
+
_report.add({ level: WARN, message })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* If we don't have to write a report, pass-through the coverage files */
|
|
133
|
+
if (this._options.reportDir == null) return _report.done(false) as any
|
|
134
|
+
|
|
135
|
+
/* Create a builder to emit our reports */
|
|
136
|
+
const builder = run.files(this._options.reportDir)
|
|
137
|
+
|
|
138
|
+
/* Thresholds to inject in the report */
|
|
139
|
+
const date = new Date().toISOString()
|
|
140
|
+
const thresholds = {
|
|
141
|
+
minimumCoverage,
|
|
142
|
+
minimumFileCoverage,
|
|
143
|
+
optimalCoverage,
|
|
144
|
+
optimalFileCoverage,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* The JSON file in the report has *absolute* file paths */
|
|
148
|
+
await builder.write('report.json', JSON.stringify({ ...report, thresholds, date }))
|
|
149
|
+
|
|
150
|
+
/* The HTML file rendering our report */
|
|
151
|
+
await builder.write('index.html', html)
|
|
152
|
+
|
|
153
|
+
/* The JSONP file (for our HTML report) has relative files and a tree */
|
|
154
|
+
const results: Record<string, CoverageResult> = {}
|
|
155
|
+
for (const [ rel, abs ] of files.pathMappings()) {
|
|
156
|
+
results[rel] = report.results[abs]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tree: Record<string, any> = {}
|
|
160
|
+
for (const relative of Object.keys(results)) {
|
|
161
|
+
const directories = relative.split(sep)
|
|
162
|
+
const file = directories.pop()!
|
|
163
|
+
|
|
164
|
+
let node = tree
|
|
165
|
+
for (const dir of directories) {
|
|
166
|
+
node = node[dir] = node[dir] || {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
node[file] = relative
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const jsonp = JSON.stringify({ ...report, results, thresholds, tree, date })
|
|
173
|
+
await builder.write('report.js', `${initFunction}(${jsonp});`)
|
|
174
|
+
|
|
175
|
+
/* Emit our coverage report */
|
|
176
|
+
_report.done(false)
|
|
177
|
+
|
|
178
|
+
/* Return emitted files */
|
|
179
|
+
return builder.build() as any
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ========================================================================== *
|
|
184
|
+
* INSTALLATION *
|
|
185
|
+
* ========================================================================== */
|
|
186
|
+
|
|
187
|
+
install('coverage', Coverage)
|
|
188
|
+
|
|
189
|
+
declare module '../pipe.js' {
|
|
190
|
+
export interface Pipe {
|
|
191
|
+
/** Analyse coverage using files generated by V8/NodeJS. */
|
|
192
|
+
coverage: PipeExtension<CoverageConstructor>
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Files } from '../files.js'
|
|
2
|
+
import { $gry, $p, $und, log } from '../log.js'
|
|
3
|
+
import { install, Plug } from '../pipe.js'
|
|
4
|
+
import { Run } from '../run.js'
|
|
5
|
+
|
|
6
|
+
/** Writes some info about the current {@link Files} being passed around. */
|
|
7
|
+
export class Debug implements Plug<Files> {
|
|
8
|
+
constructor() {}
|
|
9
|
+
|
|
10
|
+
async pipe(files: Files, run: Run): Promise<Files> {
|
|
11
|
+
log.notice('Debugging', files.length, 'files')
|
|
12
|
+
log.notice('- base dir:', $p(run.resolve('@')))
|
|
13
|
+
log.notice('- build file dir:', $p(run.resolve('.')))
|
|
14
|
+
log.notice('- files dir:', $p(files.directory))
|
|
15
|
+
if (files.length) {
|
|
16
|
+
const [ path, ...paths ] = files
|
|
17
|
+
log.notice('- relative paths:', $und($gry(path)))
|
|
18
|
+
for (const p of paths) log.notice('- :', $und($gry(p)))
|
|
19
|
+
}
|
|
20
|
+
return files
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ========================================================================== *
|
|
25
|
+
* INSTALLATION *
|
|
26
|
+
* ========================================================================== */
|
|
27
|
+
|
|
28
|
+
install('debug', Debug)
|
|
29
|
+
|
|
30
|
+
declare module '../pipe.js' {
|
|
31
|
+
export interface Pipe {
|
|
32
|
+
/** Writes some info about the current {@link Files} being passed around. */
|
|
33
|
+
debug: PipeExtension<typeof Debug>
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Plugin } from 'esbuild'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A simple ESBuild plugin bundling _only_ local files, and marking anything
|
|
5
|
+
* imported from `node_modules` as _external_.
|
|
6
|
+
*/
|
|
7
|
+
export function bundleLocals(): Plugin {
|
|
8
|
+
let disabled = false
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
name: 'bundle-locals',
|
|
12
|
+
|
|
13
|
+
setup(build): void {
|
|
14
|
+
build.onStart(() => {
|
|
15
|
+
if (build.initialOptions.bundle) return
|
|
16
|
+
|
|
17
|
+
disabled = true
|
|
18
|
+
return { warnings: [ { text: 'Plugin disabled when not bundling' } ] }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/* Intercept resolution */
|
|
22
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
23
|
+
if (disabled) return
|
|
24
|
+
|
|
25
|
+
/* Ignore the entry points (when the file is not being imported) */
|
|
26
|
+
if (! args.importer) return null
|
|
27
|
+
|
|
28
|
+
/* Anything not starting with "."? external node module */
|
|
29
|
+
return args.path.startsWith('.') ? null : { external: true }
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|