@precedence-dev/instrument 0.1.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/LICENSE +105 -0
- package/README.md +109 -0
- package/bin/precedence-instrument.js +12 -0
- package/dist/discover.d.ts +10 -0
- package/dist/discover.js +95 -0
- package/dist/generate/cli.d.ts +2 -0
- package/dist/generate/cli.js +368 -0
- package/dist/generate/explain.d.ts +21 -0
- package/dist/generate/explain.js +428 -0
- package/dist/generate/instrument.d.ts +165 -0
- package/dist/generate/instrument.js +665 -0
- package/dist/generate/unplugin.d.ts +31 -0
- package/dist/generate/unplugin.js +125 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +7 -0
- package/package.json +50 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* precedence-instrument, CI transform. Splice tracking calls into the source from a
|
|
39
|
+
* plan (the events a PM defined in the picker / viewer "copy events" export).
|
|
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
|
|
44
|
+
*/
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const discover_1 = require("../discover");
|
|
49
|
+
const util_1 = require("../util");
|
|
50
|
+
const instrument_1 = require("./instrument");
|
|
51
|
+
const explain_1 = require("./explain");
|
|
52
|
+
const HELP = `precedence-instrument, splice tracking calls into the source from a plan
|
|
53
|
+
|
|
54
|
+
USAGE
|
|
55
|
+
precedence-instrument --plan plan.json --dir src --track "<name> from <module>"
|
|
56
|
+
|
|
57
|
+
OPTIONS
|
|
58
|
+
--plan <file> the events export from the picker / viewer (required)
|
|
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
|
|
66
|
+
--types resolve declared types via the TS checker (needs a tsconfig).
|
|
67
|
+
Required for callback-edge-rule continuation anchors (mutate onSuccess/
|
|
68
|
+
onError, .then(f,g)).
|
|
69
|
+
--tsconfig <path> explicit tsconfig.json for --types
|
|
70
|
+
--apply write the changes to disk (default: dry-run diff to stdout)
|
|
71
|
+
refuses if git has uncommitted changes, so the injection
|
|
72
|
+
stays reviewable in a diff
|
|
73
|
+
--allow-dirty let --apply run against a dirty git tree anyway
|
|
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)
|
|
76
|
+
--check exit 1 if any file would change (for CI verification)
|
|
77
|
+
--json machine-readable report to stdout
|
|
78
|
+
--reanchor don't instrument, repair a plan whose ids stopped resolving
|
|
79
|
+
(e.g. a component was renamed) by matching on fingerprint,
|
|
80
|
+
write the fixed plan to --out for review
|
|
81
|
+
--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.
|
|
85
|
+
-h, --help
|
|
86
|
+
|
|
87
|
+
EXIT
|
|
88
|
+
0 nothing to do / applied / reanchor clean
|
|
89
|
+
1 --check and files would change / bad plan / reanchor left anchors unresolved
|
|
90
|
+
2 bad usage
|
|
91
|
+
`;
|
|
92
|
+
function fail(m) { process.stderr.write(`error: ${m}\n\n${HELP}`); process.exit(2); }
|
|
93
|
+
const OPTIONS = new Map([
|
|
94
|
+
["--plan", (o, next) => { o.plan = next(); }],
|
|
95
|
+
["--dir", (o, next) => { o.paths.push(next()); }],
|
|
96
|
+
["--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
|
+
["--apply", (o) => { o.apply = true; }],
|
|
104
|
+
["--allow-dirty", (o) => { o.allowDirty = true; }],
|
|
105
|
+
["--types", (o) => { o.types = true; }],
|
|
106
|
+
["--tsconfig", (o, next) => { o.tsconfig = next(); }],
|
|
107
|
+
["--check", (o) => { o.check = true; }],
|
|
108
|
+
["--out-dir", (o, next) => { o.outDir = next(); }],
|
|
109
|
+
["--runtime", (o, next) => { o.runtime = next(); }],
|
|
110
|
+
["--json", (o) => { o.json = true; }],
|
|
111
|
+
["--reanchor", (o) => { o.reanchor = true; }],
|
|
112
|
+
["--out", (o, next) => { o.out = next(); }],
|
|
113
|
+
["--explain", (o, next) => { o.explain = next(); }],
|
|
114
|
+
]);
|
|
115
|
+
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: "" };
|
|
117
|
+
const cur = { i: 0 };
|
|
118
|
+
const next = (a) => { const v = argv[++cur.i]; if (v === undefined)
|
|
119
|
+
fail(`missing value for ${a}`); return v; };
|
|
120
|
+
for (cur.i = 0; cur.i < argv.length; cur.i++) {
|
|
121
|
+
const a = argv[cur.i];
|
|
122
|
+
if (a === "-h" || a === "--help") {
|
|
123
|
+
process.stdout.write(HELP);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
const handler = OPTIONS.get(a);
|
|
127
|
+
if (handler)
|
|
128
|
+
handler(o, () => next(a));
|
|
129
|
+
else if (a.startsWith("--"))
|
|
130
|
+
fail(`unknown option: ${a}`);
|
|
131
|
+
else
|
|
132
|
+
o.paths.push(a);
|
|
133
|
+
}
|
|
134
|
+
if (!o.plan)
|
|
135
|
+
fail("--plan is required");
|
|
136
|
+
if (!o.paths.length)
|
|
137
|
+
fail("--dir is required");
|
|
138
|
+
// --track optional now: direct mode defaults to console.log (in instrument.ts)
|
|
139
|
+
return o;
|
|
140
|
+
}
|
|
141
|
+
/** fill an LCS length table, `dp[i][j]` = LCS of `A[i:]` and `B[j:]`. */
|
|
142
|
+
function lcsTable(A, B) {
|
|
143
|
+
const n = A.length, m = B.length;
|
|
144
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
145
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
146
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
147
|
+
dp[i][j] = A[i] === B[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return dp;
|
|
151
|
+
}
|
|
152
|
+
/** walk the LCS table into a flat context/-/+ op list. */
|
|
153
|
+
function diffOps(A, B) {
|
|
154
|
+
const dp = lcsTable(A, B);
|
|
155
|
+
const ops = [];
|
|
156
|
+
let i = 0, j = 0;
|
|
157
|
+
while (i < A.length && j < B.length) {
|
|
158
|
+
if (A[i] === B[j]) {
|
|
159
|
+
ops.push({ t: " ", s: A[i] });
|
|
160
|
+
i++;
|
|
161
|
+
j++;
|
|
162
|
+
}
|
|
163
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
164
|
+
ops.push({ t: "-", s: A[i] });
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
ops.push({ t: "+", s: B[j] });
|
|
169
|
+
j++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
while (i < A.length)
|
|
173
|
+
ops.push({ t: "-", s: A[i++] });
|
|
174
|
+
while (j < B.length)
|
|
175
|
+
ops.push({ t: "+", s: B[j++] });
|
|
176
|
+
return ops;
|
|
177
|
+
}
|
|
178
|
+
/** extend a hunk forward from `start` until 2+ trailing context lines. */
|
|
179
|
+
function hunkEnd(ops, start) {
|
|
180
|
+
let end = start, ctx = 0;
|
|
181
|
+
while (end < ops.length && (ops[end].t !== " " || ctx < 2 || (ops[end + 1] && ops[end + 1].t !== " "))) {
|
|
182
|
+
ctx = ops[end].t === " " ? ctx + 1 : 0;
|
|
183
|
+
end++;
|
|
184
|
+
}
|
|
185
|
+
return end;
|
|
186
|
+
}
|
|
187
|
+
/** LCS line diff → unified-ish output with 2 lines of context per hunk */
|
|
188
|
+
function unifiedDiff(file, a, b) {
|
|
189
|
+
const ops = diffOps(a.split("\n"), b.split("\n"));
|
|
190
|
+
const out = [`--- a/${file}`, `+++ b/${file}`];
|
|
191
|
+
let k = 0;
|
|
192
|
+
while (k < ops.length) {
|
|
193
|
+
if (ops[k].t === " ") {
|
|
194
|
+
k++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const from = Math.max(0, k - 2);
|
|
198
|
+
const end = hunkEnd(ops, k);
|
|
199
|
+
out.push("@@");
|
|
200
|
+
for (let x = from; x < end && x < ops.length; x++)
|
|
201
|
+
out.push(ops[x].t + ops[x].s);
|
|
202
|
+
k = end;
|
|
203
|
+
}
|
|
204
|
+
return out.join("\n");
|
|
205
|
+
}
|
|
206
|
+
/** `git status --porcelain`, trimmed. "" on any failure (not a repo, no git on
|
|
207
|
+
* PATH) — like create-react-app's eject, only block when we can positively see
|
|
208
|
+
* uncommitted work. */
|
|
209
|
+
function gitDirty() {
|
|
210
|
+
try {
|
|
211
|
+
return (0, child_process_1.execFileSync)("git", ["status", "--porcelain"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* --apply rewrites source in place; keep it reviewable by refusing on a dirty
|
|
219
|
+
* tree (a bad run is then always one `git checkout` away). --out-dir writes
|
|
220
|
+
* elsewhere, so it's exempt; --allow-dirty opts out.
|
|
221
|
+
*/
|
|
222
|
+
function guardCleanTree(o) {
|
|
223
|
+
if (!o.apply || o.allowDirty)
|
|
224
|
+
return;
|
|
225
|
+
const dirty = gitDirty();
|
|
226
|
+
if (!dirty)
|
|
227
|
+
return;
|
|
228
|
+
process.stderr.write("error: --apply rewrites your source, but git has uncommitted changes:\n\n" +
|
|
229
|
+
`${dirty.split("\n").map((l) => ` ${l}`).join("\n")}\n\n` +
|
|
230
|
+
"Commit or stash them first so the injection lands in its own diff, or pass --allow-dirty.\n");
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
function loadPlan(planPath) {
|
|
234
|
+
let plan;
|
|
235
|
+
try {
|
|
236
|
+
plan = JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
process.stderr.write(`error: cannot read plan ${planPath}: ${(0, util_1.errMsg)(e)}\n`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
if (!plan || !Array.isArray(plan.events)) {
|
|
243
|
+
process.stderr.write(`error: ${planPath} has no "events" array\n`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
return plan;
|
|
247
|
+
}
|
|
248
|
+
function readInputs(paths) {
|
|
249
|
+
const { files, errors } = (0, discover_1.discoverSources)({ paths });
|
|
250
|
+
errors.forEach((e) => process.stderr.write(`warn: ${e}\n`));
|
|
251
|
+
const cwd = process.cwd();
|
|
252
|
+
return files.map((f) => ({
|
|
253
|
+
file: path.relative(cwd, f) || f, abs: f, source: fs.readFileSync(f, "utf8"),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
function printTextReport(r) {
|
|
257
|
+
const added = r.applied.filter((x) => !x.unchanged);
|
|
258
|
+
const unchanged = r.applied.length - added.length;
|
|
259
|
+
r.applied.forEach((x) => process.stdout.write(` ${x.unchanged ? "=" : x.drifted ? "±" : "+"} ${x.event} ${x.file}:${x.line} [${x.mode}]${x.unchanged ? " already instrumented" : ` ${x.call}`}\n`));
|
|
260
|
+
r.delegated.forEach((x) => process.stdout.write(` ~ ${x.event} <${x.element}> @ ${x.ref} → delegated listener\n`));
|
|
261
|
+
r.warnings.forEach((x) => process.stderr.write(` ! ${x.event} drift: ${x.detail}\n`));
|
|
262
|
+
r.skipped.forEach((x) => process.stderr.write(` - ${x.event} skipped: ${x.reason}${x.id ? ` (${x.id})` : ""}\n`));
|
|
263
|
+
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
|
+
}
|
|
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`);
|
|
270
|
+
}
|
|
271
|
+
else if (r.runtimeModule && !o.json) {
|
|
272
|
+
process.stdout.write(`\n--- delegated listener (pass --runtime <file> to write it) ---\n${r.runtimeModule}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function writeChanges(r, o) {
|
|
276
|
+
const changed = r.files.filter((f) => f.before !== f.after);
|
|
277
|
+
if (o.apply || o.outDir) {
|
|
278
|
+
changed.forEach((f) => {
|
|
279
|
+
const dest = o.outDir ? path.join(o.outDir, f.file) : path.resolve(f.file);
|
|
280
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
281
|
+
fs.writeFileSync(dest, f.after);
|
|
282
|
+
process.stdout.write(` wrote ${dest}\n`);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (!o.json) {
|
|
286
|
+
changed.forEach((f) => process.stdout.write("\n" + unifiedDiff(f.file, f.before, f.after) + "\n"));
|
|
287
|
+
if (changed.length)
|
|
288
|
+
process.stdout.write("\n(dry run, pass --apply to write)\n");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function main() {
|
|
292
|
+
const o = parseArgs(process.argv.slice(2));
|
|
293
|
+
guardCleanTree(o);
|
|
294
|
+
const plan = loadPlan(o.plan);
|
|
295
|
+
const inputs = readInputs(o.paths);
|
|
296
|
+
if (o.reanchor) {
|
|
297
|
+
doReanchor(inputs, plan, o);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (o.explain) {
|
|
301
|
+
doExplain(inputs, plan, o);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const r = (0, instrument_1.instrument)(inputs, plan, {
|
|
305
|
+
track: o.track, emit: o.emit, types: o.types, tsconfig: o.tsconfig || undefined,
|
|
306
|
+
});
|
|
307
|
+
if (o.json)
|
|
308
|
+
process.stdout.write(JSON.stringify(r, null, 2) + "\n");
|
|
309
|
+
else
|
|
310
|
+
printTextReport(r);
|
|
311
|
+
const changed = r.files.filter((f) => f.before !== f.after);
|
|
312
|
+
// --check: the committed instrumented tree is out of date, OR the plan no
|
|
313
|
+
// longer resolves cleanly against the source, both are CI failures.
|
|
314
|
+
if (o.check)
|
|
315
|
+
process.exit(changed.length || r.skipped.length ? 1 : 0);
|
|
316
|
+
emitRuntimeModule(r, o);
|
|
317
|
+
writeChanges(r, o);
|
|
318
|
+
}
|
|
319
|
+
const VERDICT_MARK = {
|
|
320
|
+
"always-null": "✗ always null",
|
|
321
|
+
"null-in-this-branch": "✗ null here",
|
|
322
|
+
"present-in-this-branch": "✓ present",
|
|
323
|
+
"maybe-null": "⚠ may be null",
|
|
324
|
+
"resolves": "· ok",
|
|
325
|
+
};
|
|
326
|
+
function doExplain(inputs, plan, o) {
|
|
327
|
+
const r = (0, explain_1.explain)(inputs, plan, o.explain);
|
|
328
|
+
if (o.json) {
|
|
329
|
+
process.stdout.write(JSON.stringify(r, null, 2) + "\n");
|
|
330
|
+
process.exit(r.resolved ? 0 : 1);
|
|
331
|
+
}
|
|
332
|
+
if (!r.resolved) {
|
|
333
|
+
process.stderr.write(` ✗ ${o.explain}\n ${r.reason}\n`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(` ${r.event} ${r.file}:${r.line} <${r.element}> in ${r.component || "(module)"}\n`);
|
|
337
|
+
process.stdout.write(` fires ${r.firesWhen}\n\n`);
|
|
338
|
+
if (!r.props.length)
|
|
339
|
+
process.stdout.write(` (no properties selected for this event)\n`);
|
|
340
|
+
for (const p of r.props) {
|
|
341
|
+
process.stdout.write(` ${VERDICT_MARK[p.verdict]} ${p.prop}\n`);
|
|
342
|
+
for (const why of p.reasons)
|
|
343
|
+
process.stdout.write(` ${why}\n`);
|
|
344
|
+
}
|
|
345
|
+
process.exit(0);
|
|
346
|
+
}
|
|
347
|
+
function doReanchor(inputs, plan, o) {
|
|
348
|
+
const r = (0, instrument_1.reanchor)(inputs, plan, { types: o.types, tsconfig: o.tsconfig || undefined });
|
|
349
|
+
const dest = o.out || o.plan.replace(/\.json$/, "") + ".reanchored.json";
|
|
350
|
+
if (o.json) {
|
|
351
|
+
process.stdout.write(JSON.stringify(r, null, 2) + "\n");
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
r.repointed.forEach((x) => process.stdout.write(` → ${x.event}\n ${x.from}\n ${x.to} (${x.why})\n`));
|
|
355
|
+
r.unresolved.forEach((x) => process.stderr.write(` ✗ ${x.event} ${x.id}\n ${x.reason}\n`));
|
|
356
|
+
process.stdout.write(`\n${r.clean} already fine, ${r.repointed.length} re-pointed, ${r.unresolved.length} unresolved\n`);
|
|
357
|
+
}
|
|
358
|
+
if (r.repointed.length) {
|
|
359
|
+
fs.writeFileSync(dest, JSON.stringify(r.plan, null, 2) + "\n");
|
|
360
|
+
if (!o.json)
|
|
361
|
+
process.stdout.write(`\n wrote ${dest}, review the id changes, then replace your plan\n`);
|
|
362
|
+
}
|
|
363
|
+
else if (!o.json) {
|
|
364
|
+
process.stdout.write(r.unresolved.length ? "\n nothing could be re-pointed automatically\n" : "\n plan is clean, nothing to do\n");
|
|
365
|
+
}
|
|
366
|
+
process.exit(r.unresolved.length ? 1 : 0);
|
|
367
|
+
}
|
|
368
|
+
main();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BuildInput } from "@precedence-dev/cli/build";
|
|
2
|
+
import type { Plan } from "./instrument";
|
|
3
|
+
export type Verdict = "always-null" | "null-in-this-branch" | "present-in-this-branch" | "maybe-null" | "resolves";
|
|
4
|
+
export interface PropExplain {
|
|
5
|
+
prop: string;
|
|
6
|
+
verdict: Verdict;
|
|
7
|
+
reasons: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ExplainReport {
|
|
10
|
+
id: string;
|
|
11
|
+
resolved: boolean;
|
|
12
|
+
reason?: string;
|
|
13
|
+
event?: string;
|
|
14
|
+
file?: string;
|
|
15
|
+
line?: number;
|
|
16
|
+
component?: string;
|
|
17
|
+
element?: string;
|
|
18
|
+
firesWhen?: string;
|
|
19
|
+
props: PropExplain[];
|
|
20
|
+
}
|
|
21
|
+
export declare function explain(inputs: BuildInput[], plan: Plan, id: string): ExplainReport;
|