synthesisui 0.16.76 → 0.16.78
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/dist/anatomy-read.js +231 -0
- package/dist/commands/add.js +23 -1
- package/dist/commands/component.js +11 -23
- package/dist/commands/doctor.js +49 -1
- package/dist/commands/generate.js +5 -1
- package/dist/commands/import.js +71 -15
- package/dist/commands/refit.js +2 -1
- package/dist/commands/upgrade.js +6 -1
- package/dist/component-codegen.js +243 -25
- package/dist/doctor/dependencies.js +90 -0
- package/dist/doctor/transcribe.js +117 -0
- package/dist/project-facts.js +89 -0
- package/dist/skill-import.js +149 -34
- package/package.json +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ANATOMY THE SKILL READ, TURNED INTO SOMETHING THE SYSTEM CAN HOLD.
|
|
3
|
+
*
|
|
4
|
+
* The first version of this contract asked for the parts as a FLAT LIST and threw
|
|
5
|
+
* away the structure the skill had just finished reading. So an `ArticleCard`
|
|
6
|
+
* with a 16:9 cover, a title, a full editor and a footer of five buttons came
|
|
7
|
+
* back as four labels in a row - and the owner compared it to the real component
|
|
8
|
+
* and saw a broken version of it. It was not a broken version. It was not a
|
|
9
|
+
* version of anything (dono, 01/08).
|
|
10
|
+
*
|
|
11
|
+
* THE THREE FRONTIERS, and knowing which one a node is is the whole job:
|
|
12
|
+
*
|
|
13
|
+
* a PART an element of theirs it has styles, and we draw it
|
|
14
|
+
* a COMPONENT a component of theirs it has a recipe of its own, so it
|
|
15
|
+
* renders as a NAMED BLOCK and becomes
|
|
16
|
+
* a rule about the two of them
|
|
17
|
+
* an EXTERNAL a third-party library we do not have it and never will, so
|
|
18
|
+
* it renders as a block and becomes
|
|
19
|
+
* rules about how to build with it
|
|
20
|
+
*
|
|
21
|
+
* **Depth is not a number, it is where the frontier sits.** The skill descends
|
|
22
|
+
* until it meets another component or a library, stops, and records the edge -
|
|
23
|
+
* which answers "how deep should a part go" without inventing a parameter, and
|
|
24
|
+
* makes the answer different per component because it genuinely is.
|
|
25
|
+
*
|
|
26
|
+
* WHAT THIS FILE GUARANTEES, and it is the reason the two halves are derived from
|
|
27
|
+
* one source instead of being sent as two fields: the flat `parts` map and the
|
|
28
|
+
* tree can never disagree. A tree pointing at a part that does not exist would
|
|
29
|
+
* validate everywhere and render as a hole, and "valid and unread" is the worst
|
|
30
|
+
* of the three outcomes this pipeline has paid for twice.
|
|
31
|
+
*/
|
|
32
|
+
import { transcribe, } from "./doctor/transcribe.js";
|
|
33
|
+
/** The nine forms, closed. A renderer that accepts any tag draws anything. */
|
|
34
|
+
const FORMS = new Set([
|
|
35
|
+
"image",
|
|
36
|
+
"heading",
|
|
37
|
+
"text",
|
|
38
|
+
"button",
|
|
39
|
+
"field",
|
|
40
|
+
"icon",
|
|
41
|
+
"row",
|
|
42
|
+
"stack",
|
|
43
|
+
"component",
|
|
44
|
+
"external",
|
|
45
|
+
]);
|
|
46
|
+
/** The two that arrange, and the only two that take children. */
|
|
47
|
+
const CONTAINERS = new Set(["row", "stack"]);
|
|
48
|
+
/** The two frontiers: no name, no styles - what is there is not ours to hold. */
|
|
49
|
+
const FRONTIERS = new Set(["component", "external"]);
|
|
50
|
+
/**
|
|
51
|
+
* Names that ARE the element itself. A node for one of these draws a box inside
|
|
52
|
+
* its own box, because its styles already sit on the component's base.
|
|
53
|
+
*/
|
|
54
|
+
const STRUCTURAL = /^(root|wrapper|base|el|outer)$/i;
|
|
55
|
+
/** A pathological transcription of somebody's DOM, bounded. Six levels reaches
|
|
56
|
+
* any real component; past that it is layout divs all the way down. */
|
|
57
|
+
const MAX_DEPTH = 6;
|
|
58
|
+
const MAX_NODES = 48;
|
|
59
|
+
/**
|
|
60
|
+
* `Actions.Generate` → `actions-generate`, and never `actions.generate`.
|
|
61
|
+
*
|
|
62
|
+
* A dot compiles to `.ds-card-actions.generate`, which is two classes and
|
|
63
|
+
* invalid - and `z.record(z.string())` validates the key all the way through to
|
|
64
|
+
* a broken stylesheet, so the guard has to be here rather than in the schema.
|
|
65
|
+
*/
|
|
66
|
+
export function safePartName(raw) {
|
|
67
|
+
return raw
|
|
68
|
+
.trim()
|
|
69
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
72
|
+
.replace(/-+/g, "-")
|
|
73
|
+
.replace(/^-|-$/g, "")
|
|
74
|
+
.replace(/^([0-9])/, "n$1");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Which form a node takes, forgiving a wrong label without losing the subtree.
|
|
78
|
+
*
|
|
79
|
+
* A tenth form is a mistake, and the cheap response - drop the node - would take
|
|
80
|
+
* its children with it. Something with children ARRANGES, so it becomes a stack;
|
|
81
|
+
* something without carries content, so it becomes text.
|
|
82
|
+
*/
|
|
83
|
+
function formOf(node) {
|
|
84
|
+
const as = String(node.as ?? "")
|
|
85
|
+
.trim()
|
|
86
|
+
.toLowerCase();
|
|
87
|
+
if (FORMS.has(as))
|
|
88
|
+
return { as, coerced: false };
|
|
89
|
+
const arranges = Array.isArray(node.children) && node.children.length > 0;
|
|
90
|
+
return { as: arranges ? "stack" : "text", coerced: true };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fold the anatomy into a flat parts map and a tree, in one pass.
|
|
94
|
+
*
|
|
95
|
+
* `declared` is their own tokens, so a `bg-ocean-500` on a nested part resolves
|
|
96
|
+
* to the name they gave it rather than to a literal.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveAnatomy(read, declared, deps) {
|
|
99
|
+
const parts = {};
|
|
100
|
+
const composes = [];
|
|
101
|
+
const external = [];
|
|
102
|
+
const notes = [];
|
|
103
|
+
let budget = MAX_NODES;
|
|
104
|
+
let coerced = 0;
|
|
105
|
+
/**
|
|
106
|
+
* A SINGLE STRUCTURAL ROOT IS THE COMPONENT, NOT A NODE OF IT.
|
|
107
|
+
*
|
|
108
|
+
* The skill is told not to send `root`/`wrapper`, and when it does anyway at
|
|
109
|
+
* the top the honest fix is to unwrap it: its styles are the base's, and
|
|
110
|
+
* keeping it would draw the component inside a copy of itself.
|
|
111
|
+
*/
|
|
112
|
+
let top = read;
|
|
113
|
+
while (Array.isArray(top) &&
|
|
114
|
+
top.length === 1 &&
|
|
115
|
+
top[0] &&
|
|
116
|
+
STRUCTURAL.test(String(top[0].name ?? "")) &&
|
|
117
|
+
Array.isArray(top[0].children) &&
|
|
118
|
+
top[0].children.length > 0) {
|
|
119
|
+
top = top[0].children;
|
|
120
|
+
notes.push("the outermost wrapper is the component itself, so its children are the anatomy");
|
|
121
|
+
}
|
|
122
|
+
/** Part names, deduped: a second `label` becomes `label-2`, deterministically. */
|
|
123
|
+
const claim = (wanted) => {
|
|
124
|
+
if (parts[wanted] == null)
|
|
125
|
+
return wanted;
|
|
126
|
+
for (let n = 2; n < 100; n++) {
|
|
127
|
+
const candidate = `${wanted}-${n}`;
|
|
128
|
+
if (parts[candidate] == null)
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
return `${wanted}-x`;
|
|
132
|
+
};
|
|
133
|
+
const walk = (nodes, depth) => {
|
|
134
|
+
const out = [];
|
|
135
|
+
if (depth > MAX_DEPTH)
|
|
136
|
+
return out;
|
|
137
|
+
for (const node of nodes) {
|
|
138
|
+
if (!node || typeof node !== "object")
|
|
139
|
+
continue;
|
|
140
|
+
if (budget <= 0)
|
|
141
|
+
break;
|
|
142
|
+
budget -= 1;
|
|
143
|
+
const { as, coerced: wasCoerced } = formOf(node);
|
|
144
|
+
if (wasCoerced)
|
|
145
|
+
coerced += 1;
|
|
146
|
+
// A FRONTIER carries a reference and nothing else. A name or a class list
|
|
147
|
+
// on one would be us claiming styles that belong to somebody else.
|
|
148
|
+
if (FRONTIERS.has(as)) {
|
|
149
|
+
if (as === "component") {
|
|
150
|
+
const ref = String(node.ref ?? "").trim();
|
|
151
|
+
if (!ref)
|
|
152
|
+
continue;
|
|
153
|
+
if (!composes.includes(ref))
|
|
154
|
+
composes.push(ref);
|
|
155
|
+
out.push({ as, ref });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const from = String(node.from ?? "").trim();
|
|
159
|
+
if (!from)
|
|
160
|
+
continue;
|
|
161
|
+
const version = deps?.[from];
|
|
162
|
+
if (!external.some((e) => e.from === from))
|
|
163
|
+
external.push({ from, version });
|
|
164
|
+
out.push(version ? { as, from, version } : { as, from });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const node_ = { as };
|
|
168
|
+
// The NAME is what carries the styles, so it is claimed even when nothing
|
|
169
|
+
// resolved: the tree triggers on presence, and structure is most of the
|
|
170
|
+
// value. A node with no name is pure structure and that is legitimate.
|
|
171
|
+
const wanted = safePartName(String(node.name ?? ""));
|
|
172
|
+
if (wanted) {
|
|
173
|
+
const key = claim(wanted);
|
|
174
|
+
if (key !== wanted) {
|
|
175
|
+
notes.push(`two parts named \`${wanted}\` - the second is \`${key}\``);
|
|
176
|
+
}
|
|
177
|
+
const classes = typeof node.classes === "string"
|
|
178
|
+
? node.classes.split(/\s+/).filter(Boolean)
|
|
179
|
+
: [];
|
|
180
|
+
const t = transcribe(classes, declared);
|
|
181
|
+
parts[key] = { base: t.base, dark: t.dark, states: t.states };
|
|
182
|
+
node_.part = key;
|
|
183
|
+
}
|
|
184
|
+
const text = typeof node.text === "string" ? node.text.trim() : "";
|
|
185
|
+
if (text)
|
|
186
|
+
node_.text = text.slice(0, 120);
|
|
187
|
+
// Only the two arrangers descend. A leaf with children is a leaf that was
|
|
188
|
+
// labelled wrongly, and its children would render nowhere.
|
|
189
|
+
if (CONTAINERS.has(as) && Array.isArray(node.children)) {
|
|
190
|
+
const children = walk(node.children, depth + 1);
|
|
191
|
+
if (children.length > 0)
|
|
192
|
+
node_.children = children;
|
|
193
|
+
}
|
|
194
|
+
else if (Array.isArray(node.children) && node.children.length > 0) {
|
|
195
|
+
const children = walk(node.children, depth + 1);
|
|
196
|
+
// Hoist rather than lose them: the label was wrong, the structure was not.
|
|
197
|
+
out.push(node_, ...children);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
out.push(node_);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
};
|
|
204
|
+
const tree = walk(top, 0);
|
|
205
|
+
if (coerced > 0) {
|
|
206
|
+
notes.push(`${coerced} node${coerced === 1 ? "" : "s"} named a form that does not exist - kept, drawn as what ${coerced === 1 ? "it holds" : "they hold"}`);
|
|
207
|
+
}
|
|
208
|
+
if (budget <= 0) {
|
|
209
|
+
notes.push(`the anatomy was longer than ${MAX_NODES} nodes - the rest is layout, and it is not read`);
|
|
210
|
+
}
|
|
211
|
+
return { parts, tree, composes, external, notes };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The OLD flat form, folded into the same result shape.
|
|
215
|
+
*
|
|
216
|
+
* 23 standard components carry flat parts and no tree, and a skill written
|
|
217
|
+
* against the previous contract sends this. Both keep working: the parts style
|
|
218
|
+
* correctly, and the absence of a tree means the preview lays them out in a row
|
|
219
|
+
* exactly as it did before.
|
|
220
|
+
*/
|
|
221
|
+
export function resolveFlatParts(read, declared) {
|
|
222
|
+
const parts = {};
|
|
223
|
+
for (const part of read) {
|
|
224
|
+
const name = safePartName(String(part?.name ?? ""));
|
|
225
|
+
if (!name || typeof part.classes !== "string")
|
|
226
|
+
continue;
|
|
227
|
+
const t = transcribe(part.classes.split(/\s+/).filter(Boolean), declared);
|
|
228
|
+
parts[name] = { base: t.base, dark: t.dark, states: t.states };
|
|
229
|
+
}
|
|
230
|
+
return { parts, tree: [], composes: [], external: [], notes: [] };
|
|
231
|
+
}
|
package/dist/commands/add.js
CHANGED
|
@@ -6,7 +6,7 @@ import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.
|
|
|
6
6
|
import { buildGuide } from "../guide.js";
|
|
7
7
|
import { body as line, section, snippet } from "../output.js";
|
|
8
8
|
import { fetchDesignSystem } from "../registry.js";
|
|
9
|
-
import { describeFiltered, rulesForProject } from "../rule-filter.js";
|
|
9
|
+
import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.js";
|
|
10
10
|
import { detectStack } from "../stack.js";
|
|
11
11
|
async function readRootLock(path) {
|
|
12
12
|
try {
|
|
@@ -97,6 +97,28 @@ export async function add(slug, opts) {
|
|
|
97
97
|
// a thing the person should hear once, not discover by its absence.
|
|
98
98
|
if (leftOut)
|
|
99
99
|
console.log(line(leftOut));
|
|
100
|
+
/**
|
|
101
|
+
* 5b-ii. THE PACKAGES THIS SYSTEM NEEDS, kept where the doctor can read them.
|
|
102
|
+
*
|
|
103
|
+
* `rules.md` is prose with maximum authority, and prose is the wrong thing to
|
|
104
|
+
* enforce against: a check that grepped "requires @tiptap/react" out of a
|
|
105
|
+
* sentence would break the first time somebody reworded it. So the one
|
|
106
|
+
* verifiable field travels as data, filtered by THIS project's stack exactly
|
|
107
|
+
* like the prose was.
|
|
108
|
+
*
|
|
109
|
+
* Written only when there is something to say, so a system with no third-party
|
|
110
|
+
* component leaves no file behind.
|
|
111
|
+
*/
|
|
112
|
+
const required = (payload.ruleSet ?? [])
|
|
113
|
+
.filter((r) => r.requires && ruleApplies(r, stack))
|
|
114
|
+
.map((r) => ({
|
|
115
|
+
requires: r.requires,
|
|
116
|
+
applies: r.applies ?? [],
|
|
117
|
+
...(r.pinned ? { pinned: r.pinned } : {}),
|
|
118
|
+
}));
|
|
119
|
+
if (required.length > 0) {
|
|
120
|
+
await writeFile(join(slugDir, "requires.json"), `${JSON.stringify(required, null, 2)}\n`, "utf8");
|
|
121
|
+
}
|
|
100
122
|
// 5c. structured philosophy (personal DS) → philosophy.md at the slug root.
|
|
101
123
|
// Narrative guidance (mission, principles, voice, motion doctrine…); the
|
|
102
124
|
// GUIDE points the agent here right after rules.md.
|
|
@@ -5,6 +5,7 @@ import { generateComponentFiles } from "../component-codegen.js";
|
|
|
5
5
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
6
6
|
import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
|
|
7
7
|
import { body, section, snippet } from "../output.js";
|
|
8
|
+
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
8
9
|
import { fetchComponent, RegistryError } from "../registry.js";
|
|
9
10
|
/**
|
|
10
11
|
* Writes the shared `cn.ts` next to the components, built from THIS project's
|
|
@@ -27,28 +28,6 @@ async function writeCn(root, compDir, slug) {
|
|
|
27
28
|
return;
|
|
28
29
|
await writeFile(join(compDir, "..", "cn.ts"), emitCn(slug, readVocabulary(real)), "utf8");
|
|
29
30
|
}
|
|
30
|
-
/**
|
|
31
|
-
* The consumer's React major, or null when we cannot tell.
|
|
32
|
-
*
|
|
33
|
-
* It decides whether the generated components can take a `ref`: from 19 that is
|
|
34
|
-
* an ordinary prop, before it a function component needs `forwardRef`. Guessing
|
|
35
|
-
* high on an older project would emit a type that accepts a ref React then
|
|
36
|
-
* silently drops, so anything unreadable falls back to the ref-less type.
|
|
37
|
-
*/
|
|
38
|
-
async function reactMajorOf(root) {
|
|
39
|
-
const raw = await readFile(join(root, "package.json"), "utf8").catch(() => "");
|
|
40
|
-
if (!raw)
|
|
41
|
-
return null;
|
|
42
|
-
try {
|
|
43
|
-
const pkg = JSON.parse(raw);
|
|
44
|
-
const spec = pkg.dependencies?.react ?? pkg.devDependencies?.react;
|
|
45
|
-
const major = /(\d+)/.exec(spec ?? "")?.[1];
|
|
46
|
-
return major ? Number(major) : null;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
31
|
/** Slugs/names are kebab-case by contract; reject anything else before it ever
|
|
53
32
|
* reaches a filesystem path (defense-in-depth against `../` traversal). */
|
|
54
33
|
const SAFE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -103,7 +82,16 @@ export async function component(slug, name, opts) {
|
|
|
103
82
|
filenames = [`${res.name}.tsx`, `${res.name}.css`, "index.ts"];
|
|
104
83
|
}
|
|
105
84
|
else {
|
|
106
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles, await reactMajorOf(root)
|
|
85
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles, await reactMajorOf(root),
|
|
86
|
+
/**
|
|
87
|
+
* THE SPELLING, FROM THE VERSION WE JUST FETCHED.
|
|
88
|
+
*
|
|
89
|
+
* The response wins over the document on disk because it belongs to the
|
|
90
|
+
* exact version being written - and the CSS in `res.css` was compiled with
|
|
91
|
+
* it, so the TSX and the stylesheet in the same folder agree by
|
|
92
|
+
* construction. Falling back to disk keeps an older registry working.
|
|
93
|
+
*/
|
|
94
|
+
res.classNames ?? (await readInstalledConvention(root, slug)));
|
|
107
95
|
for (const file of files) {
|
|
108
96
|
await writeFile(join(compDir, file.filename), file.code, "utf8");
|
|
109
97
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, relative, resolve } from "node:path";
|
|
|
3
3
|
import { findDivergences } from "../doctor/coherence.js";
|
|
4
4
|
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
5
5
|
import { checkContracts } from "../doctor/contract-check.js";
|
|
6
|
+
import { describeMissing, missingDependencies, summarizeMissing, } from "../doctor/dependencies.js";
|
|
6
7
|
import { findFrozenBindings } from "../doctor/frozen.js";
|
|
7
8
|
import { appendEvent, readEvents, summarize } from "../doctor/ledger.js";
|
|
8
9
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
@@ -11,6 +12,7 @@ import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
|
|
|
11
12
|
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
12
13
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
13
14
|
import { body, paint, section, snippet } from "../output.js";
|
|
15
|
+
import { resolveDeps } from "../stack.js";
|
|
14
16
|
/**
|
|
15
17
|
* `synthesisui doctor` - the check nobody else ships.
|
|
16
18
|
*
|
|
@@ -114,7 +116,12 @@ export async function loadSystem(root) {
|
|
|
114
116
|
.map((e) => e.name);
|
|
115
117
|
}
|
|
116
118
|
catch {
|
|
117
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
table: EMPTY_TABLE,
|
|
121
|
+
recipes: new Map(),
|
|
122
|
+
documents: [],
|
|
123
|
+
requires: [],
|
|
124
|
+
};
|
|
118
125
|
}
|
|
119
126
|
// Several systems can live side by side; every token is prefixed --ds-, so
|
|
120
127
|
// reading them all is both correct and what the running app actually sees.
|
|
@@ -123,6 +130,7 @@ export async function loadSystem(root) {
|
|
|
123
130
|
let adopted = false;
|
|
124
131
|
const recipes = new Map();
|
|
125
132
|
const documents = [];
|
|
133
|
+
const requires = [];
|
|
126
134
|
for (const slug of slugs) {
|
|
127
135
|
const dir = join(dsDir, slug);
|
|
128
136
|
const raw = await readFile(join(dir, ".lock"), "utf8").catch(() => "");
|
|
@@ -182,11 +190,25 @@ export async function loadSystem(root) {
|
|
|
182
190
|
// A document we cannot parse costs the component pass, not the run.
|
|
183
191
|
}
|
|
184
192
|
}
|
|
193
|
+
// Written by `add` only when this system has a third-party component, so its
|
|
194
|
+
// absence is the normal case rather than a failure.
|
|
195
|
+
const reqRaw = await readFile(join(dir, "requires.json"), "utf8").catch(() => "");
|
|
196
|
+
if (reqRaw) {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(reqRaw);
|
|
199
|
+
if (Array.isArray(parsed))
|
|
200
|
+
requires.push(...parsed);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Unreadable costs this check, not the run.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
185
206
|
}
|
|
186
207
|
return {
|
|
187
208
|
table: buildTable({ css, lock, source: adopted ? "adopted" : "installed" }),
|
|
188
209
|
recipes,
|
|
189
210
|
documents,
|
|
211
|
+
requires,
|
|
190
212
|
};
|
|
191
213
|
}
|
|
192
214
|
/**
|
|
@@ -870,6 +892,32 @@ export async function doctor(opts) {
|
|
|
870
892
|
say(body("complains - the surface just stays put when the scheme"));
|
|
871
893
|
say(body("moves around it."));
|
|
872
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* THE LIBRARIES THE SYSTEM SAYS YOU NEED - the half that makes a rule about a
|
|
897
|
+
* third-party component into governance rather than a sentence.
|
|
898
|
+
*
|
|
899
|
+
* Some of their components are built on somebody else's library. We do not have
|
|
900
|
+
* it, cannot draw it, and said so: the anatomy stops at the frontier and the
|
|
901
|
+
* knowledge moved into a rule carrying the package name as DATA. This is where
|
|
902
|
+
* that gets checked, against the same manifest reader the stack detection uses.
|
|
903
|
+
*
|
|
904
|
+
* It never installs. A dependency has a licence, a bundle cost and a maintainer
|
|
905
|
+
* attached, so the answer is to report and let them decide.
|
|
906
|
+
*/
|
|
907
|
+
const missingDeps = missingDependencies(installed.requires, await resolveDeps(root).catch(() => ({})));
|
|
908
|
+
if (missingDeps.length > 0) {
|
|
909
|
+
say(section("This system needs libraries you do not have"));
|
|
910
|
+
const summary = summarizeMissing(missingDeps);
|
|
911
|
+
if (summary)
|
|
912
|
+
say(body(summary));
|
|
913
|
+
say("");
|
|
914
|
+
for (const dep of describeMissing(missingDeps))
|
|
915
|
+
say(body(dep));
|
|
916
|
+
say("");
|
|
917
|
+
say(body("Nothing was installed for you. Install what you actually use:"));
|
|
918
|
+
say("");
|
|
919
|
+
say(` npm i ${missingDeps.map((d) => d.name).join(" ")}`);
|
|
920
|
+
}
|
|
873
921
|
const frozenAt = new Set(frozen.map((f) => `${f.component}:${f.where.split(" · ").pop()}`));
|
|
874
922
|
let offSystemCount = 0;
|
|
875
923
|
let lawKeepingCount = 0;
|
|
@@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
|
+
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
5
6
|
import { postGenerate, RegistryError } from "../registry.js";
|
|
6
7
|
/** PascalCase para o hint de import (course-card → CourseCard). */
|
|
7
8
|
function pascalName(name) {
|
|
@@ -70,7 +71,10 @@ export async function generate(description, opts) {
|
|
|
70
71
|
const version = await readActiveVersion(root, slug);
|
|
71
72
|
const compDir = join(root, config.componentsDir, res.name);
|
|
72
73
|
await mkdir(compDir, { recursive: true });
|
|
73
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, version, config.styles)
|
|
74
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, version, config.styles, await reactMajorOf(root),
|
|
75
|
+
// Read off the installed document: a generated component lands in the same
|
|
76
|
+
// project as the stylesheet it has to match.
|
|
77
|
+
await readInstalledConvention(root, slug));
|
|
74
78
|
for (const file of files) {
|
|
75
79
|
await writeFile(join(compDir, file.filename), file.code, "utf8");
|
|
76
80
|
}
|
package/dist/commands/import.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { resolveAnatomy, resolveFlatParts, } from "../anatomy-read.js";
|
|
3
4
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
4
5
|
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
5
6
|
import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
|
|
@@ -11,9 +12,9 @@ import { describeConvention, describeRemainder, detectConventions, } from "../do
|
|
|
11
12
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
12
13
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
13
14
|
import { buildTable } from "../doctor/tokens.js";
|
|
14
|
-
import { rootClasses, rootTag, transcribe,
|
|
15
|
+
import { rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
15
16
|
import { body, paint, section } from "../output.js";
|
|
16
|
-
import { detectStack } from "../stack.js";
|
|
17
|
+
import { detectStack, resolveDeps } from "../stack.js";
|
|
17
18
|
import { walk, walkAll } from "./doctor.js";
|
|
18
19
|
/**
|
|
19
20
|
* How many distinct values travel, PER KIND.
|
|
@@ -896,28 +897,60 @@ function sayReach(c) {
|
|
|
896
897
|
}
|
|
897
898
|
}
|
|
898
899
|
/**
|
|
899
|
-
* Fold the skill's
|
|
900
|
+
* Fold the skill's anatomy into the transcription, in place.
|
|
901
|
+
*
|
|
902
|
+
* A CLEAN SPLIT OF WHO KNOWS WHAT. Only the skill can say that a span holds the
|
|
903
|
+
* title and that the thing in the middle is their own `TextEditor`; only this
|
|
904
|
+
* side can say that `text-ocean-500` is `{color.ocean.500}` and that
|
|
905
|
+
* `@tiptap/react` is pinned at `^2.1.0` in their manifest. The platform then
|
|
906
|
+
* turns declarations into roles, because only it has the two palettes.
|
|
900
907
|
*
|
|
901
908
|
* Their declared tokens come from the census itself, so a run that only sends a
|
|
902
909
|
* file it was handed still resolves a `bg-ocean-500` by their own name.
|
|
903
910
|
*/
|
|
904
|
-
function resolveReadParts(census) {
|
|
911
|
+
async function resolveReadParts(census, root) {
|
|
905
912
|
const read = census.reading?.components;
|
|
906
913
|
if (!read)
|
|
907
914
|
return;
|
|
908
915
|
const declared = new Map(Object.entries(census.declared));
|
|
909
|
-
|
|
916
|
+
// The manifest is read ONCE for the whole run: the version belongs in a rule's
|
|
917
|
+
// EVIDENCE, never in its text, so the rule does not go stale on an upgrade and
|
|
918
|
+
// the number is not lost either.
|
|
919
|
+
const deps = await resolveDeps(root).catch(() => ({}));
|
|
920
|
+
census.looks ??= {};
|
|
921
|
+
const looks = census.looks;
|
|
910
922
|
let named = 0;
|
|
923
|
+
let shaped = 0;
|
|
924
|
+
let edges = 0;
|
|
925
|
+
const libs = new Set();
|
|
926
|
+
const notes = new Set();
|
|
911
927
|
for (const [component, entry] of Object.entries(read)) {
|
|
912
|
-
const
|
|
913
|
-
|
|
928
|
+
const anatomy = entry?.anatomy;
|
|
929
|
+
const resolved = Array.isArray(anatomy) && anatomy.length > 0
|
|
930
|
+
? resolveAnatomy(anatomy, declared, deps)
|
|
931
|
+
: Array.isArray(entry?.parts) && entry.parts.length > 0
|
|
932
|
+
? resolveFlatParts(entry.parts, declared)
|
|
933
|
+
: null;
|
|
934
|
+
if (!resolved)
|
|
914
935
|
continue;
|
|
915
|
-
const
|
|
916
|
-
if (
|
|
936
|
+
const hasParts = Object.keys(resolved.parts).length > 0;
|
|
937
|
+
if (!hasParts && resolved.tree.length === 0)
|
|
917
938
|
continue;
|
|
918
939
|
const look = looks[component];
|
|
940
|
+
const carried = {
|
|
941
|
+
...(hasParts ? { parts: resolved.parts } : {}),
|
|
942
|
+
...(resolved.tree.length > 0 ? { tree: resolved.tree } : {}),
|
|
943
|
+
...(resolved.composes.length > 0 ? { composes: resolved.composes } : {}),
|
|
944
|
+
...(resolved.external.length > 0 ? { external: resolved.external } : {}),
|
|
945
|
+
};
|
|
919
946
|
looks[component] = look
|
|
920
|
-
? {
|
|
947
|
+
? {
|
|
948
|
+
...look,
|
|
949
|
+
...carried,
|
|
950
|
+
...(hasParts
|
|
951
|
+
? { parts: { ...(look.parts ?? {}), ...resolved.parts } }
|
|
952
|
+
: {}),
|
|
953
|
+
}
|
|
921
954
|
: {
|
|
922
955
|
base: {},
|
|
923
956
|
dark: {},
|
|
@@ -926,14 +959,37 @@ function resolveReadParts(census) {
|
|
|
926
959
|
literals: [],
|
|
927
960
|
fromToken: 0,
|
|
928
961
|
fromLiteral: 0,
|
|
929
|
-
|
|
962
|
+
...carried,
|
|
930
963
|
};
|
|
931
|
-
named += Object.keys(resolved).length;
|
|
964
|
+
named += Object.keys(resolved.parts).length;
|
|
965
|
+
if (resolved.tree.length > 0)
|
|
966
|
+
shaped += 1;
|
|
967
|
+
edges += resolved.composes.length;
|
|
968
|
+
for (const e of resolved.external)
|
|
969
|
+
libs.add(e.from);
|
|
970
|
+
for (const n of resolved.notes)
|
|
971
|
+
notes.add(n);
|
|
932
972
|
}
|
|
933
|
-
|
|
973
|
+
const total = Object.keys(read).length;
|
|
974
|
+
if (named > 0 || shaped > 0) {
|
|
934
975
|
console.log("");
|
|
935
|
-
console.log(body(`${named} part${named === 1 ? "" : "s"} your reading named across ${
|
|
976
|
+
console.log(body(`${named} part${named === 1 ? "" : "s"} your reading named across ${total} component${total === 1 ? "" : "s"} - each previews as what it IS rather than as a text box`));
|
|
977
|
+
}
|
|
978
|
+
if (shaped > 0) {
|
|
979
|
+
console.log(body(`${shaped} of them arrived with a SHAPE - the parts nested as they nest in your code, so the preview is the component and not a row of labels`));
|
|
980
|
+
}
|
|
981
|
+
// A frontier is worth its own line, because it is the half nobody else has:
|
|
982
|
+
// the edge is a rule an agent can obey, not a picture.
|
|
983
|
+
if (edges > 0) {
|
|
984
|
+
console.log(body(`${edges} place${edges === 1 ? "" : "s"} where one of your components is built out of another - ${edges === 1 ? "it becomes a rule" : "each becomes a rule"} about the two of them, not a guess`));
|
|
985
|
+
}
|
|
986
|
+
if (libs.size > 0) {
|
|
987
|
+
const named_ = [...libs].slice(0, 3).join(", ");
|
|
988
|
+
console.log(body(`${libs.size} librar${libs.size === 1 ? "y" : "ies"} your components need (${named_}) - ${libs.size === 1 ? "it does" : "they do"} not render here, and what to know about ${libs.size === 1 ? "it" : "them"} travels as rules`));
|
|
936
989
|
}
|
|
990
|
+
// NO SILENT FIX. A correction made quietly is a lie about what they wrote.
|
|
991
|
+
for (const n of notes)
|
|
992
|
+
console.log(body(n));
|
|
937
993
|
}
|
|
938
994
|
export async function runImport(opts) {
|
|
939
995
|
const root = opts.root ?? process.cwd();
|
|
@@ -981,7 +1037,7 @@ export async function runImport(opts) {
|
|
|
981
1037
|
* Resolved here rather than at measure time because the reading arrives
|
|
982
1038
|
* AFTER the census was taken - this is the first moment both halves exist.
|
|
983
1039
|
*/
|
|
984
|
-
resolveReadParts(census);
|
|
1040
|
+
await resolveReadParts(census, root);
|
|
985
1041
|
}
|
|
986
1042
|
else {
|
|
987
1043
|
console.log(section("Reading your project"));
|
package/dist/commands/refit.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, join } from "node:path";
|
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { body, section, snippet } from "../output.js";
|
|
6
|
+
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
6
7
|
import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
7
8
|
/** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
|
|
8
9
|
* a folder holding only refit artifacts doesn't count). */
|
|
@@ -132,7 +133,7 @@ export async function refit(file, opts) {
|
|
|
132
133
|
if (config.target === "next") {
|
|
133
134
|
const compDir = join(root, config.componentsDir, res.name);
|
|
134
135
|
await mkdir(compDir, { recursive: true });
|
|
135
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles);
|
|
136
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles, await reactMajorOf(root), await readInstalledConvention(root, slug));
|
|
136
137
|
for (const f of files) {
|
|
137
138
|
await writeFile(join(compDir, f.filename), f.code, "utf8");
|
|
138
139
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -4,6 +4,7 @@ import { generateComponentFiles } from "../component-codegen.js";
|
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
|
|
6
6
|
import { body, section, snippet } from "../output.js";
|
|
7
|
+
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
7
8
|
import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
8
9
|
import { add } from "./add.js";
|
|
9
10
|
/**
|
|
@@ -151,7 +152,11 @@ export async function upgrade(slug, opts) {
|
|
|
151
152
|
continue;
|
|
152
153
|
try {
|
|
153
154
|
const res = await fetchComponent(base, slug, entry);
|
|
154
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles
|
|
155
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles,
|
|
156
|
+
// Both were missing here, and `upgrade` is the command that REWRITES
|
|
157
|
+
// components somebody already has: without the convention it would have
|
|
158
|
+
// taken a working component and stripped its styles.
|
|
159
|
+
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)));
|
|
155
160
|
for (const file of files) {
|
|
156
161
|
await writeFile(join(componentsRoot, entry, file.filename), file.code, "utf8");
|
|
157
162
|
}
|