@penpair/marwes-ui 0.2.0 → 0.3.1
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/build-kit.js +24 -6
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +500 -94
- package/dist/components.js +375 -74
- package/dist/config.js +53 -16
- package/dist/index.js +21 -3
- package/dist/marwes.js +45 -12
- package/dist/mode-trial.js +325 -75
- package/dist/ownership.js +162 -37
- package/dist/penpot.js +447 -114
- package/dist/reconciliation.js +112 -37
- package/dist/resolved-values.d.ts +3 -8
- package/dist/resolved-values.js +99 -103
- package/dist/snapshot-file.js +255 -50
- package/dist/theme-input.js +12 -4
- package/dist/theme-lint.js +113 -27
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +207 -52
- package/package.json +2 -2
package/dist/ownership.js
CHANGED
|
@@ -1,66 +1,161 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { atomicWriteFile, ensureGitignoreEntry, resolveProjectInput, resolveProjectOutput } from "@penpair/core";
|
|
4
|
+
import { atomicWriteFile, ensureGitignoreEntry, resolveProjectInput, resolveProjectOutput, } from "@penpair/core";
|
|
5
5
|
import { canonical, normalizeValue } from "./tokens.js";
|
|
6
|
-
const tokenMap = (sets) => new Map(sets.map(
|
|
7
|
-
|
|
6
|
+
const tokenMap = (sets) => new Map(sets.map((set) => [
|
|
7
|
+
set.name,
|
|
8
|
+
new Map(set.tokens.map((token) => [
|
|
9
|
+
token.name,
|
|
10
|
+
token,
|
|
11
|
+
])),
|
|
12
|
+
]));
|
|
13
|
+
const values = (token) => token
|
|
14
|
+
? {
|
|
15
|
+
type: token.type,
|
|
16
|
+
value: normalizeValue(token.type, token.value),
|
|
17
|
+
description: token.description || "",
|
|
18
|
+
}
|
|
19
|
+
: {
|
|
20
|
+
type: null,
|
|
21
|
+
value: null,
|
|
22
|
+
description: null,
|
|
23
|
+
};
|
|
8
24
|
/** Report each changed property against a verified ancestor; never guess a rename. */
|
|
9
25
|
export function ownershipReport(desired, live, ancestor) {
|
|
10
|
-
const wanted = tokenMap(desired.sets)
|
|
26
|
+
const wanted = tokenMap(desired.sets);
|
|
27
|
+
const actual = tokenMap(live.sets);
|
|
28
|
+
const old = tokenMap(ancestor?.sets ?? []);
|
|
11
29
|
const changes = [];
|
|
12
|
-
const unmanagedSets = live.sets
|
|
30
|
+
const unmanagedSets = live.sets
|
|
31
|
+
.filter((set) => !wanted.has(set.name) && !old.has(set.name))
|
|
32
|
+
.map((set) => set.name);
|
|
13
33
|
const unmanagedTokens = [];
|
|
14
|
-
for (const setName of new Set([
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
for (const setName of new Set([
|
|
35
|
+
...wanted.keys(),
|
|
36
|
+
...old.keys(),
|
|
37
|
+
])) {
|
|
38
|
+
for (const name of new Set([
|
|
39
|
+
...(wanted.get(setName)?.keys() ?? []),
|
|
40
|
+
...(old.get(setName)?.keys() ?? []),
|
|
41
|
+
])) {
|
|
42
|
+
const a = values(old.get(setName)?.get(name));
|
|
43
|
+
const d = values(wanted.get(setName)?.get(name));
|
|
44
|
+
const l = values(actual.get(setName)?.get(name));
|
|
45
|
+
for (const property of [
|
|
46
|
+
"type",
|
|
47
|
+
"value",
|
|
48
|
+
"description",
|
|
49
|
+
]) {
|
|
18
50
|
const same = (x, y) => canonical(x) === canonical(y);
|
|
19
|
-
if (same(d[property], l[property]))
|
|
51
|
+
if (same(d[property], l[property])) {
|
|
20
52
|
continue;
|
|
53
|
+
}
|
|
21
54
|
let kind;
|
|
22
55
|
if (!ancestor || !old.get(setName)?.has(name)) {
|
|
23
56
|
kind = !actual.get(setName)?.has(name) ? "upstream" : "untracked";
|
|
24
57
|
}
|
|
25
58
|
else {
|
|
26
|
-
const upstream = !same(d[property], a[property])
|
|
59
|
+
const upstream = !same(d[property], a[property]);
|
|
60
|
+
const designer = !same(l[property], a[property]);
|
|
27
61
|
kind = upstream && designer ? "conflict" : designer ? "designer" : "upstream";
|
|
28
62
|
}
|
|
29
|
-
changes.push({
|
|
63
|
+
changes.push({
|
|
64
|
+
set: setName,
|
|
65
|
+
name,
|
|
66
|
+
property,
|
|
67
|
+
kind,
|
|
68
|
+
ancestor: a[property],
|
|
69
|
+
desired: d[property],
|
|
70
|
+
live: l[property],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const name of actual.get(setName)?.keys() ?? []) {
|
|
75
|
+
if (!wanted.get(setName)?.has(name) && !old.get(setName)?.has(name)) {
|
|
76
|
+
unmanagedTokens.push({
|
|
77
|
+
set: setName,
|
|
78
|
+
name,
|
|
79
|
+
});
|
|
30
80
|
}
|
|
31
81
|
}
|
|
32
|
-
for (const name of actual.get(setName)?.keys() ?? [])
|
|
33
|
-
if (!wanted.get(setName)?.has(name) && !old.get(setName)?.has(name))
|
|
34
|
-
unmanagedTokens.push({ set: setName, name });
|
|
35
82
|
}
|
|
36
|
-
const key = (theme) => canonical([
|
|
37
|
-
|
|
83
|
+
const key = (theme) => canonical([
|
|
84
|
+
theme.group,
|
|
85
|
+
theme.name,
|
|
86
|
+
]);
|
|
87
|
+
const previousThemes = new Map((ancestor?.themes ?? []).map((theme) => [
|
|
88
|
+
key(theme),
|
|
89
|
+
theme,
|
|
90
|
+
]));
|
|
38
91
|
for (const theme of desired.themes) {
|
|
39
|
-
const current = live.themes.find(t => key(t) === key(theme))
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
92
|
+
const current = live.themes.find((t) => key(t) === key(theme));
|
|
93
|
+
const previous = previousThemes.get(key(theme));
|
|
94
|
+
const managed = new Set([
|
|
95
|
+
...desired.sets,
|
|
96
|
+
...(ancestor?.sets ?? []),
|
|
97
|
+
].map((set) => set.name));
|
|
98
|
+
const orderedMembership = (sets) => sets === undefined ? null : sets.filter((name) => managed.has(name)).sort();
|
|
99
|
+
const a = orderedMembership(previous?.sets);
|
|
100
|
+
const d = orderedMembership(theme.sets);
|
|
101
|
+
const l = orderedMembership(current?.sets);
|
|
102
|
+
if (canonical(d) === canonical(l)) {
|
|
44
103
|
continue;
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
}
|
|
105
|
+
const kind = !previous
|
|
106
|
+
? !current
|
|
107
|
+
? "upstream"
|
|
108
|
+
: "untracked"
|
|
109
|
+
: canonical(l) === canonical(a)
|
|
110
|
+
? "upstream"
|
|
111
|
+
: canonical(d) === canonical(a)
|
|
112
|
+
? "designer"
|
|
113
|
+
: "conflict";
|
|
114
|
+
changes.push({
|
|
115
|
+
set: "@themes",
|
|
116
|
+
name: key(theme),
|
|
117
|
+
property: "membership",
|
|
118
|
+
kind,
|
|
119
|
+
ancestor: a,
|
|
120
|
+
desired: d,
|
|
121
|
+
live: l,
|
|
122
|
+
});
|
|
47
123
|
}
|
|
48
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
changes,
|
|
126
|
+
blockers: changes.filter((change) => change.kind !== "upstream"),
|
|
127
|
+
unmanagedSets,
|
|
128
|
+
unmanagedTokens,
|
|
129
|
+
};
|
|
49
130
|
}
|
|
50
131
|
/** Destruction is conservative across all sets, including those outside adapter ownership. */
|
|
51
132
|
export function assertSafeDestruction(actions, live, bindingCounts = {}) {
|
|
52
133
|
for (const action of actions) {
|
|
53
|
-
const names = action.kind === "removeTokens"
|
|
134
|
+
const names = action.kind === "removeTokens"
|
|
135
|
+
? action.names
|
|
136
|
+
: action.kind === "replaceTokens"
|
|
137
|
+
? action.tokens
|
|
138
|
+
.filter((token) => live.sets
|
|
139
|
+
.find((set) => set.name === action.set)
|
|
140
|
+
?.tokens.find((old) => old.name === token.name)?.type !== token.type)
|
|
141
|
+
.map((token) => token.name)
|
|
142
|
+
: [];
|
|
54
143
|
for (const name of names) {
|
|
55
|
-
if ((bindingCounts[name] ?? 0) > 0)
|
|
144
|
+
if ((bindingCounts[name] ?? 0) > 0) {
|
|
56
145
|
throw new Error(`Cannot remove or change type of ${name}: bound to ${bindingCounts[name]} retained shape(s)`);
|
|
57
|
-
|
|
146
|
+
}
|
|
147
|
+
for (const set of live.sets) {
|
|
58
148
|
for (const token of set.tokens) {
|
|
59
|
-
if (set.name ===
|
|
149
|
+
if (set.name ===
|
|
150
|
+
action.set &&
|
|
151
|
+
token.name === name) {
|
|
60
152
|
continue;
|
|
61
|
-
|
|
153
|
+
}
|
|
154
|
+
if (JSON.stringify(token.value).includes("{" + name + "}")) {
|
|
62
155
|
throw new Error(`Cannot remove or change type of ${name}: referenced by ${set.name}/${token.name}`);
|
|
156
|
+
}
|
|
63
157
|
}
|
|
158
|
+
}
|
|
64
159
|
}
|
|
65
160
|
}
|
|
66
161
|
}
|
|
@@ -68,27 +163,57 @@ const ledgerPath = (fileId) => `.penpair/token-sync/${createHash("sha256").updat
|
|
|
68
163
|
export async function readSyncAncestor(root, fileId) {
|
|
69
164
|
try {
|
|
70
165
|
const record = JSON.parse(await readFile(await resolveProjectInput(root, ledgerPath(fileId)), "utf8"));
|
|
71
|
-
if (record.schemaVersion !== 1 ||
|
|
166
|
+
if (record.schemaVersion !== 1 ||
|
|
167
|
+
record.fileId !== fileId ||
|
|
168
|
+
record.sha256 !== createHash("sha256").update(canonical(record.snapshot)).digest("hex") ||
|
|
169
|
+
![
|
|
170
|
+
1,
|
|
171
|
+
2,
|
|
172
|
+
].includes(record.snapshot?.schemaVersion) ||
|
|
173
|
+
!Array.isArray(record.snapshot?.sets) ||
|
|
174
|
+
!Array.isArray(record.snapshot?.themes)) {
|
|
72
175
|
throw new Error("Invalid token-sync ancestor; inspect it before replacing it");
|
|
176
|
+
}
|
|
73
177
|
return record.snapshot;
|
|
74
178
|
}
|
|
75
179
|
catch (error) {
|
|
76
|
-
if (error.code === "ENOENT")
|
|
180
|
+
if (error.code === "ENOENT") {
|
|
77
181
|
return undefined;
|
|
182
|
+
}
|
|
78
183
|
throw error;
|
|
79
184
|
}
|
|
80
185
|
}
|
|
81
186
|
export async function saveSyncAncestor(root, fileId, snapshot, expectedAncestor) {
|
|
82
|
-
await ensureGitignoreEntry(root, ".penpair", {
|
|
187
|
+
await ensureGitignoreEntry(root, ".penpair", {
|
|
188
|
+
directory: true,
|
|
189
|
+
comment: "penpair: private token synchronization evidence",
|
|
190
|
+
});
|
|
83
191
|
const destination = await resolveProjectOutput(root, ledgerPath(fileId));
|
|
84
|
-
await mkdir(path.dirname(destination), {
|
|
192
|
+
await mkdir(path.dirname(destination), {
|
|
193
|
+
recursive: true,
|
|
194
|
+
mode: 0o700,
|
|
195
|
+
});
|
|
85
196
|
const lockPath = destination + ".lock";
|
|
86
|
-
const lock = await open(lockPath, "wx", 0o600).catch(() => {
|
|
197
|
+
const lock = await open(lockPath, "wx", 0o600).catch(() => {
|
|
198
|
+
throw new Error("Token ancestor is locked; inspect any interrupted local sync before retrying");
|
|
199
|
+
});
|
|
87
200
|
try {
|
|
88
201
|
const current = await readSyncAncestor(root, fileId);
|
|
89
|
-
if (current &&
|
|
202
|
+
if (current &&
|
|
203
|
+
canonical(current) !== canonical(snapshot) &&
|
|
204
|
+
canonical(current) !== canonical(expectedAncestor)) {
|
|
90
205
|
throw new Error("Token ancestor advanced concurrently; newer sync evidence was preserved");
|
|
91
|
-
|
|
206
|
+
}
|
|
207
|
+
await atomicWriteFile(destination, JSON.stringify({
|
|
208
|
+
schemaVersion: 1,
|
|
209
|
+
fileId,
|
|
210
|
+
savedAt: new Date().toISOString(),
|
|
211
|
+
sha256: createHash("sha256").update(canonical(snapshot)).digest("hex"),
|
|
212
|
+
snapshot,
|
|
213
|
+
}, null, 2) + "\n", {
|
|
214
|
+
replace: true,
|
|
215
|
+
mode: 0o600,
|
|
216
|
+
});
|
|
92
217
|
}
|
|
93
218
|
finally {
|
|
94
219
|
await lock.close();
|