@yahoo/uds-create-config 5.64.3 → 5.66.0

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,97 @@
1
+ import { ConfigEdit } from "../framework/config-op.js";
2
+ import { Config } from "../framework/Config.js";
3
+ import { RewritableSpec } from "./rewrite-refs.js";
4
+ import { z } from "zod";
5
+
6
+ //#region src/spec/prop-mapping.d.ts
7
+ /** A stored value a rule can match: what a design file holds for a variant, string or flag. */
8
+ type MatchedValue = string | number | boolean;
9
+ interface PropMappingRule {
10
+ /** The stored values the rule matches, every one of them. */
11
+ readonly when: Readonly<Record<string, MatchedValue>>;
12
+ /** The props it writes. */
13
+ readonly set?: Readonly<Record<string, unknown>>;
14
+ /** The props it removes. */
15
+ readonly unset?: readonly string[];
16
+ }
17
+ interface PropMapping {
18
+ /** The component whose instances it rewrites, as a path. */
19
+ readonly component: string;
20
+ readonly rules: readonly PropMappingRule[];
21
+ }
22
+ /** A rule as a tool takes it: the shape every surface that applies a mapping parses its input with. */
23
+ declare const propMappingRuleSchema: z.ZodObject<{
24
+ when: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
25
+ set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
26
+ unset: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ }, z.core.$strip>;
28
+ /** The rules of a mapping, as a tool takes them. */
29
+ declare const propMappingRulesSchema: z.ZodArray<z.ZodObject<{
30
+ when: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
31
+ set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
32
+ unset: z.ZodOptional<z.ZodArray<z.ZodString>>;
33
+ }, z.core.$strip>>;
34
+ /**
35
+ * The mappings a batch of edits carries, in order: each `component/map-props` op as the mapping it
36
+ * applies. The op changes nothing in the config, so the host that ran the batch applies these to the
37
+ * documents it holds once the batch lands: Create to a branch's design files, a checkout to its call
38
+ * sites. An op whose rules don't parse is left out; running the batch refused it already.
39
+ */
40
+ declare function propMappingsIn(edits: readonly ConfigEdit[]): PropMapping[];
41
+ /**
42
+ * Whether a batch is mappings alone: every edit a `component/map-props` op. Such a batch changes
43
+ * nothing in the config, so a read-only branch (`main`, a published version) still runs it: its
44
+ * design files take edits, and those are all it moves.
45
+ */
46
+ declare function onlyMapsProps(edits: readonly ConfigEdit[]): boolean;
47
+ /** A bag two matching rules wrote different values into, left as it was. */
48
+ interface PropMappingConflict {
49
+ readonly element: string;
50
+ /** Where the bag sits in the element: `props`, `props._hover`, `props.layerProps.action`. */
51
+ readonly at: string;
52
+ /** The props the rules disagreed on. */
53
+ readonly props: readonly string[];
54
+ }
55
+ interface PropMappingResult {
56
+ readonly spec: RewritableSpec;
57
+ /** How many bags changed. Zero means the same spec object comes back, by identity. */
58
+ readonly changed: number;
59
+ readonly conflicts: readonly PropMappingConflict[];
60
+ }
61
+ /**
62
+ * Why `after` can't take what the mapping writes, one reason per prop or value, or none when it can.
63
+ * Checked before anything is applied, so a mapping that names a prop the component doesn't have
64
+ * is refused rather than written into every instance.
65
+ */
66
+ declare function propMappingErrors(config: Config, mapping: PropMapping): string[];
67
+ /**
68
+ * The spec with the mapping applied to every bag of the mapped component's props. `config` is the
69
+ * one the spec is being brought up to, which is what says which layers render the component.
70
+ *
71
+ * Returns the same spec object when nothing moved. Check the mapping with {@link propMappingErrors}
72
+ * first: this applies what it's given.
73
+ */
74
+ declare function applyPropMapping(config: Config, spec: RewritableSpec, mapping: PropMapping): PropMappingResult;
75
+ /** One instance's props with a mapping tried on it. */
76
+ interface MappedInstance {
77
+ readonly props: Record<string, unknown>;
78
+ readonly changed: boolean;
79
+ /** The props two matching rules wrote different values to, which leave the instance as it was. */
80
+ readonly conflicts: readonly string[];
81
+ }
82
+ type MappedProps = {
83
+ readonly ok: false;
84
+ readonly errors: readonly string[];
85
+ } | {
86
+ readonly ok: true;
87
+ readonly instances: readonly MappedInstance[];
88
+ };
89
+ /**
90
+ * A mapping tried on some instances' props, writing nothing: the `component/map-props` op. Refused,
91
+ * with each reason, when the component can't take what it writes. Each bag is mapped as a design
92
+ * file's instance is, modifier blocks and layer overrides included, so what it returns is what
93
+ * applying the mapping to a document or a call site would store.
94
+ */
95
+ declare function mapComponentProps(config: Config, mapping: PropMapping, instances: readonly Record<string, unknown>[]): MappedProps;
96
+ //#endregion
97
+ export { MappedInstance, MappedProps, MatchedValue, PropMapping, PropMappingConflict, PropMappingResult, PropMappingRule, applyPropMapping, mapComponentProps, onlyMapsProps, propMappingErrors, propMappingRuleSchema, propMappingRulesSchema, propMappingsIn };
@@ -0,0 +1,255 @@
1
+ import { kindOf, pathOf } from "../framework/utils/refs.js";
2
+ import { propValueDomain, resolveComponentProps } from "../framework/projections.js";
3
+ import { surfaceProp } from "../framework/prop-surface.js";
4
+ import { editOp } from "../framework/config-op.js";
5
+ import { layerComponent, modifierBlock, nestedBag } from "./rewrite-refs.js";
6
+ import { z } from "zod";
7
+ //#region src/spec/prop-mapping.ts
8
+ /**
9
+ * Moving what instances of a component stored onto its new API: `variant: 'brand-secondary'` →
10
+ * `color: 'brand', prominence: 'outline'`, `size: 'xs'` → `size: 'sm'`.
11
+ *
12
+ * A rename carries a document across a change with one answer per name, so it can be recorded and
13
+ * replayed (`rewrite-refs.ts`). A reshaped API has no one answer: whether `brand-secondary` is an
14
+ * outline or a ghost is a design decision, so the mapping comes from the person (with an agent
15
+ * proposing it) and is applied here once. Nothing stores it.
16
+ *
17
+ * A mapping is a list of rules for one component. Every rule's `when` is matched against the bag
18
+ * as it was stored, and every rule that matches applies, so `variant` and `size` can be mapped by
19
+ * separate rules and a pair of them by one rule naming both. A rule removes only what its `unset`
20
+ * names: a prop a rule matches on may be one the new API still has.
21
+ *
22
+ * Positions follow the rename rewriter's: an element's own props, every modifier block nested in
23
+ * them (`_hover: { variant: … }` is the same element's props under a condition), and each
24
+ * `layerProps` bag whose layer renders the component.
25
+ */
26
+ /** A rule as a tool takes it: the shape every surface that applies a mapping parses its input with. */
27
+ const propMappingRuleSchema = z.object({
28
+ when: z.record(z.string(), z.union([
29
+ z.string(),
30
+ z.number(),
31
+ z.boolean()
32
+ ])).describe("The stored values an instance must hold for the rule to apply, every one of them: { \"variant\": \"brand-secondary\" }."),
33
+ set: z.record(z.string(), z.unknown()).optional().describe("The props to write: { \"color\": \"brand\", \"prominence\": \"outline\" }."),
34
+ unset: z.array(z.string()).optional().describe("The props to remove. A prop the rule matches on stays unless it is named here, so a removed prop has to be.")
35
+ });
36
+ /** The rules of a mapping, as a tool takes them. */
37
+ const propMappingRulesSchema = z.array(propMappingRuleSchema).min(1).describe("Every rule whose `when` matches an instance as stored applies to it, so separate props are mapped by separate rules, and a combination by one rule naming both.");
38
+ /**
39
+ * The mappings a batch of edits carries, in order: each `component/map-props` op as the mapping it
40
+ * applies. The op changes nothing in the config, so the host that ran the batch applies these to the
41
+ * documents it holds once the batch lands: Create to a branch's design files, a checkout to its call
42
+ * sites. An op whose rules don't parse is left out; running the batch refused it already.
43
+ */
44
+ function propMappingsIn(edits) {
45
+ return edits.flatMap((edit) => {
46
+ const { op } = editOp(edit);
47
+ if (op.kind !== "component" || op.operation !== "map-props") return [];
48
+ const rules = propMappingRulesSchema.safeParse(op.data?.rules);
49
+ if (!rules.success || typeof op.path !== "string") return [];
50
+ return [{
51
+ component: op.path.replace(/^component:/, ""),
52
+ rules: rules.data
53
+ }];
54
+ });
55
+ }
56
+ /**
57
+ * Whether a batch is mappings alone: every edit a `component/map-props` op. Such a batch changes
58
+ * nothing in the config, so a read-only branch (`main`, a published version) still runs it: its
59
+ * design files take edits, and those are all it moves.
60
+ */
61
+ function onlyMapsProps(edits) {
62
+ return edits.length > 0 && propMappingsIn(edits).length === edits.length;
63
+ }
64
+ /**
65
+ * Why a prop can't hold `value`, or `undefined` when it can. A variant's or a slot's values come off
66
+ * its surface, which follows a forward to the prop it routes to. Any other prop with a domain (a
67
+ * composite, a style property) answers through its declaration, as validation does. A bracketed
68
+ * `[…]` literal is the style property's to accept, and a value that isn't a string isn't checked.
69
+ */
70
+ function valueRefusal(config, component, prop, value) {
71
+ if (typeof value !== "string" || value.startsWith("[")) return void 0;
72
+ if (prop.kind === "variant" || prop.kind === "slot") return prop.values && !prop.values.includes(value) ? `"${value}", which isn't one of its values (${prop.values.join(", ")})` : void 0;
73
+ const declaration = resolveComponentProps(config, component)[prop.name];
74
+ const domain = declaration ? propValueDomain(config, declaration) : null;
75
+ return domain && !domain.accepts(value) ? `"${value}", which isn't one of its values (${domain.leaves.slice(0, 12).join(", ")}${domain.leaves.length > 12 ? ", …" : ""})` : void 0;
76
+ }
77
+ /**
78
+ * Why `after` can't take what the mapping writes, one reason per prop or value, or none when it can.
79
+ * Checked before anything is applied, so a mapping that names a prop the component doesn't have
80
+ * is refused rather than written into every instance.
81
+ */
82
+ function propMappingErrors(config, mapping) {
83
+ if (!config.resolve("component", mapping.component)) return [`No component "${mapping.component}"`];
84
+ const errors = [];
85
+ mapping.rules.forEach((rule, i) => {
86
+ if (Object.keys(rule.when).length === 0) errors.push(`Rule ${i + 1} matches every instance: name what it matches in \`when\``);
87
+ for (const [name, value] of Object.entries(rule.set ?? {})) {
88
+ const prop = surfaceProp(config, mapping.component, name);
89
+ if (!prop) {
90
+ errors.push(`Rule ${i + 1} sets "${name}", which ${mapping.component} doesn't have`);
91
+ continue;
92
+ }
93
+ const refusal = valueRefusal(config, mapping.component, prop, value);
94
+ if (refusal) errors.push(`Rule ${i + 1} sets ${name} to ${refusal}`);
95
+ }
96
+ });
97
+ return errors;
98
+ }
99
+ /** The rules applied to one bag of the mapped component's props, as stored. */
100
+ function mapBag(bag, rules) {
101
+ const matched = rules.filter((rule) => Object.entries(rule.when).every(([name, value]) => name in bag && bag[name] === value));
102
+ if (matched.length === 0) return { kind: "same" };
103
+ const writes = /* @__PURE__ */ new Map();
104
+ const removes = /* @__PURE__ */ new Set();
105
+ const disagreed = /* @__PURE__ */ new Set();
106
+ for (const rule of matched) {
107
+ for (const [name, value] of Object.entries(rule.set ?? {})) {
108
+ if (writes.has(name) && writes.get(name) !== value) disagreed.add(name);
109
+ writes.set(name, value);
110
+ }
111
+ for (const name of rule.unset ?? []) removes.add(name);
112
+ }
113
+ if (disagreed.size > 0) return {
114
+ kind: "conflict",
115
+ props: [...disagreed]
116
+ };
117
+ const next = {};
118
+ for (const [name, value] of Object.entries(bag)) if (writes.has(name)) {
119
+ next[name] = writes.get(name);
120
+ writes.delete(name);
121
+ } else if (!removes.has(name)) next[name] = value;
122
+ for (const [name, value] of writes) next[name] = value;
123
+ return Object.keys(next).length === Object.keys(bag).length && Object.entries(next).every(([name, value]) => bag[name] === value) ? { kind: "same" } : {
124
+ kind: "changed",
125
+ bag: next
126
+ };
127
+ }
128
+ /**
129
+ * A props bag on `component`, with the mapping applied to it when it's the mapped component's, and
130
+ * to the modifier blocks and layer overrides inside it either way. `undefined` when nothing moved.
131
+ */
132
+ function mapProps(walk, component, props, at) {
133
+ let next;
134
+ if (component === walk.mapping.component) {
135
+ const outcome = mapBag(props, walk.mapping.rules);
136
+ if (outcome.kind === "conflict") walk.conflicts.push({
137
+ element: walk.element,
138
+ at,
139
+ props: outcome.props
140
+ });
141
+ else if (outcome.kind === "changed") {
142
+ next = outcome.bag;
143
+ walk.changed++;
144
+ }
145
+ }
146
+ for (const [key, value] of Object.entries(next ?? props)) {
147
+ let rewritten;
148
+ if (key === "layerProps") rewritten = component ? mapLayerProps(walk, component, value, `${at}.${key}`) : void 0;
149
+ else {
150
+ const block = modifierBlock(walk.config, key, value, []);
151
+ rewritten = block ? mapProps(walk, component, block, `${at}.${key}`) : void 0;
152
+ }
153
+ if (!rewritten) continue;
154
+ next ??= { ...props };
155
+ next[key] = rewritten;
156
+ }
157
+ return next;
158
+ }
159
+ /** A `layerProps` map: each bag belongs to what its layer renders, which may be the mapped component. */
160
+ function mapLayerProps(walk, component, layerProps, at) {
161
+ const bags = nestedBag(layerProps);
162
+ if (!bags) return void 0;
163
+ let next;
164
+ for (const [layer, value] of Object.entries(bags)) {
165
+ const bag = nestedBag(value);
166
+ if (!bag) continue;
167
+ const rewritten = mapProps(walk, layerComponent(walk.config, component, layer), bag, `${at}.${layer}`);
168
+ if (!rewritten) continue;
169
+ next ??= { ...bags };
170
+ next[layer] = rewritten;
171
+ }
172
+ return next;
173
+ }
174
+ /**
175
+ * The component an element names, as a path: `undefined` for an intrinsic tag or another kind. A
176
+ * stored type may carry the source it was placed through (`component:actions/Button@bridge`), and
177
+ * names the same path either way.
178
+ */
179
+ function componentOf(type) {
180
+ if (typeof type !== "string") return void 0;
181
+ if (!type.includes(":")) return pathOf(`component:${type}`);
182
+ return kindOf(type) === "component" ? pathOf(type) : void 0;
183
+ }
184
+ /**
185
+ * The spec with the mapping applied to every bag of the mapped component's props. `config` is the
186
+ * one the spec is being brought up to, which is what says which layers render the component.
187
+ *
188
+ * Returns the same spec object when nothing moved. Check the mapping with {@link propMappingErrors}
189
+ * first: this applies what it's given.
190
+ */
191
+ function applyPropMapping(config, spec, mapping) {
192
+ let elements;
193
+ let changed = 0;
194
+ const conflicts = [];
195
+ for (const [key, element] of Object.entries(spec.elements ?? {})) {
196
+ if (!element?.props) continue;
197
+ const walk = {
198
+ config,
199
+ mapping,
200
+ element: key,
201
+ conflicts,
202
+ changed: 0
203
+ };
204
+ const props = mapProps(walk, componentOf(element.type), element.props, "props");
205
+ if (!props) continue;
206
+ elements ??= { ...spec.elements };
207
+ elements[key] = {
208
+ ...element,
209
+ props
210
+ };
211
+ changed += walk.changed;
212
+ }
213
+ return elements ? {
214
+ spec: {
215
+ ...spec,
216
+ elements
217
+ },
218
+ changed,
219
+ conflicts
220
+ } : {
221
+ spec,
222
+ changed: 0,
223
+ conflicts
224
+ };
225
+ }
226
+ /**
227
+ * A mapping tried on some instances' props, writing nothing: the `component/map-props` op. Refused,
228
+ * with each reason, when the component can't take what it writes. Each bag is mapped as a design
229
+ * file's instance is, modifier blocks and layer overrides included, so what it returns is what
230
+ * applying the mapping to a document or a call site would store.
231
+ */
232
+ function mapComponentProps(config, mapping, instances) {
233
+ const errors = propMappingErrors(config, mapping);
234
+ if (errors.length > 0) return {
235
+ ok: false,
236
+ errors
237
+ };
238
+ const { spec, conflicts } = applyPropMapping(config, { elements: Object.fromEntries(instances.map((props, i) => [String(i), {
239
+ type: `component:${mapping.component}`,
240
+ props
241
+ }])) }, mapping);
242
+ return {
243
+ ok: true,
244
+ instances: instances.map((props, i) => {
245
+ const after = spec.elements?.[String(i)]?.props ?? props;
246
+ return {
247
+ props: after,
248
+ changed: after !== props,
249
+ conflicts: conflicts.filter((conflict) => conflict.element === String(i)).flatMap((conflict) => conflict.props)
250
+ };
251
+ })
252
+ };
253
+ }
254
+ //#endregion
255
+ export { applyPropMapping, mapComponentProps, onlyMapsProps, propMappingErrors, propMappingRuleSchema, propMappingRulesSchema, propMappingsIn };
@@ -246,8 +246,12 @@ function scopeFor(config, component) {
246
246
  /** The scope one `layerProps` entry's bag belongs to — the element the named layer renders. */
247
247
  function layerScope(config, scope, layer) {
248
248
  if (!scope.component) return INTRINSIC;
249
- const element = Component.get(config, scope.component)?.layers?.[layer];
250
- return scopeFor(config, isRef(element) ? pathOf(element.__ref) : void 0);
249
+ return scopeFor(config, layerComponent(config, scope.component, layer));
250
+ }
251
+ /** The component a layer of `component` renders, or `undefined` for a raw tag or an unknown layer. */
252
+ function layerComponent(config, component, layer) {
253
+ const element = Component.get(config, component)?.layers?.[layer];
254
+ return isRef(element) ? pathOf(element.__ref) : void 0;
251
255
  }
252
256
  /** Rename a KEY inside a props bag, preserving insertion order — order is meaning in a stored document. */
253
257
  function withRenamedKey(props, from, to) {
@@ -350,7 +354,7 @@ function rewriteProps(config, scope, props, rename, batch) {
350
354
  let next = rewriteOwnEntries(config, scope, props, rename);
351
355
  for (const [key, value] of Object.entries(next ?? props)) {
352
356
  let rewritten;
353
- if (key === LAYER_PROPS) rewritten = rewriteLayerProps(config, scope, value, rename, batch);
357
+ if (key === "layerProps") rewritten = rewriteLayerProps(config, scope, value, rename, batch);
354
358
  else {
355
359
  const block = modifierBlock(config, key, value, batch);
356
360
  rewritten = block ? rewriteProps(config, scope, block, rename, batch) : void 0;
@@ -452,4 +456,4 @@ function rewriteSpecRefs(config, spec, renames) {
452
456
  };
453
457
  }
454
458
  //#endregion
455
- export { composeRenames, documentRenamesFromPatches, documentRenamesFromSource, rewriteSpecRefs, styleNumberReadingRenames };
459
+ export { LAYER_PROPS, composeRenames, documentRenamesFromPatches, documentRenamesFromSource, layerComponent, modifierBlock, nestedBag, rewriteSpecRefs, styleNumberReadingRenames };