@smallpen/core 0.1.0-alpha.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.
@@ -0,0 +1,377 @@
1
+ import { combineContextAxes, resolveContext } from "./contexts.mjs";
2
+ import { fail } from "./errors.mjs";
3
+ import { tokenAliasPath, tokenValueMatchesType } from "./tokens-domain.mjs";
4
+
5
+ function contextualDefinition(token, context, axes) {
6
+ for (const [index, candidate] of token.contextValues.entries()) {
7
+ for (const [axisId, valueId] of Object.entries(candidate.when)) {
8
+ const axis = axes.get(axisId);
9
+ if (!axis || !axis.values.some(({ id }) => id === valueId)) {
10
+ fail(
11
+ "invalid_token_context_rule",
12
+ "Token Context rule references an unknown Axis or value",
13
+ {
14
+ axisId,
15
+ path: `${token.path}.contextValues[${index}].when.${axisId}`,
16
+ tokenId: token.id,
17
+ valueId,
18
+ },
19
+ );
20
+ }
21
+ }
22
+ }
23
+ const matches = token.contextValues
24
+ .filter((candidate) =>
25
+ Object.entries(candidate.when).every(
26
+ ([axisId, valueId]) => context[axisId] === valueId,
27
+ ),
28
+ )
29
+ .sort(
30
+ (left, right) =>
31
+ Object.keys(right.when).length - Object.keys(left.when).length,
32
+ );
33
+ if (matches.length > 0) {
34
+ const specificity = Object.keys(matches[0].when).length;
35
+ const equallySpecific = matches.filter(
36
+ (candidate) => Object.keys(candidate.when).length === specificity,
37
+ );
38
+ if (equallySpecific.length > 1) {
39
+ fail(
40
+ "ambiguous_token_context_rule",
41
+ `Equally specific Token Context rules match ${token.path}`,
42
+ {
43
+ path: `${token.path}.contextValues`,
44
+ specificity,
45
+ tokenId: token.id,
46
+ },
47
+ );
48
+ }
49
+ return {
50
+ found: true,
51
+ rule: structuredClone(matches[0].when),
52
+ specificity,
53
+ value: structuredClone(matches[0].value),
54
+ };
55
+ }
56
+ return token.rawValue === undefined
57
+ ? { found: false, rule: null, specificity: -1 }
58
+ : {
59
+ found: true,
60
+ rule: null,
61
+ specificity: 0,
62
+ value: structuredClone(token.rawValue),
63
+ };
64
+ }
65
+
66
+ function penpotTokenEntry(snapshot) {
67
+ return snapshot.manifest.entries.tokens.find((entry) => {
68
+ const value = snapshot.entries[entry];
69
+ return Array.isArray(value?.sets) && Array.isArray(value?.themes);
70
+ });
71
+ }
72
+
73
+ function tokenPathIndex(snapshot) {
74
+ const result = new Map();
75
+ const entry = penpotTokenEntry(snapshot);
76
+ if (entry) {
77
+ const library = snapshot.entries[entry];
78
+ const activeSetIds = new Set(
79
+ library.activeThemeIds.length > 0
80
+ ? library.themes
81
+ .filter(({ id }) => library.activeThemeIds.includes(id))
82
+ .flatMap(({ setIds }) => setIds)
83
+ : library.activeSetIds,
84
+ );
85
+ for (const tokenSet of library.sets) {
86
+ if (!activeSetIds.has(tokenSet.id)) continue;
87
+ for (const token of tokenSet.tokens) {
88
+ result.set(token.name, snapshot.domain.tokens.get(token.id));
89
+ }
90
+ }
91
+ }
92
+ for (const token of snapshot.domain.tokens.values()) {
93
+ if (token.filePath !== entry && !result.has(token.path)) {
94
+ result.set(token.path, token);
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+
100
+ function tokenByPath(snapshot, path, byPath) {
101
+ return byPath.get(path) ??
102
+ [...snapshot.domain.tokens.values()].find(
103
+ (candidate) => candidate.path === path,
104
+ );
105
+ }
106
+
107
+ function activeTargetToken(owner, token, byPath) {
108
+ return token?.filePath === penpotTokenEntry(owner)
109
+ ? (byPath.get(token.path) ?? token)
110
+ : token;
111
+ }
112
+
113
+ function contextualTokenValue(token, owner, context, axes, visiting, byPath) {
114
+ if (visiting.has(token.id)) {
115
+ fail("token_alias_cycle", `Token alias cycle includes ${token.path}`, {
116
+ path: token.path,
117
+ tokenId: token.id,
118
+ });
119
+ }
120
+ const definition = contextualDefinition(token, context, axes);
121
+ if (!definition.found) return { ...definition, value: undefined };
122
+ const alias = tokenAliasPath(definition.value);
123
+ let value = definition.value;
124
+ if (alias) {
125
+ const target = tokenByPath(owner, alias, byPath);
126
+ if (!target) {
127
+ fail("missing_token_alias", `Missing Token alias: ${alias}`, {
128
+ path: token.path,
129
+ tokenId: token.id,
130
+ });
131
+ }
132
+ if (target.type !== token.type) {
133
+ fail(
134
+ "token_alias_type_mismatch",
135
+ `Token alias type does not match ${target.path}`,
136
+ { path: token.path, tokenId: token.id },
137
+ );
138
+ }
139
+ visiting.add(token.id);
140
+ value = contextualTokenValue(
141
+ target,
142
+ owner,
143
+ context,
144
+ axes,
145
+ visiting,
146
+ byPath,
147
+ ).value;
148
+ visiting.delete(token.id);
149
+ }
150
+ if (!tokenValueMatchesType(value, token.type)) {
151
+ fail(
152
+ "invalid_token_value",
153
+ "Value does not match Token type in the selected Context",
154
+ { path: token.path, tokenId: token.id, type: token.type },
155
+ );
156
+ }
157
+ return { ...definition, value: structuredClone(value) };
158
+ }
159
+
160
+ function overrideForReference(product, reference) {
161
+ const overrides = [...product.domain.tokens.values()].filter(
162
+ (token) =>
163
+ token.overrideOf?.packageId === reference.packageId &&
164
+ token.overrideOf.assetId === reference.assetId,
165
+ );
166
+ if (overrides.length > 1) {
167
+ fail(
168
+ "duplicate_product_token_override",
169
+ `More than one Product Token overrides ${reference.assetId}`,
170
+ { path: reference.assetId, tokenIds: overrides.map(({ id }) => id) },
171
+ );
172
+ }
173
+ return overrides[0];
174
+ }
175
+
176
+ function tokenOwner(product, foundation, libraries, reference) {
177
+ if (reference.packageId === product.manifest.packageId) return product;
178
+ if (foundation?.manifest.packageId === reference.packageId) return foundation;
179
+ return libraries.find(
180
+ (library) => library.manifest.packageId === reference.packageId,
181
+ );
182
+ }
183
+
184
+ function resolveInternal(product, reference, options = {}) {
185
+ if (
186
+ !reference ||
187
+ typeof reference.packageId !== "string" ||
188
+ typeof reference.assetId !== "string"
189
+ ) {
190
+ fail("invalid_asset_reference", "Token reference requires Package and asset ids");
191
+ }
192
+ const foundation = options.foundation;
193
+ const libraries = options.libraries ?? [];
194
+ const owner = tokenOwner(product, foundation, libraries, reference);
195
+ const ownerTokensByPath = owner ? tokenPathIndex(owner) : new Map();
196
+ const targetToken = owner
197
+ ? activeTargetToken(
198
+ owner,
199
+ owner.domain.tokens.get(reference.assetId),
200
+ ownerTokensByPath,
201
+ )
202
+ : undefined;
203
+ const axes = combineContextAxes(product, foundation);
204
+ const context = resolveContext(product, foundation, options.context ?? {});
205
+ if (
206
+ !owner ||
207
+ !targetToken ||
208
+ (owner !== product && targetToken.visibility !== "public")
209
+ ) {
210
+ return {
211
+ candidates: [],
212
+ context,
213
+ resolution: null,
214
+ status: "missing",
215
+ target: structuredClone(reference),
216
+ };
217
+ }
218
+ const candidates = [];
219
+ if (owner !== product) {
220
+ const override = overrideForReference(product, reference);
221
+ if (override) {
222
+ if (override.type !== targetToken.type) {
223
+ fail(
224
+ "product_token_override_type_mismatch",
225
+ "Product Token Override type must match its Foundation Token",
226
+ { path: override.path, tokenId: override.id },
227
+ );
228
+ }
229
+ const contextual = contextualTokenValue(
230
+ override,
231
+ product,
232
+ context,
233
+ axes,
234
+ new Set(),
235
+ tokenPathIndex(product),
236
+ );
237
+ candidates.push({
238
+ layer: "product",
239
+ matched: contextual.found,
240
+ ...(!contextual.found ? { reason: "no-compatible-context-value" } : {}),
241
+ rule: contextual.rule,
242
+ specificity: contextual.specificity,
243
+ tokenId: override.id,
244
+ });
245
+ if (contextual.found) {
246
+ return {
247
+ candidates,
248
+ context,
249
+ resolution: {
250
+ context,
251
+ sourceChain: [
252
+ {
253
+ packageId: owner.manifest.packageId,
254
+ role: "target",
255
+ tokenId: targetToken.id,
256
+ },
257
+ {
258
+ packageId: product.manifest.packageId,
259
+ role: "override",
260
+ tokenId: override.id,
261
+ },
262
+ ],
263
+ sourcePackageId: product.manifest.packageId,
264
+ sourceTokenId: override.id,
265
+ target: structuredClone(reference),
266
+ token: {
267
+ ...structuredClone(override),
268
+ resolvedValue: structuredClone(contextual.value),
269
+ },
270
+ value: structuredClone(contextual.value),
271
+ },
272
+ status: "resolved",
273
+ target: structuredClone(reference),
274
+ };
275
+ }
276
+ }
277
+ }
278
+ const contextual = contextualTokenValue(
279
+ targetToken,
280
+ owner,
281
+ context,
282
+ axes,
283
+ new Set(),
284
+ ownerTokensByPath,
285
+ );
286
+ candidates.push({
287
+ layer:
288
+ owner === product
289
+ ? "product"
290
+ : owner === foundation
291
+ ? "foundation"
292
+ : "library",
293
+ matched: contextual.found,
294
+ ...(!contextual.found ? { reason: "no-compatible-context-value" } : {}),
295
+ rule: contextual.rule,
296
+ specificity: contextual.specificity,
297
+ tokenId: targetToken.id,
298
+ });
299
+ if (!contextual.found) {
300
+ return {
301
+ candidates,
302
+ context,
303
+ resolution: null,
304
+ status: "missing",
305
+ target: structuredClone(reference),
306
+ };
307
+ }
308
+ return {
309
+ candidates,
310
+ context,
311
+ resolution: {
312
+ context,
313
+ sourceChain: [
314
+ {
315
+ packageId: owner.manifest.packageId,
316
+ role: "target",
317
+ tokenId: targetToken.id,
318
+ },
319
+ ],
320
+ sourcePackageId: owner.manifest.packageId,
321
+ sourceTokenId: targetToken.id,
322
+ target: structuredClone(reference),
323
+ token: {
324
+ ...structuredClone(targetToken),
325
+ resolvedValue: structuredClone(contextual.value),
326
+ },
327
+ value: structuredClone(contextual.value),
328
+ },
329
+ status: "resolved",
330
+ target: structuredClone(reference),
331
+ };
332
+ }
333
+
334
+ export function resolveEffectiveToken(product, reference, options = {}) {
335
+ return resolveInternal(product, reference, options).resolution;
336
+ }
337
+
338
+ export function explainEffectiveToken(product, reference, options = {}) {
339
+ return resolveInternal(product, reference, options);
340
+ }
341
+
342
+ export function listEffectiveTokens(product, options = {}) {
343
+ const foundation = options.foundation;
344
+ const libraries = options.libraries ?? [];
345
+ const visibleTargets = (snapshot, publicOnly) => {
346
+ const entry = penpotTokenEntry(snapshot);
347
+ const tokens = [
348
+ ...tokenPathIndex(snapshot).values(),
349
+ ...[...snapshot.domain.tokens.values()].filter(
350
+ (token) => token.filePath !== entry,
351
+ ),
352
+ ];
353
+ return [...new Map(tokens.map((token) => [token.id, token])).values()]
354
+ .filter((token) => !publicOnly || token.visibility === "public")
355
+ .map((token) => ({
356
+ assetId: token.id,
357
+ packageId: snapshot.manifest.packageId,
358
+ }));
359
+ };
360
+ const targets = [
361
+ ...libraries.flatMap((library) => visibleTargets(library, true)),
362
+ ...(foundation
363
+ ? visibleTargets(foundation, true)
364
+ : []),
365
+ ...visibleTargets(product, false).filter(
366
+ ({ assetId }) => !product.domain.tokens.get(assetId).overrideOf,
367
+ ),
368
+ ];
369
+ return targets
370
+ .sort((left, right) =>
371
+ `${left.packageId}/${left.assetId}`.localeCompare(
372
+ `${right.packageId}/${right.assetId}`,
373
+ ),
374
+ )
375
+ .map((reference) => resolveEffectiveToken(product, reference, options))
376
+ .filter(Boolean);
377
+ }
package/src/errors.mjs ADDED
@@ -0,0 +1,12 @@
1
+ export class SmallPenError extends Error {
2
+ constructor(code, message, details = {}) {
3
+ super(message);
4
+ this.name = "SmallPenError";
5
+ this.code = code;
6
+ this.details = details;
7
+ }
8
+ }
9
+
10
+ export function fail(code, message, details) {
11
+ throw new SmallPenError(code, message, details);
12
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,112 @@
1
+ export {
2
+ canonicalJSON,
3
+ hashCanonicalFiles,
4
+ sha256Hex,
5
+ stableRuntimeUuid,
6
+ } from "./canonical.mjs";
7
+ export {
8
+ SMALLPEN_FORMAT_CAPABILITIES,
9
+ SMALLPEN_RUNTIME_CAPABILITIES,
10
+ } from "./capabilities.mjs";
11
+ export {
12
+ aggregateTokenInventory,
13
+ createCatalog,
14
+ tokenInventoryRows,
15
+ } from "./catalog.mjs";
16
+ export {
17
+ findComponentVariant,
18
+ parseComponentEntries,
19
+ } from "./components-domain.mjs";
20
+ export {
21
+ combineContextAxes,
22
+ parseContextEntries,
23
+ resolveContext,
24
+ } from "./contexts.mjs";
25
+ export { projectComponentVariant, projectScenario, projectScreen } from "./design-projection.mjs";
26
+ export { componentCombinationSnapshot } from "./component-samples.mjs";
27
+ export {
28
+ intentToOperations,
29
+ validateAuthoringIntent,
30
+ } from "./design-system-authoring.mjs";
31
+ export {
32
+ buildWorkbenchSheet,
33
+ createWorkbenchPreview,
34
+ enumerateWorkbenchCombinations,
35
+ resolveWorkbenchEditTargets,
36
+ validateWorkbenchCombination,
37
+ } from "./design-system.mjs";
38
+
39
+ export {
40
+ CANVAS_SCENE_VERSION,
41
+ CANVAS_RENDERABLE_TOKEN_TYPES,
42
+ buildCanvasScene,
43
+ collectCanvasComponentCatalog,
44
+ collectCanvasPageCatalog,
45
+ collectCanvasTokenCatalog,
46
+ collectCanvasUsageLocations,
47
+ layoutCanvasScene,
48
+ CANVAS_LAYOUT_VERSION,
49
+ deserializeCanvasScene,
50
+ serializeCanvasScene,
51
+ } from "./design-system-canvas.mjs";
52
+ export {
53
+ compileDraftMerge,
54
+ createFigmaDraftValues,
55
+ createFlatDraftValues,
56
+ diffDrafts,
57
+ draftFromSnapshot,
58
+ } from "./draft.mjs";
59
+ export {
60
+ asciiWireframe,
61
+ createCompareView,
62
+ createDiscoveryGuide,
63
+ createSemanticTree,
64
+ diffSemanticTrees,
65
+ projectDesignView,
66
+ readDesignView,
67
+ resolveDesignView,
68
+ } from "./design-read.mjs";
69
+ export {
70
+ explainEffectiveToken,
71
+ listEffectiveTokens,
72
+ resolveEffectiveToken,
73
+ } from "./effective-tokens.mjs";
74
+ export { fail, SmallPenError } from "./errors.mjs";
75
+ export {
76
+ createInitializationState,
77
+ INITIALIZATION_QUESTION_IDS,
78
+ parseInitializationAnswers,
79
+ } from "./initialization.mjs";
80
+ export {
81
+ listPackageEntries,
82
+ loadPackageFromValues,
83
+ prepareOperationBatch,
84
+ } from "./package.mjs";
85
+ export {
86
+ applyEffectiveTokenBindings,
87
+ projectEffectiveSnapshot,
88
+ } from "./projection-values.mjs";
89
+ export {
90
+ parseDesignTarget,
91
+ parseRequirementEntries,
92
+ } from "./requirements-domain.mjs";
93
+ export { parseScenarioEntries } from "./scenarios-domain.mjs";
94
+ export {
95
+ parseTokenEntries,
96
+ tokenAliasPath,
97
+ tokenValueMatchesType,
98
+ } from "./tokens-domain.mjs";
99
+ export {
100
+ designTokenWarningsForBatch,
101
+ searchEffectiveTokens,
102
+ } from "./token-advice.mjs";
103
+ export {
104
+ applyTokenSelection,
105
+ buildTokenLibrary,
106
+ diffTokenLibraries,
107
+ findTokenLibrary,
108
+ importTokens,
109
+ normalizeTokenType,
110
+ parseTokenDocument,
111
+ pruneUnresolvableTokens,
112
+ } from "./token-import.mjs";