@plumeria/codemod 18.2.19 → 18.2.21

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.
@@ -56,6 +56,23 @@ const isCreate = (node) => {
56
56
  return path?.length === 2 && path[1] === 'create';
57
57
  };
58
58
  const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
59
+ const walk = (node, visit) => {
60
+ if (!node || typeof node !== 'object')
61
+ return;
62
+ if (Array.isArray(node)) {
63
+ for (const child of node)
64
+ walk(child, visit);
65
+ return;
66
+ }
67
+ if (typeof node.type !== 'string')
68
+ return;
69
+ visit(node);
70
+ for (const key of Object.keys(node)) {
71
+ if (key === 'parent' || key === 'loc' || key === 'range')
72
+ continue;
73
+ walk(node[key], visit);
74
+ }
75
+ };
59
76
  const needsFilter = (node) => node.type === 'LogicalExpression' ||
60
77
  node.type === 'ConditionalExpression' ||
61
78
  node.type === 'SpreadElement';
@@ -79,6 +96,10 @@ exports.releaseStyles = {
79
96
  core: 'Remove the Plumeria import after exporting the styles.',
80
97
  prop: 'Use className after exporting this style to CSS Modules.',
81
98
  composition: 'Join the composed CSS Module class names.',
99
+ use: 'Read the class name straight from the CSS Module.',
100
+ token: 'Inline the value this token resolved to.',
101
+ styleType: 'A released style is a class name.',
102
+ folded: 'This only named a style key, which the export has resolved.',
82
103
  functionStyle: 'Use the CSS Module class and pass function arguments as custom properties.',
83
104
  unsupportedFunctionStyle: 'This function style call cannot be converted without changing its behavior.',
84
105
  },
@@ -90,6 +111,9 @@ exports.releaseStyles = {
90
111
  themes: { type: 'object' },
91
112
  animations: { type: 'object' },
92
113
  statics: { type: 'object' },
114
+ values: { type: 'object' },
115
+ complete: { type: 'boolean' },
116
+ constants: { type: 'object' },
93
117
  styleProp: { type: 'string' },
94
118
  active: { type: 'boolean' },
95
119
  },
