@bernierllc/nevar-loop-detector 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -28
- package/dist/chain-tracker.d.ts +27 -0
- package/dist/chain-tracker.js +51 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +16 -0
- package/dist/loop-detector.d.ts +21 -0
- package/dist/loop-detector.js +136 -0
- package/package.json +53 -7
package/README.md
CHANGED
|
@@ -1,45 +1,71 @@
|
|
|
1
1
|
# @bernierllc/nevar-loop-detector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Static and runtime loop detection for the Nevar rules engine. Analyzes rule sets for potential evaluation cycles at registration time and tracks runtime emit chains.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bernierllc/nevar-loop-detector
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Usage
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
13
|
+
```typescript
|
|
14
|
+
import { LoopDetector, ChainTracker } from '@bernierllc/nevar-loop-detector';
|
|
15
|
+
import type { Rule } from '@bernierllc/nevar-types';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
// Static analysis: detect cycles in rule definitions
|
|
18
|
+
const detector = new LoopDetector();
|
|
19
|
+
const analysis = detector.analyze(rules);
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
if (analysis.hasLoops) {
|
|
22
|
+
for (const loop of analysis.loops) {
|
|
23
|
+
console.log('Cycle:', loop.chain.join(' -> '));
|
|
24
|
+
console.log('All rules opted in:', loop.allOptedIn);
|
|
25
|
+
console.log('Rule IDs:', loop.ruleIds);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
// Runtime tracking: detect cycles during emit chain execution
|
|
30
|
+
const tracker = new ChainTracker();
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
const isCycle = tracker.enter('order.created', 'ctx-123');
|
|
33
|
+
if (isCycle) {
|
|
34
|
+
// This trigger+context pair was already seen in this chain
|
|
35
|
+
}
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
37
|
+
console.log(tracker.getDepth()); // current chain depth
|
|
38
|
+
tracker.reset(); // clear for next chain
|
|
39
|
+
```
|
|
28
40
|
|
|
29
|
-
##
|
|
41
|
+
## API
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
43
|
+
### `LoopDetector`
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
Static loop analysis for rule sets. Builds a graph of trigger-to-deferred-trigger relationships and detects cycles using depth-first search.
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
47
|
+
- `analyze(rules: Rule[])` - Returns a `LoopAnalysis` with `hasLoops: boolean` and `loops: DetectedLoop[]`. Each `DetectedLoop` contains the `chain` of trigger keys forming the cycle, `ruleIds` involved, and whether `allOptedIn` to looping.
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
### `ChainTracker`
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
Runtime cycle detection for emit chains. Tracks `(triggerKey, contextId)` pairs within a single emit chain.
|
|
52
|
+
|
|
53
|
+
- `enter(triggerKey, contextId)` - Records entry into a trigger evaluation. Returns `true` if a cycle is detected (pair already seen), `false` on first occurrence
|
|
54
|
+
- `getDepth()` - Returns the current chain depth
|
|
55
|
+
- `reset()` - Clears all recorded entries and resets depth
|
|
56
|
+
|
|
57
|
+
### `LoopDetectorError`
|
|
58
|
+
|
|
59
|
+
Error class for loop detection failures. Extends `Error` with `code` and `context` properties.
|
|
60
|
+
|
|
61
|
+
## Integration Documentation
|
|
62
|
+
|
|
63
|
+
### Logger Integration
|
|
64
|
+
This package does not integrate with `@bernierllc/logger`. As a core package, logger integration is optional and not included by default. Consumers should handle logging at the service layer.
|
|
65
|
+
|
|
66
|
+
### NeverHub Integration
|
|
67
|
+
This package does not integrate with `@bernierllc/neverhub-adapter`. As a core package, NeverHub integration is not applicable. NeverHub registration should be handled by service-layer packages that compose this package.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
Copyright (c) 2025 Bernier LLC. All rights reserved.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime cycle detection for emit chains.
|
|
3
|
+
*
|
|
4
|
+
* Tracks (triggerKey, contextId) pairs within a single emit chain
|
|
5
|
+
* to detect runtime loops.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ChainTracker {
|
|
8
|
+
private seen;
|
|
9
|
+
private depth;
|
|
10
|
+
/**
|
|
11
|
+
* Record entry into a trigger evaluation.
|
|
12
|
+
*
|
|
13
|
+
* @param triggerKey - The trigger key being evaluated
|
|
14
|
+
* @param contextId - A unique context identifier for the emit chain
|
|
15
|
+
* @returns true if this (triggerKey, contextId) pair was already seen (cycle detected),
|
|
16
|
+
* false if this is the first occurrence
|
|
17
|
+
*/
|
|
18
|
+
enter(triggerKey: string, contextId: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Get the current chain depth.
|
|
21
|
+
*/
|
|
22
|
+
getDepth(): number;
|
|
23
|
+
/**
|
|
24
|
+
* Reset the tracker, clearing all recorded entries and depth.
|
|
25
|
+
*/
|
|
26
|
+
reset(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ChainTracker = void 0;
|
|
11
|
+
/**
|
|
12
|
+
* Runtime cycle detection for emit chains.
|
|
13
|
+
*
|
|
14
|
+
* Tracks (triggerKey, contextId) pairs within a single emit chain
|
|
15
|
+
* to detect runtime loops.
|
|
16
|
+
*/
|
|
17
|
+
class ChainTracker {
|
|
18
|
+
seen = new Set();
|
|
19
|
+
depth = 0;
|
|
20
|
+
/**
|
|
21
|
+
* Record entry into a trigger evaluation.
|
|
22
|
+
*
|
|
23
|
+
* @param triggerKey - The trigger key being evaluated
|
|
24
|
+
* @param contextId - A unique context identifier for the emit chain
|
|
25
|
+
* @returns true if this (triggerKey, contextId) pair was already seen (cycle detected),
|
|
26
|
+
* false if this is the first occurrence
|
|
27
|
+
*/
|
|
28
|
+
enter(triggerKey, contextId) {
|
|
29
|
+
const key = `${triggerKey}::${contextId}`;
|
|
30
|
+
if (this.seen.has(key)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
this.seen.add(key);
|
|
34
|
+
this.depth++;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the current chain depth.
|
|
39
|
+
*/
|
|
40
|
+
getDepth() {
|
|
41
|
+
return this.depth;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reset the tracker, clearing all recorded entries and depth.
|
|
45
|
+
*/
|
|
46
|
+
reset() {
|
|
47
|
+
this.seen.clear();
|
|
48
|
+
this.depth = 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.ChainTracker = ChainTracker;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error class for loop detection failures.
|
|
3
|
+
*/
|
|
4
|
+
export declare class LoopDetectorError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly context?: Record<string, unknown>;
|
|
7
|
+
constructor(message: string, options?: {
|
|
8
|
+
cause?: Error;
|
|
9
|
+
code?: string;
|
|
10
|
+
context?: Record<string, unknown>;
|
|
11
|
+
});
|
|
12
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.LoopDetectorError = void 0;
|
|
11
|
+
/**
|
|
12
|
+
* Error class for loop detection failures.
|
|
13
|
+
*/
|
|
14
|
+
class LoopDetectorError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
context;
|
|
17
|
+
constructor(message, options) {
|
|
18
|
+
super(message, { cause: options?.cause });
|
|
19
|
+
this.name = 'LoopDetectorError';
|
|
20
|
+
this.code = options?.code ?? 'LOOP_DETECTOR_ERROR';
|
|
21
|
+
this.context = options?.context;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.LoopDetectorError = LoopDetectorError;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.LoopDetectorError = exports.ChainTracker = exports.LoopDetector = void 0;
|
|
11
|
+
var loop_detector_1 = require("./loop-detector");
|
|
12
|
+
Object.defineProperty(exports, "LoopDetector", { enumerable: true, get: function () { return loop_detector_1.LoopDetector; } });
|
|
13
|
+
var chain_tracker_1 = require("./chain-tracker");
|
|
14
|
+
Object.defineProperty(exports, "ChainTracker", { enumerable: true, get: function () { return chain_tracker_1.ChainTracker; } });
|
|
15
|
+
var errors_1 = require("./errors");
|
|
16
|
+
Object.defineProperty(exports, "LoopDetectorError", { enumerable: true, get: function () { return errors_1.LoopDetectorError; } });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Rule, LoopAnalysis } from '@bernierllc/nevar-types';
|
|
2
|
+
/**
|
|
3
|
+
* Static loop analysis for rule sets.
|
|
4
|
+
*
|
|
5
|
+
* Walks trigger -> action -> deferredTrigger chains across rules
|
|
6
|
+
* to detect cycles at registration time.
|
|
7
|
+
*/
|
|
8
|
+
export declare class LoopDetector {
|
|
9
|
+
/**
|
|
10
|
+
* Analyze a set of rules for potential evaluation loops.
|
|
11
|
+
*
|
|
12
|
+
* Builds a graph of triggerType -> deferredTrigger relationships
|
|
13
|
+
* and detects cycles using DFS.
|
|
14
|
+
*/
|
|
15
|
+
analyze(rules: Rule[]): LoopAnalysis;
|
|
16
|
+
private buildEdges;
|
|
17
|
+
private extractDeferredTriggers;
|
|
18
|
+
private findCycles;
|
|
19
|
+
private dfs;
|
|
20
|
+
private deduplicateLoops;
|
|
21
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.LoopDetector = void 0;
|
|
11
|
+
/**
|
|
12
|
+
* Static loop analysis for rule sets.
|
|
13
|
+
*
|
|
14
|
+
* Walks trigger -> action -> deferredTrigger chains across rules
|
|
15
|
+
* to detect cycles at registration time.
|
|
16
|
+
*/
|
|
17
|
+
class LoopDetector {
|
|
18
|
+
/**
|
|
19
|
+
* Analyze a set of rules for potential evaluation loops.
|
|
20
|
+
*
|
|
21
|
+
* Builds a graph of triggerType -> deferredTrigger relationships
|
|
22
|
+
* and detects cycles using DFS.
|
|
23
|
+
*/
|
|
24
|
+
analyze(rules) {
|
|
25
|
+
const edges = this.buildEdges(rules);
|
|
26
|
+
const ruleMap = new Map(rules.map((r) => [r.id, r]));
|
|
27
|
+
const loops = this.findCycles(edges, ruleMap);
|
|
28
|
+
return {
|
|
29
|
+
hasLoops: loops.length > 0,
|
|
30
|
+
loops,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
buildEdges(rules) {
|
|
34
|
+
const edges = new Map();
|
|
35
|
+
for (const rule of rules) {
|
|
36
|
+
if (!rule.isActive) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const action of rule.actions) {
|
|
40
|
+
const config = action.config;
|
|
41
|
+
const deferredTriggers = this.extractDeferredTriggers(config);
|
|
42
|
+
for (const triggerKey of deferredTriggers) {
|
|
43
|
+
if (!edges.has(rule.triggerType)) {
|
|
44
|
+
edges.set(rule.triggerType, []);
|
|
45
|
+
}
|
|
46
|
+
edges.get(rule.triggerType).push({
|
|
47
|
+
triggerKey,
|
|
48
|
+
ruleId: rule.id,
|
|
49
|
+
allowLoop: rule.allowLoop,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return edges;
|
|
55
|
+
}
|
|
56
|
+
extractDeferredTriggers(config) {
|
|
57
|
+
const triggers = [];
|
|
58
|
+
if (config['deferredTrigger'] && typeof config['deferredTrigger'] === 'object') {
|
|
59
|
+
const dt = config['deferredTrigger'];
|
|
60
|
+
if (typeof dt['triggerKey'] === 'string') {
|
|
61
|
+
triggers.push(dt['triggerKey']);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(config['deferredTriggers'])) {
|
|
65
|
+
for (const dt of config['deferredTriggers']) {
|
|
66
|
+
if (dt && typeof dt === 'object' && typeof dt['triggerKey'] === 'string') {
|
|
67
|
+
triggers.push(dt['triggerKey']);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return triggers;
|
|
72
|
+
}
|
|
73
|
+
findCycles(edges, ruleMap) {
|
|
74
|
+
const loops = [];
|
|
75
|
+
const allTriggerTypes = new Set();
|
|
76
|
+
for (const [source, targets] of edges) {
|
|
77
|
+
allTriggerTypes.add(source);
|
|
78
|
+
for (const t of targets) {
|
|
79
|
+
allTriggerTypes.add(t.triggerKey);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const startNode of allTriggerTypes) {
|
|
83
|
+
this.dfs(startNode, edges, ruleMap, [], [], new Set(), loops);
|
|
84
|
+
}
|
|
85
|
+
return this.deduplicateLoops(loops);
|
|
86
|
+
}
|
|
87
|
+
dfs(current, edges, ruleMap, path, pathRuleIds, visited, loops) {
|
|
88
|
+
const cycleStart = path.indexOf(current);
|
|
89
|
+
if (cycleStart !== -1) {
|
|
90
|
+
const cyclePath = [...path.slice(cycleStart), current];
|
|
91
|
+
const cycleRuleIds = pathRuleIds.slice(cycleStart);
|
|
92
|
+
const allOptedIn = cycleRuleIds.every((ruleId) => {
|
|
93
|
+
const rule = ruleMap.get(ruleId);
|
|
94
|
+
return rule?.allowLoop === true;
|
|
95
|
+
});
|
|
96
|
+
loops.push({
|
|
97
|
+
chain: cyclePath,
|
|
98
|
+
allOptedIn,
|
|
99
|
+
ruleIds: cycleRuleIds,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (visited.has(current)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const targets = edges.get(current);
|
|
107
|
+
if (!targets) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
visited.add(current);
|
|
111
|
+
for (const target of targets) {
|
|
112
|
+
this.dfs(target.triggerKey, edges, ruleMap, [...path, current], [...pathRuleIds, target.ruleId], new Set(visited), loops);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
deduplicateLoops(loops) {
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const unique = [];
|
|
118
|
+
for (const loop of loops) {
|
|
119
|
+
// Rotate chain to start with the lexicographically smallest element for stable dedup
|
|
120
|
+
const chain = loop.chain.slice(0, -1);
|
|
121
|
+
const minIdx = chain.indexOf(chain.reduce((min, val) => (val < min ? val : min), chain[0]));
|
|
122
|
+
const normalized = [
|
|
123
|
+
...chain.slice(minIdx),
|
|
124
|
+
...chain.slice(0, minIdx),
|
|
125
|
+
chain[minIdx],
|
|
126
|
+
];
|
|
127
|
+
const key = normalized.join('->');
|
|
128
|
+
if (!seen.has(key)) {
|
|
129
|
+
seen.add(key);
|
|
130
|
+
unique.push(loop);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return unique;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.LoopDetector = LoopDetector;
|
package/package.json
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bernierllc/nevar-loop-detector",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Static loop analysis and runtime cycle detection for the Nevar rules engine",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
5
12
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
"nevar",
|
|
14
|
+
"rules-engine",
|
|
15
|
+
"loop-detection",
|
|
16
|
+
"cycle-detection",
|
|
17
|
+
"static-analysis"
|
|
18
|
+
],
|
|
19
|
+
"author": "Bernier LLC",
|
|
20
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/bernierllc/tools.git",
|
|
31
|
+
"directory": "packages/core/nevar-loop-detector"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@bernierllc/nevar-types": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jest": "^29.5.0",
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
40
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
41
|
+
"eslint": "^8.0.0",
|
|
42
|
+
"jest": "^29.5.0",
|
|
43
|
+
"rimraf": "^5.0.0",
|
|
44
|
+
"ts-jest": "^29.1.0",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"prebuild": "npm run clean",
|
|
50
|
+
"clean": "rimraf dist",
|
|
51
|
+
"test": "jest",
|
|
52
|
+
"test:run": "jest",
|
|
53
|
+
"test:coverage": "jest --coverage",
|
|
54
|
+
"lint": "eslint src __tests__ --ext .ts"
|
|
55
|
+
}
|
|
56
|
+
}
|