npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eventra_dev/cli-plugin-angular

v1.0.2

Published

Eventra CLI plugin — extract track() calls from Angular component templates

Readme

Eventra CLI Plugin - Angular

Official Eventra CLI plugin - extracts track() calls from Angular component templates (.html), so eventra sync/check/watch understand Angular code the same way they already understand plain TypeScript.


Overview

The CLI core is framework-agnostic and only walks .ts/.tsx/.js/.jsx - it already sees an Angular component's own class body (a .ts file) without any plugin. What it can't see is the template: Angular keeps that in a separate .html file (templateUrl), which isn't TypeScript at all. This plugin teaches the CLI that file: it parses each template with the real Angular compiler (@angular/compiler) - not a regex - and hands the CLI a virtual TypeScript module per template, so every existing detection rule (direct SDK calls, dynamic-name reporting) applies to Angular templates without any Angular-specific case in the core engine.


Installation

npm install -D @eventra_dev/cli-plugin-angular @eventra_dev/eventra-cli
# or
pnpm add -D @eventra_dev/cli-plugin-angular @eventra_dev/eventra-cli

Enable it in eventra.json:

{
  "plugins": ["@eventra_dev/cli-plugin-angular"],
  "sync": {
    "include": ["**/*.{ts,tsx,js,jsx}"],
    "exclude": ["node_modules", "dist", ".angular", ".git"]
  }
}

sync.include does not need **/*.html added manually - the plugin registers it via includeGlobs.


What gets detected

Component class (.ts)

Handled exactly like a regular .ts file - direct SDK calls, function wrappers, variables, ternaries, cross-file propagation all apply, with no plugin needed for this part at all:

import { Eventra } from "@eventra_dev/eventra-sdk";

@Component({ templateUrl: "./checkout.component.html" })
export class CheckoutComponent {
  private readonly tracker = new Eventra({ apiKey: "YOUR_PROJECT_API_KEY" });

  onSubmit(): void {
    this.tracker.track("checkout.started");
  }
}

Template - literal event attributes

<button event="checkout.cta">Pay</button>

Template - dynamic event bindings

<button [attr.event]="computedEventName">Pay</button>

Use [attr.event], not [event], on a plain HTML element. [event]="expr" is a property binding - Angular's real compiler (ng build, with the template type checking that's on by default) checks it against the target's known properties, and a native element like <button> has no event DOM property, so this fails to build with NG8002: Can't bind to 'event' since it isn't a known property of 'button'. [attr.event]="expr" is an attribute binding instead - it always compiles regardless of element or component, since it goes through setAttribute rather than a property-existence check. [event]="expr" still works, and is still detected, on a custom Angular component that actually declares @Input() event - but [attr.event] is safe everywhere and costs nothing on a component either, so it's the form to reach for by default. The plugin parses both forms identically (Angular's own AST reports the same binding name, event, for [event] and [attr.event] alike - only the binding type differs, which the plugin doesn't need to care about), so nothing else below changes based on which one is used.

The expression is copied as-is into a synthetic method appended to the component's own class, so this inside it really is that component instance. A simple identifier or property chain (computedEventName, config.eventName, config?.eventName, map['key']) is automatically qualified with this. - Angular template expressions never write this explicitly (it's a compile error in Angular's own syntax), so a bare name always means "this component's own field/getter". If it resolves to a real class property's literal initializer, the event name is detected normally; otherwise it's reported as a dynamic occurrence (same mechanism as tracker.track(someVariable) in plain TypeScript) instead of being silently dropped.

A more complex expression (a method call, an operator, a pipe, a literal) is not rewritten with this. - it's copied verbatim and, since it won't match anything in scope, correctly falls back to a dynamic occurrence with no resolved value, the same honest fallback as an expression the resolver doesn't otherwise recognize.

event is recognized on any tag - plain elements, components, ng-container, ng-template, *ngIf/*ngFor structural directives, and the newer @if/@else if/@else, @for/@empty, and @switch/@case/@default block syntax - since the plugin walks the whole template tree rather than special-casing specific block kinds.

Finding the component class

The template file is paired with its component by Angular CLI's own default naming convention: foo.component.htmlfoo.component.ts in the same directory (the class decorated with @Component is used; if none carries that exact decorator - e.g. an aliased Component import - the file's first class is used instead). This covers the schematic-generated layout the overwhelming majority of Angular projects use. It does not yet resolve a templateUrl that points somewhere else, and it does not yet support an inline template (template: "..." written directly in the @Component decorator, with no separate .html file) - both are documented gaps, not silent failures: with no template file to match against **/*.html in the first place, an inline template's event/[event] bindings simply aren't seen (same as before this plugin existed).

