@plumeria/codemod 18.3.1 → 18.3.2

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/README.md CHANGED
@@ -64,6 +64,11 @@ A descendant rule has no combinator to translate into, so it becomes
64
64
  is read from the stylesheet; no markup is inspected. Class names become
65
65
  camel-case keys, and a bare pixel length loses its unit.
66
66
 
67
+ A `@keyframes` the stylesheet declares becomes a `css.keyframes` binding above
68
+ the `css.create`, and the rules naming it read the binding. An `animation`
69
+ shorthand naming one is split into its longhands, since only `animationName`
70
+ resolves a binding.
71
+
67
72
  ### What it reports instead of converting
68
73
 
69
74
  ```
@@ -214,13 +219,22 @@ reported as `dynamic-style-access` and its definitions stay in Plumeria.
214
219
 
215
220
  ### Global styles
216
221
 
217
- `css.createTheme`, `css.keyframes`, and `css.viewTransition` produce rules that
218
- belong in a global stylesheet, not in a CSS Module. The codemod extracts them and
219
- appends them to `src/styles/global.css` (or `styles/global.css` when `src` does
220
- not exist). Theme tokens become CSS custom properties under `:where(:root)` and
221
- the theme selector; keyframes become `@keyframes kf-<hash>`; view transitions
222
- become `::view-transition-*` pseudo-element rules. References in style objects
223
- are replaced by their generated names.
222
+ `css.createTheme` and `css.viewTransition` produce rules that belong in a global
223
+ stylesheet, not in a CSS Module. The codemod extracts them and appends them to
224
+ the stylesheet the project already loads — `app/global.css` or
225
+ `styles/global.css`, under `src` or not, spelled `global` or `globals` — and
226
+ writes `src/styles/global.css` (or `styles/global.css` when `src` does not
227
+ exist) when it finds none. Theme tokens become CSS custom properties under
228
+ `:where(:root)` and the theme selector; view transitions become
229
+ `::view-transition-*` pseudo-element rules. References in style objects are
230
+ replaced by their generated names. A `keyframes` or `viewTransition` nothing the
231
+ migration exports reaches is left where it is.
232
+
233
+ A CSS Module renames the value of `animation-name` the way it renames a class,
234
+ so a `css.keyframes` is written as `@keyframes kf-<hash>` at the top of each
235
+ module that reads it. In the global stylesheet the renamed reference would match
236
+ nothing. One a view transition names is written globally as well, since the
237
+ `::view-transition-*` rule is not scoped.
224
238
 
225
239
  `css.createStatic` values are statically evaluated and inlined directly into the
226
240
  generated CSS rules — no separate output file is needed.
package/dist/release.js CHANGED
@@ -117,17 +117,24 @@ const walk = (node, visit) => {
117
117
  };
118
118
  const releasedPath = (source) => source.replace(/(?:\.styles)?\.[^.]+$/, '.module.css');
119
119
  exports.releasedPath = releasedPath;
120
+ const GLOBAL_DIRECTORIES = [
121
+ ['src', 'app'],
122
+ ['app'],
123
+ ['src', 'styles'],
124
+ ['styles'],
125
+ ];
126
+ const GLOBAL_NAMES = ['global.css', 'globals.css'];
120
127
  const globalPath = (targets) => {
121
128
  const first = path.resolve(targets[0]);
122
129
  const root = fs.existsSync(first) && fs.statSync(first).isDirectory()
123
130
  ? first
124
131
  : path.dirname(first);
125
- const candidates = [
126
- path.join(root, 'src', 'styles', 'global.css'),
127
- path.join(root, 'styles', 'global.css'),
128
- ];
129
- return (candidates.find((candidate) => fs.existsSync(candidate)) ??
130
- (fs.existsSync(path.join(root, 'src')) ? candidates[0] : candidates[1]));
132
+ const existing = GLOBAL_DIRECTORIES.flatMap((directory) => GLOBAL_NAMES.map((name) => path.join(root, ...directory, name))).find((candidate) => fs.existsSync(candidate));
133
+ if (existing)
134
+ return existing;
135
+ return fs.existsSync(path.join(root, 'src'))
136
+ ? path.join(root, 'src', 'styles', 'global.css')
137
+ : path.join(root, 'styles', 'global.css');
131
138
  };
132
139
  function planRelease(targets) {
133
140
  const stylesheets = [];
@@ -141,9 +148,10 @@ function planRelease(targets) {
141
148
  const animationValues = new Map();
142
149
  const staticValues = new Map();
143
150
  const reportsBySource = new Map();
144
- const globalBySource = new Map();
145
151
  const themeKeys = new Map();
152
+ const themeCss = new Map();
146
153
  const animationKeys = new Map();
154
+ const animationCss = new Map();
147
155
  const staticKeys = new Map();
148
156
  const dependencies = new Map();
149
157
  const note = (source, reports) => {
@@ -154,15 +162,6 @@ function planRelease(targets) {
154
162
  ...reports,
155
163
  ]);
156
164
  };
157
- const addGlobal = (source, css) => {
158
- const trimmed = css.trim();
159
- if (trimmed.length === 0)
160
- return;
161
- globalBySource.set(source, [
162
- ...(globalBySource.get(source) ?? []),
163
- trimmed,
164
- ]);
165
- };
166
165
  for (const source of sources) {
167
166
  try {
168
167
  const extracted = (0, from_plumeria_1.extractPlumeriaStatics)(sourceText.get(source));
@@ -248,7 +247,7 @@ function planRelease(targets) {
248
247
  if (Object.keys(extracted.bindings).length === 0)
249
248
  continue;
250
249
  themeKeys.set(source, Object.keys(extracted.bindings));
251
- addGlobal(source, extracted.globalCss);
250
+ themeCss.set(source, extracted.cssByBinding);
252
251
  }
253
252
  catch {
254
253
  }
@@ -279,7 +278,7 @@ function planRelease(targets) {
279
278
  ...extracted.keyframes,
280
279
  ...extracted.viewTransitions,
281
280
  ]);
282
- addGlobal(source, extracted.globalCss);
281
+ animationCss.set(source, extracted.cssByBinding);
283
282
  }
284
283
  catch {
285
284
  }
@@ -564,16 +563,174 @@ function planRelease(targets) {
564
563
  }
565
564
  }
566
565
  }
566
+ const definedKey = (source, binding) => `${source}\u0000${binding}`;
567
+ const definedIn = (source) => [
568
+ ...(themeKeys.get(source) ?? []),
569
+ ...(animationKeys.get(source) ?? []),
570
+ ];
571
+ const outerReferences = new Map();
572
+ const innerReferences = new Map();
573
+ for (const source of sources) {
574
+ if (blocked.has(source))
575
+ continue;
576
+ const owned = definedIn(source);
577
+ let ast;
578
+ try {
579
+ ast = (0, parser_1.parse)(sourceText.get(source), {
580
+ sourceType: 'module',
581
+ ecmaFeatures: { jsx: true },
582
+ });
583
+ }
584
+ catch {
585
+ continue;
586
+ }
587
+ const local = new Map();
588
+ const namespaces = new Map();
589
+ for (const binding of owned)
590
+ local.set(binding, definedKey(source, binding));
591
+ for (const node of ast.body) {
592
+ if (node.type !== 'ImportDeclaration' ||
593
+ typeof node.source.value !== 'string')
594
+ continue;
595
+ const target = (0, resolve_1.resolveSourcePath)(node.source.value, source);
596
+ const available = target ? definedIn(target) : [];
597
+ if (!target || available.length === 0)
598
+ continue;
599
+ for (const specifier of node.specifiers) {
600
+ if (specifier.type === 'ImportNamespaceSpecifier') {
601
+ namespaces.set(specifier.local.name, target);
602
+ continue;
603
+ }
604
+ if (specifier.type !== 'ImportSpecifier')
605
+ continue;
606
+ const imported = specifier.imported.name ?? specifier.imported.value;
607
+ if (available.includes(imported))
608
+ local.set(specifier.local.name, definedKey(target, imported));
609
+ }
610
+ }
611
+ if (local.size === 0 && namespaces.size === 0)
612
+ continue;
613
+ const named = new Set();
614
+ walk(ast, (node) => {
615
+ if (node.type === 'MemberExpression' && !node.computed)
616
+ named.add(node.property);
617
+ else if (node.type === 'Property' && !node.computed)
618
+ named.add(node.key);
619
+ else if (node.type === 'ImportSpecifier') {
620
+ named.add(node.local);
621
+ named.add(node.imported);
622
+ }
623
+ else if (node.type === 'ImportDefaultSpecifier' ||
624
+ node.type === 'ImportNamespaceSpecifier')
625
+ named.add(node.local);
626
+ });
627
+ const definitions = new Map();
628
+ for (const raw of ast.body) {
629
+ const statement = raw.type === 'ExportNamedDeclaration' ? raw.declaration : raw;
630
+ if (statement?.type !== 'VariableDeclaration')
631
+ continue;
632
+ for (const declaration of statement.declarations) {
633
+ if (declaration.id.type !== 'Identifier' ||
634
+ !owned.includes(declaration.id.name))
635
+ continue;
636
+ named.add(declaration.id);
637
+ definitions.set(declaration.id.name, declaration.init);
638
+ }
639
+ }
640
+ const inside = new Set();
641
+ for (const init of definitions.values())
642
+ walk(init, (node) => inside.add(node));
643
+ const readsOf = (root, skip) => {
644
+ const found = new Set();
645
+ walk(root, (node) => {
646
+ if (skip(node))
647
+ return;
648
+ if (node.type === 'Identifier' && !named.has(node)) {
649
+ const key = local.get(node.name);
650
+ if (key)
651
+ found.add(key);
652
+ return;
653
+ }
654
+ if (node.type !== 'MemberExpression' ||
655
+ node.computed ||
656
+ node.object.type !== 'Identifier' ||
657
+ node.property.type !== 'Identifier')
658
+ return;
659
+ const target = namespaces.get(node.object.name);
660
+ const available = target ? definedIn(target) : [];
661
+ if (target && available.includes(node.property.name))
662
+ found.add(definedKey(target, node.property.name));
663
+ });
664
+ return [...found];
665
+ };
666
+ outerReferences.set(source, readsOf(ast, (node) => inside.has(node)));
667
+ innerReferences.set(source, new Map([...definitions].map(([binding, init]) => [
668
+ binding,
669
+ readsOf(init, () => false),
670
+ ])));
671
+ }
672
+ const reachable = new Set();
673
+ const pending = [];
674
+ const reach = (key) => {
675
+ if (reachable.has(key))
676
+ return;
677
+ reachable.add(key);
678
+ pending.push(key);
679
+ };
680
+ for (const keys of outerReferences.values())
681
+ for (const key of keys)
682
+ reach(key);
683
+ while (pending.length > 0) {
684
+ const [target, binding] = pending.pop().split('\u0000');
685
+ for (const key of innerReferences.get(target)?.get(binding) ?? [])
686
+ reach(key);
687
+ }
688
+ const releasedFrom = (source, binding) => reachable.has(definedKey(source, binding));
689
+ const keyframesByName = new Map();
690
+ for (const [source, bindings] of animationValues) {
691
+ for (const [binding, name] of Object.entries(bindings)) {
692
+ if (!name.startsWith('kf-') || !releasedFrom(source, binding))
693
+ continue;
694
+ const rule = animationCss.get(source)?.[binding]?.trim();
695
+ if (rule)
696
+ keyframesByName.set(name, rule);
697
+ }
698
+ }
699
+ const globalReads = new Set();
700
+ for (const [source, bindings] of animationValues) {
701
+ for (const [binding, name] of Object.entries(bindings)) {
702
+ if (!name.startsWith('vt-') || !releasedFrom(source, binding))
703
+ continue;
704
+ for (const read of animationCss
705
+ .get(source)?.[binding]?.match(/\bkf-[a-z0-9]+\b/g) ?? [])
706
+ globalReads.add(read);
707
+ }
708
+ }
709
+ const carried = new Map();
710
+ const placed = new Set();
711
+ for (const [source, module] of converted) {
712
+ if (blocked.has(source) || !module)
713
+ continue;
714
+ const read = [...new Set(module.css.match(/\bkf-[a-z0-9]+\b/g) ?? [])]
715
+ .filter((name) => keyframesByName.has(name))
716
+ .map((name) => {
717
+ placed.add(name);
718
+ return keyframesByName.get(name);
719
+ });
720
+ if (read.length > 0)
721
+ carried.set(source, read);
722
+ }
567
723
  const globalRules = [];
568
724
  const values = {};
569
725
  for (const source of sources) {
570
726
  const reports = reportsBySource.get(source) ?? [];
571
727
  const module = converted.get(source);
572
728
  if (module) {
729
+ const held = carried.get(source);
573
730
  stylesheets.push({
574
731
  source,
575
732
  target: (0, exports.releasedPath)(source),
576
- css: module.css,
733
+ css: held ? `${held.join('\n\n')}\n\n${module.css}` : module.css,
577
734
  reports,
578
735
  });
579
736
  }
@@ -584,16 +741,32 @@ function planRelease(targets) {
584
741
  continue;
585
742
  const resolved = {
586
743
  ...(staticValues.get(source) ?? {}),
587
- ...(themeValues.get(source) ?? {}),
588
- ...(animationValues.get(source) ?? {}),
744
+ ...Object.fromEntries(Object.entries(themeValues.get(source) ?? {}).filter(([binding]) => releasedFrom(source, binding))),
745
+ ...Object.fromEntries(Object.entries(animationValues.get(source) ?? {}).filter(([binding]) => releasedFrom(source, binding))),
589
746
  };
590
747
  if (Object.keys(resolved).length > 0)
591
748
  values[source] = resolved;
592
- globalRules.push(...(globalBySource.get(source) ?? []));
593
- if (themeKeys.has(source))
594
- themes[source] = themeKeys.get(source);
595
- if (animationKeys.has(source))
596
- animations[source] = animationKeys.get(source);
749
+ const releasedThemes = (themeKeys.get(source) ?? []).filter((binding) => releasedFrom(source, binding));
750
+ if (releasedThemes.length > 0) {
751
+ themes[source] = releasedThemes;
752
+ for (const binding of releasedThemes) {
753
+ const rule = themeCss.get(source)?.[binding]?.trim();
754
+ if (rule)
755
+ globalRules.push(rule);
756
+ }
757
+ }
758
+ const releasedAnimations = (animationKeys.get(source) ?? []).filter((binding) => releasedFrom(source, binding));
759
+ if (releasedAnimations.length > 0) {
760
+ animations[source] = releasedAnimations;
761
+ for (const binding of releasedAnimations) {
762
+ const name = animationValues.get(source)?.[binding];
763
+ if (name && placed.has(name) && !globalReads.has(name))
764
+ continue;
765
+ const rule = animationCss.get(source)?.[binding]?.trim();
766
+ if (rule)
767
+ globalRules.push(rule);
768
+ }
769
+ }
597
770
  if (staticKeys.has(source))
598
771
  statics[source] = staticKeys.get(source);
599
772
  if (!module)
@@ -25,6 +25,43 @@ const toKey = (className) => camel(className);
25
25
  exports.toKey = toKey;
26
26
  const PX = /^-?(\d+\.?\d*|\.\d+)px$/;
27
27
  const NUMBER = /^-?(\d+\.?\d*|\.\d+)$/;
28
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
29
+ const RESERVED_BINDINGS = new Set(['css', 'styles']);
30
+ const ANIMATION_PARTS = [
31
+ ['animationTimingFunction', /^(cubic-bezier|steps|linear)\(/i],
32
+ [
33
+ 'animationTimingFunction',
34
+ /^(linear|ease|ease-in|ease-out|ease-in-out|step-start|step-end)$/i,
35
+ ],
36
+ ['animationIterationCount', /^(infinite|\d+\.?\d*)$/i],
37
+ ['animationDirection', /^(normal|reverse|alternate|alternate-reverse)$/i],
38
+ ['animationFillMode', /^(none|forwards|backwards|both)$/i],
39
+ ['animationPlayState', /^(running|paused)$/i],
40
+ ];
41
+ const TIME = /^-?(\d+\.?\d*|\.\d+)(s|ms)$/i;
42
+ const words = (value) => {
43
+ const found = [];
44
+ let depth = 0;
45
+ let current = '';
46
+ for (const character of value) {
47
+ if (character === '(')
48
+ depth += 1;
49
+ if (character === ')')
50
+ depth -= 1;
51
+ if (character === ',' && depth === 0)
52
+ return null;
53
+ if (/\s/.test(character) && depth === 0) {
54
+ if (current)
55
+ found.push(current);
56
+ current = '';
57
+ continue;
58
+ }
59
+ current += character;
60
+ }
61
+ if (current)
62
+ found.push(current);
63
+ return found;
64
+ };
28
65
  const toValue = (raw) => {
29
66
  const value = raw.trim();
30
67
  if (PX.test(value))
@@ -193,7 +230,11 @@ const serialise = (node, indent) => {
193
230
  lines.push(`${indent}${key}: ${written},`);
194
231
  }
195
232
  for (const [key, child] of node.nested) {
196
- const written = key.startsWith('css.extended(') ? `[${key}]` : `'${key}'`;
233
+ const written = key.startsWith('css.extended(')
234
+ ? `[${key}]`
235
+ : IDENTIFIER.test(key)
236
+ ? key
237
+ : `'${key}'`;
197
238
  lines.push(`${indent}${written}: {`);
198
239
  lines.push(serialise(child, indent + ' '));
199
240
  lines.push(`${indent}},`);
@@ -233,6 +274,94 @@ function convertStylesheet(css) {
233
274
  hint,
234
275
  });
235
276
  };
277
+ const animations = new Map();
278
+ const animationNodes = new Map();
279
+ const reachedAnimations = new Set();
280
+ root.walkAtRules(/^(-[a-z]+-)?keyframes$/i, (atRule) => {
281
+ const name = atRule.params.trim();
282
+ const binding = (0, exports.toKey)(name);
283
+ const taken = RESERVED_BINDINGS.has(binding) ||
284
+ [...animations.values()].includes(binding);
285
+ if (!IDENTIFIER.test(binding) || taken) {
286
+ reports.push({
287
+ line: atRule.source?.start?.line ?? 0,
288
+ column: atRule.source?.start?.column ?? 0,
289
+ kind: 'unsupported-keyframes',
290
+ source: `@${atRule.name} ${atRule.params}`,
291
+ hint: `\`${name}\` cannot be written as a \`css.keyframes\` binding. Rename it, then run again.`,
292
+ });
293
+ atRule.remove();
294
+ return;
295
+ }
296
+ const node = emptyNode();
297
+ atRule.each((child) => {
298
+ if (child.type !== 'rule')
299
+ return;
300
+ const step = nest(node, child.selectors.join(', '));
301
+ child.each((declaration) => {
302
+ if (declaration.type !== 'decl')
303
+ return;
304
+ step.decls.push([
305
+ (0, exports.toProperty)(declaration.prop),
306
+ (0, exports.toValue)(declaration.value),
307
+ ]);
308
+ });
309
+ });
310
+ animations.set(name, binding);
311
+ animationNodes.set(binding, node);
312
+ atRule.remove();
313
+ });
314
+ const animationDeclarations = (declaration) => {
315
+ if (animations.size === 0)
316
+ return null;
317
+ const value = declaration.value.trim();
318
+ if (declaration.prop === 'animation-name') {
319
+ const binding = animations.get(value);
320
+ if (!binding)
321
+ return null;
322
+ reachedAnimations.add(binding);
323
+ return [['animation-name', { raw: binding }]];
324
+ }
325
+ if (declaration.prop !== 'animation')
326
+ return null;
327
+ const tokens = words(value);
328
+ const named = tokens?.filter((token) => animations.has(token)) ?? [];
329
+ if (!tokens || named.length === 0) {
330
+ const reads = (words(value.replace(/,/g, ' ')) ?? []).some((token) => animations.has(token));
331
+ if (reads)
332
+ report(declaration, 'unsupported-animation', 'Only a single `animation` naming one local `@keyframes` is split into longhands. Write the longhands instead.');
333
+ return null;
334
+ }
335
+ const written = new Map();
336
+ for (const token of tokens) {
337
+ if (animations.has(token)) {
338
+ if (written.has('animation-name'))
339
+ return null;
340
+ written.set('animation-name', { raw: animations.get(token) });
341
+ continue;
342
+ }
343
+ if (TIME.test(token)) {
344
+ const property = written.has('animation-duration')
345
+ ? 'animation-delay'
346
+ : 'animation-duration';
347
+ if (written.has(property))
348
+ return null;
349
+ written.set(property, token);
350
+ continue;
351
+ }
352
+ const part = ANIMATION_PARTS.find(([, pattern]) => pattern.test(token));
353
+ const property = part
354
+ ? part[0].replace(/([A-Z])/g, '-$1').toLowerCase()
355
+ : undefined;
356
+ if (!property || written.has(property)) {
357
+ report(declaration, 'unsupported-animation', `\`${token}\` was not recognised as an \`animation\` component, so the shorthand was left as it was written.`);
358
+ return null;
359
+ }
360
+ written.set(property, token);
361
+ }
362
+ reachedAnimations.add(written.get('animation-name').raw);
363
+ return [...written];
364
+ };
236
365
  root.walkRules((rule) => {
237
366
  for (const selector of rule.selectors) {
238
367
  const read = readSelector(selector);
@@ -302,18 +431,23 @@ function convertStylesheet(css) {
302
431
  composes[target.key] = (composes[target.key] ?? []).concat(name.split(/\s+/).map(exports.toKey));
303
432
  return;
304
433
  }
305
- const property = (0, exports.toProperty)(child.prop);
306
- props.add(`${where}::${property}`);
307
- mine.push({
308
- property: child.prop,
309
- suffix: target.pseudo,
310
- rank: (0, cascade_1.rankOf)(child.prop, conditional),
311
- place: wrote++,
312
- });
313
- const held = node.decls.findIndex(([name]) => name === property);
314
- if (held !== -1)
315
- node.decls.splice(held, 1);
316
- node.decls.push([property, (0, exports.toValue)(child.value)]);
434
+ const expanded = animationDeclarations(child) ?? [
435
+ [child.prop, (0, exports.toValue)(child.value)],
436
+ ];
437
+ for (const [name, value] of expanded) {
438
+ const property = (0, exports.toProperty)(name);
439
+ props.add(`${where}::${property}`);
440
+ mine.push({
441
+ property: name,
442
+ suffix: target.pseudo,
443
+ rank: (0, cascade_1.rankOf)(name, conditional),
444
+ place: wrote++,
445
+ });
446
+ const held = node.decls.findIndex(([held]) => held === property);
447
+ if (held !== -1)
448
+ node.decls.splice(held, 1);
449
+ node.decls.push([property, value]);
450
+ }
317
451
  });
318
452
  }
