@precedence-dev/instrument 0.1.0 → 0.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.
package/README.md CHANGED
@@ -17,42 +17,67 @@ actually run the generated code to prove it.
17
17
 
18
18
  ```
19
19
  # dry-run: print the diff, touch nothing
20
- precedence-instrument --plan plan.json --dir src --track "track from @/lib/analytics"
20
+ precedence-instrument --plan plan.json --dir src
21
21
 
22
22
  # apply it (refuses on a dirty git tree, so the injection lands in its own commit)
23
- precedence-instrument --plan plan.json --dir src --track "track from @/lib/analytics" --apply
23
+ precedence-instrument --plan plan.json --dir src --apply
24
24
 
25
25
  # CI: fail the build if the committed source has drifted from the plan
26
- precedence-instrument --plan plan.json --dir src --track track --check
26
+ precedence-instrument --plan plan.json --dir src --check
27
27
  ```
28
28
 
29
29
  `--plan` is the events export from the picker (`@precedence-dev/sdk`) or the viewer
30
30
  (`@precedence-dev/viewer`). `--dir` is repeatable. `-h` prints every flag.
31
31
 
32
- ## Emit modes
32
+ ## What it bakes
33
33
 
34
- | `--emit` | injected call | change an event without a rebuild? |
35
- | --- | --- | --- |
36
- | `direct` (default) | `track("event_name", { pm_id, ...props })`, baked in; adds the `--track` import | no — it's all in the source |
37
- | `runtime` | `globalThis.__pm?.("<anchor id>", { ...in-scope props })`, no import | yes — enable/disable, rename, retarget, change the discriminator, narrow props: all plan edits |
34
+ At every tracked branch it splices one complete call:
38
35
 
