@eventra_dev/eventra-cli 2.2.0 → 2.3.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/dist/cli/init.js CHANGED
@@ -3,10 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isInstalled = isInstalled;
6
7
  exports.init = init;
7
8
  const chalk_1 = __importDefault(require("chalk"));
8
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
10
  const inquirer_1 = __importDefault(require("inquirer"));
11
+ const module_1 = require("module");
10
12
  const path_1 = __importDefault(require("path"));
11
13
  const config_1 = require("../config/config");
12
14
  const sinkPresets_1 = require("../plugin/builtins/sinkPresets");
@@ -15,8 +17,8 @@ const projectDependencies_1 = require("./projectDependencies");
15
17
  * Looks for analytics libraries in package.json (and workspace members) and, if there are any,
16
18
  * asks which of them Eventra should read events from. Everything detected is pre-selected.
17
19
  */
18
- async function chooseSinks(yes) {
19
- const detected = (0, sinkPresets_1.detectSinkPresets)(await (0, projectDependencies_1.collectDependencyNames)(process.cwd()));
20
+ async function chooseSinks(yes, dependencyNames) {
21
+ const detected = (0, sinkPresets_1.detectSinkPresets)(dependencyNames);
20
22
  if (detected.length === 0)
21
23
  return [];
22
24
  console.log(chalk_1.default.gray(`Found analytics in package.json: ${detected.map((d) => d.packages.join(", ")).join("; ")}`));
@@ -36,12 +38,40 @@ async function chooseSinks(yes) {
36
38
  ]);
37
39
  return presets.map((preset) => ({ preset }));
38
40
  }