319
453
  });
@@ -360,9 +494,24 @@ function convertStylesheet(css) {
360
494
  return ` ${key}: (${params}) => ({\n${serialise(node, ' ')}\n }),`;
361
495
  })
362
496
  .join('\n');
497
+ const bindings = [...animationNodes]
498
+ .filter(([binding]) => reachedAnimations.has(binding))
499
+ .map(([binding, node]) => `const ${binding} = css.keyframes({\n${serialise(node, ' ')}\n});\n\n`)
500
+ .join('');
501
+ for (const [name, binding] of animations) {
502
+ if (reachedAnimations.has(binding))
503
+ continue;
504
+ reports.push({
505
+ line: 0,
506
+ column: 0,
507
+ kind: 'unused-keyframes',
508
+ source: `@keyframes ${name}`,
509
+ hint: `Nothing in the stylesheet reads \`${name}\`, so it was not written.`,
510
+ });
511
+ }
363
512
  const code = `import * as css from '@plumeria/core';
364
513
 
365
- export const styles = css.create({
514
+ ${bindings}export const styles = css.create({
366
515
  ${body}
367
516
  });
368
517
  `;
@@ -52,6 +52,7 @@ export declare const compositionSignature: (parts: {
52
52
  export interface ThemeExtraction {
53
53
  bindings: Record<string, Record<string, string>>;
54
54
  globalCss: string;
55
+ cssByBinding: Record<string, string>;
55
56
  reports: PlumeriaReport[];
56
57
  }
57
58
  export interface StaticExtraction {
@@ -63,6 +64,7 @@ export interface AnimationExtraction {
63
64
  keyframes: string[];
64
65
  viewTransitions: string[];
65
66
  globalCss: string;
67
+ cssByBinding: Record<string, string>;
66
68
  reports: PlumeriaReport[];
67
69
  }
68
70
  export declare const __private: {
@@ -202,6 +202,7 @@ const readThemes = (ast) => {
202
202
  const values = new Map();
203
203
  const reports = [];
204
204
  const globalRules = [];
205
+ const cssByBinding = {};
205
206
  const report = (node, kind, hint) => {
206
207
  reports.push({
207
208
  line: node.loc.start.line,
@@ -251,7 +252,7 @@ const readThemes = (ast) => {
251
252
  continue;
252
253
  }
253
254
  values.set(declaration.id.name, references);
254
- globalRules.push(`:where(:root) {\n${rootDeclarations.join('\n')}\n}`);
255
+ const rules = [`:where(:root) {\n${rootDeclarations.join('\n')}\n}`];
255
256
  const target = selector.startsWith('@') ||
256
257
  selector.startsWith('.') ||
257
258
  selector.startsWith('#') ||
@@ -260,18 +261,21 @@ const readThemes = (ast) => {
260
261
  ? selector
261
262
  : `.${selector}`;
262
263
  if ((0, zss_engine_1.isAtRule)(target)) {
263
- globalRules.push(`${target} {\n :where(:root) {\n${themeDeclarations
264
+ rules.push(`${target} {\n :where(:root) {\n${themeDeclarations
264
265
  .map((line) => ` ${line}`)
265
266
  .join('\n')}\n }\n}`);
266
267
  }
267
268
  else {
268
- globalRules.push(`${target} {\n${themeDeclarations.join('\n')}\n}`);
269
+ rules.push(`${target} {\n${themeDeclarations.join('\n')}\n}`);
269
270
  }
271
+ cssByBinding[declaration.id.name] = rules.join('\n\n');
272
+ globalRules.push(...rules);
270
273
  }
271
274
  }
272
275
  return {
273
276
  bindings: Object.fromEntries(values),
274
277
  globalCss: globalRules.length > 0 ? `${globalRules.join('\n\n')}\n` : '',
278
+ cssByBinding,
275
279
  reports,
276
280
  };
277
281
  };
@@ -348,6 +352,7 @@ function extractPlumeriaAnimations(source, externalValues = {}) {
348
352
  const bindings = {};
349
353
  const reports = [];
350
354
  const rules = [];
355
+ const cssByBinding = {};
351
356
  const keyframes = [];
352
357
  const viewTransitions = [];
353
358
  const report = (node, kind, hint) => {
@@ -378,7 +383,9 @@ function extractPlumeriaAnimations(source, externalValues = {}) {
378
383
  values.set(declaration.id.name, name);
379
384
  bindings[declaration.id.name] = name;
380
385
  keyframes.push(declaration.id.name);
381
- rules.push(emitGlobalObject(`@keyframes ${name}`, object));
386
+ const rule = emitGlobalObject(`@keyframes ${name}`, object);
387
+ cssByBinding[declaration.id.name] = rule;
388
+ rules.push(rule);
382
389
  }
383
390
  for (const declaration of declarations) {
384
391
  if (declaration.id.type !== 'Identifier' ||
@@ -394,19 +401,24 @@ function extractPlumeriaAnimations(source, externalValues = {}) {
394
401
  values.set(declaration.id.name, name);
395
402
  bindings[declaration.id.name] = name;
396
403
  viewTransitions.push(declaration.id.name);
404
+ const parts = [];
397
405
  for (const part of ['group', 'imagePair', 'old', 'new']) {
398
406
  const style = object[part];
399
407
  if (style && typeof style === 'object') {
400
408
  const pseudoPart = part === 'imagePair' ? 'image-pair' : part;
401
- rules.push(emitGlobalObject(`::view-transition-${pseudoPart}(${name})`, style));
409
+ parts.push(emitGlobalObject(`::view-transition-${pseudoPart}(${name})`, style));
402
410
  }
403
411
  }
412
+ if (parts.length > 0)
413
+ cssByBinding[declaration.id.name] = parts.join('\n\n');
414
+ rules.push(...parts);
404
415
  }
405
416
  return {
406
417
  bindings,
407
418
  keyframes,
408
419
  viewTransitions,
409
420
  globalCss: rules.length > 0 ? `${rules.join('\n\n')}\n` : '',
421
+ cssByBinding,
410
422
  reports,
411
423
  };
412
424
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/codemod",
3
- "version": "18.3.1",
3
+ "version": "18.3.2",
4
4
  "description": "Codemods for Plumeria: migrate a CSS Modules codebase in, export it back out, and rename the styling prop",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",