@appland/scanner 1.89.1 → 1.90.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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [@appland/scanner-v1.90.0](https://github.com/getappmap/appmap-js/compare/@appland/scanner-v1.89.1...@appland/scanner-v1.90.0) (2026-06-24)
2
+
3
+
4
+ ### Features
5
+
6
+ * **scanner:** revive enriched scan:completed telemetry (watch mode) ([4dd20b1](https://github.com/getappmap/appmap-js/commit/4dd20b1c124dbb00badc21cd44afcb0d7ae3fd67))
7
+
1
8
  # [@appland/scanner-v1.89.1](https://github.com/getappmap/appmap-js/compare/@appland/scanner-v1.89.0...@appland/scanner-v1.89.1) (2026-03-04)
2
9
 
3
10
 
@@ -1,5 +1,5 @@
1
1
  import { Event } from '@appland/models';
2
- export declare type Secret = {
2
+ export type Secret = {
3
3
  generatorEvent: Event;
4
4
  value: string;
5
5
  };
@@ -1,12 +1,9 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  const models_1 = require("@appland/models");
7
- const lru_cache_1 = __importDefault(require("lru-cache"));
8
- const NormalizedSQLBySQLString = new lru_cache_1.default({ max: 10000 });
9
- const ASTBySQLString = new lru_cache_1.default({ max: 1000 });
4
+ const lru_cache_1 = require("lru-cache");
5
+ const NormalizedSQLBySQLString = new lru_cache_1.LRUCache({ max: 10000 });
6
+ const ASTBySQLString = new lru_cache_1.LRUCache({ max: 1000 });
10
7
  class AppMapIndex {
11
8
  constructor(appMap) {
12
9
  this.appMap = appMap;
@@ -1,7 +1,7 @@
1
1
  import { Event } from '@appland/models';
2
2
  import Check from '../../check';
3
3
  import { MatchResult } from '../../types';
4
- export declare type ExecutionContext = {
4
+ export type ExecutionContext = {
5
5
  counter: number;
6
6
  depth: number;
7
7
  eventName: string;
@@ -1,5 +1,5 @@
1
1
  import Configuration from '../../configuration/types/configuration';
2
- declare type InteractiveScanOptions = {
2
+ type InteractiveScanOptions = {
3
3
  configuration: Configuration;
4
4
  appmapFile?: string | string[];
5
5
  appmapDir?: string;
@@ -1,5 +1,5 @@
1
1
  import Configuration from '../../configuration/types/configuration';
2
- declare type SingleScanOptions = {
2
+ type SingleScanOptions = {
3
3
  appmapFile?: string | string[];
4
4
  appmapDir?: string;
5
5
  configuration: Configuration;
@@ -7,7 +7,7 @@ import ProgressReporter from '../../../progressReporter';
7
7
  import { MatchResult } from '../../../types';
8
8
  import { ScopeName } from '../../../index';
9
9
  import { Breakpoint } from '../breakpoint';
10
- declare type ContextVariables = {
10
+ type ContextVariables = {
11
11
  event?: Event;
12
12
  matchResult?: string | boolean | MatchResult[];
13
13
  };
@@ -40,6 +40,7 @@ const initial_1 = __importDefault(require("./initial"));
40
40
  function hitBreakpoint(context) {
41
41
  var _a, _b;
42
42
  return __awaiter(this, void 0, void 0, function* () {
43
+ var _c;
43
44
  const choices = {
44
45
  'show hints': 'hint',
45
46
  'evaluate expression': 'eval',
@@ -62,7 +63,7 @@ function hitBreakpoint(context) {
62
63
  const action = choices[actionName];
63
64
  if (!action)
64
65
  return initial_1.default;
65
- return (yield Promise.resolve().then(() => __importStar(require(`./${action}`)))).default;
66
+ return (yield (_c = `./${action}`, Promise.resolve().then(() => __importStar(require(_c))))).default;
66
67
  });
67
68
  }
68
69
  exports.default = hitBreakpoint;
@@ -38,6 +38,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
38
  const userInteraction_1 = __importDefault(require("../userInteraction"));
39
39
  function initial(_context) {
40
40
  return __awaiter(this, void 0, void 0, function* () {
41
+ var _a;
41
42
  const choices = {
42
43
  'add breakpoint': 'addBreakpoint',
43
44
  'run scan': 'scan',
@@ -52,7 +53,7 @@ function initial(_context) {
52
53
  const action = choices[actionName];
53
54
  if (!action)
54
55
  return;
55
- return (yield Promise.resolve().then(() => __importStar(require(`./${action}`)))).default;
56
+ return (yield (_a = `./${action}`, Promise.resolve().then(() => __importStar(require(_a))))).default;
56
57
  });
57
58
  }
58
59
  exports.default = initial;
@@ -1,2 +1,2 @@
1
1
  import ScanContext from './scanContext';
2
- export declare type State = (context: ScanContext) => Promise<State | undefined>;
2
+ export type State = (context: ScanContext) => Promise<State | undefined>;
@@ -2,7 +2,7 @@
2
2
  import * as chokidar from 'chokidar';
3
3
  import { TimestampedConfiguration } from '../../configuration/configurationProvider';
4
4
  import EventEmitter from 'events';
5
- export declare type WatchScanOptions = {
5
+ export type WatchScanOptions = {
6
6
  appId?: string;
7
7
  appmapDir: string;
8
8
  configFile: string;
@@ -19,6 +19,7 @@ export declare class Watcher {
19
19
  appmapPoller?: chokidar.FSWatcher;
20
20
  configWatcher?: chokidar.FSWatcher;
21
21
  scanEventEmitter: EventEmitter;
22
+ private scanTelemetry;
22
23
  constructor(options: WatchScanOptions);
23
24
  watch(): Promise<void>;
24
25
  isError(error: unknown, code: string): boolean;
@@ -47,6 +47,7 @@ const scanner_1 = __importDefault(require("./scanner"));
47
47
  const configurationProvider_1 = require("../../configuration/configurationProvider");
48
48
  const telemetry_1 = require("@appland/telemetry");
49
49
  const events_1 = __importDefault(require("events"));
50
+ const watchScanTelemetry_1 = require("./watchScanTelemetry");
50
51
  const isAncestorPath_1 = __importDefault(require("../../util/isAncestorPath"));
51
52
  const util_1 = require("util");
52
53
  const console_1 = require("console");
@@ -80,6 +81,7 @@ class Watcher {
80
81
  this.queue = (0, async_1.queue)((0, node_util_1.callbackify)(this.scan.bind(this)), 2);
81
82
  this.processing = new Set();
82
83
  this.queue.error((error, task) => console.warn(`Problem processing ${task}:\n`, error));
84
+ this.scanTelemetry = new watchScanTelemetry_1.WatchScanTelemetry(this.scanEventEmitter, options.appmapDir);
83
85
  }
84
86
  watch() {
85
87
  return __awaiter(this, void 0, void 0, function* () {
@@ -155,6 +157,7 @@ class Watcher {
155
157
  }
156
158
  close() {
157
159
  return __awaiter(this, void 0, void 0, function* () {
160
+ yield this.scanTelemetry.cancel();
158
161
  yield Promise.all(['appmapWatcher', 'appmapPoller', 'configWatcher'].map((k) => {
159
162
  var _a;
160
163
  const closer = (_a = this[k]) === null || _a === void 0 ? void 0 : _a.close();
@@ -0,0 +1,14 @@
1
+ /// <reference types="node" />
2
+ import type { EventEmitter } from 'stream';
3
+ import { ScanResults } from '../../report/scanResults';
4
+ export type ScanEvent = {
5
+ scanResults: ScanResults;
6
+ elapsed: number;
7
+ };
8
+ export declare class WatchScanTelemetry {
9
+ private readonly appmapDir?;
10
+ private readonly aggregator;
11
+ constructor(scanEvents: EventEmitter, appmapDir?: string | undefined);
12
+ cancel(): Promise<void>;
13
+ private sendTelemetry;
14
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WatchScanTelemetry = void 0;
7
+ const eventAggregator_1 = __importDefault(require("../../util/eventAggregator"));
8
+ const scanResults_1 = require("../../report/scanResults");
9
+ class WatchScanTelemetry {
10
+ constructor(scanEvents, appmapDir) {
11
+ this.appmapDir = appmapDir;
12
+ this.aggregator = new eventAggregator_1.default(scanEvents, 'scan', (events) => this.sendTelemetry(events));
13
+ }
14
+ cancel() {
15
+ return this.aggregator.cancel();
16
+ }
17
+ sendTelemetry(scanEvents) {
18
+ let elapsed = 0;
19
+ const telemetryScanResults = new scanResults_1.ScanResults();
20
+ for (const scanEvent of scanEvents) {
21
+ telemetryScanResults.aggregate(scanEvent.scanResults);
22
+ elapsed += scanEvent.elapsed;
23
+ }
24
+ return (0, scanResults_1.sendScanResultsTelemetry)({
25
+ ruleIds: telemetryScanResults.summary.rules,
26
+ numAppMaps: telemetryScanResults.summary.numAppMaps,
27
+ numFindings: telemetryScanResults.summary.numFindings,
28
+ findingCountsByRule: telemetryScanResults.summary.findingCountsByRule,
29
+ findingCountsByImpactDomain: telemetryScanResults.summary.findingCountsByImpactDomain,
30
+ elapsedMs: elapsed,
31
+ appmapDir: this.appmapDir,
32
+ });
33
+ }
34
+ }
35
+ exports.WatchScanTelemetry = WatchScanTelemetry;
@@ -1,7 +1,7 @@
1
1
  import { Metadata } from '@appland/models';
2
2
  import Check from '../check';
3
3
  import { Finding } from '../index';
4
- declare type Result = {
4
+ type Result = {
5
5
  appMapMetadata: Record<string, Metadata>;
6
6
  findings: Finding[];
7
7
  };
@@ -3,7 +3,7 @@ import Configuration from './types/configuration';
3
3
  import RuleInstance from '../ruleInstance';
4
4
  export declare function loadRule(ruleName: string): Promise<RuleInstance>;
5
5
  export declare function loadConfig(config: Configuration): Promise<Check[]>;
6
- export declare type TimestampedConfiguration = Configuration & {
6
+ export type TimestampedConfiguration = Configuration & {
7
7
  timestampMs: number;
8
8
  };
9
9
  export declare function parseConfigFile(configPath: string): Promise<TimestampedConfiguration>;
@@ -52,9 +52,10 @@ const ajv = new ajv_1.default();
52
52
  ajv.addSchema(match_pattern_config_json_1.default);
53
53
  function loadFromFile(ruleName) {
54
54
  return () => __awaiter(this, void 0, void 0, function* () {
55
+ var _a;
55
56
  let ruleSpec;
56
57
  try {
57
- ruleSpec = yield Promise.resolve().then(() => __importStar(require(`../rules/${ruleName}`)));
58
+ ruleSpec = yield (_a = `../rules/${ruleName}`, Promise.resolve().then(() => __importStar(require(_a))));
58
59
  }
59
60
  catch (e) {
60
61
  return;
@@ -64,30 +65,31 @@ function loadFromFile(ruleName) {
64
65
  }
65
66
  function loadFromDir(ruleName) {
66
67
  return () => __awaiter(this, void 0, void 0, function* () {
68
+ var _a, _b, _c;
67
69
  let metadata;
68
70
  let rule;
69
71
  let options;
70
72
  try {
71
- metadata = (yield Promise.resolve().then(() => __importStar(require(`../rules/${ruleName}/metadata`)))).default;
73
+ metadata = (yield (_a = `../rules/${ruleName}/metadata`, Promise.resolve().then(() => __importStar(require(_a))))).default;
72
74
  }
73
75
  catch (e) {
74
76
  return;
75
77
  }
76
78
  try {
77
- rule = (yield Promise.resolve().then(() => __importStar(require(`../rules/${ruleName}/rule`)))).default;
79
+ rule = (yield (_b = `../rules/${ruleName}/rule`, Promise.resolve().then(() => __importStar(require(_b))))).default;
78
80
  }
79
- catch (_a) {
81
+ catch (_d) {
80
82
  console.warn(`Rule ${ruleName} has no rule.js or rule.ts file, or the file doesn't have a default export`);
81
83
  return;
82
84
  }
83
85
  if ((0, util_1.verbose)())
84
86
  console.log(`Loaded rule ${ruleName}: ${rule}`);
85
87
  try {
86
- options = (yield Promise.resolve().then(() => __importStar(require(`../rules/${ruleName}/options`)))).default;
88
+ options = (yield (_c = `../rules/${ruleName}/options`, Promise.resolve().then(() => __importStar(require(_c))))).default;
87
89
  if ((0, util_1.verbose)())
88
90
  console.log(`Loaded rule ${ruleName} options: ${options}`);
89
91
  }
90
- catch (_b) {
92
+ catch (_e) {
91
93
  // This is OK
92
94
  }
93
95
  const description = (0, parseRuleDescription_1.default)(ruleName);
@@ -1,5 +1,5 @@
1
1
  import MatchPatternConfig from './matchPatternConfig';
2
- declare type PropertyName = 'id' | 'type' | 'fqid' | 'query' | 'route';
2
+ type PropertyName = 'id' | 'type' | 'fqid' | 'query' | 'route';
3
3
  export default interface MatchEventConfig {
4
4
  property: PropertyName;
5
5
  test: MatchPatternConfig;
@@ -1,5 +1,5 @@
1
1
  import { SqliteParser } from '@appland/models/types/sqlite-parser';
2
- declare type Callbacks = {
2
+ type Callbacks = {
3
3
  [Node in SqliteParser.Node as `${Node['type']}.${Node['variant']}`]?: (node: Node) => void;
4
4
  };
5
5
  export declare function visit(node: SqliteParser.Node, callbacks: Callbacks): void;
package/built/index.d.ts CHANGED
@@ -19,11 +19,11 @@ import Configuration from './configuration/types/configuration';
19
19
  * of what the code is trying to do. But, anticipating that this may sometimes happen, "root" scope is a good choice for a rule that may flag code
20
20
  * anywhere in the AppMap.
21
21
  */
22
- export declare type ScopeName = 'root' | 'command' | 'http_client_request' | 'http_server_request' | 'transaction';
22
+ export type ScopeName = 'root' | 'command' | 'http_client_request' | 'http_server_request' | 'transaction';
23
23
  /**
24
24
  * Indicates the aspect of software quality that is most relevant to a rule.
25
25
  */
26
- export declare type ImpactDomain = 'Security' | 'Performance' | 'Maintainability' | 'Stability';
26
+ export type ImpactDomain = 'Security' | 'Performance' | 'Maintainability' | 'Stability';
27
27
  /**
28
28
  * Finding is the full data structure that is created when a Rule matches an Event.
29
29
  *
@@ -1,3 +1,3 @@
1
- declare type CommitStatusState = 'pending' | 'success' | 'error' | 'failure';
1
+ type CommitStatusState = 'pending' | 'success' | 'error' | 'failure';
2
2
  export default function postCommitStatus(state: CommitStatusState, description: string): Promise<unknown>;
3
3
  export {};
@@ -1,4 +1,5 @@
1
1
  import { Metadata } from '@appland/models';
2
+ import { type TelemetryData } from '@appland/telemetry';
2
3
  import Check from '../check';
3
4
  import Configuration from '../configuration/types/configuration';
4
5
  import { Finding } from '../index';
@@ -17,3 +18,18 @@ export declare class ScanResults {
17
18
  withFindings(findings: Finding[]): ScanResults;
18
19
  aggregate(sourceScanResults: ScanResults): void;
19
20
  }
21
+ export type ScanTelemetry = {
22
+ ruleIds: string[];
23
+ numAppMaps: number;
24
+ numFindings: number;
25
+ findingCountsByRule: Record<string, number>;
26
+ findingCountsByImpactDomain: Record<string, number>;
27
+ elapsedMs: number;
28
+ appmapDir?: string;
29
+ };
30
+ /**
31
+ * Build the `scan:completed` telemetry payload. Pure and synchronous: git state and
32
+ * contributor count are resolved by the caller and passed in.
33
+ */
34
+ export declare function scanCompletedEvent(telemetry: ScanTelemetry, gitState: string, contributors: number): TelemetryData;
35
+ export declare function sendScanResultsTelemetry(telemetry: ScanTelemetry): Promise<void>;
@@ -1,6 +1,49 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ScanResults = void 0;
12
+ exports.sendScanResultsTelemetry = exports.scanCompletedEvent = exports.ScanResults = void 0;
13
+ const telemetry_1 = require("@appland/telemetry");
14
+ /** Tally values in `source` into `target`, summing counts for repeated keys. */
15
+ function mergeCounts(target, source) {
16
+ var _a;
17
+ for (const [key, count] of Object.entries(source)) {
18
+ target[key] = ((_a = target[key]) !== null && _a !== void 0 ? _a : 0) + count;
19
+ }
20
+ }
21
+ /** Count findings by the value returned from `key`, skipping undefined keys. */
22
+ function countFindings(findings, key) {
23
+ var _a;
24
+ const counts = {};
25
+ for (const finding of findings) {
26
+ const value = key(finding);
27
+ if (value === undefined)
28
+ continue;
29
+ counts[value] = ((_a = counts[value]) !== null && _a !== void 0 ? _a : 0) + 1;
30
+ }
31
+ return counts;
32
+ }
33
+ /**
34
+ * Serialize a counts map to JSON with keys sorted, so the output is deterministic
35
+ * regardless of the order findings were discovered (stable to compare downstream).
36
+ *
37
+ * This relies on JSON.stringify emitting keys in own-property order, which the spec
38
+ * fixes as integer-index keys first, then other string keys in insertion order. Rule
39
+ * IDs and impact domains are never integer-like, so inserting them sorted (via
40
+ * fromEntries) yields sorted JSON. If keys could be integer-like strings, they'd be
41
+ * hoisted ahead of insertion order and this wouldn't hold.
42
+ */
43
+ function sortedCountsJson(counts) {
44
+ const sorted = Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
45
+ return JSON.stringify(sorted);
46
+ }
4
47
  class DistinctItems {
5
48
  constructor() {
6
49
  this.members = {};
@@ -66,6 +109,8 @@ class ScanResults {
66
109
  rules: [...new Set(checks.map((check) => check.rule.id))].sort(),
67
110
  ruleLabels: [...new Set(checks.map((check) => check.rule.labels || []).flat())].sort(),
68
111
  numFindings: findings.length,
112
+ findingCountsByRule: countFindings(findings, (finding) => finding.ruleId),
113
+ findingCountsByImpactDomain: countFindings(findings, (finding) => finding.impactDomain),
69
114
  appMapMetadata: collectMetadata(Object.values(appMapMetadata)),
70
115
  };
71
116
  }
@@ -80,8 +125,47 @@ class ScanResults {
80
125
  ...new Set(this.summary.ruleLabels.concat(sourceScanResults.summary.ruleLabels)),
81
126
  ];
82
127
  this.summary.numFindings += sourceScanResults.summary.numFindings;
128
+ mergeCounts(this.summary.findingCountsByRule, sourceScanResults.summary.findingCountsByRule);
129
+ mergeCounts(this.summary.findingCountsByImpactDomain, sourceScanResults.summary.findingCountsByImpactDomain);
83
130
  // we don't need sourceScanResults.summary.appMetadata
84
131
  // appMapMetadata.Git may also contain secrets we don't want to transmit.
85
132
  }
86
133
  }
87
134
  exports.ScanResults = ScanResults;
135
+ /**
136
+ * Build the `scan:completed` telemetry payload. Pure and synchronous: git state and
137
+ * contributor count are resolved by the caller and passed in.
138
+ */
139
+ function scanCompletedEvent(telemetry, gitState, contributors) {
140
+ return {
141
+ name: 'scan:completed',
142
+ properties: {
143
+ rules: [...telemetry.ruleIds].sort().join(', '),
144
+ git_state: gitState,
145
+ findingsByRule: sortedCountsJson(telemetry.findingCountsByRule),
146
+ findingsByImpactDomain: sortedCountsJson(telemetry.findingCountsByImpactDomain),
147
+ },
148
+ metrics: {
149
+ duration: telemetry.elapsedMs / 1000,
150
+ numRules: telemetry.ruleIds.length,
151
+ numAppMaps: telemetry.numAppMaps,
152
+ numFindings: telemetry.numFindings,
153
+ contributors,
154
+ },
155
+ };
156
+ }
157
+ exports.scanCompletedEvent = scanCompletedEvent;
158
+ function sendScanResultsTelemetry(telemetry) {
159
+ return __awaiter(this, void 0, void 0, function* () {
160
+ // Never let telemetry (git lookups, sending) reject and break the caller's shutdown.
161
+ try {
162
+ const gitState = telemetry_1.GitState[yield telemetry_1.Git.state(telemetry.appmapDir)];
163
+ const contributors = (yield telemetry_1.Git.contributors(60, telemetry.appmapDir)).length;
164
+ telemetry_1.Telemetry.sendEvent(scanCompletedEvent(telemetry, gitState, contributors));
165
+ }
166
+ catch (_a) {
167
+ // ignore
168
+ }
169
+ });
170
+ }
171
+ exports.sendScanResultsTelemetry = sendScanResultsTelemetry;
@@ -16,5 +16,7 @@ export interface ScanSummary {
16
16
  ruleLabels: string[];
17
17
  numChecks: number;
18
18
  numFindings: number;
19
+ findingCountsByRule: Record<string, number>;
20
+ findingCountsByImpactDomain: Record<string, number>;
19
21
  appMapMetadata: AppMapMetadata;
20
22
  }
@@ -4,7 +4,7 @@ import { Event, ValueBase } from '@appland/models';
4
4
  * its originating event and a list of any other such values that might have
5
5
  * been used in its generation.
6
6
  */
7
- export declare type TrackedValue = {
7
+ export type TrackedValue = {
8
8
  value: ValueBase;
9
9
  origin: Event;
10
10
  parents: ReadonlyArray<TrackedValue>;
@@ -1,5 +1,5 @@
1
1
  import { ImpactDomain, ScopeName } from '../../index';
2
- export declare type Metadata = {
2
+ export type Metadata = {
3
3
  title: string;
4
4
  impactDomain: ImpactDomain;
5
5
  references: Record<string, string>;
@@ -7,7 +7,7 @@ declare function responseContentType(event: Event): string | undefined;
7
7
  declare function appMapDir(appMapFileName: string): string;
8
8
  declare function isFalsey(valueObj?: ReturnValueObject): boolean;
9
9
  declare function parseValue(valueObj: ReturnValueObject): string[];
10
- declare const isTruthy: (valueObj?: ReturnValueObject | undefined) => boolean;
10
+ declare const isTruthy: (valueObj?: ReturnValueObject) => boolean;
11
11
  declare function providesAuthentication(event: Event, label: string): boolean;
12
12
  declare function ideLink(filePath: string, ide: string, eventId: number): string;
13
13
  declare const toRegExp: (value: string | RegExp) => RegExp;
package/built/scan.js CHANGED
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  /* eslint-disable @typescript-eslint/no-empty-function */
16
16
  /* eslint-disable @typescript-eslint/naming-convention */
17
17
  /* eslint-disable @typescript-eslint/no-unused-vars */
18
- const lru_cache_1 = __importDefault(require("lru-cache"));
18
+ const lru_cache_1 = require("lru-cache");
19
19
  const assert_1 = __importDefault(require("assert"));
20
20
  const promises_1 = require("fs/promises");
21
21
  const console_1 = require("console");
@@ -24,8 +24,8 @@ const configurationProvider_1 = require("./configuration/configurationProvider")
24
24
  const util_1 = require("./rules/lib/util");
25
25
  const ruleChecker_1 = __importDefault(require("./ruleChecker"));
26
26
  const appMapIndex_1 = __importDefault(require("./appMapIndex"));
27
- const ConfigurationByFileName = new lru_cache_1.default({ max: 10 });
28
- const ChecksByFileName = new lru_cache_1.default({ max: 10 });
27
+ const ConfigurationByFileName = new lru_cache_1.LRUCache({ max: 10 });
28
+ const ChecksByFileName = new lru_cache_1.LRUCache({ max: 10 });
29
29
  class StatsProgressReporter {
30
30
  constructor() {
31
31
  this.parseTime = new Array();
@@ -1,20 +1,24 @@
1
1
  /// <reference types="node" />
2
- import { EventEmitter } from 'events';
3
- export declare type PendingEvent<E> = {
4
- emitter: EventEmitter;
5
- event: string;
6
- arg: E;
7
- };
2
+ import type { EventEmitter } from 'events';
8
3
  export declare const MaxMSBetween: number;
9
- export declare type CancelFn = () => void;
4
+ /**
5
+ * Batches the payloads emitted on a single emitter/event, invoking `callback` with the
6
+ * accumulated batch once `maxMsBetween` ms pass without a new event (or at process exit).
7
+ *
8
+ * The emitter is bound at construction and there is exactly one of everything (one event
9
+ * listener, one beforeExit listener), so `cancel()` can always tear down completely —
10
+ * there is no way to half-detach it.
11
+ */
10
12
  export default class EventAggregator<E> {
11
- private callback;
12
- private maxMsBetween;
13
+ private readonly emitter;
14
+ private readonly event;
15
+ private readonly callback;
16
+ private readonly maxMsBetween;
13
17
  private pending;
14
18
  private timeout?;
15
- constructor(callback: (events: PendingEvent<E>[]) => void, maxMsBetween?: number);
16
- private push;
17
- private refresh;
18
- private emitPending;
19
- attach(emitter: EventEmitter, event: string): CancelFn;
19
+ private readonly onEvent;
20
+ private readonly onBeforeExit;
21
+ private flush;
22
+ constructor(emitter: EventEmitter, event: string, callback: (events: E[]) => void | Promise<void>, maxMsBetween?: number);
23
+ cancel(): Promise<void>;
20
24
  }
@@ -1,40 +1,62 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.MaxMSBetween = void 0;
4
13
  exports.MaxMSBetween = 10 * 1000;
5
- // TODO: Unify with the code in packages/cli - find a way to make a common import.
14
+ /**
15
+ * Batches the payloads emitted on a single emitter/event, invoking `callback` with the
16
+ * accumulated batch once `maxMsBetween` ms pass without a new event (or at process exit).
17
+ *
18
+ * The emitter is bound at construction and there is exactly one of everything (one event
19
+ * listener, one beforeExit listener), so `cancel()` can always tear down completely —
20
+ * there is no way to half-detach it.
21
+ */
6
22
  class EventAggregator {
7
- constructor(callback, maxMsBetween = exports.MaxMSBetween) {
23
+ // Emit the pending batch now and await the callback. No-op when nothing is pending.
24
+ flush() {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ if (!this.timeout)
27
+ return;
28
+ clearTimeout(this.timeout);
29
+ this.timeout = undefined;
30
+ const batch = this.pending;
31
+ this.pending = [];
32
+ yield this.callback(batch);
33
+ });
34
+ }
35
+ constructor(emitter, event, callback, maxMsBetween = exports.MaxMSBetween) {
36
+ this.emitter = emitter;
37
+ this.event = event;
8
38
  this.callback = callback;
9
39
  this.maxMsBetween = maxMsBetween;
10
40
  this.pending = [];
11
- process.on('beforeExit', () => {
12
- if (this.timeout) {
41
+ this.onEvent = (event) => {
42
+ this.pending.push(event);
43
+ if (this.timeout)
13
44
  clearTimeout(this.timeout);
14
- this.emitPending();
15
- }
16
- });
17
- }
18
- push(emitter, event, arg) {
19
- this.pending.push({ emitter, event, arg });
20
- this.refresh();
21
- }
22
- refresh() {
23
- if (this.timeout)
24
- clearTimeout(this.timeout);
25
- this.timeout = setTimeout(this.emitPending.bind(this), this.maxMsBetween).unref();
26
- }
27
- emitPending() {
28
- this.callback(this.pending);
29
- this.timeout = undefined;
30
- this.pending = [];
31
- }
32
- attach(emitter, event) {
33
- const listenerFn = (...args) => {
34
- this.push(emitter, event, args[0]);
45
+ // The periodic/exit flushes are fire-and-forget; only cancel() awaits.
46
+ this.timeout = setTimeout(() => void this.flush(), this.maxMsBetween).unref();
35
47
  };
36
- emitter.addListener(event, listenerFn);
37
- return () => emitter.removeListener(event, listenerFn);
48
+ this.onBeforeExit = () => void this.flush();
49
+ emitter.addListener(event, this.onEvent);
50
+ process.on('beforeExit', this.onBeforeExit);
51
+ }
52
+ // Detach the listeners and emit any final pending batch. Idempotent; awaiting it
53
+ // ensures the final batch is delivered before the caller proceeds (e.g. shutdown).
54
+ cancel() {
55
+ return __awaiter(this, void 0, void 0, function* () {
56
+ this.emitter.removeListener(this.event, this.onEvent);
57
+ process.removeListener('beforeExit', this.onBeforeExit);
58
+ yield this.flush();
59
+ });
38
60
  }
39
61
  }
40
62
  exports.default = EventAggregator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appland/scanner",
3
- "version": "1.89.1",
3
+ "version": "1.90.0",
4
4
  "description": "Analyze AppMaps for code flaws",
5
5
  "bin": "built/cli.js",
6
6
  "main": "built/index.js",
@@ -18,6 +18,7 @@
18
18
  "schema-up-to-date": "git diff --exit-code src/configuration/schema/options.json",
19
19
  "doc-up-to-date": "git diff --exit-code doc/",
20
20
  "lint": "eslint src --ext .ts",
21
+ "typecheck": "tsc --noEmit",
21
22
  "ci": "yarn lint && yarn build && yarn schema-up-to-date && yarn doc-up-to-date && yarn test",
22
23
  "test": "yarn jest",
23
24
  "jest": "jest --filter=./test/testFilter.js --detectOpenHandles",
@@ -35,13 +36,13 @@
35
36
  "@types/glob": "^7.2.0",
36
37
  "@types/jest": "^29.4.1",
37
38
  "@types/js-yaml": "^4.0.3",
38
- "@types/lru-cache": "^5.1.1",
39
39
  "@types/node": "^16.7.10",
40
40
  "@types/sinon": "^10.0.11",
41
41
  "@types/tar-stream": "^2.2.2",
42
42
  "@types/yargs": "^17.0.2",
43
43
  "@typescript-eslint/eslint-plugin": "^4.30.0",
44
44
  "@typescript-eslint/parser": "^4.30.0",
45
+ "@yao-pkg/pkg": "^6.20.0",
45
46
  "eslint": "^7.32.0",
46
47
  "eslint-config-prettier": "^8.3.0",
47
48
  "eslint-plugin-node": "^11.1.0",
@@ -50,7 +51,6 @@
50
51
  "jest": "^29.5.0",
51
52
  "nock": "^13.2.2",
52
53
  "openapi-types": "^9.3.0",
53
- "pkg": "5.8.1-patched",
54
54
  "prettier": "^2.7.1",
55
55
  "semantic-release": "^19.0.2",
56
56
  "sinon": "^13.0.1",
@@ -78,8 +78,8 @@
78
78
  "glob": "7.2.3",
79
79
  "inquirer": "^8.1.2",
80
80
  "js-yaml": "^4.1.0",
81
- "lru-cache": "^6.0.0",
82
- "minimatch": "^5.1.2",
81
+ "lru-cache": "^10",
82
+ "minimatch": "^5.1.9",
83
83
  "octokit": "^2.0.19",
84
84
  "openapi-diff": "^0.23.6",
85
85
  "ora": "~5",
@@ -97,10 +97,11 @@
97
97
  },
98
98
  "pkg": {
99
99
  "targets": [
100
- "node18-linux-x64",
101
- "node18-win-x64",
102
- "node18-macos-x64",
103
- "node18-macos-arm64"
100
+ "node24-linux-x64",
101
+ "node24-linux-arm64",
102
+ "node24-win-x64",
103
+ "node24-macos-x64",
104
+ "node24-macos-arm64"
104
105
  ],
105
106
  "scripts": [
106
107
  "built/scanner/*.js",