@@ -100,13 +124,90 @@ exports.releaseStyles = {
100
124
  create(context) {
101
125
  const raw = (context.options[0] ?? {});
102
126
  const declared = raw.modules?.[context.filename];
103
- const entry = declared?.definitionOnly ? undefined : declared;
104
- const themeBindings = raw.themes?.[context.filename] ?? [];
105
- const animationBindings = raw.animations?.[context.filename] ?? [];
127
+ const keptDefinition = declared?.definitionOnly === true;
128
+ const complete = raw.complete === true;
129
+ const entry = keptDefinition ? undefined : declared;
130
+ const themeBindings = keptDefinition
131
+ ? []
132
+ : (raw.themes?.[context.filename] ?? []);
133
+ const animationBindings = keptDefinition
134
+ ? []
135
+ : (raw.animations?.[context.filename] ?? []);
106
136
  const styleProp = raw.styleProp ?? 'classStyle';
107
137
  const sourceCode = context.sourceCode;
108
- let active = Boolean(entry || raw.active);
109
- let activeModule = entry;
138
+ const moduleByLocal = new Map();
139
+ const styleLocals = new Set();
140
+ let active = Boolean(entry || raw.active || raw.complete);
141
+ let coreImport;
142
+ let coreLocal;
143
+ let coreNeeded = true;
144
+ const declarationOf = (statement) => statement.type === 'ExportNamedDeclaration'
145
+ ? statement.declaration
146
+ : statement;
147
+ const removesDeclaration = (statement) => {
148
+ const declaration = declarationOf(statement);
149
+ if (declaration?.type !== 'VariableDeclaration' ||
150
+ declaration.declarations.length !== 1)
151
+ return false;
152
+ const only = declaration.declarations[0];
153
+ if (only.id.type !== 'Identifier')
154
+ return false;
155
+ const called = only.init?.type === 'CallExpression'
156
+ ? memberPath(only.init.callee)
157
+ : undefined;
158
+ if (called?.length !== 2)
159
+ return false;
160
+ if (called[1] === 'createStatic')
161
+ return Boolean(entry);
162
+ if (called[1] === 'createTheme')
163
+ return themeBindings.includes(only.id.name);
164
+ if (called[1] === 'keyframes' || called[1] === 'viewTransition')
165
+ return animationBindings.includes(only.id.name);
166
+ if (called[1] === 'create')
167
+ return Boolean(entry &&
168
+ (only.id.name === entry.binding || entry.aliases?.[only.id.name]));
169
+ return false;
170
+ };
171
+ const removesStatement = (statement) => {
172
+ if (keptDefinition)
173
+ return false;
174
+ if (statement.type !== 'ImportDeclaration')
175
+ return removesDeclaration(statement);
176
+ const specifier = String(statement.source.value);
177
+ if (specifier === '@plumeria/core')
178
+ return !coreNeeded;
179
+ const bindings = [
180
+ ...(importedAnimationBindings(specifier) ?? []),
181
+ ...(importedThemeBindings(specifier) ?? []),
182
+ ...(importedStaticBindings(specifier) ?? []),
183
+ ];
184
+ return (bindings.length > 0 &&
185
+ statement.specifiers.length > 0 &&
186
+ statement.specifiers.every((candidate) => candidate.type === 'ImportSpecifier' &&
187
+ bindings.includes(candidate.imported.name ?? candidate.imported.value)));
188
+ };
189
+ const removeStatement = (fixer, node) => {
190
+ const body = (Array.isArray(node.parent?.body)
191
+ ? node.parent.body
192
+ : sourceCode.ast.body);
193
+ const next = body.find((statement) => statement.range[0] >= node.range[1]);
194
+ if (!next)
195
+ return body === sourceCode.ast.body
196
+ ? fixer.removeRange([node.range[0], sourceCode.text.length])
197
+ : fixer.remove(node);
198
+ if (removesStatement(next))
199
+ return fixer.remove(node);
200
+ const anchor = importAnchor();
201
+ if (node.range[1] <= anchor && next.range[0] >= anchor)
202
+ return fixer.remove(node);
203
+ return fixer.removeRange([node.range[0], next.range[0]]);
204
+ };
205
+ const importAnchor = () => {
206
+ const imports = sourceCode.ast.body.filter((statement) => statement.type === 'ImportDeclaration');
207
+ return imports.length > 0
208
+ ? Math.min(imports[imports.length - 1].range[1] + 1, sourceCode.text.length)
209
+ : 0;
210
+ };
110
211
  const specifierFor = (module) => {
111
212
  if (!module.target)
112
213
  return module.source;
@@ -133,10 +234,23 @@ exports.releaseStyles = {
133
234
  const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
134
235
  return resolved ? table?.[resolved] : undefined;
135
236
  };
136
- const importedReleasedModule = (specifier) => importedFrom(specifier, raw.modules);
237
+ const byTarget = new Map(Object.values(raw.modules ?? {}).flatMap((module) => module.target ? [[module.target, module]] : []));
238
+ const importedReleasedModule = (specifier) => {
239
+ const declared = importedFrom(specifier, raw.modules);
240
+ if (declared)
241
+ return declared;
242
+ const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
243
+ return resolved ? byTarget.get(resolved) : undefined;
244
+ };
137
245
  const importedThemeBindings = (specifier) => importedFrom(specifier, raw.themes);
138
246
  const importedAnimationBindings = (specifier) => importedFrom(specifier, raw.animations);
139
247
  const importedStaticBindings = (specifier) => importedFrom(specifier, raw.statics);
248
+ const inlineByLocal = new Map();
249
+ const literalOf = (value) => typeof value === 'string'
250
+ ? `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
251
+ : typeof value === 'number'
252
+ ? String(value)
253
+ : undefined;
140
254
  const releasedRange = () => {
141
255
  if (!entry)
142
256
  return undefined;
@@ -181,19 +295,216 @@ exports.releaseStyles = {
181
295
  node,
182
296
  messageId,
183
297
  fix: (fixer) => retained.length === 0
184
- ? fixer.remove(node)
298
+ ? removeStatement(fixer, node)
185
299
  : fixer.replaceText(node, importOf(retained, node.source)),
186
300
  });
187
301
  return true;
188
302
  };
303
+ const moduleOf = (node) => node?.type === 'MemberExpression' && node.object.type === 'Identifier'
304
+ ? moduleByLocal.get(node.object.name)
305
+ : undefined;
306
+ const constants = raw.constants?.[context.filename] ?? {};
307
+ const styleKey = (node) => {
308
+ const module = moduleOf(node);
309
+ if (!module)
310
+ return undefined;
311
+ const written = node.computed
312
+ ? node.property.type === 'Literal'
313
+ ? String(node.property.value)
314
+ : node.property.type === 'Identifier'
315
+ ? constants[node.property.name]
316
+ : undefined
317
+ : node.property.name;
318
+ if (written === undefined)
319
+ return undefined;
320
+ const alias = module.aliases?.[node.object.name];
321
+ const key = alias ? alias[written] : written;
322
+ return key === undefined ? undefined : { module, key };
323
+ };
324
+ const accessOf = (module, key) => `${module.binding}${IDENTIFIER.test(key) ? `.${key}` : `['${key}']`}`;
325
+ const classText = (node) => {
326
+ const resolved = styleKey(node);
327
+ return resolved ? accessOf(resolved.module, resolved.key) : undefined;
328
+ };
329
+ const releasedUse = (node, parent) => parent?.type === 'CallExpression' &&
330
+ parent.callee === node &&
331
+ parent.arguments.length > 0 &&
332
+ parent.arguments.every((argument) => classText(argument) !== undefined);
333
+ const removesCall = (method) => method === 'create' || method === 'createStatic'
334
+ ? Boolean(entry)
335
+ : method === 'createTheme'
336
+ ? themeBindings.length > 0
337
+ : method === 'keyframes' || method === 'viewTransition'
338
+ ? animationBindings.length > 0
339
+ : false;
340
+ const survivesCore = (program) => {
341
+ if (!coreLocal)
342
+ return false;
343
+ let survives = false;
344
+ const visit = (node, parent) => {
345
+ if (survives || !node || typeof node !== 'object')
346
+ return;
347
+ if (Array.isArray(node)) {
348
+ for (const child of node)
349
+ visit(child, parent);
350
+ return;
351
+ }
352
+ if (typeof node.type !== 'string')
353
+ return;
354
+ if (node.type === 'MemberExpression' &&
355
+ node.object.type === 'Identifier' &&
356
+ node.object.name === coreLocal) {
357
+ const method = node.computed
358
+ ? node.property.type === 'Literal'
359
+ ? String(node.property.value)
360
+ : ''
361
+ : node.property.name;
362
+ survives =
363
+ method === 'use'
364
+ ? !releasedUse(node, parent)
365
+ : !removesCall(method);
366
+ return;
367
+ }
368
+ for (const key of Object.keys(node)) {
369
+ if (key === 'parent' || key === 'loc' || key === 'range')
370
+ continue;
371
+ visit(node[key], node);
372
+ }
373
+ };
374
+ visit(program.body, undefined);
375
+ return survives;
376
+ };
377
+ const functionStyle = (value) => {
378
+ if (value?.type !== 'CallExpression')
379
+ return undefined;
380
+ const resolved = styleKey(value.callee);
381
+ if (!resolved)
382
+ return undefined;
383
+ const func = resolved.module.functions?.[resolved.key];
384
+ if (!func || func.variables.length !== value.arguments.length)
385
+ return undefined;
386
+ return {
387
+ className: accessOf(resolved.module, resolved.key),
388
+ declarations: func.variables.map((variable, index) => {
389
+ const argument = sourceCode.getText(value.arguments[index]);
390
+ return `['${variable}' as string]: ${argument}`;
391
+ }),
392
+ };
393
+ };
394
+ const resolveText = (value) => {
395
+ if (!value)
396
+ return undefined;
397
+ const called = functionStyle(value);
398
+ if (called)
399
+ return called.className;
400
+ if (value.type === 'LogicalExpression') {
401
+ const right = resolveText(value.right);
402
+ return right === undefined
403
+ ? undefined
404
+ : `${sourceCode.getText(value.left)} ${value.operator} ${right}`;
405
+ }
406
+ if (value.type === 'ConditionalExpression') {
407
+ const consequent = resolveText(value.consequent);
408
+ const alternate = resolveText(value.alternate);
409
+ return consequent === undefined || alternate === undefined
410
+ ? undefined
411
+ : `${sourceCode.getText(value.test)} ? ${consequent} : ${alternate}`;
412
+ }
413
+ if (value.type === 'Literal' && !value.value)
414
+ return sourceCode.getText(value);
415
+ if (value.type === 'Identifier' && styleLocals.has(value.name))
416
+ return sourceCode.getText(value);
417
+ return classText(value);
418
+ };
419
+ const declarationsOf = (value) => {
420
+ if (!value)
421
+ return [];
422
+ const called = functionStyle(value);
423
+ if (called)
424
+ return called.declarations;
425
+ if (value.type === 'LogicalExpression')
426
+ return declarationsOf(value.right);
427
+ if (value.type === 'ConditionalExpression')
428
+ return [
429
+ ...declarationsOf(value.consequent),
430
+ ...declarationsOf(value.alternate),
431
+ ];
432
+ return [];
433
+ };
189
434
  return {
435
+ Program(program) {
436
+ if (entry) {
437
+ moduleByLocal.set(entry.binding, entry);
438
+ for (const owner of Object.keys(entry.aliases ?? {}))
439
+ moduleByLocal.set(owner, entry);
440
+ }
441
+ for (const node of program.body) {
442
+ if (node.type !== 'ImportDeclaration')
443
+ continue;
444
+ const specifier = String(node.source.value);
445
+ if (specifier === '@plumeria/core') {
446
+ coreImport = node;
447
+ for (const candidate of node.specifiers) {
448
+ if (candidate.type === 'ImportNamespaceSpecifier')
449
+ coreLocal = candidate.local.name;
450
+ else if (candidate.type === 'ImportSpecifier' &&
451
+ (candidate.imported.name ?? candidate.imported.value) === 'css')
452
+ coreLocal = candidate.local.name;
453
+ }
454
+ continue;
455
+ }
456
+ const table = importedFrom(specifier, raw.values);
457
+ if (table)
458
+ for (const candidate of node.specifiers) {
459
+ if (candidate.type !== 'ImportSpecifier')
460
+ continue;
461
+ const imported = candidate.imported.name ?? candidate.imported.value;
462
+ if (imported in table)
463
+ inlineByLocal.set(candidate.local.name, table[imported]);
464
+ }
465
+ const module = importedReleasedModule(specifier);
466
+ if (!module)
467
+ continue;
468
+ const importedBinding = node.specifiers.find((candidate) => (candidate.type === 'ImportSpecifier' &&
469
+ (candidate.imported.name ?? candidate.imported.value) ===
470
+ module.binding) ||
471
+ (candidate.type === 'ImportDefaultSpecifier' &&
472
+ specifier.endsWith('.css')));
473
+ if (!importedBinding)
474
+ continue;
475
+ const local = importedBinding.local.name;
476
+ moduleByLocal.set(local, { ...module, binding: local });
477
+ active = true;
478
+ }
479
+ walk(program.body, (node) => {
480
+ if (node.type !== 'VariableDeclarator' ||
481
+ node.id.type !== 'Identifier' ||
482
+ !styleKey(node.init))
483
+ return;
484
+ styleLocals.add(node.id.name);
485
+ });
486
+ const migrating = active || themeBindings.length > 0 || animationBindings.length > 0;
487
+ coreNeeded = keptDefinition || !migrating || survivesCore(program);
488
+ if (coreImport &&
489
+ !coreNeeded &&
490
+ !entry &&
491
+ themeBindings.length === 0 &&
492
+ animationBindings.length === 0) {
493
+ context.report({
494
+ node: coreImport,
495
+ messageId: 'core',
496
+ fix: (fixer) => removeStatement(fixer, coreImport),
497
+ });
498
+ }
499
+ },
190
500
  ImportDeclaration(node) {
191
- if ((entry || themeBindings.length > 0 || animationBindings.length > 0) &&
501
+ if (!coreNeeded &&
502
+ (entry || themeBindings.length > 0 || animationBindings.length > 0) &&
192
503
  node.source.value === '@plumeria/core') {
193
504
  context.report({
194
505
  node,
195
506
  messageId: 'core',
196
- fix: (fixer) => fixer.remove(node),
507
+ fix: (fixer) => removeStatement(fixer, node),
197
508
  });
198
509
  return;
199
510
  }
@@ -214,12 +525,10 @@ exports.releaseStyles = {
214
525
  }
215
526
  const importedModule = importedReleasedModule(String(node.source.value));
216
527
  if (importedModule) {
217
- active = true;
218
528
  const importedBinding = node.specifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
219
529
  (specifier.imported.name ?? specifier.imported.value) ===
220
530
  importedModule.binding);
221
531
  const local = importedBinding?.local.name ?? importedModule.binding;
222
- activeModule = { ...importedModule, binding: local };
223
532
  if (importedModule.definitionOnly && importedBinding) {
224
533
  const retained = node.specifiers.filter((specifier) => specifier !== importedBinding);
225
534
  const released = `import ${local} from '${specifierFor(importedModule)}';`;
@@ -256,7 +565,7 @@ exports.releaseStyles = {
256
565
  : path[1] === 'keyframes' || path[1] === 'viewTransition'
257
566
  ? 'animation'
258
567
  : 'static',
259
- fix: (fixer) => fixer.remove(target),
568
+ fix: (fixer) => removeStatement(fixer, target),
260
569
  });
261
570
  return;
262
571
  }
@@ -269,7 +578,7 @@ exports.releaseStyles = {
269
578
  context.report({
270
579
  node: aliased,
271
580
  messageId: 'definition',
272
- fix: (fixer) => fixer.remove(aliased),
581
+ fix: (fixer) => removeStatement(fixer, aliased),
273
582
  });
274
583
  return;
275
584
  }
@@ -281,13 +590,103 @@ exports.releaseStyles = {
281
590
  const replacement = exported
282
591
  ? `${imported}\nexport { ${entry.binding} };`
283
592
  : imported;
593
+ const anchor = importAnchor();
284
594
  context.report({
285
595
  node: target,
286
596
  messageId: 'definition',
287
- fix: (fixer) => fixer.replaceText(target, replacement),
597
+ fix: (fixer) => anchor >= target.range[0] && anchor <= target.range[1]
598
+ ? fixer.replaceText(target, replacement)
599
+ : [
600
+ fixer.insertTextBeforeRange([anchor, anchor], `${replacement}\n`),
601
+ removeStatement(fixer, target),
602
+ ],
603
+ });
604
+ },
605
+ Identifier(node) {
606
+ const value = inlineByLocal.get(node.name);
607
+ const literal = literalOf(value);
608
+ if (literal === undefined)
609
+ return;
610
+ const parent = node.parent;
611
+ if (parent?.type === 'ImportSpecifier' ||
612
+ parent?.type === 'ExportSpecifier' ||
613
+ (parent?.type === 'MemberExpression' && parent.property === node) ||
614
+ (parent?.type === 'Property' &&
615
+ parent.key === node &&
616
+ !parent.computed))
617
+ return;
618
+ context.report({
619
+ node,
620
+ messageId: 'token',
621
+ fix: (fixer) => fixer.replaceText(node, literal),
622
+ });
623
+ },
624
+ VariableDeclarator(node) {
625
+ if (!active ||
626
+ keptDefinition ||
627
+ node.id.type !== 'Identifier' ||
628
+ node.init?.type !== 'Literal' ||
629
+ constants[node.id.name] !== node.init.value ||
630
+ node.parent.declarations.length !== 1)
631
+ return;
632
+ const scope = sourceCode.getScope(node);
633
+ const variable = scope.variables.find((candidate) => candidate.name === node.id.name);
634
+ if (!variable || variable.references.length === 0)
635
+ return;
636
+ const onlyNamesKeys = variable.references.every((reference) => {
637
+ if (reference.init)
638
+ return true;
639
+ const parent = reference.identifier.parent;
640
+ return (parent?.type === 'MemberExpression' &&
641
+ parent.computed &&
642
+ parent.property === reference.identifier &&
643
+ Boolean(moduleOf(parent)));
644
+ });
645
+ if (!onlyNamesKeys)
646
+ return;
647
+ const target = node.parent;
648
+ context.report({
649
+ node: target,
650
+ messageId: 'folded',
651
+ fix: (fixer) => removeStatement(fixer, target),
652
+ });
653
+ },
654
+ TSTypeReference(node) {
655
+ if (!complete || keptDefinition)
656
+ return;
657
+ const name = node.typeName.type === 'TSQualifiedName'
658
+ ? node.typeName.right.name
659
+ : node.typeName.name;
660
+ if (name !== 'Style')
661
+ return;
662
+ context.report({
663
+ node,
664
+ messageId: 'styleType',
665
+ fix: (fixer) => fixer.replaceText(node, 'string'),
288
666
  });
289
667
  },
290
668
  MemberExpression(node) {
669
+ if (node.object.type === 'Identifier') {
670
+ const table = inlineByLocal.get(node.object.name);
671
+ if (table && typeof table === 'object') {
672
+ const key = node.computed
673
+ ? node.property.type === 'Literal'
674
+ ? String(node.property.value)
675
+ : undefined
676
+ : node.property.name;
677
+ const literal = key === undefined
678
+ ? undefined
679
+ : literalOf(table[key]);
680
+ if (literal !== undefined) {
681
+ context.report({
682
+ node,
683
+ messageId: 'token',
684
+ fix: (fixer) => fixer.replaceText(node, literal),
685
+ });
686
+ return;
687
+ }
688
+ }
689
+ }
291
690
  if (!entry?.aliases || node.object.type !== 'Identifier')
292
691
  return;
293
692
  const alias = entry.aliases[node.object.name];
@@ -296,7 +695,9 @@ exports.releaseStyles = {
296
695
  const key = node.computed
297
696
  ? node.property.type === 'Literal'
298
697
  ? String(node.property.value)
299
- : undefined
698
+ : node.property.type === 'Identifier'
699
+ ? constants[node.property.name]
700
+ : undefined
300
701
  : node.property.name;
301
702
  const className = key ? alias[key] : undefined;
302
703
  if (!className)
@@ -310,6 +711,28 @@ exports.releaseStyles = {
310
711
  fix: (fixer) => fixer.replaceText(node, `${entry.binding}${access}`),
311
712
  });
312
713
  },
714
+ CallExpression(node) {
715
+ if (node.callee.type !== 'MemberExpression' ||
716
+ node.callee.object.type !== 'Identifier' ||
717
+ node.callee.property.name !== 'use')
718
+ return;
719
+ const reader = node.callee.object.name;
720
+ if (coreLocal ? reader !== coreLocal : moduleByLocal.has(reader))
721
+ return;
722
+ const texts = node.arguments.map(resolveText);
723
+ if (texts.length === 0 || texts.some((text) => text === undefined))
724
+ return;
725
+ const replacement = texts.length === 1 && !node.arguments.some(needsFilter)
726
+ ? texts[0]
727
+ : node.arguments.some(needsFilter)
728
+ ? `[${texts.join(', ')}].filter(Boolean).join(' ')`
729
+ : `[${texts.join(', ')}].join(' ')`;
730
+ context.report({
731
+ node,
732
+ messageId: 'use',
733
+ fix: (fixer) => fixer.replaceText(node, replacement),
734
+ });
735
+ },
313
736
  JSXAttribute(node) {
314
737
  if (!active)
315
738
  return;
@@ -318,40 +741,49 @@ exports.releaseStyles = {
318
741
  const expression = node.value?.type === 'JSXExpressionContainer'
319
742
  ? node.value.expression
320
743
  : undefined;
321
- const moduleInfo = activeModule;
322
- const readsModule = (callee) => Boolean(moduleInfo &&
323
- callee?.type === 'MemberExpression' &&
324
- callee.object.type === 'Identifier' &&
325
- (callee.object.name === moduleInfo.binding ||
326
- moduleInfo.aliases?.[callee.object.name]));
327
- const styleKey = (callee) => {
328
- if (!readsModule(callee))
329
- return undefined;
330
- const raw = callee.computed
331
- ? callee.property.type === 'Literal'
332
- ? String(callee.property.value)
333
- : undefined
334
- : callee.property.name;
335
- if (raw === undefined)
336
- return undefined;
337
- const alias = moduleInfo.aliases?.[callee.object.name];
338
- return alias ? alias[raw] : raw;
744
+ const carried = node.parent.attributes.find((attribute) => attribute !== node &&
745
+ attribute.type === 'JSXAttribute' &&
746
+ attribute.name?.type === 'JSXIdentifier' &&
747
+ attribute.name.name === 'className');
748
+ const takeProp = (valueText) => {
749
+ if (!carried) {
750
+ context.report({
751
+ node: node.name,
752
+ messageId: 'prop',
753
+ fix: (fixer) => fixer.replaceText(node.name, 'className'),
754
+ });
755
+ return;
756
+ }
757
+ const ours = valueText ??
758
+ (node.value ? sourceCode.getText(node.value).slice(1, -1) : '');
759
+ const theirs = carried.value?.type === 'JSXExpressionContainer'
760
+ ? sourceCode.getText(carried.value.expression)
761
+ : carried.value
762
+ ? sourceCode.getText(carried.value)
763
+ : "''";
764
+ context.report({
765
+ node,
766
+ messageId: 'prop',
767
+ fix: (fixer) => [
768
+ fixer.replaceText(carried, `className={[${theirs}, ${ours}].filter(Boolean).join(' ')}`),
769
+ fixer.removeRange([
770
+ Math.min(node.range[0], carried.range[1]),
771
+ node.range[1],
772
+ ]),
773
+ ],
774
+ });
339
775
  };
340
- const functionStyle = (value) => {
341
- if (!moduleInfo || value?.type !== 'CallExpression')
776
+ const mergedClass = (elements) => {
777
+ if (elements.length < 2)
342
778
  return undefined;
343
- const key = styleKey(value.callee);
344
- const func = key ? moduleInfo.functions?.[key] : undefined;
345
- if (!key || !func || func.variables.length !== value.arguments.length)
779
+ const resolved = elements.map(styleKey);
780
+ if (resolved.some((part) => !part))
346
781
  return undefined;
347
- const access = IDENTIFIER.test(key) ? `.${key}` : `['${key}']`;
348
- return {
349
- className: `${moduleInfo.binding}${access}`,
350
- declarations: func.variables.map((variable, index) => {
351
- const argument = sourceCode.getText(value.arguments[index]);
352
- return `'${variable}': ${argument}`;
353
- }),
354
- };
782
+ const module = resolved[0].module;
783
+ if (resolved.some((part) => part.module !== module))
784
+ return undefined;
785
+ const name = module.merges?.[resolved.map((part) => part.key).join('|')];
786
+ return name ? accessOf(module, name) : undefined;
355
787
  };
356
788
  const replaceWithFunctionStyle = (className, declarations) => {
357
789
  const styleAttribute = node.parent.attributes.find((attribute) => attribute.type === 'JSXAttribute' &&
@@ -387,7 +819,7 @@ exports.releaseStyles = {
387
819
  return;
388
820
  }
389
821
  if (expression?.type === 'CallExpression' &&
390
- readsModule(expression.callee)) {
822
+ moduleOf(expression.callee)) {
391
823
  context.report({
392
824
  node,
393
825
  messageId: 'unsupportedFunctionStyle',
@@ -396,34 +828,70 @@ exports.releaseStyles = {
396
828
  }
397
829
  if (expression?.type === 'ArrayExpression') {
398
830
  const elements = expression.elements.filter(Boolean);
399
- if (elements.length > 0) {
400
- const declarations = [];
401
- const texts = elements.map((element) => {
402
- const resolved = functionStyle(element);
403
- if (!resolved)
404
- return sourceCode.getText(element);
405
- declarations.push(...resolved.declarations);
406
- return resolved.className;
407
- });
408
- const replacement = elements.some(needsFilter)
409
- ? `[${texts.join(', ')}].filter(Boolean).join(' ')`
410
- : `\`${texts.map((text) => `\${${text}}`).join(' ')}\``;
411
- if (declarations.length > 0) {
412
- replaceWithFunctionStyle(replacement, declarations);
831
+ const merged = mergedClass(elements);
832
+ if (merged) {
833
+ if (carried) {
834
+ takeProp(merged);
413
835
  return;
414
836
  }
415
837
  context.report({
416
838
  node: expression,
417
839
  messageId: 'composition',
418
- fix: (fixer) => fixer.replaceText(expression, replacement),
840
+ fix: (fixer) => fixer.replaceText(expression, merged),
419
841
  });
842
+ takeProp();
843
+ return;
844
+ }
845
+ if (elements.length === 0) {
846
+ takeProp();
847
+ return;
420
848
  }
849
+ const texts = elements.map(resolveText);
850
+ if (texts.some((text) => text === undefined))
851
+ return;
852
+ const ranked = elements.map((element) => element.type === 'LogicalExpression'
853
+ ? styleKey(element.right)
854
+ : styleKey(element));
855
+ const module = ranked[0]?.module;
856
+ const perSlot = module && ranked.every((part) => part?.module === module)
857
+ ? module.overrides?.[ranked.map((part) => part.key).join('|')]
858
+ : undefined;
859
+ if (perSlot) {
860
+ for (let slot = elements.length - 1; slot >= 0; slot--) {
861
+ const name = perSlot[slot];
862
+ if (!name)
863
+ continue;
864
+ const element = elements[slot];
865
+ const access = accessOf(module, name);
866
+ texts.splice(slot + 1, 0, element.type === 'LogicalExpression'
867
+ ? `${sourceCode.getText(element.left)} ${element.operator} ${access}`
868
+ : access);
869
+ elements.splice(slot + 1, 0, element);
870
+ }
871
+ }
872
+ const declarations = elements.flatMap(declarationsOf);
873
+ const replacement = elements.some(needsFilter)
874
+ ? `[${texts.join(', ')}].filter(Boolean).join(' ')`
875
+ : `[${texts.join(', ')}].join(' ')`;
876
+ if (declarations.length > 0) {
877
+ replaceWithFunctionStyle(replacement, declarations);
878
+ return;
879
+ }
880
+ if (carried) {
881
+ takeProp(replacement);
882
+ return;
883
+ }
884
+ context.report({
885
+ node: expression,
886
+ messageId: 'composition',
887
+ fix: (fixer) => fixer.replaceText(expression, replacement),
888
+ });
889
+ takeProp();
890
+ return;
421
891
  }
422
- context.report({
423
- node: node.name,
424
- messageId: 'prop',
425
- fix: (fixer) => fixer.replaceText(node.name, 'className'),
426
- });
892
+ if (!complete && expression && resolveText(expression) === undefined)
893
+ return;
894
+ takeProp();
427
895
  },
428
896
  };
429
897
  },