synthesisui 0.4.4 → 0.4.6
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/claude-md.js +68 -8
- package/dist/commands/upgrade.js +25 -6
- package/dist/document-diff.js +174 -0
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -26,23 +26,83 @@ async function readInstalled(projectRoot) {
|
|
|
26
26
|
}
|
|
27
27
|
return locks;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
/** First sentence of a description, capped - the manifest must stay lean. */
|
|
30
|
+
function summarize(desc) {
|
|
31
|
+
if (typeof desc !== "string" || !desc.trim())
|
|
32
|
+
return "";
|
|
33
|
+
const first = desc.trim().split(/(?<=\.)\s/)[0] ?? desc.trim();
|
|
34
|
+
return first.length > 90 ? `${first.slice(0, 87)}…` : first;
|
|
35
|
+
}
|
|
36
|
+
/** One manifest line per recipe: name, what it is, and its variant axes. */
|
|
37
|
+
function catalogLines(recipes) {
|
|
38
|
+
return Object.entries(recipes)
|
|
39
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
40
|
+
.map(([name, recipe]) => {
|
|
41
|
+
const r = recipe;
|
|
42
|
+
const axes = Object.entries(r.variants ?? {})
|
|
43
|
+
.map(([axis, options]) => {
|
|
44
|
+
const keys = Object.keys(options);
|
|
45
|
+
return keys.length <= 4 ? `${axis}: ${keys.join("|")}` : axis;
|
|
46
|
+
})
|
|
47
|
+
.join("; ");
|
|
48
|
+
const desc = summarize(r.description);
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
line: ` - \`ds-${name}\`${desc ? ` - ${desc}` : ""}${axes ? ` [${axes}]` : ""}`,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The COMPONENT MANIFEST for one installed system, read from its versioned
|
|
57
|
+
* design-system.json. This is what lets the app's agent know what it already
|
|
58
|
+
* HAS - "use these before creating new ones" - instead of re-inventing
|
|
59
|
+
* buttons. Returns null when the document isn't readable (older installs).
|
|
60
|
+
*/
|
|
61
|
+
async function readManifest(projectRoot, ds) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await readFile(join(projectRoot, "_synthesisui", "ds", ds.slug, `v${ds.version}`, "design-system.json"), "utf8");
|
|
64
|
+
const doc = JSON.parse(raw);
|
|
65
|
+
const components = catalogLines(doc.components ?? {});
|
|
66
|
+
const blocks = catalogLines(doc.blocks ?? {});
|
|
67
|
+
if (components.length === 0 && blocks.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
const lines = [];
|
|
70
|
+
if (components.length > 0) {
|
|
71
|
+
lines.push(` Components (${components.length}) - USE these before creating new ones:`);
|
|
72
|
+
lines.push(...components.map((c) => c.line));
|
|
73
|
+
}
|
|
74
|
+
if (blocks.length > 0) {
|
|
75
|
+
lines.push(` Engagement blocks (${blocks.length}):`);
|
|
76
|
+
lines.push(...blocks.map((c) => c.line));
|
|
77
|
+
}
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function renderRegion(projectRoot, installed) {
|
|
30
85
|
if (installed.length === 0) {
|
|
31
86
|
return `${START}\n${END}`;
|
|
32
87
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
.
|
|
88
|
+
const sections = [];
|
|
89
|
+
for (const ds of installed) {
|
|
90
|
+
const head = `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) - guide: \`_synthesisui/ds/${ds.slug}/v${ds.version}/GUIDE.md\``;
|
|
91
|
+
const manifest = await readManifest(projectRoot, ds);
|
|
92
|
+
sections.push(manifest ? `${head}\n${manifest}` : head);
|
|
93
|
+
}
|
|
36
94
|
const body = `## Design Systems (via SynthesisUI)
|
|
37
95
|
|
|
38
96
|
This project uses design system(s) brought in by the \`synthesisui\` CLI. **When creating or editing
|
|
39
97
|
components, read the system's GUIDE.md and follow it:** use only semantic tokens
|
|
40
98
|
(\`var(--ds-color-semantic-*)\`, \`--ds-spacing-*\`, etc.), scope the UI with \`data-ds="<slug>"\`,
|
|
41
|
-
and reuse the \`.ds-*\` classes. Do not use raw values outside the system's scale. **
|
|
99
|
+
and reuse the \`.ds-*\` classes. Do not use raw values outside the system's scale. **Before
|
|
100
|
+
creating any UI element, check the component manifest below - if it exists, use or extend
|
|
101
|
+
it (\`synthesisui component <slug> <name>\` materializes it as your code).** To review a
|
|
42
102
|
component, create an isolated sample page (e.g. \`app/synthesisui-samples/<component>/\`) - do not
|
|
43
|
-
apply it to real production pages unless asked
|
|
103
|
+
apply it to real production pages unless asked.
|
|
44
104
|
|
|
45
|
-
${
|
|
105
|
+
${sections.join("\n")}
|
|
46
106
|
|
|
47
107
|
_Block managed by the CLI - do not edit by hand; run \`synthesisui add <slug>\` to update._`;
|
|
48
108
|
return `${START}\n${body}\n${END}`;
|
|
@@ -54,7 +114,7 @@ _Block managed by the CLI - do not edit by hand; run \`synthesisui add <slug>\`
|
|
|
54
114
|
*/
|
|
55
115
|
export async function syncClaudeMd(projectRoot) {
|
|
56
116
|
const installed = await readInstalled(projectRoot);
|
|
57
|
-
const region = renderRegion(installed);
|
|
117
|
+
const region = await renderRegion(projectRoot, installed);
|
|
58
118
|
const path = join(projectRoot, "CLAUDE.md");
|
|
59
119
|
let existing = null;
|
|
60
120
|
try {
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -2,6 +2,7 @@ import { 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 { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
|
|
5
6
|
import { body, section, snippet } from "../output.js";
|
|
6
7
|
import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
7
8
|
import { add } from "./add.js";
|
|
@@ -85,11 +86,29 @@ export async function upgrade(slug, opts) {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
|
-
// 3. deterministic changelog → UPGRADE.md (the agent's migration brief)
|
|
89
|
-
|
|
89
|
+
// 3. deterministic changelog → UPGRADE.md (the agent's migration brief).
|
|
90
|
+
// Diffed against the LOCALLY installed snapshot: personal systems mutate
|
|
91
|
+
// their working draft in place under a version number, so the server's
|
|
92
|
+
// vN may have moved since this app installed it ("draft drift") - a
|
|
93
|
+
// server-side history diff can be empty while this app's files differ.
|
|
94
|
+
// Fallback to the server changelog if the local snapshot is unreadable.
|
|
95
|
+
let markdown;
|
|
96
|
+
let breaking;
|
|
97
|
+
try {
|
|
98
|
+
const beforeDoc = JSON.parse(await readFile(join(slugDir, `v${installed}`, "design-system.json"), "utf8"));
|
|
99
|
+
const afterDoc = JSON.parse(await readFile(join(slugDir, `v${latest.version}`, "design-system.json"), "utf8"));
|
|
100
|
+
const local = diffLocalDocuments(beforeDoc, afterDoc);
|
|
101
|
+
markdown = localChangelogMarkdown(slug, installed, latest.version, local);
|
|
102
|
+
breaking = local.breaking;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
const log = await fetchChangelog(base, slug, installed, latest.version);
|
|
106
|
+
markdown = log.markdown;
|
|
107
|
+
breaking = log.changelog.breaking;
|
|
108
|
+
}
|
|
90
109
|
const upgradePath = join(slugDir, "UPGRADE.md");
|
|
91
110
|
const brief = [
|
|
92
|
-
|
|
111
|
+
markdown,
|
|
93
112
|
"",
|
|
94
113
|
"---",
|
|
95
114
|
"",
|
|
@@ -110,10 +129,10 @@ export async function upgrade(slug, opts) {
|
|
|
110
129
|
await writeFile(upgradePath, brief, "utf8");
|
|
111
130
|
// ── report ──
|
|
112
131
|
console.log(section(`Upgraded ${slug}: v${installed} → v${latest.version}`));
|
|
113
|
-
if (
|
|
114
|
-
console.log(body(`Breaking changes (${
|
|
132
|
+
if (breaking.length > 0) {
|
|
133
|
+
console.log(body(`Breaking changes (${breaking.length}):`));
|
|
115
134
|
console.log("");
|
|
116
|
-
console.log(snippet(
|
|
135
|
+
console.log(snippet(breaking.map((item) => `- ${item}`)));
|
|
117
136
|
}
|
|
118
137
|
else {
|
|
119
138
|
console.log(body("No breaking changes detected."));
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LOCAL document diff for `upgrade` - computed against the design-system.json
|
|
3
|
+
* snapshots on disk, not against the server's version history.
|
|
4
|
+
*
|
|
5
|
+
* Why local: personal design systems mutate their WORKING DRAFT in place under
|
|
6
|
+
* a fixed version number; only publishing freezes it. So the server's "v3" may
|
|
7
|
+
* have moved since this app installed it ("draft drift"), and a server-side
|
|
8
|
+
* v3→v4 changelog can be legitimately empty while the app's files differ. The
|
|
9
|
+
* app's truth is what it has installed - so that's what we diff.
|
|
10
|
+
*/
|
|
11
|
+
/** Flatten a token subtree into `prefix.path → value` leaves. */
|
|
12
|
+
function flattenTokens(prefix, value, out) {
|
|
13
|
+
if (value == null)
|
|
14
|
+
return;
|
|
15
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
16
|
+
out.set(prefix, String(value));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
value.forEach((v, i) => flattenTokens(`${prefix}.${i}`, v, out));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "object") {
|
|
24
|
+
for (const [k, v] of Object.entries(value)) {
|
|
25
|
+
flattenTokens(prefix ? `${prefix}.${k}` : k, v, out);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Key-order-insensitive equality (JSON docs round-trip with unstable order). */
|
|
30
|
+
function deepEqual(a, b) {
|
|
31
|
+
if (a === b)
|
|
32
|
+
return true;
|
|
33
|
+
if (typeof a !== typeof b)
|
|
34
|
+
return false;
|
|
35
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
36
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
37
|
+
return false;
|
|
38
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
39
|
+
}
|
|
40
|
+
if (a && b && typeof a === "object") {
|
|
41
|
+
const ka = Object.keys(a);
|
|
42
|
+
const kb = Object.keys(b);
|
|
43
|
+
if (ka.length !== kb.length)
|
|
44
|
+
return false;
|
|
45
|
+
return ka.every((k) => deepEqual(a[k], b[k]));
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/** Variant options that existed and disappeared (breaking for the consumer). */
|
|
50
|
+
function removedVariantOptions(before, after) {
|
|
51
|
+
const out = [];
|
|
52
|
+
const prevVariants = (before.variants ?? {});
|
|
53
|
+
const nextVariants = (after.variants ?? {});
|
|
54
|
+
for (const [axis, options] of Object.entries(prevVariants)) {
|
|
55
|
+
const nextAxis = nextVariants[axis];
|
|
56
|
+
if (!nextAxis) {
|
|
57
|
+
out.push(axis);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const option of Object.keys(options)) {
|
|
61
|
+
if (!(option in nextAxis))
|
|
62
|
+
out.push(`${axis}="${option}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function diffRecipeMaps(before, after, label, breaking) {
|
|
68
|
+
const out = [];
|
|
69
|
+
const names = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
70
|
+
for (const name of [...names].sort()) {
|
|
71
|
+
const prev = before[name];
|
|
72
|
+
const next = after[name];
|
|
73
|
+
if (!prev) {
|
|
74
|
+
out.push({ name, kind: "added" });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!next) {
|
|
78
|
+
out.push({ name, kind: "removed" });
|
|
79
|
+
breaking.push(`${label} "${name}" was removed`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (deepEqual(prev, next))
|
|
83
|
+
continue;
|
|
84
|
+
out.push({ name, kind: "changed" });
|
|
85
|
+
for (const gone of removedVariantOptions(prev, next)) {
|
|
86
|
+
breaking.push(`${label} "${name}" no longer supports ${gone}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** Diff two installed design-system.json documents (token + recipe level). */
|
|
92
|
+
export function diffLocalDocuments(before, after) {
|
|
93
|
+
const breaking = [];
|
|
94
|
+
const prevTokens = new Map();
|
|
95
|
+
const nextTokens = new Map();
|
|
96
|
+
flattenTokens("", before.foundations, prevTokens);
|
|
97
|
+
flattenTokens("motion", before.motion, prevTokens);
|
|
98
|
+
flattenTokens("", after.foundations, nextTokens);
|
|
99
|
+
flattenTokens("motion", after.motion, nextTokens);
|
|
100
|
+
const tokens = [];
|
|
101
|
+
const paths = new Set([...prevTokens.keys(), ...nextTokens.keys()]);
|
|
102
|
+
for (const path of [...paths].sort()) {
|
|
103
|
+
const prev = prevTokens.get(path);
|
|
104
|
+
const next = nextTokens.get(path);
|
|
105
|
+
if (prev === next)
|
|
106
|
+
continue;
|
|
107
|
+
if (prev === undefined)
|
|
108
|
+
tokens.push({ path, after: next, kind: "added" });
|
|
109
|
+
else if (next === undefined) {
|
|
110
|
+
tokens.push({ path, before: prev, kind: "removed" });
|
|
111
|
+
breaking.push(`token "${path}" was removed`);
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
tokens.push({ path, before: prev, after: next, kind: "changed" });
|
|
115
|
+
}
|
|
116
|
+
const components = diffRecipeMaps((before.components ?? {}), (after.components ?? {}), "component", breaking);
|
|
117
|
+
const blocks = diffRecipeMaps((before.blocks ?? {}), (after.blocks ?? {}), "block", breaking);
|
|
118
|
+
return {
|
|
119
|
+
tokens,
|
|
120
|
+
components,
|
|
121
|
+
blocks,
|
|
122
|
+
breaking,
|
|
123
|
+
isEmpty: tokens.length === 0 && components.length === 0 && blocks.length === 0,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const CAP_TOKENS = 40;
|
|
127
|
+
/** The migration brief body, mirroring the server changelog's format. */
|
|
128
|
+
export function localChangelogMarkdown(slug, from, to, log) {
|
|
129
|
+
const lines = [
|
|
130
|
+
`# ${slug} - v${from} → v${to}`,
|
|
131
|
+
"",
|
|
132
|
+
`_Diffed against this app's installed v${from} snapshot (the app's truth) -`,
|
|
133
|
+
"personal systems can evolve in place under a version, so a server-side",
|
|
134
|
+
"history diff may miss what changed HERE._",
|
|
135
|
+
"",
|
|
136
|
+
];
|
|
137
|
+
if (log.isEmpty) {
|
|
138
|
+
lines.push("No visual-contract changes for this app.", "");
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
if (log.breaking.length > 0) {
|
|
142
|
+
lines.push("## Breaking", "");
|
|
143
|
+
for (const item of log.breaking)
|
|
144
|
+
lines.push(`- ${item}`);
|
|
145
|
+
lines.push("");
|
|
146
|
+
}
|
|
147
|
+
if (log.tokens.length > 0) {
|
|
148
|
+
lines.push(`## Tokens (${log.tokens.length} change(s))`, "");
|
|
149
|
+
for (const t of log.tokens.slice(0, CAP_TOKENS)) {
|
|
150
|
+
const val = t.kind === "removed"
|
|
151
|
+
? `removed (was \`${t.before}\`)`
|
|
152
|
+
: t.kind === "added"
|
|
153
|
+
? `added: \`${t.after}\``
|
|
154
|
+
: `\`${t.before}\` → \`${t.after}\``;
|
|
155
|
+
lines.push(`- ${t.path}: ${val}`);
|
|
156
|
+
}
|
|
157
|
+
if (log.tokens.length > CAP_TOKENS) {
|
|
158
|
+
lines.push(`- …and ${log.tokens.length - CAP_TOKENS} more`);
|
|
159
|
+
}
|
|
160
|
+
lines.push("");
|
|
161
|
+
}
|
|
162
|
+
for (const [title, entries] of [
|
|
163
|
+
["Components", log.components],
|
|
164
|
+
["Blocks", log.blocks],
|
|
165
|
+
]) {
|
|
166
|
+
if (entries.length === 0)
|
|
167
|
+
continue;
|
|
168
|
+
lines.push(`## ${title}`, "");
|
|
169
|
+
for (const entry of entries)
|
|
170
|
+
lines.push(`- \`ds-${entry.name}\` - ${entry.kind}`);
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|