@invariant-ui/cli 0.0.0-stage → 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 +248 -2
- package/dist/agent-kit/apply.js +232 -0
- package/dist/agent-kit/guidance.js +236 -0
- package/dist/agent-kit/index.js +5 -0
- package/dist/agent-kit/integrations.js +121 -0
- package/dist/agent-kit/plugin.js +115 -0
- package/dist/artifact-generator/index.js +75 -0
- package/dist/browser-verify/driver.js +2 -0
- package/dist/browser-verify/index.js +4 -0
- package/dist/browser-verify/playwright.js +82 -0
- package/dist/browser-verify/runner.js +175 -0
- package/dist/browser-verify/spec.js +103 -0
- package/dist/cli/args.js +77 -0
- package/dist/cli/baseline.js +64 -0
- package/dist/cli/cli.js +121 -0
- package/dist/cli/commands-browser.js +104 -0
- package/dist/cli/commands-discover.js +312 -0
- package/dist/cli/commands-enforcement.js +35 -0
- package/dist/cli/commands-governance.js +179 -0
- package/dist/cli/commands-hook.js +269 -0
- package/dist/cli/commands-patterns.js +324 -0
- package/dist/cli/commands-setup.js +485 -0
- package/dist/cli/commands-validate.js +157 -0
- package/dist/cli/commands.js +517 -0
- package/dist/cli/config.js +197 -0
- package/dist/cli/diff.js +66 -0
- package/dist/cli/enforcement-log.js +83 -0
- package/dist/cli/enforcement-report.js +96 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/format.js +22 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/io.js +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/cli/pipeline.js +262 -0
- package/dist/cli/publish.js +77 -0
- package/dist/cli/report.js +127 -0
- package/dist/cli/starter-rules.js +57 -0
- package/dist/cli/store.js +60 -0
- package/dist/cli/version.js +5 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli/workflow.js +52 -0
- package/dist/compiler-angular/adapter.js +115 -0
- package/dist/compiler-angular/analyze.js +222 -0
- package/dist/compiler-angular/classes.js +446 -0
- package/dist/compiler-angular/declared.js +285 -0
- package/dist/compiler-angular/emit.js +183 -0
- package/dist/compiler-angular/index.js +4 -0
- package/dist/compiler-angular/template.js +142 -0
- package/dist/compiler-core/adapter.js +2 -0
- package/dist/compiler-core/adapters/inventory.js +312 -0
- package/dist/compiler-core/compile.js +148 -0
- package/dist/compiler-core/component-facts.js +123 -0
- package/dist/compiler-core/discovery/docs.js +66 -0
- package/dist/compiler-core/discovery/entries.js +270 -0
- package/dist/compiler-core/discovery/exports.js +207 -0
- package/dist/compiler-core/discovery/graph.js +59 -0
- package/dist/compiler-core/discovery/index.js +103 -0
- package/dist/compiler-core/discovery/packages.js +148 -0
- package/dist/compiler-core/discovery/schema.js +144 -0
- package/dist/compiler-core/discovery/storybook.js +232 -0
- package/dist/compiler-core/discovery/support.js +82 -0
- package/dist/compiler-core/discovery/tokens.js +186 -0
- package/dist/compiler-core/discovery/tooling.js +146 -0
- package/dist/compiler-core/fs.js +89 -0
- package/dist/compiler-core/glob.js +3 -0
- package/dist/compiler-core/index.js +16 -0
- package/dist/compiler-core/ir-builder.js +68 -0
- package/dist/compiler-core/json-schema.js +11 -0
- package/dist/compiler-core/testing/index.js +81 -0
- package/dist/compiler-core/ts/ast.js +166 -0
- package/dist/compiler-core/ts/members.js +163 -0
- package/dist/compiler-core/ts/program.js +81 -0
- package/dist/compiler-core/ts/static-eval.js +145 -0
- package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
- package/dist/compiler-core/walk.js +46 -0
- package/dist/compiler-ir/index.js +4 -0
- package/dist/compiler-ir/ir.js +67 -0
- package/dist/compiler-ir/json-schema.js +6 -0
- package/dist/compiler-ir/validate.js +116 -0
- package/dist/compiler-react/adapter.js +147 -0
- package/dist/compiler-react/analyze.js +199 -0
- package/dist/compiler-react/ast.js +105 -0
- package/dist/compiler-react/behavior.js +75 -0
- package/dist/compiler-react/detect.js +558 -0
- package/dist/compiler-react/emit.js +188 -0
- package/dist/compiler-react/index.js +2 -0
- package/dist/compiler-react/program.js +58 -0
- package/dist/compiler-react/props.js +157 -0
- package/dist/compiler-react/styling.js +138 -0
- package/dist/compiler-react/usage.js +116 -0
- package/dist/compiler-react/variants.js +111 -0
- package/dist/compiler-storybook/adapter.js +253 -0
- package/dist/compiler-storybook/csf.js +521 -0
- package/dist/compiler-storybook/emit.js +202 -0
- package/dist/compiler-storybook/index-json.js +41 -0
- package/dist/compiler-storybook/index.js +5 -0
- package/dist/compiler-storybook/mdx.js +120 -0
- package/dist/compiler-storybook/static-eval.js +145 -0
- package/dist/compiler-tokens/adapter.js +158 -0
- package/dist/compiler-tokens/css.js +59 -0
- package/dist/compiler-tokens/index.js +5 -0
- package/dist/compiler-tokens/json-lines.js +106 -0
- package/dist/compiler-tokens/json-tokens.js +52 -0
- package/dist/compiler-vue/adapter.js +107 -0
- package/dist/compiler-vue/analyze.js +344 -0
- package/dist/compiler-vue/declared.js +343 -0
- package/dist/compiler-vue/emit.js +129 -0
- package/dist/compiler-vue/index.js +5 -0
- package/dist/compiler-vue/script.js +472 -0
- package/dist/compiler-vue/sfc.js +44 -0
- package/dist/compiler-vue/template.js +73 -0
- package/dist/contract/archetype-checks.js +153 -0
- package/dist/contract/components.js +102 -0
- package/dist/contract/conflicts.js +39 -0
- package/dist/contract/contract.js +46 -0
- package/dist/contract/evidence.js +84 -0
- package/dist/contract/fact-path.js +39 -0
- package/dist/contract/facts.js +62 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/integrity.js +21 -0
- package/dist/contract/json-schema.js +11 -0
- package/dist/contract/knowledge.js +95 -0
- package/dist/contract/pattern-checks.js +254 -0
- package/dist/contract/patterns.js +225 -0
- package/dist/contract/primitives.js +81 -0
- package/dist/contract/sources.js +30 -0
- package/dist/contract/tokens.js +41 -0
- package/dist/contract/validate.js +253 -0
- package/dist/contract-query/archetype.js +188 -0
- package/dist/contract-query/index.js +6 -0
- package/dist/contract-query/patterns.js +298 -0
- package/dist/contract-query/scan.js +164 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/validate-code.js +239 -0
- package/dist/contract-query/validate-usage.js +150 -0
- package/dist/contract-query/views.js +288 -0
- package/dist/governance/decisions.js +229 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/patterns-file.js +156 -0
- package/dist/governance/patterns.js +510 -0
- package/dist/governance/policies.js +148 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/loader.js +70 -0
- package/dist/mcp/protocol.js +27 -0
- package/dist/mcp/resources.js +138 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/stdio.js +28 -0
- package/dist/mcp/tools.js +267 -0
- package/dist/reconciliation/code.js +62 -0
- package/dist/reconciliation/components.js +401 -0
- package/dist/reconciliation/conflicts.js +126 -0
- package/dist/reconciliation/facts.js +41 -0
- package/dist/reconciliation/index.js +7 -0
- package/dist/reconciliation/reconcile.js +134 -0
- package/dist/reconciliation/resolve.js +158 -0
- package/dist/reconciliation/review.js +81 -0
- package/dist/reconciliation/storybook.js +370 -0
- package/dist/reconciliation/tokens.js +179 -0
- package/dist/reconciliation/util.js +55 -0
- package/dist/semantic-engine/additive.js +51 -0
- package/dist/semantic-engine/compounds.js +91 -0
- package/dist/semantic-engine/documents.js +157 -0
- package/dist/semantic-engine/enrich.js +42 -0
- package/dist/semantic-engine/index.js +7 -0
- package/dist/semantic-engine/infer.js +38 -0
- package/dist/semantic-engine/tokens.js +120 -0
- package/dist/shared/canonical.js +65 -0
- package/dist/shared/diagnostic.js +78 -0
- package/dist/shared/glob.js +98 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/json.js +2 -0
- package/dist/validation/card.js +50 -0
- package/dist/validation/discovery/arrangements.js +162 -0
- package/dist/validation/discovery/discover.js +70 -0
- package/dist/validation/discovery/index.js +8 -0
- package/dist/validation/discovery/model.js +127 -0
- package/dist/validation/discovery/promote.js +24 -0
- package/dist/validation/discovery/reviews.js +63 -0
- package/dist/validation/discovery/shapes.js +47 -0
- package/dist/validation/discovery/similar-files.js +146 -0
- package/dist/validation/discovery/util.js +47 -0
- package/dist/validation/index.js +11 -0
- package/dist/validation/interaction-facts.js +341 -0
- package/dist/validation/interaction-workflows.js +633 -0
- package/dist/validation/literals.js +251 -0
- package/dist/validation/pattern-rules.js +608 -0
- package/dist/validation/policy.js +241 -0
- package/dist/validation/report.js +163 -0
- package/dist/validation/rules.js +269 -0
- package/dist/validation/scan.js +295 -0
- package/dist/validation/text.js +34 -0
- package/dist/validation/validate-code.js +513 -0
- package/dist/validation/validate-usage.js +196 -0
- package/package.json +38 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { walk } from "./shapes.js";
|
|
2
|
+
import { candidateId, capEvidence, listNames, MAX_VARIABLE, round3, slug, uniqueName } from "./util.js";
|
|
3
|
+
/** A file with fewer design-system features than this is too small to say anything about its structure. */
|
|
4
|
+
export const MIN_FEATURES = 4;
|
|
5
|
+
/** A feature in at least this share of a group, but not all of it, is reported as frequent. */
|
|
6
|
+
const FREQUENT_SHARE = 0.8;
|
|
7
|
+
const bare = (id) => id.replace(/^component:/, "");
|
|
8
|
+
/** What a file contains: its top-level design-system elements and each element-inside-element pair. */
|
|
9
|
+
function featuresOf(shape) {
|
|
10
|
+
const out = new Set();
|
|
11
|
+
for (const r of shape.roots)
|
|
12
|
+
out.add(`^${bare(r.id)}`);
|
|
13
|
+
for (const n of walk(shape.roots))
|
|
14
|
+
for (const c of n.children)
|
|
15
|
+
out.add(`${bare(n.id)} > ${bare(c.id)}`);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function jaccard(a, b) {
|
|
19
|
+
let both = 0;
|
|
20
|
+
for (const x of a)
|
|
21
|
+
if (b.has(x))
|
|
22
|
+
both++;
|
|
23
|
+
const all = a.size + b.size - both;
|
|
24
|
+
return all === 0 ? 0 : both / all;
|
|
25
|
+
}
|
|
26
|
+
const idsIn = (feature) => feature.startsWith("^") ? [feature.slice(1)] : feature.split(" > ");
|
|
27
|
+
/**
|
|
28
|
+
* Method: overlap of structure. Each file is described by the design-system elements at its top level and the
|
|
29
|
+
* pairs of elements nested in each other. Files are put in a group when every pair in it overlaps by at least the
|
|
30
|
+
* threshold, taking files in path order, so the result does not depend on how they were listed. A group says its
|
|
31
|
+
* files are built from much the same parts. It does not say they do the same job.
|
|
32
|
+
*/
|
|
33
|
+
export function findSimilarFiles(files, options) {
|
|
34
|
+
const members = files
|
|
35
|
+
.map((shape) => ({ shape, features: featuresOf(shape) }))
|
|
36
|
+
.filter((m) => m.features.size >= MIN_FEATURES)
|
|
37
|
+
.sort((a, b) => (a.shape.file < b.shape.file ? -1 : a.shape.file > b.shape.file ? 1 : 0));
|
|
38
|
+
const clusters = [];
|
|
39
|
+
for (const m of members) {
|
|
40
|
+
const home = clusters.find((c) => c.every((o) => jaccard(m.features, o.features) >= options.similarity));
|
|
41
|
+
if (home)
|
|
42
|
+
home.push(m);
|
|
43
|
+
else
|
|
44
|
+
clusters.push([m]);
|
|
45
|
+
}
|
|
46
|
+
const names = new Map();
|
|
47
|
+
for (const f of files)
|
|
48
|
+
for (const n of walk(f.roots))
|
|
49
|
+
names.set(bare(n.id), n.name);
|
|
50
|
+
const taken = new Set();
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const cluster of clusters.filter((c) => c.length >= options.minFiles)) {
|
|
53
|
+
const total = cluster.length;
|
|
54
|
+
const counts = new Map();
|
|
55
|
+
for (const m of cluster)
|
|
56
|
+
for (const f of m.features)
|
|
57
|
+
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
58
|
+
const sorted = [...counts.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
59
|
+
const common = sorted.filter(([, n]) => n === total).map(([f]) => f);
|
|
60
|
+
const frequent = sorted
|
|
61
|
+
.filter(([, n]) => n < total && n / total >= FREQUENT_SHARE)
|
|
62
|
+
.map(([feature, files]) => ({ feature, files }));
|
|
63
|
+
const rest = sorted
|
|
64
|
+
.filter(([, n]) => n / total < FREQUENT_SHARE)
|
|
65
|
+
.map(([feature, files]) => ({ feature, files }))
|
|
66
|
+
.sort((a, b) => b.files - a.files || (a.feature < b.feature ? -1 : 1));
|
|
67
|
+
const pairs = [];
|
|
68
|
+
for (let i = 0; i < total; i++)
|
|
69
|
+
for (let j = i + 1; j < total; j++)
|
|
70
|
+
pairs.push(jaccard(cluster[i].features, cluster[j].features));
|
|
71
|
+
const key = common.join("|");
|
|
72
|
+
const rolesById = new Map();
|
|
73
|
+
for (const f of common)
|
|
74
|
+
for (const id of idsIn(f))
|
|
75
|
+
rolesById.set(id, "required");
|
|
76
|
+
for (const { feature } of frequent)
|
|
77
|
+
for (const id of idsIn(feature))
|
|
78
|
+
if (!rolesById.has(id))
|
|
79
|
+
rolesById.set(id, "optional");
|
|
80
|
+
const used = new Set();
|
|
81
|
+
const parts = [...rolesById.entries()]
|
|
82
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
83
|
+
.map(([id, role]) => ({
|
|
84
|
+
name: uniqueName(slug(names.get(id) ?? id), used),
|
|
85
|
+
ref: `component:${id}`,
|
|
86
|
+
role
|
|
87
|
+
}));
|
|
88
|
+
if (parts.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
const named = [...rolesById.keys()].sort().map((id) => names.get(id) ?? id);
|
|
91
|
+
const topNames = common.filter((f) => f.startsWith("^")).map((f) => names.get(f.slice(1)) ?? f.slice(1));
|
|
92
|
+
const label = listNames(topNames.length > 0 ? topNames : named);
|
|
93
|
+
const evidence = cluster.map((m) => {
|
|
94
|
+
const first = m.shape.roots[0];
|
|
95
|
+
return {
|
|
96
|
+
file: m.shape.file,
|
|
97
|
+
line: first.line,
|
|
98
|
+
column: first.column,
|
|
99
|
+
detail: `${m.features.size} design-system features, ${common.length} shared by the whole group`
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const capped = capEvidence(evidence);
|
|
103
|
+
const minimum = Math.min(...pairs);
|
|
104
|
+
const mean = pairs.reduce((a, b) => a + b, 0) / pairs.length;
|
|
105
|
+
out.push({
|
|
106
|
+
id: candidateId("similar-files", key, taken),
|
|
107
|
+
kind: "similar-files",
|
|
108
|
+
status: "proposed",
|
|
109
|
+
basis: "inferred",
|
|
110
|
+
equivalence: "not-established",
|
|
111
|
+
suggestedName: `Files built from ${label}`,
|
|
112
|
+
nameSource: "derived",
|
|
113
|
+
summary: `${total} files built from much the same design-system parts: ${listNames(named)}.`,
|
|
114
|
+
explanation: [
|
|
115
|
+
`Each file was described by the design-system elements at its top level and the pairs of elements nested in each other. These ${total} files were grouped because every pair of them overlaps by at least ${options.similarity}; the lowest overlap in the group is ${round3(minimum)}.`,
|
|
116
|
+
`${common.length} features are in every file, ${frequent.length} in at least ${Math.round(FREQUENT_SHARE * 100)}% of them, and ${rest.length} in fewer.`,
|
|
117
|
+
"Overlap of structure is measured here. It is not evidence that the files serve the same purpose or that one should look like another.",
|
|
118
|
+
"Files are grouped in path order, one group at a time, so a group depends on the files present."
|
|
119
|
+
],
|
|
120
|
+
support: { files: total, occurrences: total },
|
|
121
|
+
consistency: { minimumPairwise: round3(minimum), meanPairwise: round3(mean) },
|
|
122
|
+
structure: {
|
|
123
|
+
key,
|
|
124
|
+
common,
|
|
125
|
+
frequent,
|
|
126
|
+
variable: rest.slice(0, MAX_VARIABLE),
|
|
127
|
+
variableTruncated: rest.length > MAX_VARIABLE
|
|
128
|
+
},
|
|
129
|
+
evidence: capped.evidence,
|
|
130
|
+
evidenceTruncated: capped.truncated,
|
|
131
|
+
definition: {
|
|
132
|
+
name: `Files built from ${label}`,
|
|
133
|
+
category: "composition",
|
|
134
|
+
description: `The design-system parts shared by ${total} similar files, found by invariant discover. Parts in every file are required; parts in most are optional. Review before relying on it.`,
|
|
135
|
+
composition: { parts }
|
|
136
|
+
},
|
|
137
|
+
derivation: {
|
|
138
|
+
method: "complete-linkage-overlap",
|
|
139
|
+
version: 1,
|
|
140
|
+
parameters: { minFiles: options.minFiles, similarity: options.similarity, minFeatures: MIN_FEATURES }
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return out.sort((a, b) => b.support.files - a.support.files || (a.structure.key < b.structure.key ? -1 : 1));
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=similar-files.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { sha256Hex } from "../../shared/index.js";
|
|
2
|
+
export const MAX_EVIDENCE = 25;
|
|
3
|
+
export const MAX_VARIABLE = 30;
|
|
4
|
+
/** A stable id from what the candidate is, so the same finding keeps its id from run to run and reviews stay attached. */
|
|
5
|
+
export function candidateId(kind, key, taken) {
|
|
6
|
+
const base = `candidate:${kind}.${sha256Hex(`${kind}\n${key}`).slice(0, 8)}`;
|
|
7
|
+
let id = base;
|
|
8
|
+
for (let n = 2; taken.has(id); n++)
|
|
9
|
+
id = `${base}-${n}`;
|
|
10
|
+
taken.add(id);
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
/** `CardHeader` and `Card.Header` become `card-header`: a legal part name, readable, and the same every time. */
|
|
14
|
+
export function slug(name) {
|
|
15
|
+
const out = name
|
|
16
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
19
|
+
.replace(/^[^a-z0-9]+/, "")
|
|
20
|
+
.replace(/-+$/, "");
|
|
21
|
+
return (out === "" ? "part" : out).slice(0, 56);
|
|
22
|
+
}
|
|
23
|
+
/** Part names must be unique in a pattern; a repeat gets a number. */
|
|
24
|
+
export function uniqueName(base, used) {
|
|
25
|
+
let name = base;
|
|
26
|
+
for (let n = 2; used.has(name); n++)
|
|
27
|
+
name = `${base}-${n}`;
|
|
28
|
+
used.add(name);
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
export const byPosition = (a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.column - b.column;
|
|
32
|
+
/** Evidence capped to a fixed number, always the same ones (the first in file order), with a flag saying so. */
|
|
33
|
+
export function capEvidence(all) {
|
|
34
|
+
const sorted = [...all].sort(byPosition);
|
|
35
|
+
return { evidence: sorted.slice(0, MAX_EVIDENCE), truncated: sorted.length > MAX_EVIDENCE };
|
|
36
|
+
}
|
|
37
|
+
export const round3 = (n) => Math.round(n * 1000) / 1000;
|
|
38
|
+
export function listNames(names) {
|
|
39
|
+
if (names.length <= 1)
|
|
40
|
+
return names.join("");
|
|
41
|
+
const shown = names.slice(0, 3);
|
|
42
|
+
const more = names.length - shown.length;
|
|
43
|
+
const head = shown.slice(0, -1).join(", ");
|
|
44
|
+
const last = shown[shown.length - 1];
|
|
45
|
+
return more > 0 ? `${shown.join(", ")} and ${more} more` : `${head} and ${last}`;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=util.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { CARD_FINDINGS_LIMIT, enforcementCard, passCard } from "./card.js";
|
|
2
|
+
export * from "./discovery/index.js";
|
|
3
|
+
export { evaluatePatternRules, PATTERN_RULE_CODES, patternRules } from "./pattern-rules.js";
|
|
4
|
+
export { activePolicies, evaluateUse, policyRuleSchema, relationshipRules } from "./policy.js";
|
|
5
|
+
export { formatGithub, formatText, patternSummaryText, shouldFail, sortEvaluations, sortViolations, summarize, toJson, toSarif } from "./report.js";
|
|
6
|
+
export { PLAN_RULES, RULES, ruleFor } from "./rules.js";
|
|
7
|
+
export { scanJsx, scanMarkup } from "./scan.js";
|
|
8
|
+
export { distance, nearest } from "./text.js";
|
|
9
|
+
export { buildTree, defaultFramework, MAX_CODE_CHARS, validateCode } from "./validate-code.js";
|
|
10
|
+
export { isDynamic, validateUsage } from "./validate-usage.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
const KNOWN_HOOKS = new Set([
|
|
3
|
+
"useState",
|
|
4
|
+
"useReducer",
|
|
5
|
+
"useEffect",
|
|
6
|
+
"useLayoutEffect",
|
|
7
|
+
"useRef",
|
|
8
|
+
"useMemo",
|
|
9
|
+
"useCallback",
|
|
10
|
+
"useContext",
|
|
11
|
+
"useId",
|
|
12
|
+
"useTransition",
|
|
13
|
+
"useDeferredValue",
|
|
14
|
+
"useFormStatus",
|
|
15
|
+
"useActionState",
|
|
16
|
+
"useFormState",
|
|
17
|
+
"useImperativeHandle",
|
|
18
|
+
"useQuery",
|
|
19
|
+
"useMutation",
|
|
20
|
+
"useSWR",
|
|
21
|
+
"useForm",
|
|
22
|
+
"useNavigate",
|
|
23
|
+
"useRouter"
|
|
24
|
+
]);
|
|
25
|
+
export const lineOf = (sf, node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
26
|
+
export const unwrap = (node) => {
|
|
27
|
+
let n = node;
|
|
28
|
+
while (ts.isParenthesizedExpression(n) ||
|
|
29
|
+
ts.isAsExpression(n) ||
|
|
30
|
+
ts.isNonNullExpression(n) ||
|
|
31
|
+
ts.isSatisfiesExpression(n))
|
|
32
|
+
n = n.expression;
|
|
33
|
+
return n;
|
|
34
|
+
};
|
|
35
|
+
export function bindingNames(name) {
|
|
36
|
+
if (ts.isIdentifier(name))
|
|
37
|
+
return [name.text];
|
|
38
|
+
return name.elements.flatMap((el) => (ts.isOmittedExpression(el) ? [] : bindingNames(el.name)));
|
|
39
|
+
}
|
|
40
|
+
const isFunction = (n) => ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n);
|
|
41
|
+
export function argKind(node) {
|
|
42
|
+
if (!node)
|
|
43
|
+
return "other";
|
|
44
|
+
const e = unwrap(node);
|
|
45
|
+
if (e.kind === ts.SyntaxKind.TrueKeyword)
|
|
46
|
+
return "true";
|
|
47
|
+
if (e.kind === ts.SyntaxKind.FalseKeyword)
|
|
48
|
+
return "false";
|
|
49
|
+
if (e.kind === ts.SyntaxKind.NullKeyword)
|
|
50
|
+
return "null";
|
|
51
|
+
if (ts.isStringLiteralLike(e) && e.text === "")
|
|
52
|
+
return "empty";
|
|
53
|
+
return "other";
|
|
54
|
+
}
|
|
55
|
+
function calleeNames(e) {
|
|
56
|
+
const c = unwrap(e);
|
|
57
|
+
if (ts.isIdentifier(c))
|
|
58
|
+
return { name: c.text, full: c.text };
|
|
59
|
+
if (ts.isPropertyAccessExpression(c)) {
|
|
60
|
+
const inner = calleeNames(c.expression);
|
|
61
|
+
return { name: c.name.text, full: `${inner?.full ?? "?"}.${c.name.text}` };
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/** What a function does, in the order it is written. Nested functions are read only as promise callbacks. */
|
|
66
|
+
export function analyzeFunction(sf, fn, name) {
|
|
67
|
+
const info = {
|
|
68
|
+
...(name ? { name } : {}),
|
|
69
|
+
line: lineOf(sf, fn),
|
|
70
|
+
setters: [],
|
|
71
|
+
calls: [],
|
|
72
|
+
guards: [],
|
|
73
|
+
hasCatch: false
|
|
74
|
+
};
|
|
75
|
+
const body = fn.body;
|
|
76
|
+
if (!body)
|
|
77
|
+
return info;
|
|
78
|
+
const catchRanges = [];
|
|
79
|
+
const finallyRanges = [];
|
|
80
|
+
const within = (ranges, pos) => ranges.some(([a, b]) => pos >= a && pos < b);
|
|
81
|
+
const markAwait = (pos) => {
|
|
82
|
+
if (info.awaitPos === undefined || pos < info.awaitPos)
|
|
83
|
+
info.awaitPos = pos;
|
|
84
|
+
};
|
|
85
|
+
// First pass: where the awaits, catches and finallys are, so the second pass can classify every call.
|
|
86
|
+
const scan = (n, top) => {
|
|
87
|
+
if (!top && isFunction(n) && !isPromiseCallback(n))
|
|
88
|
+
return;
|
|
89
|
+
if (ts.isAwaitExpression(n))
|
|
90
|
+
markAwait(n.getStart(sf));
|
|
91
|
+
if (ts.isTryStatement(n)) {
|
|
92
|
+
if (n.catchClause) {
|
|
93
|
+
info.hasCatch = true;
|
|
94
|
+
catchRanges.push([n.catchClause.getStart(sf), n.catchClause.getEnd()]);
|
|
95
|
+
}
|
|
96
|
+
if (n.finallyBlock)
|
|
97
|
+
finallyRanges.push([n.finallyBlock.getStart(sf), n.finallyBlock.getEnd()]);
|
|
98
|
+
}
|
|
99
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
100
|
+
const m = n.expression.name.text;
|
|
101
|
+
if (m === "then" || m === "catch" || m === "finally") {
|
|
102
|
+
markAwait(n.expression.name.getStart(sf));
|
|
103
|
+
if (m === "catch") {
|
|
104
|
+
info.hasCatch = true;
|
|
105
|
+
for (const a of n.arguments)
|
|
106
|
+
catchRanges.push([a.getStart(sf), a.getEnd()]);
|
|
107
|
+
}
|
|
108
|
+
if (m === "finally")
|
|
109
|
+
for (const a of n.arguments)
|
|
110
|
+
finallyRanges.push([a.getStart(sf), a.getEnd()]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
ts.forEachChild(n, (c) => scan(c, false));
|
|
114
|
+
};
|
|
115
|
+
scan(body, true);
|
|
116
|
+
const walk = (n, top) => {
|
|
117
|
+
if (!top && isFunction(n) && !isPromiseCallback(n))
|
|
118
|
+
return;
|
|
119
|
+
if (ts.isCallExpression(n)) {
|
|
120
|
+
const callee = calleeNames(n.expression);
|
|
121
|
+
if (callee) {
|
|
122
|
+
const pos = n.getStart(sf);
|
|
123
|
+
const inCatch = within(catchRanges, pos);
|
|
124
|
+
if (/^set[A-Z0-9_]/.test(callee.name) && callee.name === callee.full)
|
|
125
|
+
info.setters.push({
|
|
126
|
+
setter: callee.name,
|
|
127
|
+
arg: argKind(n.arguments[0]),
|
|
128
|
+
pos,
|
|
129
|
+
line: lineOf(sf, n),
|
|
130
|
+
inCatch,
|
|
131
|
+
inFinally: within(finallyRanges, pos)
|
|
132
|
+
});
|
|
133
|
+
else
|
|
134
|
+
info.calls.push({ name: callee.name, full: callee.full, pos, line: lineOf(sf, n), inCatch });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (top && ts.isBlock(n))
|
|
138
|
+
for (const stmt of n.statements) {
|
|
139
|
+
// `if (x) return` near the top: a guard against running twice.
|
|
140
|
+
if (ts.isIfStatement(stmt) && (isReturn(stmt.thenStatement) || returnsInBlock(stmt.thenStatement)))
|
|
141
|
+
collectIdentifiers(stmt.expression, info.guards);
|
|
142
|
+
}
|
|
143
|
+
ts.forEachChild(n, (c) => walk(c, false));
|
|
144
|
+
};
|
|
145
|
+
walk(body, true);
|
|
146
|
+
return info;
|
|
147
|
+
}
|
|
148
|
+
function isReturn(n) {
|
|
149
|
+
return ts.isReturnStatement(n);
|
|
150
|
+
}
|
|
151
|
+
function returnsInBlock(n) {
|
|
152
|
+
return (ts.isBlock(n) &&
|
|
153
|
+
n.statements.length > 0 &&
|
|
154
|
+
n.statements.every((s) => ts.isReturnStatement(s) || ts.isExpressionStatement(s)) &&
|
|
155
|
+
n.statements.some((s) => ts.isReturnStatement(s)));
|
|
156
|
+
}
|
|
157
|
+
function collectIdentifiers(n, into) {
|
|
158
|
+
if (ts.isIdentifier(n))
|
|
159
|
+
into.push(n.text);
|
|
160
|
+
ts.forEachChild(n, (c) => collectIdentifiers(c, into));
|
|
161
|
+
}
|
|
162
|
+
/** An arrow or function passed to `.then`, `.catch` or `.finally` is part of the same flow as its caller. */
|
|
163
|
+
function isPromiseCallback(n) {
|
|
164
|
+
const p = n.parent;
|
|
165
|
+
return (p !== undefined &&
|
|
166
|
+
ts.isCallExpression(p) &&
|
|
167
|
+
ts.isPropertyAccessExpression(p.expression) &&
|
|
168
|
+
["then", "catch", "finally"].includes(p.expression.name.text));
|
|
169
|
+
}
|
|
170
|
+
/** Identifiers, and `obj.prop` pairs, read in the JSX of a subtree. */
|
|
171
|
+
export function consumedIn(root) {
|
|
172
|
+
const out = new Set();
|
|
173
|
+
const collect = (n) => {
|
|
174
|
+
if (ts.isIdentifier(n))
|
|
175
|
+
out.add(n.text);
|
|
176
|
+
if (ts.isPropertyAccessExpression(n) && ts.isIdentifier(n.expression))
|
|
177
|
+
out.add(`${n.expression.text}.${n.name.text}`);
|
|
178
|
+
ts.forEachChild(n, collect);
|
|
179
|
+
};
|
|
180
|
+
const visit = (n) => {
|
|
181
|
+
if (ts.isJsxExpression(n) || ts.isJsxAttribute(n) || ts.isJsxSpreadAttribute(n))
|
|
182
|
+
collect(n);
|
|
183
|
+
else
|
|
184
|
+
ts.forEachChild(n, visit);
|
|
185
|
+
};
|
|
186
|
+
visit(root);
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
export function enclosingFunctions(node) {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (let p = node.parent; p; p = p.parent)
|
|
192
|
+
if (isFunction(p) || ts.isMethodDeclaration(p))
|
|
193
|
+
out.push(p);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
/** Reads the declarations of one function: state, hook results, handlers, effects and parameters. */
|
|
197
|
+
export function buildScope(sf, fn, parent) {
|
|
198
|
+
const scope = {
|
|
199
|
+
fn,
|
|
200
|
+
parent,
|
|
201
|
+
params: new Set(),
|
|
202
|
+
states: new Map(),
|
|
203
|
+
hookNames: new Map(),
|
|
204
|
+
hookObjects: new Map(),
|
|
205
|
+
unknownHooks: [],
|
|
206
|
+
handlers: new Map(),
|
|
207
|
+
effects: [],
|
|
208
|
+
consumed: consumedIn(fn)
|
|
209
|
+
};
|
|
210
|
+
const f = fn;
|
|
211
|
+
for (const p of f.parameters ?? [])
|
|
212
|
+
for (const n of bindingNames(p.name))
|
|
213
|
+
scope.params.add(n);
|
|
214
|
+
const body = f.body;
|
|
215
|
+
if (!body || !ts.isBlock(body))
|
|
216
|
+
return scope;
|
|
217
|
+
for (const stmt of body.statements) {
|
|
218
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name)
|
|
219
|
+
scope.handlers.set(stmt.name.text, analyzeFunction(sf, stmt, stmt.name.text));
|
|
220
|
+
if (ts.isExpressionStatement(stmt) && ts.isCallExpression(unwrap(stmt.expression))) {
|
|
221
|
+
const call = unwrap(stmt.expression);
|
|
222
|
+
if (ts.isIdentifier(call.expression) &&
|
|
223
|
+
(call.expression.text === "useEffect" || call.expression.text === "useLayoutEffect")) {
|
|
224
|
+
const cb = call.arguments[0];
|
|
225
|
+
if (cb && isFunction(cb)) {
|
|
226
|
+
scope.effects.push(analyzeFunction(sf, cb));
|
|
227
|
+
// An effect usually declares the async function it then calls; read those too.
|
|
228
|
+
const inner = cb.body;
|
|
229
|
+
if (inner && ts.isBlock(inner))
|
|
230
|
+
for (const s of inner.statements) {
|
|
231
|
+
if (ts.isFunctionDeclaration(s))
|
|
232
|
+
scope.effects.push(analyzeFunction(sf, s, s.name?.text));
|
|
233
|
+
if (ts.isVariableStatement(s))
|
|
234
|
+
for (const d of s.declarationList.declarations) {
|
|
235
|
+
const init = d.initializer && unwrap(d.initializer);
|
|
236
|
+
if (init && isFunction(init))
|
|
237
|
+
scope.effects.push(analyzeFunction(sf, init));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!ts.isVariableStatement(stmt))
|
|
244
|
+
continue;
|
|
245
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
246
|
+
const init = decl.initializer ? unwrap(decl.initializer) : undefined;
|
|
247
|
+
if (!init)
|
|
248
|
+
continue;
|
|
249
|
+
if (isFunction(init) && ts.isIdentifier(decl.name)) {
|
|
250
|
+
scope.handlers.set(decl.name.text, analyzeFunction(sf, init, decl.name.text));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!ts.isCallExpression(init) || !ts.isIdentifier(init.expression))
|
|
254
|
+
continue;
|
|
255
|
+
const hook = init.expression.text;
|
|
256
|
+
if (hook === "useCallback" && ts.isIdentifier(decl.name)) {
|
|
257
|
+
const cb = init.arguments[0] && unwrap(init.arguments[0]);
|
|
258
|
+
if (cb && isFunction(cb))
|
|
259
|
+
scope.handlers.set(decl.name.text, analyzeFunction(sf, cb, decl.name.text));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (hook === "useState" && ts.isArrayBindingPattern(decl.name)) {
|
|
263
|
+
const [a, b] = decl.name.elements;
|
|
264
|
+
if (a &&
|
|
265
|
+
b &&
|
|
266
|
+
!ts.isOmittedExpression(a) &&
|
|
267
|
+
!ts.isOmittedExpression(b) &&
|
|
268
|
+
ts.isIdentifier(a.name) &&
|
|
269
|
+
ts.isIdentifier(b.name)) {
|
|
270
|
+
const arg = init.arguments[0] && unwrap(init.arguments[0]);
|
|
271
|
+
const kind = argKind(init.arguments[0]);
|
|
272
|
+
scope.states.set(a.name.text, {
|
|
273
|
+
name: a.name.text,
|
|
274
|
+
setter: b.name.text,
|
|
275
|
+
init: kind !== "other"
|
|
276
|
+
? kind
|
|
277
|
+
: arg && ts.isArrayLiteralExpression(arg)
|
|
278
|
+
? "array"
|
|
279
|
+
: arg && ts.isStringLiteralLike(arg)
|
|
280
|
+
? "string"
|
|
281
|
+
: arg && ts.isNumericLiteral(arg)
|
|
282
|
+
? "number"
|
|
283
|
+
: "other",
|
|
284
|
+
line: lineOf(sf, decl)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (/^use[A-Z]/.test(hook)) {
|
|
290
|
+
for (const n of bindingNames(decl.name)) {
|
|
291
|
+
scope.hookNames.set(n, hook);
|
|
292
|
+
if (ts.isIdentifier(decl.name))
|
|
293
|
+
scope.hookObjects.set(n, hook);
|
|
294
|
+
}
|
|
295
|
+
if (!KNOWN_HOOKS.has(hook))
|
|
296
|
+
scope.unknownHooks.push(hook);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return scope;
|
|
301
|
+
}
|
|
302
|
+
export const chainStates = (c) => c.scopes.flatMap((s) => [...s.states.values()]);
|
|
303
|
+
export const chainConsumed = (c) => new Set(c.scopes.flatMap((s) => [...s.consumed]));
|
|
304
|
+
export const chainHandler = (c, name) => {
|
|
305
|
+
for (const s of c.scopes) {
|
|
306
|
+
const h = s.handlers.get(name);
|
|
307
|
+
if (h)
|
|
308
|
+
return h;
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
};
|
|
312
|
+
export const chainUnknownHooks = (c) => c.scopes.flatMap((s) => s.unknownHooks);
|
|
313
|
+
/**
|
|
314
|
+
* Names in scope that look like a given kind of flag, by name: state, values a hook returns, props, and
|
|
315
|
+
* properties read off a hook's result. This is a naming heuristic, and the evidence says so.
|
|
316
|
+
*/
|
|
317
|
+
export function candidates(c, test) {
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const s of c.scopes) {
|
|
320
|
+
for (const st of s.states.values())
|
|
321
|
+
if (test.test(st.name))
|
|
322
|
+
out.push({ name: st.name, source: "state", setter: st.setter });
|
|
323
|
+
for (const [n, hook] of s.hookNames)
|
|
324
|
+
if (test.test(n))
|
|
325
|
+
out.push({ name: n, source: "hook", hook });
|
|
326
|
+
for (const p of s.params)
|
|
327
|
+
if (test.test(p))
|
|
328
|
+
out.push({ name: p, source: "prop" });
|
|
329
|
+
for (const [obj, hook] of s.hookObjects)
|
|
330
|
+
for (const used of s.consumed) {
|
|
331
|
+
const m = new RegExp(`^${obj}\\.(.+)$`).exec(used);
|
|
332
|
+
if (m && test.test(m[1]))
|
|
333
|
+
out.push({ name: used, source: "hook", hook });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return [...new Map(out.map((x) => [x.name, x])).values()];
|
|
337
|
+
}
|
|
338
|
+
/** True when a name is read in the JSX of this component, directly or as a property of a hook's result. */
|
|
339
|
+
export const shown = (c, name) => chainConsumed(c).has(name);
|
|
340
|
+
export const isDeleteLike = (name) => /(?:^|[^a-z])(delete|remove|destroy|discard|erase|purge)|(?:Delete|Remove|Destroy|Discard|Erase|Purge)/.test(name);
|
|
341
|
+
//# sourceMappingURL=interaction-facts.js.map
|