@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.
- package/package.json +18 -0
- package/src/canonical.mjs +64 -0
- package/src/capabilities.mjs +495 -0
- package/src/catalog.mjs +362 -0
- package/src/component-samples.mjs +84 -0
- package/src/components-domain.mjs +335 -0
- package/src/contexts.mjs +248 -0
- package/src/design-projection.mjs +657 -0
- package/src/design-read.mjs +825 -0
- package/src/design-system-authoring.mjs +102 -0
- package/src/design-system-canvas.mjs +862 -0
- package/src/design-system.mjs +437 -0
- package/src/design-validation.mjs +324 -0
- package/src/draft.mjs +784 -0
- package/src/effective-tokens.mjs +377 -0
- package/src/errors.mjs +12 -0
- package/src/index.mjs +112 -0
- package/src/initialization.mjs +310 -0
- package/src/package.mjs +5935 -0
- package/src/projection-values.mjs +138 -0
- package/src/requirements-domain.mjs +222 -0
- package/src/scenarios-domain.mjs +268 -0
- package/src/token-advice.mjs +272 -0
- package/src/token-import.mjs +607 -0
- package/src/tokens-domain.mjs +410 -0
package/src/catalog.mjs
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { listEffectiveTokens } from "./effective-tokens.mjs";
|
|
2
|
+
|
|
3
|
+
function compareText(left, right) {
|
|
4
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function componentCatalog(snapshot, source, publicOnly = false) {
|
|
8
|
+
const componentSets = [...snapshot.domain.componentSets.values()]
|
|
9
|
+
.filter((componentSet) => componentSet.deprecated !== true)
|
|
10
|
+
.filter((componentSet) => !publicOnly || componentSet.visibility === "public")
|
|
11
|
+
.map((componentSet) => ({
|
|
12
|
+
category: componentSet.category,
|
|
13
|
+
description: componentSet.description,
|
|
14
|
+
id: componentSet.id,
|
|
15
|
+
kind: "set",
|
|
16
|
+
legalSelections: componentSet.variants.map((variant) =>
|
|
17
|
+
structuredClone(variant.selection),
|
|
18
|
+
),
|
|
19
|
+
name: componentSet.name,
|
|
20
|
+
packageId: snapshot.manifest.packageId,
|
|
21
|
+
replaces: structuredClone(componentSet.replaces),
|
|
22
|
+
revision: snapshot.revision,
|
|
23
|
+
scenarios: [...snapshot.domain.scenarios.values()]
|
|
24
|
+
.filter(
|
|
25
|
+
(scenario) =>
|
|
26
|
+
scenario.target.kind === "component" &&
|
|
27
|
+
scenario.target.component.assetId === componentSet.id,
|
|
28
|
+
)
|
|
29
|
+
.map(({ id }) => id)
|
|
30
|
+
.sort(),
|
|
31
|
+
source,
|
|
32
|
+
// DSP-002-B: variant identities so consumers can switch selections.
|
|
33
|
+
variants: componentSet.variants.map((variant) => ({
|
|
34
|
+
id: variant.id,
|
|
35
|
+
rootId: variant.rootId,
|
|
36
|
+
selection: structuredClone(variant.selection),
|
|
37
|
+
})),
|
|
38
|
+
}));
|
|
39
|
+
const located = [...snapshot.domain.locatedComponents.values()].map(
|
|
40
|
+
(component) => ({
|
|
41
|
+
category: component.path,
|
|
42
|
+
id: component.id,
|
|
43
|
+
kind: "located",
|
|
44
|
+
legalSelections: [{}],
|
|
45
|
+
mainNodeId: component.mainNodeId,
|
|
46
|
+
name: component.name,
|
|
47
|
+
packageId: snapshot.manifest.packageId,
|
|
48
|
+
presentationId: component.presentationId,
|
|
49
|
+
revision: snapshot.revision,
|
|
50
|
+
scenarios: [],
|
|
51
|
+
screenId: component.screenId,
|
|
52
|
+
source,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
return [...componentSets, ...located].sort((left, right) =>
|
|
56
|
+
compareText(left.id, right.id),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// SP-023: public asset inventory per Library snapshot with qualified ownership.
|
|
61
|
+
function assetInventory(snapshot) {
|
|
62
|
+
const assets = snapshot.manifest.entries.assets
|
|
63
|
+
.map((entry) => snapshot.entries[entry])
|
|
64
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
65
|
+
return {
|
|
66
|
+
colors: assets
|
|
67
|
+
.flatMap((asset) =>
|
|
68
|
+
asset.colors.map(({ id, name, path }) => ({ id, name, path })))
|
|
69
|
+
.sort((left, right) => compareText(left.id, right.id)),
|
|
70
|
+
fonts: assets
|
|
71
|
+
.flatMap((asset) =>
|
|
72
|
+
asset.fonts.map((family) => ({
|
|
73
|
+
family: family.family,
|
|
74
|
+
id: family.id,
|
|
75
|
+
variants: family.variants.map(({ id, name, style, weight }) => ({
|
|
76
|
+
id,
|
|
77
|
+
name,
|
|
78
|
+
style,
|
|
79
|
+
weight,
|
|
80
|
+
})),
|
|
81
|
+
})))
|
|
82
|
+
.sort((left, right) => compareText(left.id, right.id)),
|
|
83
|
+
media: assets
|
|
84
|
+
.flatMap((asset) =>
|
|
85
|
+
asset.media.map(({ height, id, mimeType, name, path, width }) => ({
|
|
86
|
+
height,
|
|
87
|
+
id,
|
|
88
|
+
mimeType,
|
|
89
|
+
name,
|
|
90
|
+
path,
|
|
91
|
+
width,
|
|
92
|
+
})))
|
|
93
|
+
.sort((left, right) => compareText(left.id, right.id)),
|
|
94
|
+
typographies: assets
|
|
95
|
+
.flatMap((asset) =>
|
|
96
|
+
asset.typographies.map(({ id, name, path }) => ({ id, name, path })))
|
|
97
|
+
.sort((left, right) => compareText(left.id, right.id)),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function screens(snapshot) {
|
|
102
|
+
return snapshot.manifest.entries.screens
|
|
103
|
+
.map((entry) => snapshot.entries[entry])
|
|
104
|
+
.sort((left, right) => compareText(left.id, right.id))
|
|
105
|
+
.map((screen) => ({
|
|
106
|
+
basePresentationId: screen.basePresentationId,
|
|
107
|
+
id: screen.id,
|
|
108
|
+
name: screen.name,
|
|
109
|
+
presentations: screen.presentations.map(({ id, name, platform }) => ({
|
|
110
|
+
id,
|
|
111
|
+
name,
|
|
112
|
+
...(platform ? { platform } : {}),
|
|
113
|
+
})),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
// DSP-002-A: full token inventory across the package, its Foundation and
|
|
119
|
+
// linked Libraries. Rows keep a qualified key and carry an active flag so
|
|
120
|
+
// consumers can separate the current combination from archived cells.
|
|
121
|
+
export function tokenInventoryRows(snapshot, source) {
|
|
122
|
+
const packageId = snapshot.manifest.packageId;
|
|
123
|
+
const readOnly = source !== "product";
|
|
124
|
+
const rows = [];
|
|
125
|
+
for (const entry of snapshot.manifest.entries.tokens) {
|
|
126
|
+
const library = snapshot.entries[entry];
|
|
127
|
+
const activeSets = new Set([
|
|
128
|
+
...(library.activeSetIds ?? []),
|
|
129
|
+
...(library.themes ?? [])
|
|
130
|
+
.filter((theme) => (library.activeThemeIds ?? []).includes(theme.id))
|
|
131
|
+
.flatMap((theme) => theme.setIds),
|
|
132
|
+
]);
|
|
133
|
+
for (const set of library.sets ?? []) {
|
|
134
|
+
for (const token of set.tokens ?? []) {
|
|
135
|
+
const raw = token.value;
|
|
136
|
+
const aliasMatch =
|
|
137
|
+
typeof raw === "string" ? /^\{([^{}]+)\}$/.exec(raw) : null;
|
|
138
|
+
rows.push({
|
|
139
|
+
active: activeSets.has(set.id),
|
|
140
|
+
deprecated: token.deprecated === true,
|
|
141
|
+
definitionSource: { packageId, setName: set.name },
|
|
142
|
+
path: token.name,
|
|
143
|
+
tokenId: token.id,
|
|
144
|
+
qualifiedKey: `${packageId}/${set.name}/${token.name}`,
|
|
145
|
+
readOnly,
|
|
146
|
+
setId: set.id,
|
|
147
|
+
setName: set.name,
|
|
148
|
+
source,
|
|
149
|
+
// aliasPath is the raw alias reference; the aggregation pass below
|
|
150
|
+
// resolves it against earlier sources and fills effective fields.
|
|
151
|
+
status: aliasMatch ? `alias:${aliasMatch[1]}` : "ok",
|
|
152
|
+
type: token.type,
|
|
153
|
+
value: structuredClone(raw),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return rows.sort(
|
|
159
|
+
(left, right) =>
|
|
160
|
+
compareText(left.qualifiedKey, right.qualifiedKey) ||
|
|
161
|
+
compareText(left.setId, right.setId),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// DSP-002-C: aggregate per-source inventories, resolving alias rows against
|
|
166
|
+
// rows defined earlier (product, then foundation, then libraries — mirroring
|
|
167
|
+
// the effective-token precedence). Returns rows plus a synthesized inventory
|
|
168
|
+
// revision over the contributing source revisions.
|
|
169
|
+
export function aggregateTokenInventory(sources) {
|
|
170
|
+
const resolvedByPath = new Map();
|
|
171
|
+
const rows = [];
|
|
172
|
+
const sourceRevisions = [];
|
|
173
|
+
for (const { revision, rows: sourceRows, source } of sources) {
|
|
174
|
+
sourceRevisions.push(`${source}:${packageIdOf(sourceRows)}@${revision}`);
|
|
175
|
+
for (const row of sourceRows) {
|
|
176
|
+
const aliasMatch = /^alias:(.+)$/.exec(row.status);
|
|
177
|
+
if (!aliasMatch) {
|
|
178
|
+
const resolved = {
|
|
179
|
+
...row,
|
|
180
|
+
effectiveSource: {
|
|
181
|
+
ownerPackageId: row.definitionSource.packageId,
|
|
182
|
+
path: row.path,
|
|
183
|
+
setName: row.definitionSource.setName,
|
|
184
|
+
},
|
|
185
|
+
effectiveValue: structuredClone(row.value),
|
|
186
|
+
status: "ok",
|
|
187
|
+
};
|
|
188
|
+
resolvedByPath.set(row.path, {
|
|
189
|
+
ownerPackageId: row.definitionSource.packageId,
|
|
190
|
+
path: row.path,
|
|
191
|
+
setName: row.definitionSource.setName,
|
|
192
|
+
value: row.value,
|
|
193
|
+
});
|
|
194
|
+
rows.push(resolved);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const target = resolvedByPath.get(aliasMatch[1]);
|
|
198
|
+
if (!target) {
|
|
199
|
+
rows.push({
|
|
200
|
+
...row,
|
|
201
|
+
effectiveSource: null,
|
|
202
|
+
effectiveValue: null,
|
|
203
|
+
status: `unresolved_alias:${aliasMatch[1]}`,
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
rows.push({
|
|
208
|
+
...row,
|
|
209
|
+
effectiveSource: {
|
|
210
|
+
ownerPackageId: target.ownerPackageId,
|
|
211
|
+
path: target.path,
|
|
212
|
+
setName: target.setName,
|
|
213
|
+
},
|
|
214
|
+
effectiveValue: structuredClone(target.value),
|
|
215
|
+
status: "ok",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
rows: rows.sort(
|
|
221
|
+
(left, right) =>
|
|
222
|
+
compareText(left.qualifiedKey, right.qualifiedKey) ||
|
|
223
|
+
compareText(left.setId, right.setId),
|
|
224
|
+
),
|
|
225
|
+
revision: synthesizeInventoryRevision(rows, sourceRevisions),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function packageIdOf(rows) {
|
|
230
|
+
return rows[0]?.ownerPackageId ?? "unknown";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function synthesizeInventoryRevision(rows, sourceRevisions) {
|
|
234
|
+
// Deterministic non-cryptographic digest: document order-sensitive inputs.
|
|
235
|
+
let hash = 0x811c9dc5;
|
|
236
|
+
const input = `${sourceRevisions.join("|")}#${rows
|
|
237
|
+
.map((row) => row.qualifiedKey)
|
|
238
|
+
.join(",")}`;
|
|
239
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
240
|
+
hash ^= input.charCodeAt(index);
|
|
241
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
242
|
+
}
|
|
243
|
+
return `inv_${hash.toString(16).padStart(8, "0")}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function createCatalog(product, options = {}) {
|
|
247
|
+
const foundation = options.foundation;
|
|
248
|
+
const libraries = options.libraries ?? [];
|
|
249
|
+
const effective = listEffectiveTokens(product, {
|
|
250
|
+
context: options.context,
|
|
251
|
+
foundation,
|
|
252
|
+
libraries: options.libraries,
|
|
253
|
+
});
|
|
254
|
+
const tokenInventory = aggregateTokenInventory([
|
|
255
|
+
{
|
|
256
|
+
revision: product.revision,
|
|
257
|
+
rows: tokenInventoryRows(product, "product"),
|
|
258
|
+
source: "product",
|
|
259
|
+
},
|
|
260
|
+
...(foundation
|
|
261
|
+
? [
|
|
262
|
+
{
|
|
263
|
+
revision: foundation.revision,
|
|
264
|
+
rows: tokenInventoryRows(foundation, "foundation"),
|
|
265
|
+
source: "foundation",
|
|
266
|
+
},
|
|
267
|
+
]
|
|
268
|
+
: []),
|
|
269
|
+
...libraries.map((library) => ({
|
|
270
|
+
revision: library.revision,
|
|
271
|
+
rows: tokenInventoryRows(library, "library"),
|
|
272
|
+
source: "library",
|
|
273
|
+
})),
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
components: [
|
|
278
|
+
...componentCatalog(product, "product"),
|
|
279
|
+
...(foundation
|
|
280
|
+
? componentCatalog(foundation, "foundation", true)
|
|
281
|
+
: []),
|
|
282
|
+
...libraries.flatMap((library) =>
|
|
283
|
+
componentCatalog(library, "library", true),
|
|
284
|
+
),
|
|
285
|
+
].sort(
|
|
286
|
+
(left, right) =>
|
|
287
|
+
compareText(left.name, right.name) ||
|
|
288
|
+
compareText(left.packageId, right.packageId) ||
|
|
289
|
+
compareText(left.id, right.id),
|
|
290
|
+
),
|
|
291
|
+
contexts: [
|
|
292
|
+
...product.domain.contextProfiles.values(),
|
|
293
|
+
...(foundation?.domain.contextProfiles.values() ?? []),
|
|
294
|
+
]
|
|
295
|
+
.sort((left, right) => compareText(left.id, right.id))
|
|
296
|
+
.map(({ default: isDefault, id, name, values }) => ({
|
|
297
|
+
default: isDefault,
|
|
298
|
+
id,
|
|
299
|
+
name,
|
|
300
|
+
values: structuredClone(values),
|
|
301
|
+
})),
|
|
302
|
+
effectiveTokens: effective.map(({ sourceChain, target, token, value }) => ({
|
|
303
|
+
id: target.assetId,
|
|
304
|
+
path: token.path,
|
|
305
|
+
sourceChain,
|
|
306
|
+
target,
|
|
307
|
+
value,
|
|
308
|
+
})),
|
|
309
|
+
libraries: libraries.map((library) => ({
|
|
310
|
+
assets: assetInventory(library),
|
|
311
|
+
packageId: library.manifest.packageId,
|
|
312
|
+
revision: library.revision,
|
|
313
|
+
})),
|
|
314
|
+
inherited: foundation
|
|
315
|
+
? {
|
|
316
|
+
components: [...foundation.domain.componentSets.values()]
|
|
317
|
+
.filter(({ deprecated, visibility }) =>
|
|
318
|
+
visibility === "public" && deprecated !== true)
|
|
319
|
+
.map(({ id }) => id)
|
|
320
|
+
.concat([...foundation.domain.locatedComponents.keys()])
|
|
321
|
+
.sort(),
|
|
322
|
+
packageId: foundation.manifest.packageId,
|
|
323
|
+
tokens: [...foundation.domain.tokens.values()]
|
|
324
|
+
.filter(({ visibility }) => visibility === "public")
|
|
325
|
+
.map(({ id }) => id)
|
|
326
|
+
.sort(),
|
|
327
|
+
}
|
|
328
|
+
: undefined,
|
|
329
|
+
packageId: product.manifest.packageId,
|
|
330
|
+
requirements: [...product.domain.requirements.values()]
|
|
331
|
+
.sort((left, right) => compareText(left.id, right.id))
|
|
332
|
+
.map(({ id, links, title }) => ({
|
|
333
|
+
coverage: links.length === 0 ? "missing" : "linked",
|
|
334
|
+
id,
|
|
335
|
+
linkCount: links.length,
|
|
336
|
+
title,
|
|
337
|
+
})),
|
|
338
|
+
role: product.manifest.role,
|
|
339
|
+
scenarios: [...product.domain.scenarios.values()]
|
|
340
|
+
.sort((left, right) => compareText(left.id, right.id))
|
|
341
|
+
.map(({ id, name, target }) => ({ id, name, targetKind: target.kind })),
|
|
342
|
+
screens: screens(product),
|
|
343
|
+
tokenInventory: tokenInventory.rows,
|
|
344
|
+
tokenInventoryRevision: tokenInventory.revision,
|
|
345
|
+
tokens: [...product.domain.tokens.values()]
|
|
346
|
+
.sort((left, right) => compareText(left.id, right.id))
|
|
347
|
+
.map(({ deprecated, id, path, type }) => ({
|
|
348
|
+
deprecated,
|
|
349
|
+
id,
|
|
350
|
+
path,
|
|
351
|
+
type,
|
|
352
|
+
})),
|
|
353
|
+
warnings: [...product.domain.requirements.values()]
|
|
354
|
+
.filter(({ links }) => links.length === 0)
|
|
355
|
+
.map(({ id }) => ({
|
|
356
|
+
code: "missing_requirement_coverage",
|
|
357
|
+
message: `Requirement has no Design Link: ${id}`,
|
|
358
|
+
requirementId: id,
|
|
359
|
+
severity: "warning",
|
|
360
|
+
})),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { stableRuntimeUuid } from "./canonical.mjs";
|
|
2
|
+
import { projectComponentVariant } from "./design-projection.mjs";
|
|
3
|
+
import { resolveEffectiveToken } from "./effective-tokens.mjs";
|
|
4
|
+
|
|
5
|
+
// A view changes resolution, never the canonical active themes or node tree.
|
|
6
|
+
export function componentCombinationSnapshot(snapshot, combination) {
|
|
7
|
+
if (!combination) return snapshot;
|
|
8
|
+
const entries = { ...snapshot.entries };
|
|
9
|
+
for (const path of snapshot.manifest.entries.tokens) {
|
|
10
|
+
const library = entries[path];
|
|
11
|
+
if (!Array.isArray(library?.sets)) continue;
|
|
12
|
+
const themed = new Set(library.themes.flatMap((theme) => theme.setIds));
|
|
13
|
+
entries[path] = {
|
|
14
|
+
...library,
|
|
15
|
+
activeThemeIds: [],
|
|
16
|
+
activeSetIds: [...new Set([
|
|
17
|
+
...library.activeSetIds.filter((id) => !themed.has(id)),
|
|
18
|
+
...combination.setIds,
|
|
19
|
+
])],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { ...snapshot, entries };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function createComponentSamples(snapshot, families, combinations) {
|
|
26
|
+
const samples = [];
|
|
27
|
+
const reverse = {};
|
|
28
|
+
for (const family of families.filter((item) => item.kind === "variant")) {
|
|
29
|
+
const set = snapshot.domain.componentSets.get(family.componentSetId);
|
|
30
|
+
const variant = set.variants.find((item) => item.id === family.variantId);
|
|
31
|
+
const occurrences = Object.values(variant.nodes).filter((node) => node.instance);
|
|
32
|
+
for (const combination of combinations.length ? combinations : [null]) {
|
|
33
|
+
const key = `${set.id}\0${variant.id}\0${combination?.id ?? "default"}`;
|
|
34
|
+
const sample = {
|
|
35
|
+
...family, key, familyName: set.name, axes: set.axes,
|
|
36
|
+
selection: variant.selection,
|
|
37
|
+
classification: set.variants.some((item) => Object.values(item.nodes).some((node) => node.instance)) ? "Composite" : "Primitive",
|
|
38
|
+
combinationId: combination?.id ?? null,
|
|
39
|
+
combinationIndex: combination ? combinations.indexOf(combination) : 0,
|
|
40
|
+
combinationLabel: combination?.label ?? "Default",
|
|
41
|
+
caption: await stableRuntimeUuid(snapshot.manifest.packageId, "component-sample-caption", key),
|
|
42
|
+
runtimeNodes: {}, sources: {},
|
|
43
|
+
};
|
|
44
|
+
reverse[sample.caption] = { kind: "label" };
|
|
45
|
+
try {
|
|
46
|
+
const view = componentCombinationSnapshot(snapshot, combination);
|
|
47
|
+
sample.nodes = projectComponentVariant(view, variant);
|
|
48
|
+
for (const [nodeId, node] of Object.entries(sample.nodes)) {
|
|
49
|
+
const runtimeId = await stableRuntimeUuid(snapshot.manifest.packageId, "component-sample-node", `${key}\0${nodeId}`);
|
|
50
|
+
// Longest exact occurrence prefix, never labels or array indices.
|
|
51
|
+
const occurrence = occurrences.filter((item) => nodeId === item.id || nodeId.startsWith(`${item.id}__`))
|
|
52
|
+
.sort((a, b) => b.id.length - a.id.length)[0];
|
|
53
|
+
const source = {
|
|
54
|
+
kind: "component-sample", componentId: set.id, variantId: variant.id,
|
|
55
|
+
ownerPackageId: snapshot.manifest.packageId,
|
|
56
|
+
nodeId: occurrence?.id ?? nodeId, displayNodeId: nodeId,
|
|
57
|
+
combinationId: sample.combinationId, combinationLabel: sample.combinationLabel,
|
|
58
|
+
familyName: set.name, selection: variant.selection,
|
|
59
|
+
occurrencePath: occurrence ? nodeId : null,
|
|
60
|
+
overrideNodeId: occurrence ? (nodeId === occurrence.id ? node.sourceNodeId : nodeId.slice(occurrence.id.length + 2)) : null,
|
|
61
|
+
bindings: {},
|
|
62
|
+
};
|
|
63
|
+
for (const [field, reference] of Object.entries(node.tokenBindings ?? {})) {
|
|
64
|
+
const resolved = resolveEffectiveToken(view, reference);
|
|
65
|
+
source.bindings[field] = resolved ? {
|
|
66
|
+
tokenId: resolved.sourceTokenId, ownerPackageId: resolved.sourcePackageId,
|
|
67
|
+
path: resolved.token.path, value: resolved.value,
|
|
68
|
+
alias: typeof resolved.token.rawValue === "string" && /^\{[^{}]+\}$/.test(resolved.token.rawValue),
|
|
69
|
+
contextual: (resolved.token.contextValues?.length ?? 0) > 0,
|
|
70
|
+
} : { missing: true };
|
|
71
|
+
}
|
|
72
|
+
sample.runtimeNodes[nodeId] = runtimeId;
|
|
73
|
+
sample.sources[nodeId] = source;
|
|
74
|
+
reverse[runtimeId] = source;
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
sample.error = `${error.code ?? "component_projection_failed"}: ${error.message}`;
|
|
78
|
+
sample.nodes = {};
|
|
79
|
+
}
|
|
80
|
+
samples.push(sample);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { samples, reverse };
|
|
84
|
+
}
|