recma-static-refiner 0.9.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.
package/dist/index.js ADDED
@@ -0,0 +1,1109 @@
1
+ import { is, traverse } from 'estree-toolkit';
2
+ import { valueToEstree } from 'estree-util-value-to-estree';
3
+
4
+ // src/index.ts
5
+ var JSX_FACTORY_EXPORTS = /* @__PURE__ */ new Set(["jsx", "jsxs", "jsxDEV"]);
6
+ function normalizeJsxFactoryName(name) {
7
+ return name.startsWith("_") ? name.slice(1) : name;
8
+ }
9
+ function getCallArgumentPath(callPath, index) {
10
+ const node = callPath.node;
11
+ if (!node || !is.callExpression(node)) return null;
12
+ const argPaths = callPath.get("arguments");
13
+ if (!Array.isArray(argPaths)) return null;
14
+ return argPaths.at(index) ?? null;
15
+ }
16
+ function isJsxPropsArgumentExpressionPath(path) {
17
+ return !!path?.node && is.expression(path.node);
18
+ }
19
+ function getJsxRuntimeCallArgs(callPath) {
20
+ const node = callPath.node;
21
+ if (!node || !is.callExpression(node)) return null;
22
+ if (!is.identifier(node.callee)) return null;
23
+ const calleeName = normalizeJsxFactoryName(node.callee.name);
24
+ if (!JSX_FACTORY_EXPORTS.has(calleeName)) return null;
25
+ const componentPath = getCallArgumentPath(callPath, 0);
26
+ const propsPath = getCallArgumentPath(callPath, 1);
27
+ if (!componentPath) return null;
28
+ if (!isJsxPropsArgumentExpressionPath(propsPath)) return null;
29
+ return { componentPath, propsPath };
30
+ }
31
+
32
+ // src/rule-validator.ts
33
+ function validateRule(rule, componentName) {
34
+ if (!rule || typeof rule !== "object") {
35
+ throw new Error(
36
+ `[ValidationPlugin] Invalid rule for "${componentName}": Expected a rule object, got ${typeof rule}.`
37
+ );
38
+ }
39
+ const isEffective = rule.schema != null || rule.derive != null || rule.pruneKeys != null;
40
+ if (!isEffective) {
41
+ throw new Error(
42
+ `[ValidationPlugin] Invalid rule for "${componentName}": The rule is empty. It must define at least one of: 'schema', 'derive', or 'pruneKeys'.`
43
+ );
44
+ }
45
+ return rule;
46
+ }
47
+
48
+ // src/jsx-callsite-resolver.ts
49
+ function extractComponentName(node) {
50
+ if (!node) return null;
51
+ if (is.identifier(node)) {
52
+ return node.name;
53
+ }
54
+ if (is.literal(node) && typeof node.value === "string") {
55
+ return node.value;
56
+ }
57
+ return null;
58
+ }
59
+ function createComponentResolver(rules) {
60
+ const registeredComponentNames = new Set(Object.keys(rules));
61
+ return (path) => {
62
+ const args = getJsxRuntimeCallArgs(path);
63
+ if (!args) return null;
64
+ const componentName = extractComponentName(args.componentPath.node);
65
+ if (!componentName) return null;
66
+ if (!registeredComponentNames.has(componentName)) return null;
67
+ const ruleEntry = rules[componentName];
68
+ if (!ruleEntry) return null;
69
+ const componentRule = validateRule(ruleEntry, componentName);
70
+ return {
71
+ componentName,
72
+ componentRule,
73
+ propsExpressionPath: args.propsPath
74
+ };
75
+ };
76
+ }
77
+
78
+ // src/validator.ts
79
+ function validateWithSchema(schema, inputProps, componentName) {
80
+ if (!schema) {
81
+ return inputProps;
82
+ }
83
+ if (!("~standard" in schema)) {
84
+ throw new Error(
85
+ `The schema for "${componentName}" is invalid. Expected an object with the "~standard" property (e.g. Zod, Valibot), but received a plain object.`
86
+ );
87
+ }
88
+ const result = schema["~standard"].validate(inputProps);
89
+ if (result instanceof Promise) {
90
+ throw new Error(
91
+ `Async schema validation is not supported for "${componentName}".`
92
+ );
93
+ }
94
+ const [firstIssue] = result.issues ?? [];
95
+ if (firstIssue) {
96
+ const issuePath = firstIssue.path?.join(".") ?? "unknown";
97
+ throw new Error(
98
+ `Invalid props for "${componentName}" at "${issuePath}": ${firstIssue.message}`
99
+ );
100
+ }
101
+ if ("value" in result) {
102
+ return result.value;
103
+ }
104
+ return inputProps;
105
+ }
106
+
107
+ // src/differ/utils.ts
108
+ var Tag = {
109
+ String: "[object String]",
110
+ Number: "[object Number]",
111
+ Boolean: "[object Boolean]",
112
+ BigInt: "[object BigInt]",
113
+ Date: "[object Date]",
114
+ RegExp: "[object RegExp]"
115
+ };
116
+ var RICH_TYPES = /* @__PURE__ */ new Set([
117
+ // Boxed Primitives
118
+ Tag.String,
119
+ Tag.Number,
120
+ Tag.Boolean,
121
+ Tag.BigInt,
122
+ // Complex Types
123
+ Tag.Date,
124
+ Tag.RegExp
125
+ ]);
126
+ function isRichType(value) {
127
+ return RICH_TYPES.has(Object.prototype.toString.call(value));
128
+ }
129
+ function unbox(wrapper) {
130
+ return wrapper.valueOf();
131
+ }
132
+ function areBoxedPrimitivesEqual(left, right) {
133
+ const leftTypeTag = Object.prototype.toString.call(left);
134
+ const rightTypeTag = Object.prototype.toString.call(right);
135
+ if (leftTypeTag !== rightTypeTag) return false;
136
+ switch (leftTypeTag) {
137
+ // Group 1: Rely on .valueOf() (via unbox)
138
+ case Tag.Number: {
139
+ const leftValue = unbox(left);
140
+ const rightValue = unbox(right);
141
+ if (Number.isNaN(leftValue)) {
142
+ return Number.isNaN(rightValue);
143
+ }
144
+ return leftValue === rightValue;
145
+ }
146
+ case Tag.String:
147
+ return unbox(left) === unbox(right);
148
+ case Tag.Boolean:
149
+ return unbox(left) === unbox(right);
150
+ case Tag.BigInt:
151
+ return unbox(left) === unbox(right);
152
+ case Tag.Date:
153
+ return unbox(left) === unbox(right);
154
+ // Group 2: Relies on .toString()
155
+ case Tag.RegExp:
156
+ return left.toString() === right.toString();
157
+ default:
158
+ return false;
159
+ }
160
+ }
161
+ function areArraysShallowEqual(left, right) {
162
+ if (left === right) return true;
163
+ if (left?.length !== right?.length) return false;
164
+ return left.every((val, index) => Object.is(val, right[index]));
165
+ }
166
+ function normalizeOptions(options) {
167
+ return {
168
+ trackCircularReferences: true,
169
+ arrays: "atomic",
170
+ arrayEquality: "reference",
171
+ keysToSkip: [],
172
+ ...options
173
+ };
174
+ }
175
+ function isContainer(value) {
176
+ return typeof value === "object" && value !== null;
177
+ }
178
+ function getSafeValue(container, key) {
179
+ if (Array.isArray(container)) {
180
+ return container[Number(key)];
181
+ }
182
+ return container[key];
183
+ }
184
+ function shouldSkipKey(key, isContainerArray, keysToSkip) {
185
+ if (isContainerArray) return false;
186
+ if (!keysToSkip) return false;
187
+ return keysToSkip.includes(key);
188
+ }
189
+ function formatPathKey(key, isContainerArray) {
190
+ return isContainerArray ? Number(key) : key;
191
+ }
192
+ function prependPath(differences, pathSegment) {
193
+ for (const difference of differences) {
194
+ difference.path.unshift(pathSegment);
195
+ }
196
+ return differences;
197
+ }
198
+ function createChange(path, value, oldValue) {
199
+ return { type: "CHANGE", path, value, oldValue };
200
+ }
201
+ function createCreate(path, value) {
202
+ return { type: "CREATE", path, value };
203
+ }
204
+ function createRemove(path, oldValue) {
205
+ return { type: "REMOVE", path, oldValue };
206
+ }
207
+
208
+ // src/differ/strategies.ts
209
+ function getArrayStrategy(options) {
210
+ switch (options.arrays) {
211
+ case "diff":
212
+ return { mode: "diff" };
213
+ case "ignore":
214
+ return { mode: "ignore" };
215
+ case "atomic": {
216
+ const comparator = options.arrayEquality === "reference" ? (a, b) => a === b : (a, b) => areArraysShallowEqual(a, b);
217
+ return { mode: "atomic", comparator };
218
+ }
219
+ default:
220
+ throw new Error(`Invalid array policy: ${options.arrays}`);
221
+ }
222
+ }
223
+ function checkArrayStrategy(strategy, previous, current) {
224
+ switch (strategy.mode) {
225
+ case "ignore":
226
+ return { shouldRecurse: false, diffs: [] };
227
+ case "atomic":
228
+ return strategy.comparator(previous, current) ? { shouldRecurse: false, diffs: [] } : {
229
+ shouldRecurse: false,
230
+ diffs: [createChange([], current, previous)]
231
+ };
232
+ case "diff":
233
+ return { shouldRecurse: true, diffs: [] };
234
+ }
235
+ }
236
+
237
+ // src/differ/index.ts
238
+ function isCycleDetected(stack, previous, current) {
239
+ for (const [seenPrevious, seenCurrent] of stack) {
240
+ if (seenPrevious === previous && seenCurrent === current) return true;
241
+ }
242
+ return false;
243
+ }
244
+ function pushCycleEntry(options, stack, previous, current) {
245
+ if (!options.trackCircularReferences) return stack;
246
+ const newEntry = [previous, current];
247
+ return stack.concat([newEntry]);
248
+ }
249
+ function compareChildren(previousContainer, currentContainer, options, cycleStack, arrayStrategy) {
250
+ const differences = [];
251
+ const isPreviousArray = Array.isArray(previousContainer);
252
+ const isCurrentArray = Array.isArray(currentContainer);
253
+ const previousKeys = Object.keys(previousContainer);
254
+ for (const key of previousKeys) {
255
+ if (shouldSkipKey(key, isPreviousArray, options.keysToSkip)) continue;
256
+ const previousValue = getSafeValue(previousContainer, key);
257
+ const pathSegment = formatPathKey(key, isPreviousArray);
258
+ if (!(key in currentContainer)) {
259
+ differences.push(createRemove([pathSegment], previousValue));
260
+ continue;
261
+ }
262
+ const currentValue = getSafeValue(currentContainer, key);
263
+ if (isContainer(previousValue) && isContainer(currentValue) && Array.isArray(previousValue) === Array.isArray(currentValue)) {
264
+ if (options.trackCircularReferences && isCycleDetected(cycleStack, previousValue, currentValue)) {
265
+ continue;
266
+ }
267
+ if (!isRichType(previousValue)) {
268
+ const nextStack = pushCycleEntry(
269
+ options,
270
+ cycleStack,
271
+ previousValue,
272
+ currentValue
273
+ );
274
+ const childDiffs = compare(
275
+ previousValue,
276
+ currentValue,
277
+ options,
278
+ nextStack,
279
+ arrayStrategy
280
+ );
281
+ differences.push(...prependPath(childDiffs, pathSegment));
282
+ continue;
283
+ }
284
+ }
285
+ const hasValueMismatch = !Object.is(previousValue, currentValue);
286
+ const areBoxedPrimitives = isContainer(previousValue) && isContainer(currentValue) && areBoxedPrimitivesEqual(previousValue, currentValue);
287
+ if (hasValueMismatch && !areBoxedPrimitives) {
288
+ differences.push(
289
+ createChange([pathSegment], currentValue, previousValue)
290
+ );
291
+ }
292
+ }
293
+ const currentKeys = Object.keys(currentContainer);
294
+ for (const key of currentKeys) {
295
+ if (shouldSkipKey(key, isCurrentArray, options.keysToSkip)) continue;
296
+ if (!(key in previousContainer)) {
297
+ const currentValue = getSafeValue(currentContainer, key);
298
+ differences.push(
299
+ createCreate([formatPathKey(key, isCurrentArray)], currentValue)
300
+ );
301
+ }
302
+ }
303
+ return differences;
304
+ }
305
+ function compare(previous, current, options, cycleStack, arrayStrategy) {
306
+ if (Array.isArray(previous) && Array.isArray(current)) {
307
+ const { shouldRecurse, diffs } = checkArrayStrategy(
308
+ arrayStrategy,
309
+ previous,
310
+ current
311
+ );
312
+ if (!shouldRecurse) return diffs;
313
+ }
314
+ return compareChildren(previous, current, options, cycleStack, arrayStrategy);
315
+ }
316
+ function diff(previous, current, options = {}) {
317
+ const normalized = normalizeOptions(options);
318
+ const strategy = getArrayStrategy(normalized);
319
+ return compare(previous, current, normalized, [], strategy);
320
+ }
321
+ function isPatchablePropsRoot(node) {
322
+ return !!node && is.objectExpression(node);
323
+ }
324
+ function isPlainObject(value) {
325
+ if (typeof value !== "object" || value === null) return false;
326
+ const proto = Object.getPrototypeOf(value);
327
+ return proto === null || proto === Object.prototype;
328
+ }
329
+ function isRecord(value) {
330
+ return typeof value === "object" && value !== null;
331
+ }
332
+ function isPropertyPath(value) {
333
+ return Array.isArray(value) && value.every((seg) => typeof seg === "string" || typeof seg === "number");
334
+ }
335
+
336
+ // src/utils/path-utils.ts
337
+ function isKeyPreserved(key, preservedKeys) {
338
+ const lookupKey = String(key);
339
+ return preservedKeys.has(lookupKey);
340
+ }
341
+
342
+ // src/object-overlay.ts
343
+ function overlayObject(base, overlay, preservedKeys) {
344
+ let resultObject;
345
+ for (const candidateKey of Object.keys(overlay)) {
346
+ if (isKeyPreserved(candidateKey, preservedKeys)) continue;
347
+ if (!Object.hasOwn(base, candidateKey)) continue;
348
+ const basePropValue = base[candidateKey];
349
+ const overlayPropValue = overlay[candidateKey];
350
+ const mergedValue = overlayValue(
351
+ basePropValue,
352
+ overlayPropValue,
353
+ preservedKeys
354
+ );
355
+ if (Object.is(mergedValue, basePropValue)) continue;
356
+ if (!resultObject) resultObject = { ...base };
357
+ resultObject[candidateKey] = mergedValue;
358
+ }
359
+ return resultObject ?? base;
360
+ }
361
+ function overlayValue(basePropValue, overlayPropValue, preservedKeys) {
362
+ if (Object.is(basePropValue, overlayPropValue)) return basePropValue;
363
+ if (Array.isArray(basePropValue) && Array.isArray(overlayPropValue)) {
364
+ return overlayPropValue;
365
+ }
366
+ if (isPlainObject(basePropValue) && isPlainObject(overlayPropValue)) {
367
+ return overlayObject(basePropValue, overlayPropValue, preservedKeys);
368
+ }
369
+ return overlayPropValue;
370
+ }
371
+ function applyOverlay(base, overlay, preservedKeys) {
372
+ if (Object.is(base, overlay)) return base;
373
+ return overlayObject(base, overlay, preservedKeys);
374
+ }
375
+
376
+ // src/patch-planner.ts
377
+ function calculatePatches(extractedProps, validatedProps, preservedKeys) {
378
+ if (!isPlainObject(extractedProps) || !isPlainObject(validatedProps)) {
379
+ return [];
380
+ }
381
+ const diffTarget = applyOverlay(
382
+ extractedProps,
383
+ validatedProps,
384
+ preservedKeys
385
+ );
386
+ const difference = diff(extractedProps, diffTarget);
387
+ const patches = [];
388
+ for (const entry of difference) {
389
+ if (entry.type === "CHANGE") {
390
+ patches.push({
391
+ operation: "set",
392
+ path: entry.path,
393
+ value: entry.value
394
+ });
395
+ continue;
396
+ }
397
+ if (entry.type === "CREATE") {
398
+ continue;
399
+ }
400
+ if (entry.type === "REMOVE") {
401
+ continue;
402
+ }
403
+ }
404
+ return patches;
405
+ }
406
+
407
+ // src/extractor/constants.ts
408
+ var UNRESOLVED = { success: false };
409
+ function resolved(value) {
410
+ return { success: true, value };
411
+ }
412
+ var SKIP_VALUE = /* @__PURE__ */ Symbol.for("recma.extraction.skip");
413
+
414
+ // src/extractor/static-resolver.ts
415
+ function tryResolveLiteral(node) {
416
+ if (is.literal(node)) {
417
+ switch (typeof node.value) {
418
+ case "string":
419
+ case "number":
420
+ case "boolean":
421
+ case "bigint":
422
+ return resolved(node.value);
423
+ case "object":
424
+ if (node.value === null) {
425
+ return resolved(null);
426
+ }
427
+ if (node.value instanceof RegExp) {
428
+ return resolved(node.value);
429
+ }
430
+ break;
431
+ }
432
+ }
433
+ return UNRESOLVED;
434
+ }
435
+ function tryResolveIdentifier(node) {
436
+ if (is.identifier(node)) {
437
+ switch (node.name) {
438
+ case "undefined":
439
+ return resolved(void 0);
440
+ case "NaN":
441
+ return resolved(NaN);
442
+ case "Infinity":
443
+ return resolved(Infinity);
444
+ }
445
+ }
446
+ return UNRESOLVED;
447
+ }
448
+ function tryResolveTemplate(node) {
449
+ if (is.templateLiteral(node)) {
450
+ const parts = [];
451
+ for (const [index, quasi] of node.quasis.entries()) {
452
+ const text = quasi.value.cooked;
453
+ if (typeof text !== "string") return UNRESOLVED;
454
+ parts.push(text);
455
+ if (index < node.expressions.length) {
456
+ const exprResult = tryResolveStaticValue(node.expressions[index]);
457
+ if (!exprResult.success) return UNRESOLVED;
458
+ parts.push(`${exprResult.value}`);
459
+ }
460
+ }
461
+ return resolved(parts.join(""));
462
+ }
463
+ return UNRESOLVED;
464
+ }
465
+ function tryResolveStaticValue(node) {
466
+ let result;
467
+ if ((result = tryResolveLiteral(node)).success) return result;
468
+ if ((result = tryResolveIdentifier(node)).success) return result;
469
+ if ((result = tryResolveTemplate(node)).success) return result;
470
+ return UNRESOLVED;
471
+ }
472
+
473
+ // src/extractor/key-extractor.ts
474
+ function tryExtractNamedKey(property) {
475
+ if (!property.computed && is.identifier(property.key)) {
476
+ return property.key.name;
477
+ }
478
+ return null;
479
+ }
480
+ function extractPropertyKey(property) {
481
+ const namedKey = tryExtractNamedKey(property);
482
+ if (namedKey !== null) {
483
+ return namedKey;
484
+ }
485
+ const resolution = tryResolveStaticValue(property.key);
486
+ if (resolution.success) {
487
+ const resolvedValue = resolution.value;
488
+ if (typeof resolvedValue === "string" || typeof resolvedValue === "number") {
489
+ return resolvedValue;
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+
495
+ // src/expression-ref.ts
496
+ var EXPRESSION_REF_KIND = /* @__PURE__ */ Symbol.for("recma.preservation.expression_ref");
497
+ function createExpressionRef(path) {
498
+ return { __kind: EXPRESSION_REF_KIND, path };
499
+ }
500
+ function isExpressionRef(value) {
501
+ if (!isRecord(value)) return false;
502
+ if (value.__kind !== EXPRESSION_REF_KIND) return false;
503
+ return isPropertyPath(value.path);
504
+ }
505
+
506
+ // src/utils/array-utils.ts
507
+ function preserveArrayElision(result, slotIndex) {
508
+ if (result.length < slotIndex + 1) result.length = slotIndex + 1;
509
+ }
510
+ function isArrayElision(array, slotIndex) {
511
+ return !(slotIndex in array);
512
+ }
513
+
514
+ // src/utils/type-guards.ts
515
+ function is6(type) {
516
+ return (value) => typeof value === type;
517
+ }
518
+ var isNumber = is6("number");
519
+ function isNaNValue(value) {
520
+ return isNumber(value) && Number.isNaN(value);
521
+ }
522
+ function isInfinityValue(value) {
523
+ return isNumber(value) && value === Infinity;
524
+ }
525
+
526
+ // src/utils/property-path-key.ts
527
+ function composeSegmentCanonicalizers(canonicalizers) {
528
+ return function canonicalizeComposed(segment) {
529
+ let currentSegment = segment;
530
+ for (const canonicalize of canonicalizers) {
531
+ currentSegment = canonicalize(currentSegment);
532
+ }
533
+ return currentSegment;
534
+ };
535
+ }
536
+ function canonicalizeIdentifierNumberConstants(segment) {
537
+ if (!isNumber(segment)) return segment;
538
+ if (isNaNValue(segment)) return "NaN";
539
+ if (isInfinityValue(segment)) return "Infinity";
540
+ return segment;
541
+ }
542
+ function canonicalizeTemplateResults(segment) {
543
+ return segment;
544
+ }
545
+ var propertyPathSegmentCanonicalizer = composeSegmentCanonicalizers([
546
+ canonicalizeIdentifierNumberConstants,
547
+ canonicalizeTemplateResults
548
+ // canonicalizeUnaryNumberResults,
549
+ // canonicalizeArithmeticNumberResults
550
+ ]);
551
+ function canonicalizePropertyPath(path) {
552
+ return path.map((segment) => propertyPathSegmentCanonicalizer(segment));
553
+ }
554
+ function stringifyPropertyPath(path) {
555
+ return JSON.stringify(canonicalizePropertyPath(path));
556
+ }
557
+
558
+ // src/patcher-object.ts
559
+ function getFirstUnappliedPathKey(setPatchByPathKey, deletePathKeys) {
560
+ const firstSetKeyIterator = setPatchByPathKey.keys().next();
561
+ if (!firstSetKeyIterator.done) return firstSetKeyIterator.value;
562
+ const firstDeleteKeyIterator = deletePathKeys.values().next();
563
+ if (!firstDeleteKeyIterator.done) return firstDeleteKeyIterator.value;
564
+ return void 0;
565
+ }
566
+ function buildEstreeValue(value, expressionRefResolver) {
567
+ if (isExpressionRef(value)) {
568
+ const resolvedExpression = expressionRefResolver(value);
569
+ if (!resolvedExpression) {
570
+ throw new Error(
571
+ `[recma] Cannot inline ExpressionRef at path "${value.path.join(".")}".`
572
+ );
573
+ }
574
+ return resolvedExpression;
575
+ }
576
+ if (Array.isArray(value)) {
577
+ const elements = new Array(value.length);
578
+ for (let slotIndex = 0; slotIndex < value.length; slotIndex++) {
579
+ if (isArrayElision(value, slotIndex)) {
580
+ elements[slotIndex] = null;
581
+ continue;
582
+ }
583
+ elements[slotIndex] = buildEstreeValue(
584
+ value[slotIndex],
585
+ expressionRefResolver
586
+ );
587
+ }
588
+ return {
589
+ type: "ArrayExpression",
590
+ elements
591
+ };
592
+ }
593
+ if (isPlainObject(value)) {
594
+ const properties = [];
595
+ for (const [key, child] of Object.entries(value)) {
596
+ properties.push({
597
+ type: "Property",
598
+ /**
599
+ * Static object key (no brackets):
600
+ * - `{ foo: 1 }` ✅
601
+ * - `{ [foo]: 1 }` ❌
602
+ */
603
+ computed: false,
604
+ /**
605
+ * Always emit explicit key/value pairs:
606
+ * - `{ foo: foo }` ✅
607
+ * - `{ foo }` ❌ (shorthand)
608
+ */
609
+ shorthand: false,
610
+ /**
611
+ * Not method syntax:
612
+ * - `{ foo: () => {} }` ✅ (value is an expression)
613
+ * - `{ foo() {} }` ❌ (method)
614
+ */
615
+ method: false,
616
+ /**
617
+ * Regular initializer property:
618
+ * - `{ foo: value }` ✅
619
+ * - `{ get foo() {} }` ❌
620
+ * - `{ set foo(v) {} }` ❌
621
+ */
622
+ kind: "init",
623
+ /**
624
+ * Use a string-literal key so any JS object key is representable
625
+ * (including keys that are not valid identifiers, e.g. `"data-id"`, `"1"`, `"a b"`).
626
+ */
627
+ key: { type: "Literal", value: key },
628
+ /**
629
+ * Encode the value as an ESTree expression.
630
+ * "Recursive" means nested arrays/objects are encoded by calling `buildEstreeValue`
631
+ * again for their children until primitives/ExpressionRefs are reached.
632
+ */
633
+ value: buildEstreeValue(child, expressionRefResolver)
634
+ });
635
+ }
636
+ return {
637
+ type: "ObjectExpression",
638
+ properties
639
+ };
640
+ }
641
+ return valueToEstree(value);
642
+ }
643
+ function tryAppendObjectKeySegment(cursor, pathSegments) {
644
+ const parentNode = cursor.parentPath?.node;
645
+ if (!parentNode) return void 0;
646
+ if (is.property(cursor.node) && is.objectExpression(parentNode)) {
647
+ const keySegment = extractPropertyKey(cursor.node);
648
+ if (keySegment === null) return null;
649
+ pathSegments.push(keySegment);
650
+ return cursor.parentPath;
651
+ }
652
+ return void 0;
653
+ }
654
+ function getRelativePathFromRoot(targetPath) {
655
+ const pathSegments = [];
656
+ let cursor = targetPath;
657
+ while (cursor?.parentPath) {
658
+ const parentNode = cursor.parentPath.node;
659
+ const node = cursor.node;
660
+ if (!parentNode || !node) break;
661
+ if (is.property(parentNode) && cursor.key === "value") {
662
+ cursor = cursor.parentPath;
663
+ continue;
664
+ }
665
+ const nextFromObject = tryAppendObjectKeySegment(cursor, pathSegments);
666
+ if (nextFromObject === null) return null;
667
+ if (nextFromObject) {
668
+ cursor = nextFromObject;
669
+ continue;
670
+ }
671
+ cursor = cursor.parentPath;
672
+ }
673
+ return pathSegments.reverse();
674
+ }
675
+ function buildPatchIndex(patches) {
676
+ const setPatchByPathKey = /* @__PURE__ */ new Map();
677
+ const deletePathKeys = /* @__PURE__ */ new Set();
678
+ for (const patch of patches) {
679
+ const pathKey = stringifyPropertyPath(patch.path);
680
+ if (patch.operation === "set") {
681
+ setPatchByPathKey.set(pathKey, patch);
682
+ } else {
683
+ deletePathKeys.add(pathKey);
684
+ }
685
+ }
686
+ return { setPatchByPathKey, deletePathKeys };
687
+ }
688
+ function applyObjectPropertyPatchesIfNeeded(path, state) {
689
+ if (!path.node) return;
690
+ const propertyKey = extractPropertyKey(path.node);
691
+ if (propertyKey === null) return;
692
+ if (isKeyPreserved(propertyKey, state.preservedKeys)) {
693
+ path.skipChildren();
694
+ return;
695
+ }
696
+ const propPath = getRelativePathFromRoot(path);
697
+ if (!propPath) return;
698
+ const pathKey = stringifyPropertyPath(propPath);
699
+ if (state.deletePathKeys.has(pathKey)) {
700
+ path.remove();
701
+ state.deletePathKeys.delete(pathKey);
702
+ return;
703
+ }
704
+ const patch = state.setPatchByPathKey.get(pathKey);
705
+ if (!patch) return;
706
+ path.get("value").replaceWith(buildEstreeValue(patch.value, state.expressionRefResolver));
707
+ state.setPatchByPathKey.delete(pathKey);
708
+ }
709
+ function finalizeApplyPatchesResult(setPatchByPathKey, deletePathKeys) {
710
+ const remainingSetPathKeys = Array.from(setPatchByPathKey.keys());
711
+ const remainingDeletePathKeys = Array.from(deletePathKeys.values());
712
+ const firstUnappliedPathKey = getFirstUnappliedPathKey(
713
+ setPatchByPathKey,
714
+ deletePathKeys
715
+ );
716
+ return {
717
+ firstUnappliedPathKey,
718
+ remainingSetPathKeys,
719
+ remainingDeletePathKeys,
720
+ remainingSetCount: remainingSetPathKeys.length,
721
+ remainingDeleteCount: remainingDeletePathKeys.length
722
+ };
723
+ }
724
+ function createPatchApplicationVisitors() {
725
+ return {
726
+ Property(path, state) {
727
+ applyObjectPropertyPatchesIfNeeded(path, state);
728
+ }
729
+ // ArrayExpression(path, state) {
730
+ // applyArrayElementPatchesIfNeeded(path, state);
731
+ // }
732
+ };
733
+ }
734
+ function applyPatchesToEstree(propsRootPath, patches, preservedKeys, options) {
735
+ const { setPatchByPathKey, deletePathKeys } = buildPatchIndex(patches);
736
+ if (!isPatchablePropsRoot(propsRootPath.node)) {
737
+ return finalizeApplyPatchesResult(setPatchByPathKey, deletePathKeys);
738
+ }
739
+ const state = {
740
+ preservedKeys,
741
+ setPatchByPathKey,
742
+ deletePathKeys,
743
+ expressionRefResolver: options.expressionRefResolver
744
+ };
745
+ const visitors = createPatchApplicationVisitors();
746
+ traverse(propsRootPath.node, visitors, state);
747
+ return finalizeApplyPatchesResult(setPatchByPathKey, deletePathKeys);
748
+ }
749
+
750
+ // src/consolidatePatches.ts
751
+ function consolidatePatches(groups) {
752
+ const patchByPathKey = /* @__PURE__ */ new Map();
753
+ const phaseByPathKey = /* @__PURE__ */ new Map();
754
+ for (const { phase, patches } of groups) {
755
+ for (const patch of patches) {
756
+ const pathKey = stringifyPropertyPath(patch.path);
757
+ patchByPathKey.set(pathKey, patch);
758
+ phaseByPathKey.set(pathKey, phase);
759
+ }
760
+ }
761
+ return {
762
+ patches: Array.from(patchByPathKey.values()),
763
+ phaseByPathKey
764
+ };
765
+ }
766
+
767
+ // src/report.ts
768
+ function formatPathKeyForDisplay(pathKey) {
769
+ try {
770
+ const parsed = JSON.parse(pathKey);
771
+ if (isPropertyPath(parsed) && parsed.length > 0) {
772
+ return parsed.join(".");
773
+ }
774
+ } catch {
775
+ }
776
+ return pathKey;
777
+ }
778
+ function formatPatchFailureSummary(context, getPhase) {
779
+ const setKeys = context.remainingSetPathKeys;
780
+ const deleteKeys = context.remainingDeletePathKeys;
781
+ if (setKeys.length === 0 && deleteKeys.length === 0) return void 0;
782
+ const parts = [];
783
+ const operationParts = [];
784
+ if (setKeys.length > 0) {
785
+ const setPhaseStats = formatPhaseDistribution(setKeys, getPhase);
786
+ operationParts.push(
787
+ `set=${setKeys.length}${setPhaseStats ? ` (${setPhaseStats})` : ""}`
788
+ );
789
+ }
790
+ if (deleteKeys.length > 0) {
791
+ const deletePhaseStats = formatPhaseDistribution(deleteKeys, getPhase);
792
+ operationParts.push(
793
+ `delete=${deleteKeys.length}${deletePhaseStats ? ` (${deletePhaseStats})` : ""}`
794
+ );
795
+ }
796
+ parts.push(`Summary: unapplied ${operationParts.join(", ")}`);
797
+ const allKeys = [...setKeys, ...deleteKeys];
798
+ const previewLimit = context.maxPreviewPaths != null ? context.maxPreviewPaths : 5;
799
+ if (previewLimit > 0) {
800
+ const previewStats = formatPathPreview(allKeys, previewLimit, getPhase);
801
+ if (previewStats) {
802
+ parts.push(previewStats);
803
+ }
804
+ }
805
+ return parts.join("; ");
806
+ }
807
+ function formatPhaseDistribution(pathKeys, getPhase) {
808
+ if (pathKeys.length === 0) return void 0;
809
+ const countByPhase = /* @__PURE__ */ new Map();
810
+ for (const key of pathKeys) {
811
+ const phase = getPhase(key) ?? "unknown";
812
+ const current = countByPhase.get(phase) ?? 0;
813
+ countByPhase.set(phase, current + 1);
814
+ }
815
+ return Array.from(countByPhase.entries()).map(([phase, count]) => `${phase}=${count}`).join(", ");
816
+ }
817
+ function formatPathPreview(pathKeys, limit, getPhase) {
818
+ if (pathKeys.length === 0 || limit <= 0) return void 0;
819
+ const items = pathKeys.slice(0, limit).map((key) => {
820
+ const phase = getPhase(key) ?? "unknown";
821
+ return `"${formatPathKeyForDisplay(key)}" (${phase})`;
822
+ });
823
+ if (pathKeys.length > limit) {
824
+ items.push(`\u2026 (${pathKeys.length - limit} more)`);
825
+ }
826
+ return `preview: ${items.join(", ")}`;
827
+ }
828
+ function formatPatchFailureHint(phase) {
829
+ switch (phase) {
830
+ case "derive":
831
+ return "Hint: derive patches are leaf-only. Ensure the prop exists in MDX (e.g. <CustomComponent test={...} />) before setting it.";
832
+ // covers diff, prune, and undefined
833
+ default:
834
+ return void 0;
835
+ }
836
+ }
837
+ function reportPatchFailure(firstUnappliedPathKey, options) {
838
+ const phase = options.getPatchPhaseByPathKey(firstUnappliedPathKey);
839
+ const propPath = formatPathKeyForDisplay(firstUnappliedPathKey);
840
+ const hint = formatPatchFailureHint(phase);
841
+ const phaseAnnotation = phase ? ` (phase: ${phase})` : "";
842
+ const summaryLine = formatPatchFailureSummary(
843
+ options.summaryContext,
844
+ options.getPatchPhaseByPathKey
845
+ );
846
+ const messageParts = [
847
+ `[recma] Cannot fully apply patches for ${options.componentName}.`,
848
+ `First un-applied path: "${propPath}"${phaseAnnotation} (non-literal AST shape or missing path)`
849
+ ];
850
+ if (hint) messageParts.push(hint);
851
+ if (summaryLine) messageParts.push(summaryLine);
852
+ const message = messageParts.join("\n");
853
+ throw new Error(message);
854
+ }
855
+
856
+ // src/prune-patches.ts
857
+ function planPrunePatches(existingProps, pruneKeys, preservedKeys) {
858
+ if (!pruneKeys || pruneKeys.length === 0) return [];
859
+ if (!isPlainObject(existingProps)) return [];
860
+ const patches = [];
861
+ const uniquePruneKeys = new Set(pruneKeys);
862
+ for (const key of uniquePruneKeys) {
863
+ if (preservedKeys.has(key)) continue;
864
+ if (Object.hasOwn(existingProps, key)) {
865
+ patches.push({ operation: "delete", path: [key] });
866
+ }
867
+ }
868
+ return patches;
869
+ }
870
+ function formatPath(base, segment) {
871
+ return typeof segment === "number" ? `${base}[${segment}]` : `${base}.${segment}`;
872
+ }
873
+ function integrateArrayElement(target, value) {
874
+ if (value === SKIP_VALUE) {
875
+ return SKIP_VALUE;
876
+ }
877
+ target.push(value);
878
+ }
879
+ function integrateObjectEntry(target, key, value) {
880
+ if (value !== SKIP_VALUE) {
881
+ target[key] = value;
882
+ }
883
+ }
884
+ function extractStaticValueFromArrayExpression(expressionNode, options, pathLabel, pathSegments) {
885
+ const candidate = [];
886
+ for (const [index, elementNode] of expressionNode.elements.entries()) {
887
+ if (elementNode === null) {
888
+ preserveArrayElision(candidate, index);
889
+ continue;
890
+ }
891
+ if (is.spreadElement(elementNode)) {
892
+ return SKIP_VALUE;
893
+ }
894
+ const elementLabel = formatPath(pathLabel, index);
895
+ const elementPath = [...pathSegments, index];
896
+ const extracted = extractStaticValueFromExpression(
897
+ elementNode,
898
+ options,
899
+ elementLabel,
900
+ elementPath
901
+ );
902
+ const integrationStatus = integrateArrayElement(candidate, extracted);
903
+ if (integrationStatus === SKIP_VALUE) {
904
+ return SKIP_VALUE;
905
+ }
906
+ }
907
+ return candidate;
908
+ }
909
+ function extractStaticValueFromObjectExpression(expressionNode, options, pathLabel, pathSegments) {
910
+ const aggregate = {};
911
+ for (const propertyNode of expressionNode.properties) {
912
+ if (is.spreadElement(propertyNode)) {
913
+ continue;
914
+ }
915
+ const key = extractPropertyKey(propertyNode);
916
+ if (key === null) {
917
+ continue;
918
+ }
919
+ const valueLabel = formatPath(pathLabel, key);
920
+ const valuePath = [...pathSegments, key];
921
+ if (isKeyPreserved(key, options.preservedKeys)) {
922
+ if (is.expression(propertyNode.value)) {
923
+ options.onPreservedExpression?.({
924
+ path: valuePath,
925
+ expression: propertyNode.value
926
+ });
927
+ }
928
+ aggregate[key] = createExpressionRef(valuePath);
929
+ continue;
930
+ }
931
+ const extracted = extractStaticValueFromExpression(
932
+ propertyNode.value,
933
+ options,
934
+ valueLabel,
935
+ valuePath
936
+ );
937
+ integrateObjectEntry(aggregate, key, extracted);
938
+ }
939
+ return aggregate;
940
+ }
941
+ function extractStaticValueFromExpression(expressionNode, options, pathLabel, pathSegments = []) {
942
+ const staticResolution = tryResolveStaticValue(expressionNode);
943
+ if (staticResolution.success) {
944
+ return staticResolution.value;
945
+ }
946
+ if (is.arrayExpression(expressionNode)) {
947
+ return extractStaticValueFromArrayExpression(
948
+ expressionNode,
949
+ options,
950
+ pathLabel,
951
+ pathSegments
952
+ );
953
+ }
954
+ if (is.objectExpression(expressionNode)) {
955
+ return extractStaticValueFromObjectExpression(
956
+ expressionNode,
957
+ options,
958
+ pathLabel,
959
+ pathSegments
960
+ );
961
+ }
962
+ return SKIP_VALUE;
963
+ }
964
+ function extractStaticProps(propsNode, options, pathLabel) {
965
+ const extracted = extractStaticValueFromExpression(
966
+ propsNode,
967
+ options,
968
+ pathLabel
969
+ );
970
+ if (extracted === SKIP_VALUE) return null;
971
+ if (!isPlainObject(extracted)) return null;
972
+ return extracted;
973
+ }
974
+
975
+ // src/patch-guards.ts
976
+ function findRestrictedKey(patch, restrictedKeys, scope) {
977
+ {
978
+ const [first] = patch.path;
979
+ return typeof first === "string" && restrictedKeys.has(first) ? first : null;
980
+ }
981
+ }
982
+ function capitalize(str) {
983
+ return str.charAt(0).toUpperCase() + str.slice(1);
984
+ }
985
+ function assertPatchesRespectPreservation(patches, restrictedKeys, componentName, options) {
986
+ for (const patch of patches) {
987
+ const violatedKey = findRestrictedKey(patch, restrictedKeys);
988
+ if (!violatedKey) continue;
989
+ const pathDetail = "";
990
+ const capitalizedLabel = capitalize(options.keyTypeLabel);
991
+ throw new Error(
992
+ `[recma] Patch targets ${options.keyTypeLabel} key "${violatedKey}"${pathDetail} for ${componentName}. ${capitalizedLabel} keys must not be patched as they represent runtime-owned subtrees.`
993
+ );
994
+ }
995
+ }
996
+
997
+ // src/derive-patches.ts
998
+ function collectDerivePatches(deriveFn, derivationInput) {
999
+ if (!deriveFn) return [];
1000
+ const patches = [];
1001
+ deriveFn(derivationInput, (values) => {
1002
+ for (const [key, value] of Object.entries(values)) {
1003
+ patches.push({ operation: "set", path: [key], value });
1004
+ }
1005
+ });
1006
+ return patches;
1007
+ }
1008
+
1009
+ // src/visitor.ts
1010
+ function processComponent(match, options, preservedKeys) {
1011
+ const { componentName, componentRule, propsExpressionPath } = match;
1012
+ const applyTransforms = options.applyTransforms ?? true;
1013
+ const propsNode = propsExpressionPath.node;
1014
+ if (!propsNode) return;
1015
+ const preservedExpressionsByPath = /* @__PURE__ */ new Map();
1016
+ const extractedProps = extractStaticProps(
1017
+ propsNode,
1018
+ {
1019
+ preservedKeys,
1020
+ onPreservedExpression: (info) => {
1021
+ preservedExpressionsByPath.set(
1022
+ stringifyPropertyPath(info.path),
1023
+ info.expression
1024
+ );
1025
+ }
1026
+ },
1027
+ `${componentName}.props`
1028
+ );
1029
+ const derivationInput = validateWithSchema(
1030
+ componentRule.schema,
1031
+ extractedProps,
1032
+ componentName
1033
+ );
1034
+ if (!applyTransforms) return;
1035
+ const diffPatches = calculatePatches(
1036
+ extractedProps,
1037
+ derivationInput,
1038
+ preservedKeys
1039
+ );
1040
+ const derivedPatches = collectDerivePatches(
1041
+ componentRule.derive,
1042
+ derivationInput
1043
+ );
1044
+ const prunePatches = planPrunePatches(
1045
+ extractedProps,
1046
+ componentRule.pruneKeys,
1047
+ preservedKeys
1048
+ );
1049
+ const { patches, phaseByPathKey } = consolidatePatches([
1050
+ { phase: "diff", patches: diffPatches },
1051
+ { phase: "derive", patches: derivedPatches },
1052
+ { phase: "prune", patches: prunePatches }
1053
+ ]);
1054
+ assertPatchesRespectPreservation(patches, preservedKeys, componentName, {
1055
+ keyTypeLabel: "preserved"
1056
+ });
1057
+ if (patches.length === 0) return;
1058
+ const applyResult = applyPatchesToEstree(
1059
+ propsExpressionPath,
1060
+ patches,
1061
+ preservedKeys,
1062
+ {
1063
+ expressionRefResolver: (ref) => preservedExpressionsByPath.get(stringifyPropertyPath(ref.path)) ?? null
1064
+ }
1065
+ );
1066
+ if (applyResult.firstUnappliedPathKey) {
1067
+ reportPatchFailure(applyResult.firstUnappliedPathKey, {
1068
+ componentName,
1069
+ getPatchPhaseByPathKey: (pathKey) => phaseByPathKey.get(pathKey),
1070
+ summaryContext: {
1071
+ remainingSetPathKeys: applyResult.remainingSetPathKeys,
1072
+ remainingDeletePathKeys: applyResult.remainingDeletePathKeys
1073
+ }
1074
+ });
1075
+ }
1076
+ }
1077
+ function createScopedVisitor(resolveComponentMatch, options) {
1078
+ const preservedKeys = new Set(options.preservedKeys ?? ["children"]);
1079
+ return {
1080
+ CallExpression(path) {
1081
+ const match = resolveComponentMatch(path);
1082
+ if (!match) return;
1083
+ processComponent(match, options, preservedKeys);
1084
+ }
1085
+ };
1086
+ }
1087
+
1088
+ // src/types/registry.ts
1089
+ function defineRuleRegistry(registry) {
1090
+ return registry;
1091
+ }
1092
+ function defineRule() {
1093
+ return (rule) => rule;
1094
+ }
1095
+
1096
+ // src/index.ts
1097
+ var recmaStaticRefiner = (options) => {
1098
+ const resolveComponentMatch = createComponentResolver(options.rules);
1099
+ const visitor = createScopedVisitor(resolveComponentMatch, options);
1100
+ const transformer = (tree) => {
1101
+ if (!is.program(tree)) return;
1102
+ traverse(tree, visitor);
1103
+ };
1104
+ return transformer;
1105
+ };
1106
+
1107
+ export { defineRule, defineRuleRegistry, recmaStaticRefiner };
1108
+ //# sourceMappingURL=index.js.map
1109
+ //# sourceMappingURL=index.js.map