@eventra_dev/eventra-cli 2.1.0 → 2.2.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.
@@ -0,0 +1,77 @@
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.cleanSkipReason = cleanSkipReason;
7
+ exports.errorMessage = errorMessage;
8
+ exports.skippedFrom = skippedFrom;
9
+ exports.formatSkipped = formatSkipped;
10
+ exports.reportSkipped = reportSkipped;
11
+ exports.formatIgnored = formatIgnored;
12
+ exports.reportIgnored = reportIgnored;
13
+ exports.plural = plural;
14
+ const chalk_1 = __importDefault(require("chalk"));
15
+ const path_1 = __importDefault(require("path"));
16
+ /**
17
+ * A skipped file's error is wrapped once by the CLI, once by the plugin
18
+ * adapter and once by the plugin itself, each repeating the path. Keep only the
19
+ * innermost message.
20
+ */
21
+ function cleanSkipReason(file, message) {
22
+ let text = message.split(file).join("").trim();
23
+ text = text.replace(/^Plugin "[^"]*" failed to transform\s*:\s*/, "");
24
+ text = text.replace(/^Failed to parse [A-Za-z][A-Za-z ]*?\s*:\s*/, "");
25
+ return text.replace(/\s+/g, " ").trim() || "unknown error";
26
+ }
27
+ function errorMessage(err) {
28
+ return err instanceof Error ? err.message : String(err);
29
+ }
30
+ function skippedFrom(file, err) {
31
+ return { file, reason: cleanSkipReason(file, errorMessage(err)) };
32
+ }
33
+ function formatSkipped(skipped, cwd) {
34
+ if (skipped.length === 0)
35
+ return [];
36
+ const lines = [
37
+ `Skipped ${skipped.length} file${skipped.length === 1 ? "" : "s"} that could not be analysed:`,
38
+ ];
39
+ for (const { file, reason } of skipped) {
40
+ lines.push(` ${path_1.default.relative(cwd, file).replace(/\\/g, "/")}: ${reason}`);
41
+ }
42
+ return lines;
43
+ }
44
+ /** Prints one summary block (stderr) for everything skipped in a scan. */
45
+ function reportSkipped(skipped, cwd = process.cwd()) {
46
+ const lines = formatSkipped(skipped, cwd);
47
+ if (lines.length === 0)
48
+ return;
49
+ console.error(chalk_1.default.yellow(lines.join("\n")));
50
+ console.error(chalk_1.default.gray("Events from these files are not in this scan; previously known events were kept. Use --strict to fail on skipped files."));
51
+ }
52
+ /**
53
+ * Files that were deliberately not analysed (for example generated bundles
54
+ * over the size limit). Unlike skipped files they are not failures: their
55
+ * absence neither keeps known events nor makes a catalog snapshot incomplete.
56
+ */
57
+ function formatIgnored(ignored, cwd) {
58
+ if (ignored.length === 0)
59
+ return [];
60
+ const lines = [
61
+ `Ignored ${ignored.length} file${ignored.length === 1 ? "" : "s"} (not analysed):`,
62
+ ];
63
+ for (const { file, reason } of ignored) {
64
+ lines.push(` ${path_1.default.relative(cwd, file).replace(/\\/g, "/")}: ${reason}`);
65
+ }
66
+ return lines;
67
+ }
68
+ /** Prints one note (stderr) for everything ignored in a scan. */
69
+ function reportIgnored(ignored, cwd = process.cwd()) {
70
+ const lines = formatIgnored(ignored, cwd);
71
+ if (lines.length === 0)
72
+ return;
73
+ console.error(chalk_1.default.gray(lines.join("\n")));
74
+ }
75
+ function plural(count, singular, pluralForm = `${singular}s`) {
76
+ return `${count} ${count === 1 ? singular : pluralForm}`;
77
+ }
package/dist/index.js CHANGED
@@ -46,22 +46,25 @@ program
46
46
  .version(package_json_1.default.version);
47
47
  program
48
48
  .command("init")
