@plumeria/codemod 18.2.19 → 18.2.20

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.
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.__private = void 0;
3
+ exports.__private = exports.compositionSignature = void 0;
4
4
  exports.extractPlumeriaThemes = extractPlumeriaThemes;
5
5
  exports.extractPlumeriaStatics = extractPlumeriaStatics;
6
6
  exports.extractPlumeriaAnimations = extractPlumeriaAnimations;
7
7
  exports.convertPlumeriaModule = convertPlumeriaModule;
8
8
  const parser_1 = require("@typescript-eslint/parser");
9
9
  const zss_engine_1 = require("zss-engine");
10
+ const compositionSignature = (classNames) => classNames.join('|');
11
+ exports.compositionSignature = compositionSignature;
10
12
  const toIdent = (value) => value.replace(/[^A-Za-z0-9-]/g, '');
13
+ const capitalize = (value) => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
14
+ const camelJoin = (names) => names
15
+ .map((name, index) => {
16
+ const camel = name
17
+ .split('-')
18
+ .map((part, position) => (position === 0 ? part : capitalize(part)))
19
+ .join('');
20
+ return index === 0 ? camel : capitalize(camel);
21
+ })
22
+ .join('');
11
23
  const markerVariable = (id, pseudo) => {
12
24
  const hash = (0, zss_engine_1.genBase36Hash)({ [id]: pseudo }, 1, 8);
13
25
  return `--${hash}-${toIdent(id)}-${toIdent(pseudo.split('(')[0])}`;
@@ -64,6 +76,14 @@ const evaluate = (node, values) => {
64
76
  }
65
77
  if (node.type === 'Literal')
66
78
  return node.value;
79
+ if (node.type === 'UnaryExpression') {
80
+ if (node.operator !== '-' && node.operator !== '+')
81
+ return undefined;
82
+ const operand = evaluate(node.argument, values);
83
+ if (typeof operand !== 'number')
84
+ return undefined;
85
+ return node.operator === '-' ? -operand : operand;
86
+ }
67
87
  if (node.type === 'Identifier')
68
88
  return values.get(node.name);
69
89
  if (node.type === 'MemberExpression') {
@@ -346,7 +366,7 @@ function extractPlumeriaAnimations(source, externalValues = {}) {
346
366
  reports,
347
367
  };
348
368
  }
349
- function convertPlumeriaModule(source, externalValues = {}) {
369
+ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
350
370
  const ast = (0, parser_1.parse)(source, {
351
371
  sourceType: 'module',
352
372
  ecmaFeatures: { jsx: true },
@@ -403,9 +423,37 @@ function convertPlumeriaModule(source, externalValues = {}) {
403
423
  if (creates.length === 0)
404
424
  return null;
405
425
  const { binding } = creates[0];
406
- const rules = [];
426
+ const groups = [];
427
+ let rules = [];
428
+ const openGroup = (name) => {
429
+ rules = [];
430
+ groups.push({ name, rules });
431
+ };
407
432
  const keys = [];
408
433
  const aliases = {};
434
+ const merges = {};
435
+ const members = new Map();
436
+ const footprints = new Map();
437
+ const track = (className) => {
438
+ const map = new Map();
439
+ footprints.set(className, map);
440
+ return { root: `.${className}`, map };
441
+ };
442
+ const conflicts = (first, second) => {
443
+ const left = footprints.get(first);
444
+ const right = footprints.get(second);
445
+ if (!left || !right)
446
+ return true;
447
+ for (const [bucket, properties] of left) {
448
+ const other = right.get(bucket);
449
+ if (!other)
450
+ continue;
451
+ for (const property of properties)
452
+ if (other.has(property))
453
+ return true;
454
+ }
455
+ return false;
456
+ };
409
457
  const taken = new Set();
410
458
  const functions = {};
411
459
  const classNameFor = (owner, key) => {
@@ -418,11 +466,17 @@ function convertPlumeriaModule(source, externalValues = {}) {
418
466
  candidate = `${namespaced}-${suffix++}`;
419
467
  return candidate;
420
468
  };
421
- const emit = (style, selector, wrappers = [], scope = values) => {
469
+ const bucketOf = (wrappers, suffix) => `${wrappers.join('\u0000')}\u0001${suffix}`;
470
+ const selectorRoot = (selector) => {
471
+ const match = /^\.[A-Za-z0-9_-]+(?::not\(#\\#\))?/.exec(selector);
472
+ return match ? match[0].length : 0;
473
+ };
474
+ const emit = (style, selector, wrappers = [], scope = values, phase = 'both', footprint, restrict) => {
422
475
  if (style?.type !== 'ObjectExpression') {
423
476
  report(style, 'dynamic-style', 'Only static style objects are exported.');
424
477
  return;
425
478
  }
479
+ const restrictRoot = restrict ? selectorRoot(selector) : 0;
426
480
  const declarations = [];
427
481
  const nested = [];
428
482
  const markers = [];
@@ -460,6 +514,17 @@ function convertPlumeriaModule(source, externalValues = {}) {
460
514
  continue;
461
515
  }
462
516
  const cssProperty = (0, zss_engine_1.camelToKebabCase)(rawKey);
517
+ if (restrict &&
518
+ !restrict
519
+ .get(bucketOf(wrappers, selector.slice(restrictRoot)))
520
+ ?.has(cssProperty))
521
+ continue;
522
+ if (footprint) {
523
+ const bucket = bucketOf(wrappers, selector.slice(footprint.root.length));
524
+ const held = footprint.map.get(bucket) ?? new Set();
525
+ held.add(cssProperty);
526
+ footprint.map.set(bucket, held);
527
+ }
463
528
  declarations.push(` ${cssProperty}: ${(0, zss_engine_1.applyCssValue)(value, cssProperty)};`);
464
529
  }
465
530
  const pushRule = (target, lines) => {
@@ -474,16 +539,19 @@ function convertPlumeriaModule(source, externalValues = {}) {
474
539
  }
475
540
  rules.push(rule);
476
541
  };
477
- pushRule(selector, declarations);
542
+ if (phase !== 'nested')
543
+ pushRule(selector, declarations);
544
+ if (phase === 'declarations')
545
+ return;
478
546
  for (const marker of markers) {
479
547
  pushRule(`${selector}${marker.pseudo}`, [` ${marker.variable}: 1;`]);
480
548
  }
481
549
  for (const child of nested) {
482
550
  if (child.key.startsWith('@')) {
483
- emit(child.value, selector, [...wrappers, child.key], scope);
551
+ emit(child.value, selector, [...wrappers, child.key], scope, 'both', footprint, restrict);
484
552
  }
485
553
  else {
486
- emit(child.value, `${selector}${child.key}`, wrappers, scope);
554
+ emit(child.value, `${selector}${child.key}`, wrappers, scope, 'both', footprint, restrict);
487
555
  }
488
556
  }
489
557
  };
@@ -506,6 +574,7 @@ function convertPlumeriaModule(source, externalValues = {}) {
506
574
  taken.add(className);
507
575
  if (owner !== binding)
508
576
  alias[key] = className;
577
+ members.set(`${owner}.${key}`, { className, value: property.value });
509
578
  if (property.value.type === 'ArrowFunctionExpression' ||
510
579
  property.value.type === 'FunctionExpression') {
511
580
  const params = [];
@@ -533,22 +602,219 @@ function convertPlumeriaModule(source, externalValues = {}) {
533
602
  });
534
603
  functions[className] = { params, variables };
535
604
  keys.push(className);
536
- emit(body, `.${className}`, [], scope);
605
+ openGroup(className);
606
+ emit(body, `.${className}`, [], scope, 'both', track(className));
537
607
  continue;
538
608
  }
539
609
  keys.push(className);
540
- emit(property.value, `.${className}`);
610
+ openGroup(className);
611
+ emit(property.value, `.${className}`, [], values, 'both', track(className));
541
612
  }
542
613
  if (owner !== binding)
543
614
  aliases[owner] = alias;
544
615
  }
616
+ const resolved = compositions.map((composition) => ({
617
+ composition,
618
+ parts: composition.parts.flatMap((part) => {
619
+ const member = members.get(`${part.binding}.${part.key}`);
620
+ return member && !functions[member.className] ? [member] : [];
621
+ }),
622
+ slots: composition.slots.map((slot) => slot.flatMap((part) => {
623
+ const member = members.get(`${part.binding}.${part.key}`);
624
+ return member ? [member.className] : [];
625
+ })),
626
+ }));
627
+ const collapsible = new Set(resolved.filter((entry) => entry.composition.parts.length > 1 &&
628
+ entry.parts.length === entry.composition.parts.length));
629
+ const position = new Map(groups.map((group, index) => [group.name, index]));
630
+ const inlined = new Set();
631
+ const sort = () => {
632
+ const edges = new Map();
633
+ const blame = new Map();
634
+ for (const entry of resolved) {
635
+ if (inlined.has(entry))
636
+ continue;
637
+ for (let first = 0; first < entry.slots.length; first++) {
638
+ for (let second = first + 1; second < entry.slots.length; second++) {
639
+ for (const before of entry.slots[first]) {
640
+ for (const after of entry.slots[second]) {
641
+ if (before === after || !conflicts(before, after))
642
+ continue;
643
+ edges.set(before, (edges.get(before) ?? new Set()).add(after));
644
+ blame.set(`${before}|${after}`, entry.composition);
645
+ }
646
+ }
647
+ }
648
+ }
649
+ }
650
+ const incoming = new Map(groups.map((group) => [group.name, 0]));
651
+ for (const [, targets] of edges)
652
+ for (const target of targets)
653
+ if (incoming.has(target))
654
+ incoming.set(target, incoming.get(target) + 1);
655
+ const ready = groups
656
+ .filter((group) => incoming.get(group.name) === 0)
657
+ .map((group) => group.name);
658
+ const ordered = [];
659
+ while (ready.length > 0) {
660
+ ready.sort((a, b) => position.get(a) - position.get(b));
661
+ const name = ready.shift();
662
+ ordered.push(name);
663
+ for (const target of edges.get(name) ?? []) {
664
+ if (!incoming.has(target))
665
+ continue;
666
+ const remaining = incoming.get(target) - 1;
667
+ incoming.set(target, remaining);
668
+ if (remaining === 0)
669
+ ready.push(target);
670
+ }
671
+ }
672
+ const stuck = new Set(groups
673
+ .map((group) => group.name)
674
+ .filter((name) => !ordered.includes(name)));
675
+ return { ordered, stuck, edges, blame };
676
+ };
677
+ let sorted = sort();
678
+ while (sorted.stuck.size > 0) {
679
+ const offender = [...collapsible].find((entry) => !inlined.has(entry) &&
680
+ entry.parts.filter((part) => sorted.stuck.has(part.className)).length >
681
+ 1);
682
+ if (!offender)
683
+ break;
684
+ inlined.add(offender);
685
+ sorted = sort();
686
+ }
687
+ const order = new Map([...sorted.ordered, ...sorted.stuck].map((name, index) => [name, index]));
688
+ const overrides = {};
689
+ const pending = [];
690
+ for (const entry of resolved) {
691
+ if (inlined.has(entry))
692
+ continue;
693
+ if (entry.slots.some((slot) => slot.length !== 1))
694
+ continue;
695
+ const names = entry.slots.map((slot) => slot[0]);
696
+ const signature = (0, exports.compositionSignature)(names);
697
+ if (overrides[signature])
698
+ continue;
699
+ const perSlot = {};
700
+ for (let slot = 1; slot < names.length; slot++) {
701
+ const losers = names
702
+ .slice(0, slot)
703
+ .filter((earlier) => earlier !== names[slot] &&
704
+ conflicts(earlier, names[slot]) &&
705
+ order.get(earlier) > order.get(names[slot]));
706
+ if (losers.length === 0)
707
+ continue;
708
+ pending.push({ signature, slot, winner: names[slot], losers });
709
+ perSlot[slot] = '';
710
+ }
711
+ if (Object.keys(perSlot).length > 0)
712
+ overrides[signature] = perSlot;
713
+ }
714
+ for (const request of pending) {
715
+ const winner = members.get([...members.keys()].find((key) => members.get(key)?.className === request.winner));
716
+ if (!winner)
717
+ continue;
718
+ const disputed = new Map();
719
+ const mine = footprints.get(request.winner);
720
+ for (const loser of request.losers) {
721
+ const theirs = footprints.get(loser);
722
+ if (!mine || !theirs)
723
+ continue;
724
+ for (const [bucket, properties] of mine) {
725
+ const other = theirs.get(bucket);
726
+ if (!other)
727
+ continue;
728
+ for (const property of properties) {
729
+ if (!other.has(property))
730
+ continue;
731
+ const held = disputed.get(bucket) ?? new Set();
732
+ held.add(property);
733
+ disputed.set(bucket, held);
734
+ }
735
+ }
736
+ }
737
+ if (disputed.size === 0)
738
+ continue;
739
+ const name = classNameFor(binding, camelJoin([request.winner, `over-${request.losers.join('-')}`]));
740
+ taken.add(name);
741
+ openGroup(name);
742
+ emit(winner.value, `.${name}:not(#\\#)`, [], values, 'both', undefined, disputed);
743
+ keys.push(name);
744
+ overrides[request.signature][request.slot] = name;
745
+ }
746
+ for (const entry of resolved) {
747
+ if (inlined.has(entry))
748
+ continue;
749
+ const signature = entry.slots.every((slot) => slot.length === 1) &&
750
+ (0, exports.compositionSignature)(entry.slots.map((slot) => slot[0]));
751
+ let unresolved;
752
+ for (let first = 0; first < entry.slots.length && !unresolved; first++) {
753
+ for (let second = first + 1; second < entry.slots.length; second++) {
754
+ if (signature && overrides[signature]?.[second])
755
+ continue;
756
+ for (const before of entry.slots[first]) {
757
+ for (const after of entry.slots[second]) {
758
+ if (before === after || !conflicts(before, after))
759
+ continue;
760
+ if (order.get(before) < order.get(after))
761
+ continue;
762
+ unresolved = [before, after];
763
+ }
764
+ }
765
+ }
766
+ }
767
+ if (!unresolved)
768
+ continue;
769
+ reports.push({
770
+ line: entry.composition.line,
771
+ column: entry.composition.column,
772
+ kind: 'composition-order',
773
+ hint: `\`${unresolved[1]}\` has to win over \`${unresolved[0]}\` here and lose elsewhere, and this call site cannot carry an override.`,
774
+ });
775
+ }
776
+ for (const entry of collapsible) {
777
+ const signature = (0, exports.compositionSignature)(entry.parts.map((part) => part.className));
778
+ if (merges[signature])
779
+ continue;
780
+ const merged = classNameFor(binding, camelJoin(entry.parts.map((part) => part.className)));
781
+ taken.add(merged);
782
+ openGroup(merged);
783
+ if (inlined.has(entry)) {
784
+ for (const part of entry.parts)
785
+ emit(part.value, `.${merged}`, [], values, 'declarations');
786
+ for (const part of entry.parts)
787
+ emit(part.value, `.${merged}`, [], values, 'nested');
788
+ }
789
+ else {
790
+ rules.push(`.${merged} {\n composes: ${entry.parts
791
+ .map((part) => part.className)
792
+ .join(' ')};\n}`);
793
+ }
794
+ keys.push(merged);
795
+ merges[signature] = merged;
796
+ }
797
+ const emitted = [
798
+ ...sorted.ordered,
799
+ ...[...sorted.stuck],
800
+ ...groups.slice(position.size).map((group) => group.name),
801
+ ];
802
+ const seen = new Set();
803
+ const css = emitted.flatMap((name) => {
804
+ if (seen.has(name))
805
+ return [];
806
+ seen.add(name);
807
+ return groups.find((group) => group.name === name)?.rules ?? [];
808
+ });
545
809
  return {
546
810
  binding,
547
- css: rules.length > 0 ? `${rules.join('\n\n')}\n` : '',
811
+ css: css.length > 0 ? `${css.join('\n\n')}\n` : '',
548
812
  globalCss: `${themes.globalCss}${animations.globalCss}`,
549
813
  keys,
550
814
  functions,
551
815
  aliases,
816
+ merges,
817
+ overrides,
552
818
  definitionOnly: creates.every((create) => !bindsElsewhere(ast, create.binding, create.declaration.id)),
553
819
  reports,
554
820
  };
@@ -9,12 +9,16 @@ export interface ReleaseModule {
9
9
  params: string[];
10
10
  variables: string[];
11
11
  }>;
12
+ merges?: Record<string, string>;
13
+ overrides?: Record<string, Record<number, string>>;
12
14
  }
13
15
  export interface ReleaseStylesOptions {
14
16
  modules: Record<string, ReleaseModule>;
15
17
  themes?: Record<string, string[]>;
16
18
  animations?: Record<string, string[]>;
17
19
  statics?: Record<string, string[]>;
20
+ values?: Record<string, Record<string, unknown>>;
21
+ complete?: boolean;
18
22
  styleProp?: string;
19
23
  active?: boolean;
20
24
  }