When the paired .ts file can't be found (or has no class at all), literal event="..." bindings still resolve normally - they don't need any scope - and dynamic [event]="..." bindings are still reported as dynamic occurrences, just never resolved to a literal.

Template parse errors

If @angular/compiler can't parse a template at all - most commonly a text node that starts with what looks like a control-flow block opener (@if, @for, @switch, ...) but isn't escaped, e.g. a button labeled literally @if control-flow event instead of &#64;if control-flow event - this plugin throws instead of silently treating the file as if it had zero event bindings. The host CLI reports it as skip: <file> Plugin "angular" failed to transform <file>: Failed to parse Angular template <file>: <compiler diagnostic> and continues scanning every other file; only that one template is excluded from the run.


Known limitations

  • [event]="expr" on a native element fails a real ng build (NG8002). See "Template - dynamic event bindings" above - use [attr.event]="expr" instead, which the plugin detects identically.
  • Inline templates are unsupported. Only templateUrl-based (external .html file) components are covered - see "Finding the component class" above.
  • The this.-qualification heuristic is simple-shape-only. It recognizes a bare identifier or a chain of .prop/?.prop/['key'] accessors - this covers both a plain class field (EVENT_NAME = "x") and a getter (get computedEventName() { return "x"; }, including one with conditional returns, which resolves as a dynamic occurrence unioning every branch), since the host CLI's resolver checks both member kinds for this.field. Anything else is left as written and reported as an unresolved dynamic occurrence rather than partially rewritten - most notably Angular Signals ([event]="mySignal()"): the call syntax doesn't match the simple-shape regex, and even if it did, a signal isn't a plain function the resolver can walk into (its value lives in a separate reactive primitive, not a return statement) - resolving it would need dedicated Signals-aware logic this plugin doesn't have yet.
  • The paired component class is read from disk as a second copy. If that class already has its own real track()/tracker.track() calls in its methods (unrelated to the template), those calls get scanned twice - once via the real .ts file (as always), once via this plugin's virtual copy of it. The final events list in eventra.json is unaffected (it's a set, duplicates collapse), but eventra sync's "Dynamic event names" console output can list the same source line twice, once under each file path.

Configuration

No plugin-specific config - it activates purely by being listed in eventra.json's plugins array (see Installation).


Plugin contract

export interface CliPluginAngular {
  readonly id: string;
  readonly version: string;
  readonly includeGlobs: readonly string[];
  readonly staticSinks?: readonly CliPluginStaticCalleeSink[];
  match(path: string): boolean;
  transform(input: { path: string; source: string }): Promise<{
    modules: Array<{ path: string; content: string }>;
  }>;
}

No dependency on @eventra_dev/eventra-cli - the CLI adapts this shape internally. .html → one virtual .html.ts module: either the paired component's own source with a synthetic method spliced into its class (when at least one dynamic binding needs a this-scope), or a small standalone function (when every binding is literal, or no component class could be found). staticSinks describes the synthetic calls it emits; the CLI builds its own sink detector from them. See @eventra_dev/eventra-cli's plugin docs for the full external-plugin contract.


Requirements

  • Node.js ^20.19.0 || ^22.12.0 || >=24.0.0 (the version range required by @angular/compiler itself - higher than the >=18 floor of this project's other framework plugins, since Angular's own tooling has moved past Node 18)
  • @eventra_dev/eventra-cli as the host CLI

Test Coverage

100% statement/branch/function/line coverage (v8 provider, pnpm test:coverage), enforced via a coverage.thresholds block in vitest.config.ts.

40 unit tests (vitest), covering:

| Area | Covers | |---|---| | Template parsing | Literal and dynamic event bindings, [event] and [attr.event] parsed identically, *ngIf/*ngFor, @if/@else if/@else, @for/@empty, @switch/@case/@default, ng-container/ng-template, empty/boolean-shorthand event, throws on a template the compiler can't parse (e.g. an unescaped @if-like text node) instead of silently reporting zero bindings | | this.-qualification | Bare identifier, property chain, optional chaining, element access all prefixed; method calls, operators, and literals left untouched | | Component-class pairing | @Component-decorated class found and used, undecorated-class fallback, decorated class preferred over an earlier undecorated one, no-class-in-file and missing-file fallbacks | | Virtual module output | Synthetic method spliced inside the real class before its closing brace, top-level-function fallback for literal-only templates and unpaired templates, export stub for an empty template | | Plugin contract | match(), includeGlobs, staticSinks, transform(), end-to-end dynamic-binding resolution through a real paired component class |

Run locally:

pnpm --filter @eventra_dev/cli-plugin-angular test
pnpm --filter @eventra_dev/cli-plugin-angular test:coverage

License

MIT