@invariant-ui/cli 0.0.0-stage → 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/LICENSE +105 -0
- package/README.md +250 -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/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/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-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/search.js +132 -0
- package/dist/contract-query/system.js +148 -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,67 @@
|
|
|
1
|
+
import { evidenceIdSchema, evidenceSchema, factPathSchema, jsonValueSchema, sourceIdSchema, sourceTypeSchema, sourceVersionSchema } from "../contract/index.js";
|
|
2
|
+
import { diagnosticSchema } from "../shared/index.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const IR_VERSION = "1.3.0";
|
|
5
|
+
/**
|
|
6
|
+
* Kinds up to `story` model design-system content. `package`, `export`, `config` and `token-file` were
|
|
7
|
+
* added in IR 1.1.0 so project discovery can be represented honestly without calling an export a component.
|
|
8
|
+
* `story-group` (a CSF `meta`: title, component, shared args and argTypes) was added in IR 1.2.0.
|
|
9
|
+
* `module` (a unit that declares, imports and exports other entities, such as an Angular NgModule) was added in IR 1.3.0.
|
|
10
|
+
*/
|
|
11
|
+
export const irEntityKindSchema = z.enum([
|
|
12
|
+
"component",
|
|
13
|
+
"token",
|
|
14
|
+
"theme",
|
|
15
|
+
"pattern",
|
|
16
|
+
"guideline",
|
|
17
|
+
"constraint",
|
|
18
|
+
"doc-page",
|
|
19
|
+
"story",
|
|
20
|
+
"package",
|
|
21
|
+
"export",
|
|
22
|
+
"config",
|
|
23
|
+
"token-file",
|
|
24
|
+
"story-group",
|
|
25
|
+
"module"
|
|
26
|
+
]);
|
|
27
|
+
/** Something a source can talk about. `ref` is local to one IR document, never a contract id. */
|
|
28
|
+
export const irEntitySchema = z.strictObject({
|
|
29
|
+
ref: z.string().min(1),
|
|
30
|
+
kind: irEntityKindSchema,
|
|
31
|
+
name: z.string().min(1)
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* One claim by one source: (subject, path) has `value`. Paths use the contract's fact-path grammar,
|
|
35
|
+
* for example `props.size.default`; framework-specific detail goes under `extensions.<namespace>`.
|
|
36
|
+
* `origin` is only what an adapter can honestly say. Adapters cannot emit `inferred` or `approved`.
|
|
37
|
+
*/
|
|
38
|
+
export const irFactSchema = z.strictObject({
|
|
39
|
+
subject: z.string().min(1),
|
|
40
|
+
path: factPathSchema,
|
|
41
|
+
value: jsonValueSchema,
|
|
42
|
+
origin: z.enum(["extracted", "authored"]),
|
|
43
|
+
evidenceIds: z.array(evidenceIdSchema).min(1),
|
|
44
|
+
completeness: z.enum(["complete", "partial", "unknown"]).optional()
|
|
45
|
+
});
|
|
46
|
+
export const irSourceSchema = z.strictObject({
|
|
47
|
+
id: sourceIdSchema,
|
|
48
|
+
type: sourceTypeSchema.exclude(["patterns"]),
|
|
49
|
+
location: z.string().min(1),
|
|
50
|
+
version: sourceVersionSchema.optional(),
|
|
51
|
+
/** Hash of the source inputs read, so an unchanged source can be skipped on recompile. */
|
|
52
|
+
inputHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
53
|
+
adapter: z.strictObject({ name: z.string().min(1), version: z.string().min(1) })
|
|
54
|
+
});
|
|
55
|
+
export const irDocumentSchema = z.strictObject({
|
|
56
|
+
irVersion: z.literal(IR_VERSION),
|
|
57
|
+
source: irSourceSchema,
|
|
58
|
+
/** Fact-path patterns this adapter can supply; `*` matches exactly one path segment. */
|
|
59
|
+
capabilities: z.array(z.string().min(1)),
|
|
60
|
+
/** `partial` and `failed` mean absent facts are not evidence that something does not exist. */
|
|
61
|
+
completeness: z.enum(["complete", "partial", "failed"]),
|
|
62
|
+
entities: z.array(irEntitySchema),
|
|
63
|
+
facts: z.array(irFactSchema),
|
|
64
|
+
evidence: z.array(evidenceSchema),
|
|
65
|
+
diagnostics: z.array(diagnosticSchema)
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=ir.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { computeEvidenceId } from "../contract/index.js";
|
|
2
|
+
import { canonicalStringify, hashJson, sortDiagnostics } from "../shared/index.js";
|
|
3
|
+
import { IR_VERSION, irDocumentSchema } from "./ir.js";
|
|
4
|
+
export function parseIrDocument(input) {
|
|
5
|
+
const version = typeof input === "object" && input !== null ? input.irVersion : undefined;
|
|
6
|
+
if (typeof version === "string" && version.split(".")[0] !== IR_VERSION.split(".")[0]) {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
diagnostics: [
|
|
10
|
+
{
|
|
11
|
+
code: "ir.unsupported-version",
|
|
12
|
+
severity: "error",
|
|
13
|
+
message: `IR version ${version} is not supported; this build reads ${IR_VERSION}`
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const result = irDocumentSchema.safeParse(input);
|
|
19
|
+
return result.success
|
|
20
|
+
? { ok: true, value: result.data }
|
|
21
|
+
: { ok: false, diagnostics: schemaDiagnostics(result.error) };
|
|
22
|
+
}
|
|
23
|
+
/** Sorts every collection so the same logical document always serializes identically. */
|
|
24
|
+
export function canonicalizeIrDocument(doc) {
|
|
25
|
+
return {
|
|
26
|
+
...doc,
|
|
27
|
+
capabilities: [...new Set(doc.capabilities)].sort(),
|
|
28
|
+
entities: [...doc.entities].sort((a, b) => cmp(a.ref, b.ref)),
|
|
29
|
+
facts: [...doc.facts].sort((a, b) => cmp(a.subject, b.subject) ||
|
|
30
|
+
cmp(a.path, b.path) ||
|
|
31
|
+
cmp(canonicalStringify(a.value), canonicalStringify(b.value))),
|
|
32
|
+
evidence: [...doc.evidence].sort((a, b) => cmp(a.id, b.id)),
|
|
33
|
+
diagnostics: sortDiagnostics(doc.diagnostics)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Content hash of an IR document excluding evidence capture times, so re-running extraction on
|
|
38
|
+
* unchanged inputs yields the same hash even though `capturedAt` differs.
|
|
39
|
+
*/
|
|
40
|
+
export function computeIrContentHash(doc) {
|
|
41
|
+
const canonical = canonicalizeIrDocument(doc);
|
|
42
|
+
return hashJson({
|
|
43
|
+
...canonical,
|
|
44
|
+
evidence: canonical.evidence.map(({ capturedAt: _capturedAt, ...rest }) => rest)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function serializeIrDocument(doc) {
|
|
48
|
+
return `${canonicalStringify(canonicalizeIrDocument(doc), 2)}\n`;
|
|
49
|
+
}
|
|
50
|
+
/** `*` matches exactly one path segment. */
|
|
51
|
+
export function matchesCapability(pattern, path) {
|
|
52
|
+
const p = pattern.split(".");
|
|
53
|
+
const s = path.split(".");
|
|
54
|
+
if (p.length > s.length)
|
|
55
|
+
return false;
|
|
56
|
+
// A capability covers its subtree: `props.*.default` covers `props.size.default.text`.
|
|
57
|
+
return p.every((segment, i) => segment === "*" || segment === s[i]);
|
|
58
|
+
}
|
|
59
|
+
/** Semantic validation of an IR document. Never throws; returns diagnostics. */
|
|
60
|
+
export function verifyIrDocument(doc) {
|
|
61
|
+
const found = [];
|
|
62
|
+
const sourceId = doc.source.id;
|
|
63
|
+
const add = (severity, code, message) => {
|
|
64
|
+
found.push({ code, severity, message, sourceId });
|
|
65
|
+
};
|
|
66
|
+
const refs = new Set();
|
|
67
|
+
for (const entity of doc.entities) {
|
|
68
|
+
if (refs.has(entity.ref))
|
|
69
|
+
add("error", "ir.duplicate-entity-ref", `Duplicate entity ref ${entity.ref}`);
|
|
70
|
+
refs.add(entity.ref);
|
|
71
|
+
}
|
|
72
|
+
const evidenceIds = new Set();
|
|
73
|
+
for (const evidence of doc.evidence) {
|
|
74
|
+
if (evidenceIds.has(evidence.id))
|
|
75
|
+
add("error", "ir.duplicate-evidence", `Duplicate evidence ${evidence.id}`);
|
|
76
|
+
evidenceIds.add(evidence.id);
|
|
77
|
+
if (computeEvidenceId(evidence) !== evidence.id)
|
|
78
|
+
add("error", "ir.evidence-id-mismatch", `Evidence ${evidence.id} id does not match its content`);
|
|
79
|
+
if (evidence.sourceId !== sourceId)
|
|
80
|
+
add("error", "ir.evidence-source-mismatch", `Evidence ${evidence.id} belongs to source ${evidence.sourceId}, not ${sourceId}`);
|
|
81
|
+
if (evidence.method === "llm-assisted") {
|
|
82
|
+
add("error", "ir.llm-evidence", `Evidence ${evidence.id} is LLM-assisted; adapters must be deterministic (model output belongs to the semantic engine)`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const seen = new Map();
|
|
86
|
+
for (const fact of doc.facts) {
|
|
87
|
+
if (!refs.has(fact.subject))
|
|
88
|
+
add("error", "ir.unknown-subject", `Fact '${fact.path}' targets unknown entity ${fact.subject}`);
|
|
89
|
+
for (const id of fact.evidenceIds) {
|
|
90
|
+
if (!evidenceIds.has(id))
|
|
91
|
+
add("error", "ir.unknown-evidence", `Fact ${fact.subject} '${fact.path}' cites missing evidence ${id}`);
|
|
92
|
+
}
|
|
93
|
+
if (!doc.capabilities.some((pattern) => matchesCapability(pattern, fact.path))) {
|
|
94
|
+
add("warning", "ir.undeclared-capability", `Fact ${fact.subject} '${fact.path}' is outside the adapter's declared capabilities`);
|
|
95
|
+
}
|
|
96
|
+
const key = `${fact.subject}\u0000${fact.path}`;
|
|
97
|
+
const value = canonicalStringify(fact.value);
|
|
98
|
+
const previous = seen.get(key);
|
|
99
|
+
if (previous !== undefined && previous !== value) {
|
|
100
|
+
add("warning", "ir.conflicting-facts", `Source disagrees with itself at ${fact.subject} '${fact.path}'`);
|
|
101
|
+
}
|
|
102
|
+
seen.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
return sortDiagnostics(found);
|
|
105
|
+
}
|
|
106
|
+
function schemaDiagnostics(error) {
|
|
107
|
+
return sortDiagnostics(error.issues.map((issue) => ({
|
|
108
|
+
code: "ir.invalid-schema",
|
|
109
|
+
severity: "error",
|
|
110
|
+
message: `${issue.path.reduce((acc, part) => (typeof part === "number" ? `${acc}[${part}]` : `${acc}.${String(part)}`), "$")}: ${issue.message}`
|
|
111
|
+
})));
|
|
112
|
+
}
|
|
113
|
+
function cmp(a, b) {
|
|
114
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { DEFAULT_IGNORED_DIRS, hashText, IrBuilder, walk } from "../compiler-core/index.js";
|
|
2
|
+
import { hashJson } from "../shared/index.js";
|
|
3
|
+
import { analyzePackage } from "./analyze.js";
|
|
4
|
+
import { CAPABILITIES, emitComponent } from "./emit.js";
|
|
5
|
+
export const ADAPTER_ID = "react";
|
|
6
|
+
export const ADAPTER_VERSION = "0.1.0";
|
|
7
|
+
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?|css)$|(?:^|\/)tsconfig[^/]*\.json$/;
|
|
8
|
+
const MAX_FILE_BYTES = 1_000_000;
|
|
9
|
+
/**
|
|
10
|
+
* React/TypeScript adapter. For every workspace package that declares React, it type-checks the
|
|
11
|
+
* package's own files (never third-party code, never executing anything) and emits one IR document
|
|
12
|
+
* with the components its entry points export: props, defaults, variants, slots, accessibility,
|
|
13
|
+
* styling references, composition, and static usage.
|
|
14
|
+
*/
|
|
15
|
+
export const reactAdapter = {
|
|
16
|
+
id: ADAPTER_ID,
|
|
17
|
+
version: ADAPTER_VERSION,
|
|
18
|
+
type: "react",
|
|
19
|
+
async discover({ fs, project }) {
|
|
20
|
+
const sources = [];
|
|
21
|
+
const used = new Set();
|
|
22
|
+
for (const pkg of project.packages.filter((p) => p.frameworks.some((f) => f.name === "react"))) {
|
|
23
|
+
const others = project.packages.filter((p) => p.id !== pkg.id && p.dir !== "." && (pkg.dir === "." || p.dir.startsWith(`${pkg.dir}/`)));
|
|
24
|
+
const { files, hashes } = await readPackageFiles(fs, pkg, others);
|
|
25
|
+
const slug = (pkg.name ?? pkg.dir)
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
28
|
+
.replace(/^[^a-z0-9]+|[-.]+$/g, "") || "root";
|
|
29
|
+
let id = `react-${slug}`;
|
|
30
|
+
for (let n = 2; used.has(id); n++)
|
|
31
|
+
id = `react-${slug}-${n}`;
|
|
32
|
+
used.add(id);
|
|
33
|
+
sources.push({
|
|
34
|
+
id,
|
|
35
|
+
type: "react",
|
|
36
|
+
location: pkg.dir,
|
|
37
|
+
config: {
|
|
38
|
+
packageId: pkg.id,
|
|
39
|
+
files: [...files.keys()],
|
|
40
|
+
tailwind: pkg.dependencies.some((d) => d.name === "tailwindcss") ||
|
|
41
|
+
project.tokenFiles.some((f) => f.kind === "tailwind-config" && f.package === pkg.id)
|
|
42
|
+
},
|
|
43
|
+
...(pkg.version ? { version: { kind: "package-version", value: pkg.version } } : {}),
|
|
44
|
+
inputHash: hashJson(hashes)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return { sources, diagnostics: [] };
|
|
48
|
+
},
|
|
49
|
+
async extract({ fs, project, source, clock }, config) {
|
|
50
|
+
const pkg = project.packages.find((p) => p.id === config.packageId);
|
|
51
|
+
if (!pkg)
|
|
52
|
+
throw new Error(`Package ${config.packageId} is not in the project discovery`);
|
|
53
|
+
const files = new Map();
|
|
54
|
+
for (const path of config.files)
|
|
55
|
+
files.set(path, await fs.readFile(path));
|
|
56
|
+
const analysis = analyzePackage({
|
|
57
|
+
...(pkg.name ? { packageName: pkg.name } : {}),
|
|
58
|
+
packageDir: pkg.dir,
|
|
59
|
+
entryPoints: pkg.entryPoints,
|
|
60
|
+
files,
|
|
61
|
+
...(pkg.dir.split("/").includes("node_modules") ? { typeFiles: await readTypeFiles(fs) } : {}),
|
|
62
|
+
tailwind: config.tailwind
|
|
63
|
+
});
|
|
64
|
+
const builder = new IrBuilder({
|
|
65
|
+
source: {
|
|
66
|
+
id: source.id,
|
|
67
|
+
type: source.type,
|
|
68
|
+
location: source.location,
|
|
69
|
+
...(source.version ? { version: source.version } : {}),
|
|
70
|
+
inputHash: source.inputHash,
|
|
71
|
+
adapter: { name: ADAPTER_ID, version: ADAPTER_VERSION }
|
|
72
|
+
},
|
|
73
|
+
extractor: { name: "react-ts", version: ADAPTER_VERSION },
|
|
74
|
+
capabilities: CAPABILITIES,
|
|
75
|
+
clock
|
|
76
|
+
});
|
|
77
|
+
for (const component of analysis.components)
|
|
78
|
+
emitComponent(builder, component, pkg);
|
|
79
|
+
for (const d of analysis.diagnostics)
|
|
80
|
+
builder.diagnostic(d);
|
|
81
|
+
const partial = analysis.components.some((c) => !c.props.complete) ||
|
|
82
|
+
analysis.diagnostics.some((d) => d.code === "react.entry-not-analyzed");
|
|
83
|
+
return { ir: builder.build(partial ? "partial" : "complete") };
|
|
84
|
+
},
|
|
85
|
+
async validate({ ir }) {
|
|
86
|
+
return ir.entities.length === 0
|
|
87
|
+
? [
|
|
88
|
+
{
|
|
89
|
+
code: "react.no-components",
|
|
90
|
+
severity: "info",
|
|
91
|
+
message: `No React components were found in the entry points of ${ir.source.id}`,
|
|
92
|
+
sourceId: ir.source.id
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
: [];
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Declaration files of everything installed next to an installed package (`@types/react`, `csstype`, the primitives a
|
|
100
|
+
* library builds on). Without them a props type that mentions any of them resolves to nothing, and so does the whole
|
|
101
|
+
* intersection it is part of. They are read for type resolution only: nothing declared in them is a project node.
|
|
102
|
+
*/
|
|
103
|
+
async function readTypeFiles(fs) {
|
|
104
|
+
const files = new Map();
|
|
105
|
+
if (!(await fs.exists("node_modules")))
|
|
106
|
+
return files;
|
|
107
|
+
const walked = await walk(fs, {
|
|
108
|
+
ignoreDirs: new Set([".invariant", ".bin", ".cache"]),
|
|
109
|
+
maxFiles: 60_000,
|
|
110
|
+
start: "node_modules"
|
|
111
|
+
});
|
|
112
|
+
let bytes = 0;
|
|
113
|
+
for (const path of walked.files) {
|
|
114
|
+
if (!/\.d\.[cm]?ts$|(?:^|\/)package\.json$/.test(path))
|
|
115
|
+
continue;
|
|
116
|
+
const text = await fs.readFile(path);
|
|
117
|
+
if (text.length > 5_000_000 || bytes + text.length > 60_000_000)
|
|
118
|
+
continue;
|
|
119
|
+
bytes += text.length;
|
|
120
|
+
files.set(path, text);
|
|
121
|
+
}
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
async function readPackageFiles(fs, pkg, nested) {
|
|
125
|
+
// An installed package (opted in through a declaration root) keeps its declaration files in build output.
|
|
126
|
+
const installed = pkg.dir.split("/").includes("node_modules");
|
|
127
|
+
const walked = await walk(fs, {
|
|
128
|
+
ignoreDirs: new Set(installed
|
|
129
|
+
? DEFAULT_IGNORED_DIRS.filter((d) => !["dist", "build", "out"].includes(d))
|
|
130
|
+
: DEFAULT_IGNORED_DIRS),
|
|
131
|
+
maxFiles: 20_000,
|
|
132
|
+
start: pkg.dir
|
|
133
|
+
});
|
|
134
|
+
const files = new Map();
|
|
135
|
+
const hashes = [];
|
|
136
|
+
for (const path of walked.files) {
|
|
137
|
+
if (!SOURCE_FILE.test(path) || nested.some((n) => path === n.dir || path.startsWith(`${n.dir}/`)))
|
|
138
|
+
continue;
|
|
139
|
+
const text = await fs.readFile(path);
|
|
140
|
+
if (text.length > MAX_FILE_BYTES)
|
|
141
|
+
continue;
|
|
142
|
+
files.set(path, text);
|
|
143
|
+
hashes.push([path, hashText(text)]);
|
|
144
|
+
}
|
|
145
|
+
return { files, hashes };
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createAnalysis, declarationOf, isProjectNode, jsxTargetDeclaration, lineOf, posix, relativeFileOf, resolveSymbol, subTarget, tsconfigPaths, unwrap } from "../compiler-core/index.js";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { accessibilityOf, docsOf, slotsOf } from "./behavior.js";
|
|
4
|
+
import { classifyCompound, classifyDeclaration } from "./detect.js";
|
|
5
|
+
import { extractProps } from "./props.js";
|
|
6
|
+
import { stylingOf } from "./styling.js";
|
|
7
|
+
import { analyzeUsage } from "./usage.js";
|
|
8
|
+
import { variantsOf } from "./variants.js";
|
|
9
|
+
const NON_SOURCE = /\.(?:test|spec|stories)\.[cm]?[jt]sx?$|(?:^|\/)__(?:tests|mocks)__\//;
|
|
10
|
+
/** Finds the package's public React components (exports of its entry points) and analyzes each. */
|
|
11
|
+
export function analyzePackage(input) {
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
const analysis = createAnalysis(input.files, {
|
|
14
|
+
paths: tsconfigPaths(input.files, input.packageDir),
|
|
15
|
+
...(input.typeFiles ? { externalFiles: input.typeFiles } : {})
|
|
16
|
+
});
|
|
17
|
+
const { checker } = analysis;
|
|
18
|
+
const note = (severity, code, message, node) => {
|
|
19
|
+
const file = node ? relativeFileOf(analysis, node) : undefined;
|
|
20
|
+
diagnostics.push({
|
|
21
|
+
code,
|
|
22
|
+
severity,
|
|
23
|
+
message,
|
|
24
|
+
...(file && node ? { location: { path: file, line: lineOf(node) } } : {})
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
// 1. Public candidates: whatever the entry points export.
|
|
28
|
+
const candidates = new Map();
|
|
29
|
+
for (const entry of input.entryPoints) {
|
|
30
|
+
const sf = entry.file ? analysis.sourceFile(entry.file) : undefined;
|
|
31
|
+
if (!entry.file || entry.resolution === "unresolved" || !sf) {
|
|
32
|
+
// Stylesheets, JSON and the like are entries a component library lists but not modules to analyze.
|
|
33
|
+
if (entry.file && /\.[cm]?[jt]sx?$/.test(entry.file))
|
|
34
|
+
note("warning", "react.entry-not-analyzed", `Entry ${entry.subpath} (${entry.file}) is not a TypeScript or JavaScript module in the analyzed files`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const moduleSymbol = checker.getSymbolAtLocation(sf);
|
|
38
|
+
if (!moduleSymbol)
|
|
39
|
+
continue;
|
|
40
|
+
for (const exported of checker.getExportsOfModule(moduleSymbol)) {
|
|
41
|
+
const resolved = resolveSymbol(checker, exported);
|
|
42
|
+
const decl = declarationOf(resolved);
|
|
43
|
+
if (!decl || !isProjectNode(analysis, decl))
|
|
44
|
+
continue;
|
|
45
|
+
const result = classifyDeclaration(checker, decl, exported.name === "default" ? baseName(decl) : exported.name);
|
|
46
|
+
if (!result) {
|
|
47
|
+
// A declaration file's `{ Root: typeof X }` object: its members are the components.
|
|
48
|
+
const compound = classifyCompound(checker, decl, exported.name === "default" ? baseName(decl) : exported.name);
|
|
49
|
+
if (compound) {
|
|
50
|
+
for (const { key, candidate } of compound) {
|
|
51
|
+
const displayName = `${exported.name}.${key}`;
|
|
52
|
+
const existingChild = candidates.get(candidate.decl);
|
|
53
|
+
const exportEntry = { subpath: entry.subpath, name: displayName };
|
|
54
|
+
if (existingChild)
|
|
55
|
+
existingChild.exports.push(exportEntry);
|
|
56
|
+
else
|
|
57
|
+
candidates.set(candidate.decl, { ...candidate, displayName, exports: [exportEntry] });
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (looksLikeFactoryComponent(decl, exported.name)) {
|
|
62
|
+
note("info", "react.unclassified-export", `${exported.name} is a Pascal-case export created by a function call, so it could not be classified as a component statically`, decl);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if ("hoc" in result) {
|
|
67
|
+
note("info", "react.higher-order-component", `${result.name} returns a component (a higher-order component) and was not analyzed`, result.node);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const key = result.decl;
|
|
71
|
+
const existing = candidates.get(key);
|
|
72
|
+
if (existing)
|
|
73
|
+
existing.exports.push({ subpath: entry.subpath, name: exported.name });
|
|
74
|
+
else
|
|
75
|
+
candidates.set(key, { ...result, exports: [{ subpath: entry.subpath, name: exported.name }] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 2. Compound children become components of their own, named `Parent.Child`.
|
|
79
|
+
const parents = new Map();
|
|
80
|
+
const children = [];
|
|
81
|
+
const queue = [...candidates.values()];
|
|
82
|
+
for (let i = 0; i < queue.length; i++) {
|
|
83
|
+
const parent = queue[i];
|
|
84
|
+
for (const sub of parent.subcomponents) {
|
|
85
|
+
// A declaration file's `Item: (props: P) => JSX.Element` member is its own declaration.
|
|
86
|
+
const decl = ts.isPropertySignature(sub.target) ? sub.target : subTarget(checker, sub.target);
|
|
87
|
+
if (!decl)
|
|
88
|
+
continue;
|
|
89
|
+
const child = classifyDeclaration(checker, decl, sub.key);
|
|
90
|
+
if (!child || "hoc" in child || candidates.has(child.decl))
|
|
91
|
+
continue;
|
|
92
|
+
const named = { ...child, displayName: `${parent.displayName ?? parent.name}.${sub.key}`, exports: [] };
|
|
93
|
+
parents.set(named, parent);
|
|
94
|
+
children.push(named);
|
|
95
|
+
queue.push(named);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const child of children)
|
|
99
|
+
candidates.set(child.decl, child);
|
|
100
|
+
// 3. Per-component facts.
|
|
101
|
+
const list = [...candidates.values()].sort((a, b) => cmp(`${relOf(analysis, a.decl)}#${a.name}`, `${relOf(analysis, b.decl)}#${b.name}`));
|
|
102
|
+
const refs = new Map();
|
|
103
|
+
const usedRefs = new Set();
|
|
104
|
+
for (const c of list) {
|
|
105
|
+
let ref = `react:${relOf(analysis, c.decl)}#${c.name}`;
|
|
106
|
+
if (usedRefs.has(ref))
|
|
107
|
+
ref = `${ref}@${lineOf(c.decl)}`;
|
|
108
|
+
usedRefs.add(ref);
|
|
109
|
+
refs.set(c, ref);
|
|
110
|
+
}
|
|
111
|
+
const sources = [...input.files.keys()]
|
|
112
|
+
.filter((f) => /\.(?:[cm]?[jt]sx?)$/.test(f) && !NON_SOURCE.test(f))
|
|
113
|
+
.sort();
|
|
114
|
+
const byDecl = new Map();
|
|
115
|
+
for (const c of list) {
|
|
116
|
+
byDecl.set(c.decl, c);
|
|
117
|
+
if (c.impl)
|
|
118
|
+
byDecl.set(c.impl, c);
|
|
119
|
+
}
|
|
120
|
+
const propsByCandidate = new Map();
|
|
121
|
+
for (const c of list)
|
|
122
|
+
propsByCandidate.set(c, extractProps(analysis, c));
|
|
123
|
+
const usage = analyzeUsage({
|
|
124
|
+
analysis,
|
|
125
|
+
files: sources,
|
|
126
|
+
resolveTarget: (tag) => {
|
|
127
|
+
const decl = jsxTargetDeclaration(checker, tag);
|
|
128
|
+
return decl ? byDecl.get(decl) : undefined;
|
|
129
|
+
},
|
|
130
|
+
enclosing: (node) => {
|
|
131
|
+
for (let n = node.parent; n; n = n.parent) {
|
|
132
|
+
const found = byDecl.get(n);
|
|
133
|
+
if (found)
|
|
134
|
+
return found;
|
|
135
|
+
if (ts.isVariableDeclaration(n) && byDecl.has(n))
|
|
136
|
+
return byDecl.get(n);
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
},
|
|
140
|
+
declared: (c) => {
|
|
141
|
+
const props = propsByCandidate.get(c);
|
|
142
|
+
return { names: new Set(props?.props.map((p) => p.name) ?? []), closed: props?.complete === true };
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const components = list.map((c) => {
|
|
146
|
+
const props = propsByCandidate.get(c);
|
|
147
|
+
for (const name of props.skipped)
|
|
148
|
+
note("info", "react.unsupported-prop-name", `Prop "${name}" of ${c.name} cannot be addressed by a fact path and was skipped`, c.decl);
|
|
149
|
+
if (props.props.length === 0 && !props.complete && props.notes.length > 0)
|
|
150
|
+
note("warning", "react.props-unresolved", `${c.name}: ${props.notes[0]}`, c.decl);
|
|
151
|
+
const parent = parents.get(c);
|
|
152
|
+
const stats = usage.byTarget.get(c);
|
|
153
|
+
const accessibility = accessibilityOf(c);
|
|
154
|
+
const styling = stylingOf(analysis, c, { files: input.files, tailwind: input.tailwind });
|
|
155
|
+
return {
|
|
156
|
+
ref: refs.get(c),
|
|
157
|
+
candidate: c,
|
|
158
|
+
displayName: c.displayName ?? c.name,
|
|
159
|
+
file: relOf(analysis, c.decl),
|
|
160
|
+
line: lineOf(c.decl),
|
|
161
|
+
exports: [...c.exports].sort((a, b) => cmp(a.subpath, b.subpath) || cmp(a.name, b.name)),
|
|
162
|
+
props,
|
|
163
|
+
variants: variantsOf(analysis, c, props.props),
|
|
164
|
+
slots: slotsOf(props.props),
|
|
165
|
+
...(accessibility ? { accessibility } : {}),
|
|
166
|
+
docs: docsOf(analysis, c),
|
|
167
|
+
...(styling ? { styling } : {}),
|
|
168
|
+
...(stats ? { usage: stats } : {}),
|
|
169
|
+
renders: [...(usage.renders.get(c) ?? [])]
|
|
170
|
+
.map(([target, count]) => ({ target: refs.get(target), count }))
|
|
171
|
+
.sort((a, b) => cmp(a.target, b.target)),
|
|
172
|
+
...(parent ? { compoundParent: refs.get(parent) } : {}),
|
|
173
|
+
subcomponents: c.subcomponents.flatMap((s) => {
|
|
174
|
+
const child = children.find((x) => x.displayName === `${c.displayName ?? c.name}.${s.key}`);
|
|
175
|
+
return child ? [{ key: s.key, ref: refs.get(child) }] : [];
|
|
176
|
+
})
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
return { components, diagnostics, analyzedFiles: sources };
|
|
180
|
+
}
|
|
181
|
+
/** `export const Slot = createSlot(…)`: possibly a component made by a factory, which static analysis cannot confirm. */
|
|
182
|
+
function looksLikeFactoryComponent(decl, exportName) {
|
|
183
|
+
if (!ts.isVariableDeclaration(decl) || !decl.initializer || !ts.isIdentifier(decl.name))
|
|
184
|
+
return false;
|
|
185
|
+
const name = exportName === "default" ? decl.name.text : exportName;
|
|
186
|
+
const init = unwrap(decl.initializer);
|
|
187
|
+
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name) && ts.isCallExpression(init);
|
|
188
|
+
}
|
|
189
|
+
function relOf(analysis, node) {
|
|
190
|
+
return relativeFileOf(analysis, node) ?? node.getSourceFile().fileName;
|
|
191
|
+
}
|
|
192
|
+
function baseName(decl) {
|
|
193
|
+
const base = posix.basename(decl.getSourceFile().fileName).replace(/\.[^.]+$/, "");
|
|
194
|
+
return base.charAt(0).toUpperCase() + base.slice(1);
|
|
195
|
+
}
|
|
196
|
+
function cmp(a, b) {
|
|
197
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { walkSameFunction } from "../compiler-core/index.js";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
/**
|
|
4
|
+
* Slots are derived from props: `children` is the default slot, any other prop whose declared type is
|
|
5
|
+
* renderable (ReactNode, ReactElement, JSX.Element) is a named slot, and a function prop returning
|
|
6
|
+
* renderable output is a render-prop slot.
|
|
7
|
+
*/
|
|
8
|
+
export function slotsOf(props) {
|
|
9
|
+
return props
|
|
10
|
+
.filter((p) => p.role === "children" || p.role === "slot" || p.role === "render-prop")
|
|
11
|
+
.map((p) => ({
|
|
12
|
+
name: p.role === "children" ? "default" : p.name,
|
|
13
|
+
required: p.required,
|
|
14
|
+
prop: p.name,
|
|
15
|
+
typeText: p.typeText,
|
|
16
|
+
renderProp: p.role === "render-prop"
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
/** Static `role` and `aria-*` attributes on intrinsic (lowercase) JSX elements in the component body. */
|
|
20
|
+
export function accessibilityOf(candidate) {
|
|
21
|
+
const impl = candidate.impl;
|
|
22
|
+
if (!impl)
|
|
23
|
+
return undefined;
|
|
24
|
+
const roles = new Set();
|
|
25
|
+
const attributes = new Set();
|
|
26
|
+
const visit = (n) => {
|
|
27
|
+
const element = ts.isJsxSelfClosingElement(n) ? n : ts.isJsxOpeningElement(n) ? n : undefined;
|
|
28
|
+
if (!element || !/^[a-z]/.test(element.tagName.getText()))
|
|
29
|
+
return;
|
|
30
|
+
for (const attribute of element.attributes.properties) {
|
|
31
|
+
if (!ts.isJsxAttribute(attribute))
|
|
32
|
+
continue;
|
|
33
|
+
const name = attribute.name.getText();
|
|
34
|
+
if (name === "role" && attribute.initializer && ts.isStringLiteral(attribute.initializer))
|
|
35
|
+
roles.add(attribute.initializer.text);
|
|
36
|
+
else if (name.startsWith("aria-"))
|
|
37
|
+
attributes.add(name);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const walk = (n) => {
|
|
41
|
+
visit(n);
|
|
42
|
+
ts.forEachChild(n, walk);
|
|
43
|
+
};
|
|
44
|
+
// Nested component definitions inside the body are not this component's markup.
|
|
45
|
+
if (ts.isFunctionLike(impl) && impl.body)
|
|
46
|
+
walkSameFunction(impl, visit);
|
|
47
|
+
else if (ts.isClassLike(impl))
|
|
48
|
+
walk(impl);
|
|
49
|
+
if (roles.size === 0 && attributes.size === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const only = roles.size === 1 ? [...roles][0] : undefined;
|
|
52
|
+
return { ...(only ? { role: only } : {}), attributes: [...attributes].sort() };
|
|
53
|
+
}
|
|
54
|
+
/** JSDoc on the component declaration: description, `@deprecated`, and `@example` blocks. */
|
|
55
|
+
export function docsOf(analysis, candidate) {
|
|
56
|
+
const { checker } = analysis;
|
|
57
|
+
const nameNode = candidate.decl.name;
|
|
58
|
+
const symbol = nameNode ? checker.getSymbolAtLocation(nameNode) : undefined;
|
|
59
|
+
if (!symbol)
|
|
60
|
+
return { examples: [] };
|
|
61
|
+
const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
62
|
+
const tags = symbol.getJsDocTags(checker);
|
|
63
|
+
const deprecated = tags.find((t) => t.name === "deprecated");
|
|
64
|
+
const examples = tags
|
|
65
|
+
.filter((t) => t.name === "example")
|
|
66
|
+
.map((t) => ts.displayPartsToString(t.text).trim())
|
|
67
|
+
.filter((t) => t.length > 0);
|
|
68
|
+
const reason = deprecated ? ts.displayPartsToString(deprecated.text).trim() : undefined;
|
|
69
|
+
return {
|
|
70
|
+
...(description ? { description } : {}),
|
|
71
|
+
...(deprecated ? { deprecated: reason ? { reason } : {} } : {}),
|
|
72
|
+
examples
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=behavior.js.map
|