41
+ /** Framework dependency -> the plugin that reads its template files (`.vue`, `.svelte`, `.astro`, Angular templates). */
42
+ const FRAMEWORK_PLUGINS = {
43
+ vue: "@eventra_dev/cli-plugin-vue",
44
+ svelte: "@eventra_dev/cli-plugin-svelte",
45
+ astro: "@eventra_dev/cli-plugin-astro",
46
+ "@angular/core": "@eventra_dev/cli-plugin-angular",
47
+ };
48
+ /** True when the plugin package can be loaded from the project, which is where `sync` will look for it. */
49
+ function isInstalled(plugin) {
50
+ try {
51
+ (0, module_1.createRequire)(path_1.default.join(process.cwd(), "package.json")).resolve(plugin);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * The plugins for the frameworks found in package.json (and workspace members), split by whether they are
60
+ * installed. Without a plugin, events fired from component files are never seen. Only an installed one is
61
+ * written to `plugins`: `sync` stops on a plugin it cannot load, and an `init` followed by `sync` must not.
62
+ */
63
+ function detectPlugins(dependencyNames, installed) {
64
+ const found = Object.entries(FRAMEWORK_PLUGINS)
65
+ .filter(([dependency]) => dependencyNames.has(dependency))
66
+ .map(([, plugin]) => plugin);
67
+ return { enabled: found.filter(installed), missing: found.filter((plugin) => !installed(plugin)) };
68
+ }
39
69
  const DEFAULT_ENDPOINT = "https://api.eventra.dev/api/v1/cli/events";
40
70
  /**
41
71
  * `yes` is the non-interactive mode (CI, scripts): defaults for every answer,
42
72
  * and an existing eventra.json is never overwritten.
43
73
  */
44
- async function init({ yes = false } = {}) {
74
+ async function init({ yes = false, isPluginInstalled = isInstalled } = {}) {
45
75
  if (await fs_extra_1.default.pathExists(path_1.default.join(process.cwd(), config_1.CONFIG_NAME))) {
46
76
  if (yes) {
47
77
  console.log(chalk_1.default.yellow(`${config_1.CONFIG_NAME} already exists, keeping it`));
@@ -76,7 +106,9 @@ async function init({ yes = false } = {}) {
76
106
  default: DEFAULT_ENDPOINT,
77
107
  },
78
108
  ]);
79
- const sinks = await chooseSinks(yes);
109
+ const dependencyNames = await (0, projectDependencies_1.collectDependencyNames)(process.cwd());
110
+ const sinks = await chooseSinks(yes, dependencyNames);
111
+ const { enabled: plugins, missing: missingPlugins } = detectPlugins(dependencyNames, isPluginInstalled);
80
112
  console.log("");
81
113
  console.log(chalk_1.default.gray("Eventra detects @eventra_dev/eventra-sdk usage:"));
82
114
  console.log(chalk_1.default.gray('• tracker.track("event.name")'));
@@ -88,21 +120,24 @@ async function init({ yes = false } = {}) {
88
120
  endpoint,
89
121
  events: [],
90
122
  functionWrappers: [],
123
+ ...(plugins.length > 0 ? { plugins } : {}),
91
124
  ...(sinks.length > 0 ? { sinks } : {}),
92
125
  sync: {
93
126
  include: [
94
127
  "**/*.{ts,tsx,js,jsx}",
95
128
  ],
96
- exclude: [
97
- "node_modules",
98
- "dist",
99
- ".next",
100
- ".git",
101
- ],
129
+ exclude: [...config_1.INIT_EXCLUDE],
102
130
  },
103
131
  };
104
132
  await (0, config_1.saveConfig)(config);
105
133
  console.log(chalk_1.default.green("eventra.json created"));
134
+ if (plugins.length > 0) {
135
+ console.log(chalk_1.default.green(`plugins: ${plugins.join(", ")}`));
136
+ }
137
+ if (missingPlugins.length > 0) {
138
+ console.log(chalk_1.default.yellow(`Component files are not read yet: npm install -D ${missingPlugins.join(" ")}`));
139
+ console.log(chalk_1.default.yellow(`then add ${missingPlugins.map((p) => `"${p}"`).join(", ")} to "plugins" in ${config_1.CONFIG_NAME}`));
140
+ }
106
141
  if (sinks.length > 0) {
107
142
  console.log(chalk_1.default.green(`sinks: ${sinks.map((s) => s.preset).join(", ")}`));
108
143
  }
@@ -3,13 +3,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.memberGlob = exports.MAX_WORKSPACE_PACKAGES = void 0;
7
+ exports.stringList = stringList;
8
+ exports.pnpmWorkspacePatterns = pnpmWorkspacePatterns;
9
+ exports.staysInsideProject = staysInsideProject;
6
10
  exports.collectDependencyNames = collectDependencyNames;
7
11
  const fast_glob_1 = __importDefault(require("fast-glob"));
8
12
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
13
  const path_1 = __importDefault(require("path"));
10
14
  const DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
11
15
  /** Upper bound on workspace members read, so a huge monorepo cannot make `init` crawl. */
12
- const MAX_WORKSPACE_PACKAGES = 300;
16
+ exports.MAX_WORKSPACE_PACKAGES = 300;
13
17
  /**
14
18
  * Parsed package.json, or null when it is missing or not JSON. Whatever else the file holds (an
15
19
  * array, a number) is returned as is: every reader below only looks up named properties on it, which
@@ -86,10 +90,11 @@ async function workspacePatterns(rootDir, rootPackage) {
86
90
  * `!apps/skipped/package.json`, which fast-glob treats as an exclusion of exactly those files.
87
91
  */
88
92
  const memberGlob = (pattern) => `${pattern}/package.json`;
93
+ exports.memberGlob = memberGlob;
89
94
  /** Member package.json paths for the patterns; a pattern fast-glob rejects (a NUL byte, say) yields none. */
90
95
  async function findMembers(rootDir, patterns) {
91
96
  try {
92
- return await (0, fast_glob_1.default)(patterns.map(memberGlob), { cwd: rootDir, ignore: ["**/node_modules/**"] });
97
+ return await (0, fast_glob_1.default)(patterns.map(exports.memberGlob), { cwd: rootDir, ignore: ["**/node_modules/**"] });
93
98
  }
94
99
  catch {
95
100
  // Stryker disable next-line ArrayDeclaration: a stray path that is not a package.json is read as "no package.json"
@@ -108,7 +113,7 @@ async function collectDependencyNames(rootDir) {
108
113
  for (const name of dependencyNames(rootPackage))
109
114
  names.add(name);
110
115
  const patterns = await workspacePatterns(rootDir, rootPackage);
111
- const members = (await findMembers(rootDir, patterns)).slice(0, MAX_WORKSPACE_PACKAGES);
116
+ const members = (await findMembers(rootDir, patterns)).slice(0, exports.MAX_WORKSPACE_PACKAGES);
112
117
  for (const member of members) {
113
118
  const memberPackage = await readPackage(path_1.default.join(rootDir, member));
114
119
  if (memberPackage !== null)
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const typescript_1 = __importDefault(require("typescript"));
10
10
  const documentRegistry_1 = require("./documentRegistry");
11
+ const workspacePackages_1 = require("./workspacePackages");
11
12
  class CompilerContext {
12
13
  rootDir;
13
14
  registry = new documentRegistry_1.DocumentRegistry();
@@ -22,9 +23,15 @@ class CompilerContext {
22
23
  // scan and flushes once, so an eager build in the constructor was thrown away.
23
24
  programInstance;
24
25
  rootNames = new Set();
25
- // Only set while withResolutionCache() runs: it memoises fileExists results,
26
- // so it must never outlive one batch of resolutions against one program.
27
- activeResolutionCache;
26
+ // compiler options of the nearest tsconfig.json above a folder (the root one for files outside any other)
27
+ optionsByDir = new Map();
28
+ // the options a project starts from before any tsconfig.json is applied
29
+ defaultOptions;
30
+ workspacePackages;
31
+ // one module resolution cache per set of options; only alive while a program is built or a batch runs
32
+ resolutionCaches = new Map();
33
+ // how many builds or batches are running that share the caches (a rebuild can start inside a batch)
34
+ cachingDepth = 0;
28
35
  constructor(rootDir) {
29
36
  this.rootDir = rootDir;
30
37
  const localTsconfig = path_1.default.join(rootDir, "tsconfig.json");
@@ -45,6 +52,7 @@ class CompilerContext {
45
52
  allowNonTsExtensions: true,
46
53
  incremental: false,
47
54
  };
55
+ this.defaultOptions = compilerOptions;
48
56
  if (configPath) {
49
57
  const config = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
50
58
  if (config.error) {
@@ -55,17 +63,138 @@ class CompilerContext {
55
63
  ...compilerOptions,
56
64
  ...parsed.options,
57
65
  };
66
+ // A solution-style tsconfig (`"files": []` plus `references`, the Vite and Vue template) keeps its
67
+ // `@/*` aliases in a referenced config, so without this every aliased import stays unresolved.
68
+ if (!parsed.options.paths) {
69
+ Object.assign(compilerOptions, this.pathOptionsFromReferences(parsed.projectReferences));
70
+ }
58
71
  for (const file of parsed.fileNames) {
59
72
  this.rootNames.add(this.normalize(file));
60
73
  }
61
74
  }
62
75
  this.compilerOptions = compilerOptions;
76
+ this.workspacePackages = (0, workspacePackages_1.findWorkspacePackages)(rootDir);
63
77
  this.host = typescript_1.default.createCompilerHost(this.compilerOptions, true);
64
78
  this.patchCompilerHost();
65
79
  }
66
80
  normalize(fileName) {
67
81
  return this.registry.normalize(fileName);
68
82
  }
83
+ /**
84
+ * Compiler options for resolving the imports of `fromFile`: those of the nearest tsconfig.json above
85
+ * it. A monorepo keeps one per app (`@/*` means a different folder in each) and often none at the root;
86
+ * a single root-level tsconfig.json answers for every file below it, as it always did.
87
+ */
88
+ optionsFor(fromFile) {
89
+ // an installed package may ship a tsconfig.json of its own: its settings are for building it, not for this project
90
+ if (fromFile.split(/[\\/]/).includes("node_modules"))
91
+ return this.compilerOptions;
92
+ const visited = [];
93
+ let options = this.compilerOptions;
94
+ for (let dir = path_1.default.dirname(fromFile);; dir = path_1.default.dirname(dir)) {
95
+ const cached = this.optionsByDir.get(dir);
96
+ const own = cached ?? this.readDirectoryOptions(dir);
97
+ if (own) {
98
+ options = own;
99
+ visited.push(dir);
100
+ break;
101
+ }
102
+ visited.push(dir);
103
+ // a file outside the project, or the top of the filesystem, keeps the root options
104
+ if (path_1.default.relative(this.rootDir, dir).startsWith("..") || path_1.default.dirname(dir) === dir || dir === this.rootDir)
105
+ break;
106
+ }
107
+ for (const dir of visited)
108
+ this.optionsByDir.set(dir, options);
109
+ return options;
110
+ }
111
+ /** The options a folder's own tsconfig.json sets, `undefined` when it has none (the root's are already loaded). */
112
+ readDirectoryOptions(dir) {
113
+ if (dir === this.rootDir)
114
+ return this.compilerOptions;
115
+ const configPath = path_1.default.join(dir, "tsconfig.json");
116
+ if (!fs_1.default.existsSync(configPath))
117
+ return undefined;
118
+ const config = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
119
+ if (config.error)
120
+ return undefined;
121
+ const parsed = typescript_1.default.parseJsonConfigFileContent(config.config, typescript_1.default.sys, dir);
122
+ return {
123
+ ...this.defaultOptions,
124
+ ...parsed.options,
125
+ ...(parsed.options.paths ? {} : this.aliasesWithout(parsed.options, parsed.projectReferences)),
126
+ };
127
+ }
128
+ /**
129
+ * Aliases for a config that sets no `paths` of its own: those of its project references, or else the root's.
130
+ * A tsconfig.json that only adjusts a few options (a `tests/` one, say) does not mean the root's `@/*` stopped
131
+ * being valid there, and the root's aliases applied to every file before configs were read per folder.
132
+ */
133
+ aliasesWithout(own, references) {
134
+ const fromReferences = this.pathOptionsFromReferences(references);
135
+ if (fromReferences.paths)
136
+ return fromReferences;
137
+ const { paths, baseUrl, pathsBasePath } = this.compilerOptions;
138
+ return { paths, baseUrl: own.baseUrl ?? baseUrl, pathsBasePath };
139
+ }
140
+ cacheFor(options) {
141
+ if (this.cachingDepth === 0)
142
+ return undefined;
143
+ let cache = this.resolutionCaches.get(options);
144
+ if (!cache) {
145
+ cache = typescript_1.default.createModuleResolutionCache(this.rootDir, (fileName) => this.normalize(fileName), options);
146
+ this.resolutionCaches.set(options, cache);
147
+ }
148
+ return cache;
149
+ }
150
+ /** Where a monorepo package's own folder is for an import of it (`@acme/lib`, `@acme/lib/hooks/x`), if it is one. */
151
+ workspaceTargets(moduleName) {
152
+ let best;
153
+ for (const [name, dir] of this.workspacePackages) {
154
+ if ((moduleName === name || moduleName.startsWith(`${name}/`)) && (!best || name.length > best.name.length)) {
155
+ best = { name, dir };
156
+ }
157
+ }
158
+ if (!best)
159
+ return [];
160
+ const sub = moduleName.slice(best.name.length + 1);
161
+ // the package's sources may sit in src/ while its package.json points at a build that is not there
162
+ return [path_1.default.join(best.dir, sub), path_1.default.join(best.dir, "src", sub)];
163
+ }
164
+ resolveImport(moduleName, containingFile, redirectedReference, mode) {
165
+ const options = this.optionsFor(containingFile);
166
+ const cache = this.cacheFor(options);
167
+ const resolved = typescript_1.default.resolveModuleName(moduleName, containingFile, options, this.host, cache, redirectedReference, mode);
168
+ if (resolved.resolvedModule)
169
+ return resolved;
170
+ // not installed, so no node_modules link: a package of this very monorepo is found by its folder
171
+ for (const target of this.workspaceTargets(moduleName)) {
172
+ const viaWorkspace = typescript_1.default.resolveModuleName(target, containingFile, options, this.host, cache, redirectedReference, mode);
173
+ if (viaWorkspace.resolvedModule)
174
+ return viaWorkspace;
175
+ }
176
+ return resolved;
177
+ }
178
+ /**
179
+ * Alias options (`paths`, `baseUrl`) of the first referenced tsconfig that defines `paths`. A
180
+ * reference that is unreadable or has no aliases is skipped; only aliases are taken, never the files.
181
+ */
182
+ pathOptionsFromReferences(references) {
183
+ for (const reference of references ?? []) {
184
+ const referenced = typescript_1.default.sys.directoryExists(reference.path)
185
+ ? path_1.default.join(reference.path, "tsconfig.json")
186
+ : reference.path;
187
+ const config = typescript_1.default.readConfigFile(referenced, typescript_1.default.sys.readFile);
188
+ if (config.error)
189
+ continue;
190
+ const { options } = typescript_1.default.parseJsonConfigFileContent(config.config, typescript_1.default.sys, path_1.default.dirname(referenced));
191
+ if (options.paths) {
192
+ const { paths, baseUrl, pathsBasePath } = options;
193
+ return { paths, baseUrl, pathsBasePath };
194
+ }
195
+ }
196
+ return {};
197
+ }
69
198
  resolveLanguageVersion(languageVersionOrOptions) {
70
199
  if (typeof languageVersionOrOptions === "object") {
71
200
  return languageVersionOrOptions.languageVersion;
@@ -127,6 +256,7 @@ class CompilerContext {
127
256
  const normalized = this.normalize(fileName);
128
257
  return this.registry.has(normalized) || fs_1.default.existsSync(normalized);
129
258
  };
259
+ this.host.resolveModuleNameLiterals = (literals, containingFile, redirectedReference, _options, containingSourceFile) => literals.map((literal) => this.resolveImport(literal.text, containingFile, redirectedReference, typescript_1.default.getModeForUsageLocation(containingSourceFile, literal, this.compilerOptions)));
130
260
  this.host.getCanonicalFileName = (f) => this.normalize(f);
131
261
  this.host.useCaseSensitiveFileNames = () => true;
132
262
  this.host.getCurrentDirectory = () => this.rootDir;
@@ -146,7 +276,29 @@ class CompilerContext {
146
276
  return typescript_1.default.ScriptKind.Unknown;
147
277
  }
148
278
  rebuildProgram() {
149
- this.programInstance = typescript_1.default.createProgram([...this.rootNames], this.compilerOptions, this.host);
279
+ // options are re-read from disk with every build, so an edited nested tsconfig.json is seen
280
+ this.optionsByDir.clear();
281
+ this.withSharedCaches(() => {
282
+ this.programInstance = typescript_1.default.createProgram([...this.rootNames], this.compilerOptions, this.host);
283
+ });
284
+ }
285
+ /**
286
+ * Imports resolved while `fn` runs share caches, as they do in TypeScript's own host. Files may appear or
287
+ * go between runs, so nothing is kept after the outermost one; a run started inside another (a lazy
288
+ * program build inside a batch) joins it instead of dropping its caches.
289
+ */
290
+ withSharedCaches(fn) {
291
+ if (this.cachingDepth === 0)
292
+ this.resolutionCaches.clear();
293
+ this.cachingDepth++;
294
+ try {
295
+ return fn();
296
+ }
297
+ finally {
298
+ this.cachingDepth--;
299
+ if (this.cachingDepth === 0)
300
+ this.resolutionCaches.clear();
301
+ }
150
302
  }
151
303
  get program() {
152
304
  if (!this.programInstance) {
@@ -160,16 +312,7 @@ class CompilerContext {
160
312
  * same directory lookups. The cache is dropped when `fn` returns.
161
313
  */
162
314
  withResolutionCache(fn) {
163
- if (this.activeResolutionCache) {
164
- return fn();
165
- }
166
- this.activeResolutionCache = typescript_1.default.createModuleResolutionCache(this.rootDir, (fileName) => this.normalize(fileName), this.compilerOptions);
167
- try {
168
- return fn();
169
- }
170
- finally {
171
- this.activeResolutionCache = undefined;
172
- }
315
+ return this.withSharedCaches(fn);
173
316
  }
174
317
  /** Register file content without rebuilding the TS program (batch via {@link flushUpdates}). */
175
318
  stageFile(fileName, content) {
@@ -271,8 +414,7 @@ class CompilerContext {
271
414
  ];
272
415
  }
273
416
  resolveModule(moduleName, fromFile) {
274
- const resolved = typescript_1.default.resolveModuleName(moduleName, this.normalize(fromFile), this.compilerOptions, this.host, this.activeResolutionCache);
275
- return resolved.resolvedModule?.resolvedFileName;
417
+ return this.resolveImport(moduleName, this.normalize(fromFile)).resolvedModule?.resolvedFileName;
276
418
  }
277
419
  getResolvedModules(fileName) {
278
420
  const source = this.getSourceFile(fileName);
@@ -0,0 +1,58 @@
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.findWorkspacePackages = findWorkspacePackages;
7
+ const fast_glob_1 = __importDefault(require("fast-glob"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const projectDependencies_1 = require("../cli/projectDependencies");
11
+ function readJson(file) {
12
+ try {
13
+ return JSON.parse(fs_1.default.readFileSync(file, "utf8"));
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /**
20
+ * The packages of a monorepo (`workspaces` in package.json, or pnpm-workspace.yaml) by name, mapped to
21
+ * their folder. `import "@acme/lib/hooks/x"` then resolves to a folder of the repo itself, so a project
22
+ * whose dependencies are not installed still links its own packages together.
23
+ */
24
+ function findWorkspacePackages(rootDir) {
25
+ const packages = new Map();
26
+ const rootPackage = readJson(path_1.default.join(rootDir, "package.json"));
27
+ if (!rootPackage)
28
+ return packages;
29
+ const workspaces = rootPackage.workspaces;
30
+ const fromPackageJson = Array.isArray(workspaces)
31
+ ? (0, projectDependencies_1.stringList)(workspaces)
32
+ : (0, projectDependencies_1.stringList)(workspaces?.packages);
33
+ let fromPnpm = [];
34
+ try {
35
+ fromPnpm = (0, projectDependencies_1.pnpmWorkspacePatterns)(fs_1.default.readFileSync(path_1.default.join(rootDir, "pnpm-workspace.yaml"), "utf8"));
36
+ }
37
+ catch {
38
+ // no pnpm-workspace.yaml (or an unreadable one): the package.json patterns are all there is
39
+ }
40
+ const patterns = [...fromPackageJson, ...fromPnpm].filter(projectDependencies_1.staysInsideProject);
41
+ if (patterns.length === 0)
42
+ return packages;
43
+ let members;
44
+ try {
45
+ members = fast_glob_1.default.sync(patterns.map(projectDependencies_1.memberGlob), { cwd: rootDir, ignore: ["**/node_modules/**"] });
46
+ }
47
+ catch {
48
+ // a pattern fast-glob rejects: no members
49
+ return packages;
50
+ }
51
+ for (const member of members.slice(0, projectDependencies_1.MAX_WORKSPACE_PACKAGES)) {
52
+ const name = readJson(path_1.default.join(rootDir, member))?.name;
53
+ if (typeof name === "string" && name !== "" && !packages.has(name)) {
54
+ packages.set(name, path_1.default.dirname(path_1.default.join(rootDir, member)));
55
+ }
56
+ }
57
+ return packages;
58
+ }
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.LOCAL_CONFIG_NAME = exports.CONFIG_NAME = void 0;
6
+ exports.INIT_EXCLUDE = exports.LOCAL_CONFIG_NAME = exports.CONFIG_NAME = void 0;
7
7
  exports.normalizeConfig = normalizeConfig;
8
8
  exports.loadConfig = loadConfig;
9
9
  exports.describeConfigProblem = describeConfigProblem;
@@ -22,7 +22,22 @@ exports.CONFIG_NAME = "eventra.json";
22
22
  */
23
23
  exports.LOCAL_CONFIG_NAME = "eventra.local.json";
24
24
  const DEFAULT_INCLUDE = ["**/*.{ts,tsx,js,jsx}"];
25
+ // what a config without `sync.exclude` has always skipped; changing it would change the scan of every
26
+ // existing project that never wrote its own list
25
27
  const DEFAULT_EXCLUDE = ["node_modules", "dist", ".next", ".git"];
28
+ /**
29
+ * What `eventra init` writes into a new eventra.json: the default above plus folders and files whose
30
+ * code is not the product. Events fired from a test are not features, and on a large repository the tests
31
+ * alone can double the files scanned. Written out, so it is visible and editable. `build` and `out` are
32
+ * left out on purpose: they are as likely to be a source folder as output.
33
+ */
34
+ exports.INIT_EXCLUDE = [
35
+ ...DEFAULT_EXCLUDE,
36
+ "coverage",
37
+ "**/*.{test,spec}.{ts,tsx,js,jsx}",
38
+ "**/__tests__/**",
39
+ "**/__mocks__/**",
40
+ ];
26
41
  function normalizeSinks(sinks) {
27
42
  if (!Array.isArray(sinks))
28
43
  return [];
@@ -14,6 +14,7 @@ const resolvedCallCache_1 = require("../analysis/cache/resolvedCallCache");
14
14
  const resolvedExportCache_1 = require("../analysis/cache/resolvedExportCache");
15
15
  const returnPropagationCache_1 = require("../analysis/cache/returnPropagationCache");
16
16
  const analyzeFile_1 = require("../analysis/engine/analyzeFile");
17
+ const wrapperChains_1 = require("../analysis/scanner/wrapperChains");
17
18
  const wrapperRegistry_1 = require("../analysis/symbols/wrapperRegistry");
18
19
  const loadPlugins_1 = require("../plugin/loadPlugins");
19
20
  const EMPTY_RESULT = () => ({
@@ -117,6 +118,17 @@ class EventraEngine {
117
118
  for (const sf of sources) {
118
119
  this.indexFile(sf.fileName, config);
119
120
  }
121
+ this.resolveWrapperChains();
122
+ }
123
+ // Wrappers that reach a sink only through another wrapper (`trackEvent` -> `capture` -> the SDK
124
+ // call) need every file indexed first, so this runs after the files are indexed, never per file.
125
+ resolveWrapperChains() {
126
+ const found = (0, wrapperChains_1.resolveWrapperChains)(this.fileIndices.values(), this.compiler.getChecker(), this.wrapperRegistry, this.resolvedCallCache, this.resolvedExportCache);
127
+ for (const [file, wrappers] of found) {
128
+ const index = this.fileIndices.get(file);
129
+ const kept = index.wrappers.filter((w) => !wrappers.some((n) => n.declaration === w.declaration));
130
+ this.fileIndices.set(file, { ...index, wrappers: [...kept, ...wrappers] });
131
+ }
120
132
  }
121
133
  beginPreload() {
122
134
  this.isPreloading = true;
@@ -213,6 +225,7 @@ class EventraEngine {
213
225
  this.scannedFiles.add(file);
214
226
  this.indexFile(file, config);
215
227
  }
228
+ this.resolveWrapperChains();
216
229
  for (const file of normalized) {
217
230
  this.extractFile(file, config);
218
231
  }
@@ -310,6 +323,7 @@ class EventraEngine {
310
323
  this.scannedFiles.add(this.normalize(fileName));
311
324
  this.refreshChecker();
312
325
  this.indexFile(fileName, config);
326
+ this.resolveWrapperChains();
313
327
  return this.extractFile(fileName, config);
314
328
  }
315
329
  async removeFile(fileName, config) {
package/dist/index.js CHANGED
@@ -39,45 +39,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  const commander_1 = require("commander");
41
41
  const package_json_1 = __importDefault(require("../package.json"));
42
- const program = new commander_1.Command();
43
- program
44
- .name("eventra")
45
- .description("Eventra CLI")
46
- .version(package_json_1.default.version);
47
- program
48
- .command("init")
49
- .option("-y, --yes", "do not prompt: accept defaults and keep an existing eventra.json")
50
- .action(async (opts) => {
51
- const { init } = await Promise.resolve().then(() => __importStar(require("./cli/init")));
52
- await init({ yes: opts.yes });
42
+ const respawn_1 = require("./respawn");
43
+ // a large repository can need more memory than Node's default: start again with enough
44
+ (0, respawn_1.respawnWithMoreHeap)().then((respawned) => {
45
+ if (!respawned)
46
+ run();
53
47
  });
54
- program
55
- .command("sync")
56
- .option("--strict", "exit with a non-zero code if any file could not be analysed")
57
- .action(async (opts) => {
58
- const { sync } = await Promise.resolve().then(() => __importStar(require("./cli/sync")));
59
- await sync({ strict: opts.strict });
60
- });
61
- program
62
- .command("check")
63
- .option("--fix", "auto fix issues")
64
- .option("--strict", "exit with a non-zero code if any file could not be analysed")
65
- .action(async (opts) => {
66
- const { check } = await Promise.resolve().then(() => __importStar(require("./cli/check")));
67
- await check({ fix: opts.fix, strict: opts.strict });
68
- });
69
- program
70
- .command("watch")
71
- .description("Watch files and detect events in real-time")
72
- .action(async () => {
73
- const { watch } = await Promise.resolve().then(() => __importStar(require("./cli/watch")));
74
- await watch();
75
- });
76
- program
77
- .command("send")
78
- .option("--trust-endpoint", "approve the current eventra.json endpoint for local use")
79
- .action(async (opts) => {
80
- const { send } = await Promise.resolve().then(() => __importStar(require("./cli/send")));
81
- await send({ trustEndpoint: opts.trustEndpoint });
82
- });
83
- program.parse();
48
+ function run() {
49
+ const program = new commander_1.Command();
50
+ program
51
+ .name("eventra")
52
+ .description("Eventra CLI")
53
+ .version(package_json_1.default.version);
54
+ program
55
+ .command("init")
56
+ .option("-y, --yes", "do not prompt: accept defaults and keep an existing eventra.json")
57
+ .action(async (opts) => {
58
+ const { init } = await Promise.resolve().then(() => __importStar(require("./cli/init")));
59
+ await init({ yes: opts.yes });
60
+ });
61
+ program
62
+ .command("sync")
63
+ .option("--strict", "exit with a non-zero code if any file could not be analysed")
64
+ .action(async (opts) => {
65
+ const { sync } = await Promise.resolve().then(() => __importStar(require("./cli/sync")));
66
+ await sync({ strict: opts.strict });
67
+ });
68
+ program
69
+ .command("check")
70
+ .option("--fix", "auto fix issues")
71
+ .option("--strict", "exit with a non-zero code if any file could not be analysed")
72
+ .action(async (opts) => {
73
+ const { check } = await Promise.resolve().then(() => __importStar(require("./cli/check")));
74
+ await check({ fix: opts.fix, strict: opts.strict });
75
+ });
76
+ program
77
+ .command("watch")
78
+ .description("Watch files and detect events in real-time")
79
+ .action(async () => {
80
+ const { watch } = await Promise.resolve().then(() => __importStar(require("./cli/watch")));
81
+ await watch();
82
+ });
83
+ program
84
+ .command("send")
85
+ .option("--trust-endpoint", "approve the current eventra.json endpoint for local use")
86
+ .action(async (opts) => {
87
+ const { send } = await Promise.resolve().then(() => __importStar(require("./cli/send")));
88
+ await send({ trustEndpoint: opts.trustEndpoint });
89
+ });
90
+ program.parse();
91
+ }