49
- .action(async () => {
49
+ .option("-y, --yes", "do not prompt: accept defaults and keep an existing eventra.json")
50
+ .action(async (opts) => {
50
51
  const { init } = await Promise.resolve().then(() => __importStar(require("./cli/init")));
51
- await init();
52
+ await init({ yes: opts.yes });
52
53
  });
53
54
  program
54
55
  .command("sync")
55
- .action(async () => {
56
+ .option("--strict", "exit with a non-zero code if any file could not be analysed")
57
+ .action(async (opts) => {
56
58
  const { sync } = await Promise.resolve().then(() => __importStar(require("./cli/sync")));
57
- await sync();
59
+ await sync({ strict: opts.strict });
58
60
  });
59
61
  program
60
62
  .command("check")
61
63
  .option("--fix", "auto fix issues")
64
+ .option("--strict", "exit with a non-zero code if any file could not be analysed")
62
65
  .action(async (opts) => {
63
66
  const { check } = await Promise.resolve().then(() => __importStar(require("./cli/check")));
64
- await check({ fix: opts.fix });
67
+ await check({ fix: opts.fix, strict: opts.strict });
65
68
  });
66
69
  program
67
70
  .command("watch")
@@ -5,16 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerConsoleDynamicEventReporterPlugin = registerConsoleDynamicEventReporterPlugin;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
+ const path_1 = __importDefault(require("path"));
9
+ // paths are printed relative to where the command runs, with forward slashes
10
+ function displayPath(fileName) {
11
+ const relative = path_1.default.relative(process.cwd(), fileName);
12
+ const outside = relative === ".." || relative.startsWith(`..${path_1.default.sep}`) || relative.startsWith("../");
13
+ const shown = relative && !outside && !path_1.default.isAbsolute(relative) ? relative : fileName;
14
+ return shown.replace(/\\/g, "/");
15
+ }
8
16
  const consoleDynamicEventReporter = {
9
17
  name: "console",
10
- report({ occurrences }) {
18
+ report({ occurrences: all }) {
19
+ // the copy of a component class inside an Angular template module repeats a call the class file already reports
20
+ const occurrences = all.filter((occ) => !occ.inGeneratedCode);
11
21
  if (occurrences.length === 0) {
12
22
  return;
13
23
  }
14
24
  console.log(chalk_1.default.blue("\nDynamic event names:"));
15
25
  for (const occ of occurrences) {
16
26
  const values = occ.resolvedValues.length > 0 ? ` (resolved: ${occ.resolvedValues.join(", ")})` : "";
17
- console.log(chalk_1.default.yellow(`~ ${occ.fileName}:${occ.line} ${occ.calleeText}(...)${values}`));
27
+ const file = displayPath(occ.fileName);
28
+ if (occ.inTemplate) {
29
+ console.log(chalk_1.default.yellow(`~ template of ${file}: event="${occ.eventExpression ?? ""}"${values}`));
30
+ }
31
+ else {
32
+ console.log(chalk_1.default.yellow(`~ ${file}:${occ.line} ${occ.calleeText}(...)${values}`));
33
+ }
18
34
  }
19
35
  },
20
36
  };
@@ -32,10 +32,15 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.isTrustedPluginSpecifier = isTrustedPluginSpecifier;
37
40
  exports.createBuiltinPluginRegistry = createBuiltinPluginRegistry;
38
41
  exports.createPluginRegistry = createPluginRegistry;
42
+ const module_1 = require("module");
43
+ const path_1 = __importDefault(require("path"));
39
44
  const external_1 = require("./adapters/external");
40
45
  const configuredSinks_1 = require("./builtins/configuredSinks");
41
46
  const eventra_sdk_1 = require("./builtins/eventra-sdk");
@@ -69,6 +74,19 @@ function pluginLoadHint(spec) {
69
74
  }
70
75
  return "";
71
76
  }
77
+ /**
78
+ * Plugins are installed in the user's project, not next to the CLI (a global
79
+ * install, npx or a pnpm layout would not find them from here), so resolve from
80
+ * the project first and fall back to normal resolution relative to the CLI.
81
+ */
82
+ function resolveFromProject(spec) {
83
+ try {
84
+ return (0, module_1.createRequire)(path_1.default.join(process.cwd(), "package.json")).resolve(spec);
85
+ }
86
+ catch {
87
+ return spec;
88
+ }
89
+ }
72
90
  async function loadExternalPlugin(registry, spec) {
73
91
  const trimmed = spec.trim();
74
92
  // Defensive-only: the sole caller (createPluginRegistry) already does
@@ -80,7 +98,7 @@ async function loadExternalPlugin(registry, spec) {
80
98
  return;
81
99
  let mod;
82
100
  try {
83
- mod = await Promise.resolve(`${trimmed}`).then(s => __importStar(require(s)));
101
+ mod = await Promise.resolve(`${resolveFromProject(trimmed)}`).then(s => __importStar(require(s)));
84
102
  }
85
103
  catch (err) {
86
104
  // Node's dynamic import() only ever rejects with an Error (module
@@ -114,7 +132,7 @@ async function createPluginRegistry(config) {
114
132
  if (!trimmed)
115
133
  continue;
116
134
  if (!isTrustedPluginSpecifier(trimmed)) {
117
- console.warn(`Eventra: skipping plugin "${trimmed}" — only official @eventra_dev/cli-plugin-* packages can be loaded.${pluginLoadHint(trimmed)}`);
135
+ console.warn(`Eventra: skipping plugin "${trimmed}" - only official @eventra_dev/cli-plugin-* packages can be loaded.${pluginLoadHint(trimmed)}`);
118
136
  continue;
119
137
  }
120
138
  await loadExternalPlugin(registry, trimmed);
@@ -5,6 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PluginRegistry = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
+ // the synthetic callee each framework plugin uses for a template `event=...`
9
+ const TEMPLATE_CALLEE = /^__eventra_[a-z]+_template_event__$/;
10
+ // `__eventra_x_template_event__(expr)` (callText may be cut with an ellipsis)
11
+ function templateExpression(occurrence) {
12
+ const open = occurrence.callText.indexOf("(");
13
+ let expression = open === -1 ? occurrence.callText : occurrence.callText.slice(open + 1);
14
+ expression = expression.endsWith(")") ? expression.slice(0, -1) : expression;
15
+ // Angular qualifies a bare name with `this.`; the template never writes it
16
+ return occurrence.calleeText.includes("_angular_") ? expression.replace(/^this\./, "") : expression;
17
+ }
8
18
  class PluginRegistry {
9
19
  preprocessors = [];
10
20
  sinkDetectors = [];
@@ -101,9 +111,48 @@ class PluginRegistry {
101
111
  return null;
102
112
  }
103
113
  async runDynamicEventReporters(occurrences) {
114
+ const attributed = this.attributeToSources(occurrences);
104
115
  for (const reporter of this.dynamicEventReporters) {
105
- await reporter.report({ occurrences });
116
+ await reporter.report({ occurrences: attributed });
117
+ }
118
+ }
119
+ /**
120
+ * Occurrences are found in virtual modules (`App.vue.ts`). Point them back
121
+ * at the file the user wrote, and flag the ones that come from generated
122
+ * code so reporters do not print a line number that exists nowhere.
123
+ */
124
+ attributeToSources(occurrences) {
125
+ if (this.sourceToVirtuals.size === 0) {
126
+ return occurrences;
106
127
  }
128
+ const virtualToSource = new Map();
129
+ for (const [source, virtuals] of this.sourceToVirtuals) {
130
+ for (const virtual of virtuals) {
131
+ if (virtual !== source) {
132
+ virtualToSource.set(virtual, source);
133
+ }
134
+ }
135
+ }
136
+ return occurrences.map((occurrence) => {
137
+ const source = virtualToSource.get(this.normalizePath(occurrence.fileName));
138
+ if (!source) {
139
+ return occurrence;
140
+ }
141
+ const template = TEMPLATE_CALLEE.test(occurrence.calleeText);
142
+ if (template) {
143
+ return {
144
+ ...occurrence,
145
+ fileName: source,
146
+ inTemplate: true,
147
+ eventExpression: templateExpression(occurrence),
148
+ };
149
+ }
150
+ // an Angular template module embeds the component class: lines there
151
+ // are generated, the real call is also reported from the class file
152
+ return source.toLowerCase().endsWith(".html")
153
+ ? { ...occurrence, fileName: source, inGeneratedCode: true }
154
+ : { ...occurrence, fileName: source };
155
+ });
107
156
  }
108
157
  normalizePath(fileName) {
109
158
  return path_1.default.resolve(fileName).replace(/\\/g, "/");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -37,7 +37,8 @@
37
37
  "test:e2e": "pnpm run clean && pnpm run build && tsx tests/run.ts",
38
38
  "clean": "rm -rf dist",
39
39
  "test:mutation": "stryker run",
40
- "check:presets": "tsx scripts/check-preset-packages.ts"
40
+ "check:presets": "tsx scripts/check-preset-packages.ts",
41
+ "typecheck": "tsc --noEmit -p tsconfig.json"
41
42
  },
42
43
  "dependencies": {
43
44
  "chalk": "^4.1.2",