39
- `runtime` mode needs [`@precedence-dev/sdk`](https://github.com/precedence-dev/sdk)'s
40
- `installPrecedence` called once at your app root — it maps the anchor id back to
41
- the event name, adds `pm_id`, layers on the plan's static props, and catches the
42
- clicks on synthetic anchors (which `runtime` mode only stamps with `data-precedence-id`,
43
- no listener). In `direct` mode the delegated listener is generated instead
44
- `--runtime <file>` writes it, import it once at the app root.
36
+ ```js
37
+ precedence.track("checkout_completed", { psc_id: "p_1qzcbxk", amount });
38
+ ```
39
+
40
+ plus `import { precedence } from "@precedence-dev/sdk"` in that file.
41
+
42
+ - **`psc_id`** — a short cyrb53 hash of the anchor's structural id
43
+ (`src/Checkout.tsx#Checkout::form|onSubmit|ok`). The readable id never enters
44
+ the bundle or a third-party destination; the plan maps the two.
45
+ - **the customer's selected properties** — `{ amount }` here, shorthand for an
46
+ in-scope binding, or an accessor expression, or `undefined /* not in scope */`.
47
+ - **not** the `outcome` / `placement` discriminator — that stays in the plan and
48
+ the SDK overlay adds it.
49
+
50
+ The call works on its own — deliver it by calling `installPrecedence({ endpoint })`
51
+ once at your app root ([`@precedence-dev/sdk`](https://github.com/precedence-dev/sdk)).
52
+
53
+ Add `planUrl` to `installPrecedence` and the SDK loads the plan as a **live
54
+ overlay**: it matches each baked call by `psc_id` and lets the plan rename the
55
+ event, narrow its props, add or change the discriminator, or disable it — no
56
+ rebuild. The next `precedence-instrument` run re-bakes the source to match, and
57
+ the overlay for that anchor goes quiet. So plan edits are live immediately and
58
+ converge back into the source on the next build; only a brand-new call site (a
59
+ new anchor) or a newly-surfaced prop needs a build.
60
+
61
+ `--track "<callee> from <module>"` retargets the call to your own function (the
62
+ callee may be dotted). Then that function is yours to keep matching the plan.
63
+
64
+ **Synthetic anchors** — links and bare `<button>`s with no handler — have no call
65
+ site to bake into. `precedence-instrument` stamps them with `data-precedence-id`
66
+ and generates a self-contained click-listener module: `--delegated <file>` writes
67
+ it, import it once at your app root. It calls the same `precedence.track`, so the
68
+ overlay applies to it too.
45
69
 
46
70
  ## Debug a live event: `--explain`
47
71
 
48
- Every emitted event carries `pm_id` (the structural anchor id). Copy one from
49
- the dashboard and run:
72
+ Every baked call carries `psc_id`. Copy one from the dashboard and run:
50
73
 
51
74
  ```
52
- precedence-instrument --plan plan.json --dir src --explain "src/Checkout.tsx#Checkout::form|onSubmit|result_ok"
53
- precedence-instrument --plan plan.json --dir src --explain "<pm_id>" --json # structured
75
+ precedence-instrument --plan plan.json --dir src --explain "p_1qzcbxk"
76
+ precedence-instrument --plan plan.json --dir src --explain "<psc_id>" --json # structured
54
77
  ```
55
78
 
79
+ (`--explain` also accepts the readable structural id, if your plan is stale.)
80
+
56
81
  It doesn't instrument anything — it traces the id back to its exact fire site
57
82
  and reports, per property, the **structural** reasons a value can arrive null
58
83
  (out of scope here → baked `undefined`; the branch fires precisely when the
@@ -92,7 +117,7 @@ src/
92
117
  ├── generate/
93
118
  │ ├── instrument.ts the resolver + edit primitives + safety wrapping
94
119
  │ ├── unplugin.ts the bundler-plugin delivery path
95
- │ ├── explain.ts trace a fired event's pm_id back to its source
120
+ │ ├── explain.ts trace a fired event's psc_id back to its source
96
121
  │ └── cli.ts the precedence-instrument binary
97
122
  ├── discover.ts resolve --dir/--files/--changed-since to a file list
98
123
  └── util.ts (tiny, no analysis logic — see below)
@@ -38,9 +38,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
38
  * precedence-instrument, CI transform. Splice tracking calls into the source from a
39
39
  * plan (the events a PM defined in the picker / viewer "copy events" export).
40
40
  *
41
- * precedence-instrument --plan plan.json --dir src --track "track from @/lib/analytics"
42
- * precedence-instrument --plan plan.json --dir src --track track --apply
43
- * precedence-instrument --plan plan.json --dir src --track track --check # CI: fail if drifted
41
+ * precedence-instrument --plan plan.json --dir src --apply # precedence.track + import
42
+ * precedence-instrument --plan plan.json --dir src --check # CI: fail if drifted
43
+ * precedence-instrument --plan plan.json --dir src --track "t from @/lib/analytics" --apply
44
44
  */
45
45
  const fs = __importStar(require("fs"));
46
46
  const path = __importStar(require("path"));
@@ -49,20 +49,20 @@ const discover_1 = require("../discover");
49
49
  const util_1 = require("../util");
50
50
  const instrument_1 = require("./instrument");
51
51
  const explain_1 = require("./explain");
52
- const HELP = `precedence-instrument, splice tracking calls into the source from a plan
52
+ const HELP = `precedence-instrument, bake tracking calls into the source from a plan
53
53
 
54
54
  USAGE
55
- precedence-instrument --plan plan.json --dir src --track "<name> from <module>"
55
+ precedence-instrument --plan plan.json --dir src --apply
56
56
 
57
57
  OPTIONS
58
58
  --plan <file> the events export from the picker / viewer (required)
59
59
  --dir <path> source dir to scan (repeatable) (required)
60
- --track <spec> direct mode: "track from @/lib/analytics" -> adds an import
61
- "track" -> assume it's already in scope / global
62
- (default: console.log, every event logs "[pm] <name>", no import)
63
- --emit <mode> "direct" (default): track("name", { props }) baked in
64
- "runtime": globalThis.__pm?.("<id>", { props }), toggle/rename
65
- become plan edits; needs @precedence-dev/sdk at the app root
60
+ --track <spec> the call target a complete precedence.track("name",
61
+ { psc_id, ...props }) call is baked in (a plan overlay in
62
+ @precedence-dev/sdk can still rename / retune it live)
63
+ (default: "precedence.track from @precedence-dev/sdk", import added)
64
+ "myTrack from @/lib/analytics" -> bake into your own fn + import it
65
+ "myTrack" -> assume it's already in scope
66
66
  --types resolve declared types via the TS checker (needs a tsconfig).
67
67
  Required for callback-edge-rule continuation anchors (mutate onSuccess/
68
68
  onError, .then(f,g)).
@@ -72,16 +72,18 @@ OPTIONS
72
72
  stays reviewable in a diff
73
73
  --allow-dirty let --apply run against a dirty git tree anyway
74
74
  --out-dir <dir> write changed files here instead of in place
75
- --runtime <file> write the delegated click/navigate listener here (for synthetic anchors)
75
+ --delegated <file> write the synthetic-anchor click listener here (links / bare
76
+ buttons — they have no call site); import it once at your app root
76
77
  --check exit 1 if any file would change (for CI verification)
77
78
  --json machine-readable report to stdout
78
79
  --reanchor don't instrument, repair a plan whose ids stopped resolving
79
80
  (e.g. a component was renamed) by matching on fingerprint,
80
81
  write the fixed plan to --out for review
81
82
  --out <file> where --reanchor writes the repaired plan (default: <plan>.reanchored.json)
82
- --explain <pm_id> don't instrument, trace one pm_id (as seen in the dashboard)
83
- back to its fire site and report, per property, when it can
84
- arrive null. Pair with --json for the structured report.
83
+ --explain <psc_id> don't instrument, trace one psc_id (the p_… hash seen in the
84
+ dashboard, or the readable structural id) back to its fire
85
+ site and report, per property, when it can arrive null.
86
+ Pair with --json for the structured report.
85
87
  -h, --help
86
88
 
87
89
  EXIT
@@ -94,26 +96,20 @@ const OPTIONS = new Map([
94
96
  ["--plan", (o, next) => { o.plan = next(); }],
95
97
  ["--dir", (o, next) => { o.paths.push(next()); }],
96
98
  ["--track", (o, next) => { o.track = next(); }],
97
- ["--emit", (o, next) => {
98
- const v = next();
99
- if (v !== "direct" && v !== "runtime")
100
- fail("--emit must be direct|runtime");
101
- o.emit = v;
102
- }],
103
99
  ["--apply", (o) => { o.apply = true; }],
104
100
  ["--allow-dirty", (o) => { o.allowDirty = true; }],
105
101
  ["--types", (o) => { o.types = true; }],
106
102
  ["--tsconfig", (o, next) => { o.tsconfig = next(); }],
107
103
  ["--check", (o) => { o.check = true; }],
108
104
  ["--out-dir", (o, next) => { o.outDir = next(); }],
109
- ["--runtime", (o, next) => { o.runtime = next(); }],
105
+ ["--delegated", (o, next) => { o.delegated = next(); }],
110
106
  ["--json", (o) => { o.json = true; }],
111
107
  ["--reanchor", (o) => { o.reanchor = true; }],
112
108
  ["--out", (o, next) => { o.out = next(); }],
113
109
  ["--explain", (o, next) => { o.explain = next(); }],
114
110
  ]);
115
111
  function parseArgs(argv) {
116
- const o = { plan: "", paths: [], track: "", emit: "direct", apply: false, allowDirty: false, check: false, types: false, tsconfig: "", outDir: "", json: false, runtime: "", reanchor: false, out: "", explain: "" };
112
+ const o = { plan: "", paths: [], track: "", apply: false, allowDirty: false, check: false, types: false, tsconfig: "", outDir: "", json: false, delegated: "", reanchor: false, out: "", explain: "" };
117
113
  const cur = { i: 0 };
118
114
  const next = (a) => { const v = argv[++cur.i]; if (v === undefined)
119
115
  fail(`missing value for ${a}`); return v; };
@@ -135,7 +131,7 @@ function parseArgs(argv) {
135
131
  fail("--plan is required");
136
132
  if (!o.paths.length)
137
133
  fail("--dir is required");
138
- // --track optional now: direct mode defaults to console.log (in instrument.ts)
134
+ // --track optional: defaults to precedence.track from @precedence-dev/sdk (in instrument.ts)
139
135
  return o;
140
136
  }
141
137
  /** fill an LCS length table, `dp[i][j]` = LCS of `A[i:]` and `B[j:]`. */
@@ -262,14 +258,14 @@ function printTextReport(r) {
262
258
  r.skipped.forEach((x) => process.stderr.write(` - ${x.event} skipped: ${x.reason}${x.id ? ` (${x.id})` : ""}\n`));
263
259
  process.stdout.write(`\n${added.length} call(s) in ${r.files.length} file(s)${unchanged ? `, ${unchanged} already in place` : ""}, ${r.delegated.length} delegated, ${r.warnings.length} drift, ${r.skipped.length} skipped\n`);
264
260
  }
265
- function emitRuntimeModule(r, o) {
266
- if (o.runtime && r.runtimeModule) {
267
- fs.mkdirSync(path.dirname(path.resolve(o.runtime)), { recursive: true });
268
- fs.writeFileSync(o.runtime, r.runtimeModule);
269
- process.stdout.write(` wrote ${o.runtime} (${r.delegated.length} delegated event(s), import it once at your app root)\n`);
261
+ function emitDelegatedModule(r, o) {
262
+ if (o.delegated && r.delegatedModule) {
263
+ fs.mkdirSync(path.dirname(path.resolve(o.delegated)), { recursive: true });
264
+ fs.writeFileSync(o.delegated, r.delegatedModule);
265
+ process.stdout.write(` wrote ${o.delegated} (${r.delegated.length} synthetic anchor(s), import it once at your app root)\n`);
270
266
  }
271
- else if (r.runtimeModule && !o.json) {
272
- process.stdout.write(`\n--- delegated listener (pass --runtime <file> to write it) ---\n${r.runtimeModule}\n`);
267
+ else if (r.delegatedModule && !o.json) {
268
+ process.stdout.write(`\n--- synthetic-anchor listener (pass --delegated <file> to write it) ---\n${r.delegatedModule}\n`);
273
269
  }
274
270
  }
275
271
  function writeChanges(r, o) {
@@ -302,7 +298,7 @@ function main() {
302
298
  return;
303
299
  }
304
300
  const r = (0, instrument_1.instrument)(inputs, plan, {
305
- track: o.track, emit: o.emit, types: o.types, tsconfig: o.tsconfig || undefined,
301
+ track: o.track, types: o.types, tsconfig: o.tsconfig || undefined,
306
302
  });
307
303
  if (o.json)
308
304
  process.stdout.write(JSON.stringify(r, null, 2) + "\n");
@@ -313,7 +309,7 @@ function main() {
313
309
  // longer resolves cleanly against the source, both are CI failures.
314
310
  if (o.check)
315
311
  process.exit(changed.length || r.skipped.length ? 1 : 0);
316
- emitRuntimeModule(r, o);
312
+ emitDelegatedModule(r, o);
317
313
  writeChanges(r, o);
318
314
  }
319
315
  const VERDICT_MARK = {
@@ -323,8 +319,19 @@ const VERDICT_MARK = {
323
319
  "maybe-null": "⚠ may be null",
324
320
  "resolves": "· ok",
325
321
  };
322
+ /** `--explain` accepts the readable structural id or the baked `p_…` hash; the
323
+ * plan maps one to the other. */
324
+ function resolveExplainId(plan, arg) {
325
+ if (!arg.startsWith("p_"))
326
+ return arg;
327
+ for (const ev of plan.events)
328
+ for (const a of ev.anchors)
329
+ if ((0, instrument_1.pscId)(a.id) === arg)
330
+ return a.id;
331
+ return arg; // let explain() report the miss
332
+ }
326
333
  function doExplain(inputs, plan, o) {
327
- const r = (0, explain_1.explain)(inputs, plan, o.explain);
334
+ const r = (0, explain_1.explain)(inputs, plan, resolveExplainId(plan, o.explain));
328
335
  if (o.json) {
329
336
  process.stdout.write(JSON.stringify(r, null, 2) + "\n");
330
337
  process.exit(r.resolved ? 0 : 1);
@@ -35,11 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.explain = explain;
37
37
  /**
38
- * `precedence-instrument --explain <pm_id>` — the reverse direction of the pipeline.
38
+ * `precedence-instrument --explain <psc_id>` — the reverse direction of the pipeline.
39
39
  *
40
- * Every emitted event carries `pm_id` (the structural anchor id). Given one seen
41
- * in the analytics dashboard, this traces it back to the exact fire site and
42
- * says, per property, whether and when the value can arrive null.
40
+ * Every emitted event carries `psc_id` (a hash of the structural anchor id; the
41
+ * plan maps it back). Given one seen in the analytics dashboard, this traces it
42
+ * to the exact fire site and says, per property, whether and when the value can
43
+ * arrive null. (The CLI resolves a `p_…` hash to the structural id via the plan
44
+ * before calling this; `explain()` itself takes the structural id.)
43
45
  *
44
46
  * Static analysis only. It enumerates the STRUCTURAL reasons a value is absent:
45
47
  * · out of lexical scope at this site → instrumenter bakes `undefined`
@@ -131,7 +133,7 @@ function anchorFor(plan, id) {
131
133
  function whyMiss(loaded, id) {
132
134
  const m = id.split("|")[0].match(/^(.+)#([^#]*)::(.+)$/);
133
135
  if (!m)
134
- return "unrecognised id — copy it from the dashboard's pm_id property verbatim";
136
+ return "unrecognised id — copy the psc_id from the dashboard verbatim (the p_… hash, or the readable id if the plan is stale)";
135
137
  const [, file, comp] = m;
136
138
  const L = loaded.find((x) => (0, build_1.norm)(x.file) === (0, build_1.norm)(file) || (0, build_1.norm)(x.file).endsWith("/" + (0, build_1.norm)(file)));
137
139
  if (!L)
@@ -31,19 +31,14 @@ export interface Plan {
31
31
  events: PlanEvent[];
32
32
  }
33
33
  export interface InstrumentOpts {
34
- /**
35
- * "direct" (default): inject `track("event_name", { props })` + an import.
36
- * Self-contained, readable, no runtime dependency. To change anything rebuild.
37
- * "runtime": inject `globalThis.__pm?.("<anchor id>", { props })`, no import,
38
- * no static/discriminator props baked in. `@precedence-dev/sdk` (installPrecedence)
39
- * looks the id up in the fetched plan and shapes the event. Toggle / rename /
40
- * static values / prop narrowing become plan edits, no rebuild.
41
- */
42
- emit?: "direct" | "runtime";
43
- /** direct mode only: "<name> from <module>" adds an import; "<name>" assumes global */
34
+ /** the call target. Default `precedence.track from @precedence-dev/sdk` — a
35
+ * complete `precedence.track("event", { psc_id, ...props })` call plus an
36
+ * `import { precedence } from "@precedence-dev/sdk"`. The call works on its
37
+ * own; loading a plan overlay in the SDK lets a rename / prop-narrow /
38
+ * discriminator change / disable take effect before the next build re-bakes it.
39
+ * `"<callee> from <module>"` retargets it (callee may be dotted); a bare
40
+ * `"<callee>"` assumes it's already in scope. */
44
41
  track?: string;
45
- /** runtime mode: the global to call (default "globalThis.__pm") */
46
- emitGlobal?: string;
47
42
  /**
48
43
  * resolve declared types via the TS checker. REQUIRED to instrument callback-edge-rule
49
44
  * `continuation` anchors (`mutate(x, { onSuccess })`, `p.then(f, g)`) — the
@@ -78,7 +73,6 @@ export interface Delegation {
78
73
  element: string;
79
74
  ref: string;
80
75
  attrs: string[];
81
- props: Record<string, string>;
82
76
  }
83
77
  export interface InstrumentResult {
84
78
  files: {
@@ -92,9 +86,11 @@ export interface InstrumentResult {
92
86
  /** id resolved, but the code moved on since the plan, injected anyway, verify */
93
87
  warnings: Warning[];
94
88
  delegated: Delegation[];
95
- /** a delegated click/navigate listener for every synthetic anchor, needs
96
- * `data-precedence-id` stamps in the production build (browser/stamp-loader.js) */
97
- runtimeModule?: string;
89
+ /** a self-contained click/navigate listener module for the synthetic anchors
90
+ * (links / bare buttons) they have no call site to bake into. Import it
91
+ * once at your app root; it calls `precedence.track`, so the plan overlay
92
+ * applies to it too. */
93
+ delegatedModule?: string;
98
94
  }
99
95
  /** what a single-module transform returns; null when the plan has nothing here */
100
96
  export interface FileInstrumentResult {
@@ -108,9 +104,14 @@ export interface FileInstrumentResult {
108
104
  }
109
105
  /** every distinct source file a plan references */
110
106
  export declare function planFiles(plan: Plan): string[];
111
- /** every emitted event carries the anchor id under this key, so a value seen in
112
- * the dashboard can be traced back to the exact fire site (file/component/branch). */
113
- export declare const PM_ID_KEY = "pm_id";
107
+ /** reserved property key: the anchor hash on every baked call. It's the key the
108
+ * SDK's plan overlay matches on; the readable structural id lives only in the
109
+ * plan. */
110
+ export declare const PSC_ID_KEY = "psc_id";
111
+ /** cyrb53 — a tiny 53-bit string hash. `@precedence-dev/sdk` has a byte-for-byte
112
+ * copy (`pscId` in its `plan.ts`); the two must stay in sync so the baked
113
+ * `psc_id` matches the SDK's plan index. */
114
+ export declare function pscId(structuralId: string): string;
114
115
  /**
115
116
  * Wrap the raw tracking call so it can never affect the surrounding control
116
117
  * flow: a synchronous throw is caught and swallowed, and if it returns a
@@ -121,6 +122,11 @@ export declare const PM_ID_KEY = "pm_id";
121
122
  * code does; there is no prior behavior here to disturb.
122
123
  */
123
124
  export declare function safeStatement(call: string): string;
125
+ /** direct mode's default call target: the SDK's own client. `precedence.track` is
126
+ * a contract `@precedence-dev/sdk` owns end-to-end, so the injected call stays
127
+ * deterministic without Precedence ever seeing the analytics vendor. Override
128
+ * with `--track "<fn> from <module>"` to bake calls into your own function. */
129
+ export declare const DEFAULT_TRACK = "precedence.track from @precedence-dev/sdk";
124
130
  export interface ReanchorResult {
125
131
  plan: Plan;
126
132
  repointed: {
@@ -159,7 +165,9 @@ export declare function instrumentFile(code: string, id: string, plan: Plan, opt
159
165
  * transformed every file. */
160
166
  export declare function planDelegations(plan: Plan): Delegation[];
161
167
  /** one document-level listener for every synthetic (link / bare-button) anchor,
162
- * keyed on the `data-precedence-id="<structural id>"` the transform stamps onto the
163
- * same element, one string, produced by one code path, so it can't drift. */
168
+ * keyed on the `data-precedence-id="<structural id>"` the transform stamps onto
169
+ * the same element. It calls the same `precedence.track`, so a loaded plan
170
+ * overlays these clicks exactly as it overlays baked calls. One string, one
171
+ * code path, so it can't drift. Import it once at your app root. */
164
172
  export declare function buildDelegatedModule(dels: Delegation[], track: string): string;
165
173
  export {};
@@ -36,8 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.PM_ID_KEY = void 0;
39
+ exports.DEFAULT_TRACK = exports.PSC_ID_KEY = void 0;
40
40
  exports.planFiles = planFiles;
41
+ exports.pscId = pscId;
41
42
  exports.safeStatement = safeStatement;
42
43
  exports.reanchor = reanchor;
43
44
  exports.instrument = instrument;
@@ -213,9 +214,6 @@ function resolveContextAccessor(ap, via, out, evName) {
213
214
  }
214
215
  /** how to render one property in the call object */
215
216
  function propPart(p, ev, anchor) {
216
- const stat = anchor.staticProps || {};
217
- if (p in stat)
218
- return `${p}: ${JSON.stringify(stat[p])}`;
219
217
  const acc = ev.accessors && ev.accessors[p];
220
218
  if (typeof acc === "string")
221
219
  return acc === p ? p : `${p}: ${acc}`;
@@ -223,31 +221,37 @@ function propPart(p, ev, anchor) {
223
221
  return `${p}: undefined /* not in scope here */`;
224
222
  return p; // shorthand, the picker checked it resolves
225
223
  }
226
- /** every emitted event carries the anchor id under this key, so a value seen in
227
- * the dashboard can be traced back to the exact fire site (file/component/branch). */
228
- exports.PM_ID_KEY = "pm_id";
229
- /** direct mode: `track("event_name", { pm_id: "<id>", …everything baked in })` */
230
- function directCall(fnName, ev, anchor) {
224
+ /** reserved property key: the anchor hash on every baked call. It's the key the
225
+ * SDK's plan overlay matches on; the readable structural id lives only in the
226
+ * plan. */
227
+ exports.PSC_ID_KEY = "psc_id";
228
+ /** cyrb53 a tiny 53-bit string hash. `@precedence-dev/sdk` has a byte-for-byte
229
+ * copy (`pscId` in its `plan.ts`); the two must stay in sync so the baked
230
+ * `psc_id` matches the SDK's plan index. */
231
+ function pscId(structuralId) {
232
+ let h1 = 0xdeadbeef;
233
+ let h2 = 0x41c6ce57;
234
+ for (let i = 0; i < structuralId.length; i++) {
235
+ const ch = structuralId.charCodeAt(i);
236
+ h1 = Math.imul(h1 ^ ch, 2654435761);
237
+ h2 = Math.imul(h2 ^ ch, 1597334677);
238
+ }
239
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
240
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
241
+ return "p_" + (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
242
+ }
243
+ /** the baked call: `precedence.track("event_name", { psc_id: "p_…", …the props
244
+ * the plan selects })`. The discriminator (`outcome` / `placement`) is *not*
245
+ * baked — the SDK's plan overlay adds it, and can rename / narrow / drop the
246
+ * call, all by `psc_id`. */
247
+ function emitCall(ev, anchor, callee) {
231
248
  const seen = new Set();
232
- const parts = [...new Set(ev.properties)].map((p) => { seen.add(p); return propPart(p, ev, anchor); });
233
- Object.keys(anchor.staticProps || {}).forEach((k) => { if (!seen.has(k))
234
- parts.push(`${k}: ${JSON.stringify((anchor.staticProps || {})[k])}`); });
235
- if (!seen.has(exports.PM_ID_KEY) && !(exports.PM_ID_KEY in (anchor.staticProps || {})))
236
- parts.unshift(`${exports.PM_ID_KEY}: ${JSON.stringify(anchor.id)}`);
237
- return `${fnName}(${JSON.stringify(ev.name)}${obj(parts)})`;
238
- }
239
- /** runtime mode: `globalThis.__pm?.("<id>", { …in-scope + ambient props })`.
240
- * the id is arg 1 — the runtime layers it onto the payload as `pm_id` at emit
241
- * time, along with static / discriminator props from the plan. */
242
- function runtimeCall(g, ev, anchor) {
243
- const skip = new Set([...Object.keys(anchor.staticProps || {}), ...(anchor.missingProps || [])]);
244
- const parts = [...new Set(ev.properties)].filter((p) => !skip.has(p)).map((p) => propPart(p, ev, anchor));
245
- return `${g}?.(${JSON.stringify(anchor.id)}${obj(parts)})`;
246
- }
247
- function emitCall(ev, anchor, opts, fnName) {
248
- return opts.emit === "runtime"
249
- ? runtimeCall(opts.emitGlobal || "globalThis.__pm", ev, anchor)
250
- : directCall(fnName, ev, anchor);
249
+ const parts = [...new Set(ev.properties)]
250
+ .filter((p) => !(anchor.staticProps && p in anchor.staticProps))
251
+ .map((p) => { seen.add(p); return propPart(p, ev, anchor); });
252
+ if (!seen.has(exports.PSC_ID_KEY))
253
+ parts.unshift(`${exports.PSC_ID_KEY}: ${JSON.stringify(pscId(anchor.id))}`);
254
+ return `${callee}(${JSON.stringify(ev.name)}${obj(parts)})`;
251
255
  }
252
256
  function indentAt(sf, pos) {
253
257
  const lineStart = sf.text.lastIndexOf("\n", pos - 1) + 1;
@@ -277,7 +281,7 @@ function assertNever(x) {
277
281
  function safeStatement(call) {
278
282
  // Promise.resolve covers void / Promise / thenable clients and, unlike `.catch`
279
283
  // on an inferred `void`, type-checks in a strict consumer project.
280
- return `try { void Promise.resolve(${call}).catch(() => {}); } catch (__pmE) {}`;
284
+ return `try { void Promise.resolve(${call}).catch(() => {}); } catch (__pscE) {}`;
281
285
  }
282
286
  /** the same guard, as a single expression — for sites where `call` has to sit
283
287
  * inside a larger expression (a comma operator) rather than stand alone. */
@@ -328,22 +332,34 @@ function planEdit(site, call, sf) {
328
332
  return assertNever(site);
329
333
  }
330
334
  }
335
+ /** direct mode's default call target: the SDK's own client. `precedence.track` is
336
+ * a contract `@precedence-dev/sdk` owns end-to-end, so the injected call stays
337
+ * deterministic without Precedence ever seeing the analytics vendor. Override
338
+ * with `--track "<fn> from <module>"` to bake calls into your own function. */
339
+ exports.DEFAULT_TRACK = "precedence.track from @precedence-dev/sdk";
340
+ /** `"<callee> from <module>"` → `{ callee, importName, spec }`; a bare `"<callee>"`
341
+ * → no import. `callee` may be dotted (`precedence.track`, `client.track`); the
342
+ * import binds its head (`precedence`, `client`). */
331
343
  function parseTrack(track) {
332
- const m = track.match(/^\s*([A-Za-z_$][\w$]*)\s+from\s+(.+?)\s*$/);
344
+ const m = track.match(/^\s*([A-Za-z_$][\w$]*(?:\.[\w$]+)*)\s+from\s+(.+?)\s*$/);
333
345
  if (!m)
334
- return { fnName: track.trim() || "track", importLine: "", spec: "" };
346
+ return { callee: track.trim() || "track", importName: "", spec: "" };
335
347
  const spec = m[2].replace(/^['"]|['"]$/g, "");
336
- return { fnName: m[1], importLine: `import { ${m[1]} } from ${JSON.stringify(spec)};\n`, spec };
348
+ return { callee: m[1], importName: m[1].split(".")[0], spec };
349
+ }
350
+ function importLineFor(track) {
351
+ const { importName, spec } = parseTrack(track);
352
+ return spec ? `import { ${importName} } from ${JSON.stringify(spec)};` : "";
337
353
  }
338
- /** add `import { track } from "spec"` unless the file already has it */
354
+ /** add `import { <head> } from "spec"` unless the file already has that binding */
339
355
  function addImport(s, code, track) {
340
- const { fnName, spec } = parseTrack(track);
356
+ const { importName, spec } = parseTrack(track);
341
357
  if (!spec)
342
358
  return;
343
- const re = new RegExp(`import\\s*\\{[^}]*\\b${fnName}\\b[^}]*\\}\\s*from\\s*['"]${spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]`);
359
+ const re = new RegExp(`import\\s*\\{[^}]*\\b${importName}\\b[^}]*\\}\\s*from\\s*['"]${spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]`);
344
360
  if (re.test(code))
345
361
  return;
346
- const line = `import { ${fnName} } from ${JSON.stringify(spec)};`;
362
+ const line = importLineFor(track);
347
363
  // after the last `import … "spec";` (spans multi-line lists, matches side-effect imports)
348
364
  const lastImp = [...code.matchAll(/^import\b[\s\S]*?['"][^'"]*['"]\s*;?/gm)].pop();
349
365
  if (lastImp && lastImp.index != null)
@@ -360,35 +376,35 @@ function render(code, edits, id, opts) {
360
376
  else
361
377
  s.overwrite(e.start, e.end, e.text);
362
378
  }
363
- // direct mode needs the `track` import, unless the only edits are `data-precedence-id`
364
- // stamps. runtime mode calls a global, so it never needs one.
365
- if (opts.emit !== "runtime" && edits.some((e) => e.kind !== "stamp"))
366
- addImport(s, code, opts.track || "track");
379
+ // the baked calls need the track import, unless the only edits are
380
+ // `data-precedence-id` stamps (whose listener module carries its own import).
381
+ if (edits.some((e) => e.kind !== "stamp"))
382
+ addImport(s, code, opts.track || exports.DEFAULT_TRACK);
367
383
  return {
368
384
  code: s.toString(),
369
385
  map: s.generateMap({ source: id, hires: true, includeContent: true }),
370
386
  };
371
387
  }
372
388
  /** resolve + verify + plan-edit one anchor, appending its results to `out`. */
373
- function collectAnchor(a, ev, index, loaded, opts, fnName, out) {
389
+ function collectAnchor(a, ev, index, loaded, opts, callee, out) {
374
390
  const ap = resolveAnchor(a, ev, index, loaded, out, opts);
375
391
  if (!ap)
376
392
  return;
377
393
  const drifted = recordDrift(a, ap, ev, out);
378
394
  const evR = ev.accessors ? resolveEventAccessors(ev, ap, out) : ev;
379
- const call = emitCall(evR, a, opts, fnName);
395
+ const call = emitCall(evR, a, callee);
380
396
  if (ap.site.kind === "delegated")
381
397
  applyDelegated(a, ev, ap, out);
382
398
  else
383
399
  applyInlineEdit(a, ev, ap, call, drifted, out);
384
400
  }
385
401
  /** resolve + verify + plan-edit every plan anchor (optionally just one file's) */
386
- function collect(plan, index, loaded, opts, fnName, onlyFile) {
402
+ function collect(plan, index, loaded, opts, callee, onlyFile) {
387
403
  const out = { byFile: new Map(), applied: [], skipped: [], warnings: [], delegated: [] };
388
404
  for (const ev of plan.events) {
389
405
  for (const a of ev.anchors) {
390
406
  if (!onlyFile || anchorFile(a.id) === onlyFile) {
391
- collectAnchor(a, ev, index, loaded, opts, fnName, out);
407
+ collectAnchor(a, ev, index, loaded, opts, callee, out);
392
408
  }
393
409
  }
394
410
  }
@@ -435,9 +451,8 @@ function resolveEventAccessors(ev, ap, out) {
435
451
  function applyDelegated(a, ev, ap, out) {
436
452
  const statKeys = new Set(Object.keys(a.staticProps || {}));
437
453
  out.delegated.push({
438
- event: ev.name, element: ap.element, ref: ap.id, // the structural id, the runtime join key
454
+ event: ev.name, element: ap.element, ref: ap.id, // the structural id hashed to psc_id in the module
439
455
  attrs: (ap.autoAttrs && ap.autoAttrs.length ? ap.autoAttrs : (ev.properties || [])).filter((p) => !statKeys.has(p)),
440
- props: a.staticProps || {},
441
456
  });
442
457
  const en = ap.elementNode;
443
458
  if (!en || !(ts.isJsxOpeningElement(en) || ts.isJsxSelfClosingElement(en)))
@@ -451,13 +466,12 @@ function applyDelegated(a, ev, ap, out) {
451
466
  out.byFile.set(ap.file, list);
452
467
  }
453
468
  /** true when a previous run already spliced this anchor's call into the file.
454
- * Direct mode writes `pm_id: "<id>"`, runtime mode passes the id as arg 1, and
455
- * the anchor id is globally unique, so a substring hit is a safe "already done".
456
- * An inserting codemod needs this: unlike a pattern-replacing one, an insert
457
- * doesn't stop matching on the next pass (cf. addImport's regex guard,
458
- * applyDelegated's data-precedence-id check, create-react-app eject's git check). */
469
+ * The baked call writes `psc_id: "p_…"` and the hash is globally unique, so a
470
+ * substring hit is a safe "already done". An inserting codemod needs this:
471
+ * unlike a pattern-replacing one, an insert doesn't stop matching on the next
472
+ * pass (cf. addImport's regex guard, applyDelegated's data-precedence-id check). */
459
473
  function alreadyInjected(sf, anchorId) {
460
- return sf.text.includes(JSON.stringify(anchorId));
474
+ return sf.text.includes(JSON.stringify(pscId(anchorId)));
461
475
  }
462
476
  /** an outcome anchor: splice the `track(...)` call at the branch's inject site */
463
477
  function applyInlineEdit(a, ev, ap, call, drifted, out) {
@@ -563,8 +577,8 @@ function reanchor(inputs, plan, opts = {}) {
563
577
  function instrument(inputs, plan, opts) {
564
578
  const { loaded } = (0, build_1.analyze)(inputs, { types: opts.types, tsconfig: opts.tsconfig });
565
579
  const index = attachIndex(loaded);
566
- const { fnName } = parseTrack(opts.track || "console.log");
567
- const c = collect(plan, index, loaded, opts, fnName);
580
+ const { callee } = parseTrack(opts.track || exports.DEFAULT_TRACK);
581
+ const c = collect(plan, index, loaded, opts, callee);
568
582
  const src = new Map(loaded.map((L) => [(0, build_1.norm)(L.file), L.sf.text]));
569
583
  const res = {
570
584
  files: [], applied: c.applied, skipped: c.skipped, warnings: c.warnings, delegated: c.delegated,
@@ -587,9 +601,10 @@ function instrument(inputs, plan, opts) {
587
601
  }
588
602
  res.files.push({ file, before, after: r.code, map: r.map });
589
603
  }
590
- // direct mode ships a generated PM_DELEGATED module; runtime mode uses @precedence-dev/sdk
591
- if (opts.emit !== "runtime" && res.delegated.length) {
592
- res.runtimeModule = buildDelegatedModule(res.delegated, opts.track || "track");
604
+ // synthetic anchors (links / bare buttons) have no call site to bake into —
605
+ // ship a self-contained listener module for them.
606
+ if (res.delegated.length) {
607
+ res.delegatedModule = buildDelegatedModule(res.delegated, opts.track || exports.DEFAULT_TRACK);
593
608
  }
594
609
  return res;
595
610
  }
@@ -602,8 +617,8 @@ function instrumentFile(code, id, plan, opts) {
602
617
  if (!loaded.length)
603
618
  return { file: planFile, code, map: null, applied: [], warnings: [], skipped: [], delegated: [] };
604
619
  const index = attachIndex(loaded);
605
- const { fnName } = parseTrack(opts.track || "console.log");
606
- const c = collect(plan, index, loaded, opts, fnName, planFile);
620
+ const { callee } = parseTrack(opts.track || exports.DEFAULT_TRACK);
621
+ const c = collect(plan, index, loaded, opts, callee, planFile);
607
622
  const edits = c.byFile.get((0, build_1.norm)(planFile)) || [...c.byFile.values()][0] || [];
608
623
  const r = edits.length ? render(code, edits, id, opts) : { code, map: null };
609
624
  return {
@@ -625,17 +640,20 @@ function planDelegations(plan) {
625
640
  event: ev.name, element: a.element || "",
626
641
  ref: a.id, // the structural id, matches what the transform stamps as data-precedence-id
627
642
  attrs: (ev.properties || []).filter((p) => !statKeys.has(p)),
628
- props: a.staticProps || {},
629
643
  });
630
644
  }
631
645
  }
632
646
  return out;
633
647
  }
634
648
  /** one document-level listener for every synthetic (link / bare-button) anchor,
635
- * keyed on the `data-precedence-id="<structural id>"` the transform stamps onto the
636
- * same element, one string, produced by one code path, so it can't drift. */
649
+ * keyed on the `data-precedence-id="<structural id>"` the transform stamps onto
650
+ * the same element. It calls the same `precedence.track`, so a loaded plan
651
+ * overlays these clicks exactly as it overlays baked calls. One string, one
652
+ * code path, so it can't drift. Import it once at your app root. */
637
653
  function buildDelegatedModule(dels, track) {
638
- const { fnName, importLine } = parseTrack(track);
654
+ const { callee } = parseTrack(track);
655
+ const imp = importLineFor(track);
656
+ const importLine = imp ? imp + "\n" : "";
639
657
  const rows = dels.map((d) => {
640
658
  const readers = d.attrs.map((a) => {
641
659
  const dom = a === "label" ? "getAttribute('aria-label')"
@@ -643,13 +661,12 @@ function buildDelegatedModule(dels, track) {
643
661
  : "getAttribute(" + JSON.stringify(a) + ")";
644
662
  return `${JSON.stringify(a)}: el.${dom}`;
645
663
  });
646
- const stat = Object.entries(d.props).map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`);
647
- const idPart = `${JSON.stringify(exports.PM_ID_KEY)}: ${JSON.stringify(d.ref)}`;
648
- return ` ${JSON.stringify(d.ref)}: { name: ${JSON.stringify(d.event)}, props: (el: Element): Record<string, unknown> => ({ ${[idPart].concat(readers, stat).join(", ")} }) },`;
664
+ const idPart = `${JSON.stringify(exports.PSC_ID_KEY)}: ${JSON.stringify(pscId(d.ref))}`;
665
+ return ` ${JSON.stringify(d.ref)}: { name: ${JSON.stringify(d.event)}, props: (el: Element): Record<string, unknown> => ({ ${[idPart].concat(readers).join(", ")} }) },`;
649
666
  });
650
667
  return `${importLine}// generated by precedence-instrument, needs data-precedence-id stamps in the production build
651
- type PmDelegated = { name: string; props: (el: Element) => Record<string, unknown> };
652
- const PM_DELEGATED: Record<string, PmDelegated> = {
668
+ type PscDelegated = { name: string; props: (el: Element) => Record<string, unknown> };
669
+ const PSC_DELEGATED: Record<string, PscDelegated> = {
653
670
  ${rows.join("\n")}
654
671
  };
655
672
  if (typeof document !== "undefined") {
@@ -657,8 +674,8 @@ if (typeof document !== "undefined") {
657
674
  const el = (e.target instanceof Element) ? e.target.closest("[data-precedence-id]") : null;
658
675
  if (!el) return;
659
676
  const id = el.getAttribute("data-precedence-id");
660
- const hit = id ? PM_DELEGATED[id] : undefined;
661
- if (hit) { try { void Promise.resolve(${fnName}(hit.name, hit.props(el))).catch(() => {}); } catch (__pmE) {} }
677
+ const hit = id ? PSC_DELEGATED[id] : undefined;
678
+ if (hit) { try { void Promise.resolve(${callee}(hit.name, hit.props(el))).catch(() => {}); } catch (__pscE) {} }
662
679
  }, true);
663
680
  }
664
681
  `;
@@ -2,17 +2,14 @@ import { type Plan, type Skipped } from "./instrument";
2
2
  export interface PmInstrumentOptions {
3
3
  /** the `copy events` export, a parsed plan, or a path to the JSON file */
4
4
  plan: Plan | string;
5
- /**
6
- * "direct" (default): inject `track(…)` calls + the delegated-listener module.
7
- * "runtime": inject `globalThis.__pm?.(id, …)` calls, stamp delegated elements,
8
- * and let `@precedence-dev/sdk` (installPrecedence) do the rest at runtime.
9
- * Toggling / renaming an event no longer needs a rebuild.
10
- */
11
- emit?: "direct" | "runtime";
12
- /** direct mode: "<name> from <module>" adds the import; "<name>" assumes it's global.
13
- * default "console.log", every event logs "[pm] <name>", no import. */
5
+ /** the call target. Default `precedence.track from @precedence-dev/sdk` — a
6
+ * complete `precedence.track("event", { psc_id, ...props })` call plus an
7
+ * auto-added import. Load a plan overlay in the SDK to rename / disable /
8
+ * retune events before the next build re-bakes them. `"<callee> from
9
+ * <module>"` retargets it; a bare `"<callee>"` assumes it's in scope. */
14
10
  track?: string;
15
- /** direct mode: id for the delegated-listener module (import once at your app root; default "virtual:pm-tracking") */
11
+ /** id for the synthetic-anchor listener module (import once at your app root;
12
+ * default "virtual:pm-tracking") */
16
13
  virtualModuleId?: string;
17
14
  /** per-file callback, wire drift / skips into your build log or fail the build */
18
15
  onReport?: (r: {
@@ -38,8 +38,8 @@ exports.farmPlugin = exports.esbuildPlugin = exports.rspackPlugin = exports.roll
38
38
  * precedence-instrument as a bundler transform, the production delivery path.
39
39
  *
40
40
  * One `transform` hook, adapted to Vite / webpack / Rollup / esbuild / Rspack by
41
- * unplugin. Source files never change on disk; the `track(…)` calls exist only
42
- * in the bundle, so instrumentation is idempotent and sourcemap-clean by
41
+ * unplugin. Source files never change on disk; the `precedence.track(…)` calls
42
+ * exist only in the bundle, so instrumentation is idempotent and sourcemap-clean by
43
43
  * construction, and CI reviews the plan diff, not a machine-rewritten tree.
44
44
  *
45
45
  * One implementation, six bundlers: per-module `transform(code, id)`, plan/config
@@ -49,19 +49,16 @@ exports.farmPlugin = exports.esbuildPlugin = exports.rspackPlugin = exports.roll
49
49
  * import { webpackPlugin as pmInstrument } from "@precedence-dev/instrument/unplugin";
50
50
  * const nextConfig = {
51
51
  * webpack(config) {
52
- * config.plugins.push(pmInstrument({
53
- * plan: "./tracking-plan.json",
54
- * track: "track from @/lib/analytics",
55
- * }));
52
+ * config.plugins.push(pmInstrument({ plan: "./tracking-plan.json" }));
56
53
  * return config;
57
54
  * },
58
55
  * };
59
56
  *
60
57
  * // vite.config.ts
61
58
  * import { vitePlugin as pmInstrument } from "@precedence-dev/instrument/unplugin";
62
- * export default { plugins: [pmInstrument({ plan, track })] };
59
+ * export default { plugins: [pmInstrument({ plan })] };
63
60
  *
64
- * // app entry, ONCE, pulls in the delegated click listener for synthetic anchors
61
+ * // app entry, ONCE, pulls in the click listener for synthetic anchors
65
62
  * import "virtual:pm-tracking";
66
63
  */
67
64
  const fs = __importStar(require("fs"));
@@ -72,7 +69,7 @@ exports.unpluginPmInstrument = (0, unplugin_1.createUnplugin)((options) => {
72
69
  const plan = typeof options.plan === "string"
73
70
  ? JSON.parse(fs.readFileSync(options.plan, "utf8"))
74
71
  : options.plan;
75
- const opts = { track: options.track, emit: options.emit }; // track defaults to console.log in instrument.ts
72
+ const opts = { track: options.track }; // track defaults to precedence.track in instrument.ts
76
73
  const files = (0, instrument_1.planFiles)(plan);
77
74
  const delegated = (0, instrument_1.planDelegations)(plan); // from the plan alone, no build ordering dependency
78
75
  const VID = options.virtualModuleId || "virtual:pm-tracking";
@@ -108,12 +105,9 @@ exports.unpluginPmInstrument = (0, unplugin_1.createUnplugin)((options) => {
108
105
  load(id) {
109
106
  if (id !== RESOLVED)
110
107
  return null;
111
- if (options.emit === "runtime") {
112
- return "// runtime mode: import { installPrecedence } from '@precedence-dev/sdk' and call it at your app root instead of importing this\nexport {};\n";
113
- }
114
108
  return delegated.length
115
- ? (0, instrument_1.buildDelegatedModule)(delegated, options.track || "track")
116
- : "// precedence: no delegated (synthetic) anchors in the plan\nexport {};\n";
109
+ ? (0, instrument_1.buildDelegatedModule)(delegated, options.track || instrument_1.DEFAULT_TRACK)
110
+ : "// precedence: no synthetic (link / bare-button) anchors in the plan\nexport {};\n";
117
111
  },
118
112
  };
119
113
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@precedence-dev/instrument",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Splice tracking calls into source (or a build) from a plan produced by Precedence's analysis engine. Source-available so anyone can read exactly what code it generates and where it puts it.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "author": "Precedence",
@@ -39,7 +39,7 @@
39
39
  "test": "npm run build && node --test \"test/**/*.test.mjs\" && node test/invariants.mjs"
40
40
  },
41
41
  "dependencies": {
42
- "@precedence-dev/cli": "^0.1.0",
42
+ "@precedence-dev/cli": "^0.2.0",
43
43
  "magic-string": "^0.30.21",
44
44
  "typescript": "^5.6.3",
45
45
  "unplugin": "^2.3.11"