@plumeria/codemod 18.2.18 → 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.
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.releaseStyles = exports.__private = void 0;
37
37
  const path = __importStar(require("node:path"));
38
+ const resolve_1 = require("../resolve");
38
39
  const memberPath = (node) => {
39
40
  if (node?.type === 'Identifier')
40
41
  return [node.name];
@@ -54,6 +55,7 @@ const isCreate = (node) => {
54
55
  const path = memberPath(node.callee);
55
56
  return path?.length === 2 && path[1] === 'create';
56
57
  };
58
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
57
59
  const needsFilter = (node) => node.type === 'LogicalExpression' ||
58
60
  node.type === 'ConditionalExpression' ||
59
61
  node.type === 'SpreadElement';
@@ -72,9 +74,14 @@ exports.releaseStyles = {
72
74
  themeImport: 'Remove the createTheme import after inlining its CSS variables.',
73
75
  animation: '`keyframes` or `viewTransition` was written globally.',
74
76
  animationImport: 'Remove the animation import after replacing it with its generated name.',
77
+ staticImport: 'Remove the createStatic import after inlining its values into the CSS.',
78
+ alias: 'Read this style from the CSS Module the whole file was exported to.',
75
79
  core: 'Remove the Plumeria import after exporting the styles.',
76
80
  prop: 'Use className after exporting this style to CSS Modules.',
77
81
  composition: 'Join the composed CSS Module class names.',
82
+ use: 'Read the class name straight from the CSS Module.',
83
+ token: 'Inline the value this token resolved to.',
84
+ styleType: 'A released style is a class name.',
78
85
  functionStyle: 'Use the CSS Module class and pass function arguments as custom properties.',
79
86
  unsupportedFunctionStyle: 'This function style call cannot be converted without changing its behavior.',
80
87
  },
@@ -85,6 +92,9 @@ exports.releaseStyles = {
85
92
  modules: { type: 'object' },
86
93
  themes: { type: 'object' },
87
94
  animations: { type: 'object' },
95
+ statics: { type: 'object' },
96
+ values: { type: 'object' },
97
+ complete: { type: 'boolean' },
88
98
  styleProp: { type: 'string' },
89
99
  active: { type: 'boolean' },
90
100
  },
@@ -94,43 +104,165 @@ exports.releaseStyles = {
94
104
  },
95
105
  create(context) {
96
106
  const raw = (context.options[0] ?? {});
97
- const entry = raw.modules?.[context.filename];
98
- const themeBindings = raw.themes?.[context.filename] ?? [];
99
- const animationBindings = raw.animations?.[context.filename] ?? [];
107
+ const declared = raw.modules?.[context.filename];
108
+ const keptDefinition = declared?.definitionOnly === true;
109
+ const complete = raw.complete === true;
110
+ const entry = keptDefinition ? undefined : declared;
111
+ const themeBindings = keptDefinition
112
+ ? []
113
+ : (raw.themes?.[context.filename] ?? []);
114
+ const animationBindings = keptDefinition
115
+ ? []
116
+ : (raw.animations?.[context.filename] ?? []);
100
117
  const styleProp = raw.styleProp ?? 'classStyle';
101
118
  const sourceCode = context.sourceCode;
102
- let active = Boolean(entry || raw.active);
103
- let activeModule = entry;
119
+ const moduleByLocal = new Map();
120
+ let active = Boolean(entry || raw.active || raw.complete);
121
+ let coreImport;
122
+ let coreLocal;
123
+ let coreNeeded = true;
124
+ const declarationOf = (statement) => statement.type === 'ExportNamedDeclaration'
125
+ ? statement.declaration
126
+ : statement;
127
+ const removesDeclaration = (statement) => {
128
+ const declaration = declarationOf(statement);
129
+ if (declaration?.type !== 'VariableDeclaration' ||
130
+ declaration.declarations.length !== 1)
131
+ return false;
132
+ const only = declaration.declarations[0];
133
+ if (only.id.type !== 'Identifier')
134
+ return false;
135
+ const called = only.init?.type === 'CallExpression'
136
+ ? memberPath(only.init.callee)
137
+ : undefined;
138
+ if (called?.length !== 2)
139
+ return false;
140
+ if (called[1] === 'createStatic')
141
+ return Boolean(entry);
142
+ if (called[1] === 'createTheme')
143
+ return themeBindings.includes(only.id.name);
144
+ if (called[1] === 'keyframes' || called[1] === 'viewTransition')
145
+ return animationBindings.includes(only.id.name);
146
+ if (called[1] === 'create')
147
+ return Boolean(entry &&
148
+ (only.id.name === entry.binding || entry.aliases?.[only.id.name]));
149
+ return false;
150
+ };
151
+ const removesStatement = (statement) => {
152
+ if (keptDefinition)
153
+ return false;
154
+ if (statement.type !== 'ImportDeclaration')
155
+ return removesDeclaration(statement);
156
+ const specifier = String(statement.source.value);
157
+ if (specifier === '@plumeria/core')
158
+ return !coreNeeded;
159
+ const bindings = [
160
+ ...(importedAnimationBindings(specifier) ?? []),
161
+ ...(importedThemeBindings(specifier) ?? []),
162
+ ...(importedStaticBindings(specifier) ?? []),
163
+ ];
164
+ return (bindings.length > 0 &&
165
+ statement.specifiers.length > 0 &&
166
+ statement.specifiers.every((candidate) => candidate.type === 'ImportSpecifier' &&
167
+ bindings.includes(candidate.imported.name ?? candidate.imported.value)));
168
+ };
169
+ const removeStatement = (fixer, node) => {
170
+ const body = sourceCode.ast.body;
171
+ const next = body.find((statement) => statement.range[0] >= node.range[1]);
172
+ if (!next)
173
+ return fixer.removeRange([node.range[0], sourceCode.text.length]);
174
+ if (removesStatement(next))
175
+ return fixer.remove(node);
176
+ const anchor = importAnchor();
177
+ if (node.range[1] <= anchor && next.range[0] >= anchor)
178
+ return fixer.remove(node);
179
+ return fixer.removeRange([node.range[0], next.range[0]]);
180
+ };
181
+ const importAnchor = () => {
182
+ const imports = sourceCode.ast.body.filter((statement) => statement.type === 'ImportDeclaration');
183
+ return imports.length > 0
184
+ ? Math.min(imports[imports.length - 1].range[1] + 1, sourceCode.text.length)
185
+ : 0;
186
+ };
187
+ const specifierFor = (module) => {
188
+ if (!module.target)
189
+ return module.source;
190
+ const relative = path
191
+ .relative(path.dirname(context.filename), module.target)
192
+ .split(path.sep)
193
+ .join('/');
194
+ return relative.startsWith('.') ? relative : `./${relative}`;
195
+ };
196
+ const importOf = (specifiers, source) => {
197
+ const defaultImport = specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
198
+ const namedImports = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier');
199
+ const clauses = [];
200
+ if (defaultImport)
201
+ clauses.push(sourceCode.getText(defaultImport));
202
+ if (namedImports.length > 0) {
203
+ clauses.push(`{ ${namedImports
204
+ .map((specifier) => sourceCode.getText(specifier))
205
+ .join(', ')} }`);
206
+ }
207
+ return `import ${clauses.join(', ')} from ${sourceCode.getText(source)};`;
208
+ };
209
+ const importedFrom = (specifier, table) => {
210
+ const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
211
+ return resolved ? table?.[resolved] : undefined;
212
+ };
213
+ const byTarget = new Map(Object.values(raw.modules ?? {}).flatMap((module) => module.target ? [[module.target, module]] : []));
104
214
  const importedReleasedModule = (specifier) => {
105
- if (!specifier.startsWith('.'))
106
- return undefined;
107
- const resolved = path.resolve(path.dirname(context.filename), specifier);
108
- const withoutExtension = resolved.replace(/\.[^.]+$/, '');
109
- const source = Object.keys(raw.modules ?? {}).find((candidate) => candidate.replace(/\.[^.]+$/, '') === withoutExtension);
110
- return source ? raw.modules?.[source] : undefined;
215
+ const declared = importedFrom(specifier, raw.modules);
216
+ if (declared)
217
+ return declared;
218
+ const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
219
+ return resolved ? byTarget.get(resolved) : undefined;
111
220
  };
112
- const importedThemeBindings = (specifier) => {
113
- if (!specifier.startsWith('.'))
221
+ const importedThemeBindings = (specifier) => importedFrom(specifier, raw.themes);
222
+ const importedAnimationBindings = (specifier) => importedFrom(specifier, raw.animations);
223
+ const importedStaticBindings = (specifier) => importedFrom(specifier, raw.statics);
224
+ const inlineByLocal = new Map();
225
+ const literalOf = (value) => typeof value === 'string'
226
+ ? `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
227
+ : typeof value === 'number'
228
+ ? String(value)
229
+ : undefined;
230
+ const releasedRange = () => {
231
+ if (!entry)
114
232
  return undefined;
115
- const resolved = path.resolve(path.dirname(context.filename), specifier);
116
- const withoutExtension = resolved.replace(/\.[^.]+$/, '');
117
- const source = Object.keys(raw.themes ?? {}).find((candidate) => candidate.replace(/\.[^.]+$/, '') === withoutExtension);
118
- return source ? raw.themes?.[source] : undefined;
233
+ for (const statement of sourceCode.ast.body) {
234
+ const declaration = statement.type === 'ExportNamedDeclaration'
235
+ ? statement.declaration
236
+ : statement;
237
+ if (declaration?.type !== 'VariableDeclaration')
238
+ continue;
239
+ for (const candidate of declaration.declarations) {
240
+ if (candidate.id.type === 'Identifier' &&
241
+ candidate.id.name === entry.binding &&
242
+ isCreate(candidate.init))
243
+ return statement.range;
244
+ }
245
+ }
246
+ return undefined;
119
247
  };
120
- const importedAnimationBindings = (specifier) => {
121
- if (!specifier.startsWith('.'))
122
- return undefined;
123
- const resolved = path.resolve(path.dirname(context.filename), specifier);
124
- const withoutExtension = resolved.replace(/\.[^.]+$/, '');
125
- const source = Object.keys(raw.animations ?? {}).find((candidate) => candidate.replace(/\.[^.]+$/, '') === withoutExtension);
126
- return source ? raw.animations?.[source] : undefined;
248
+ const onlyUsedByReleasedStyles = (local) => {
249
+ const range = releasedRange();
250
+ if (!range)
251
+ return false;
252
+ const scope = sourceCode.getScope(sourceCode.ast);
253
+ const moduleScope = scope.type === 'global' ? (scope.childScopes[0] ?? scope) : scope;
254
+ const variable = moduleScope.variables.find((candidate) => candidate.name === local);
255
+ if (!variable || variable.references.length === 0)
256
+ return false;
257
+ return variable.references.every((reference) => reference.identifier.range[0] >= range[0] &&
258
+ reference.identifier.range[1] <= range[1]);
127
259
  };
128
- const removeImportedBindings = (node, bindings, messageId) => {
260
+ const removeImportedBindings = (node, bindings, messageId, dead) => {
129
261
  const removed = node.specifiers.filter((specifier) => {
130
262
  if (specifier.type !== 'ImportSpecifier')
131
263
  return false;
132
264
  const name = specifier.imported.name ?? specifier.imported.value;
133
- return bindings.includes(name);
265
+ return bindings.includes(name) && (!dead || dead(specifier));
134
266
  });
135
267
  if (removed.length === 0)
136
268
  return false;
@@ -138,32 +270,205 @@ exports.releaseStyles = {
138
270
  context.report({
139
271
  node,
140
272
  messageId,
141
- fix: (fixer) => {
142
- if (retained.length === 0)
143
- return fixer.remove(node);
144
- const defaultImport = retained.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
145
- const namedImports = retained.filter((specifier) => specifier.type === 'ImportSpecifier');
146
- const clauses = [];
147
- if (defaultImport)
148
- clauses.push(sourceCode.getText(defaultImport));
149
- if (namedImports.length > 0) {
150
- clauses.push(`{ ${namedImports
151
- .map((specifier) => sourceCode.getText(specifier))
152
- .join(', ')} }`);
153
- }
154
- return fixer.replaceText(node, `import ${clauses.join(', ')} from ${sourceCode.getText(node.source)};`);
155
- },
273
+ fix: (fixer) => retained.length === 0
274
+ ? removeStatement(fixer, node)
275
+ : fixer.replaceText(node, importOf(retained, node.source)),
156
276
  });
157
277
  return true;
158
278
  };
279
+ const moduleOf = (node) => node?.type === 'MemberExpression' && node.object.type === 'Identifier'
280
+ ? moduleByLocal.get(node.object.name)
281
+ : undefined;
282
+ const styleKey = (node) => {
283
+ const module = moduleOf(node);
284
+ if (!module)
285
+ return undefined;
286
+ const written = node.computed
287
+ ? node.property.type === 'Literal'
288
+ ? String(node.property.value)
289
+ : undefined
290
+ : node.property.name;
291
+ if (written === undefined)
292
+ return undefined;
293
+ const alias = module.aliases?.[node.object.name];
294
+ const key = alias ? alias[written] : written;
295
+ return key === undefined ? undefined : { module, key };
296
+ };
297
+ const accessOf = (module, key) => `${module.binding}${IDENTIFIER.test(key) ? `.${key}` : `['${key}']`}`;
298
+ const classText = (node) => {
299
+ const resolved = styleKey(node);
300
+ return resolved ? accessOf(resolved.module, resolved.key) : undefined;
301
+ };
302
+ const releasedUse = (node, parent) => parent?.type === 'CallExpression' &&
303
+ parent.callee === node &&
304
+ parent.arguments.length > 0 &&
305
+ parent.arguments.every((argument) => classText(argument) !== undefined);
306
+ const removesCall = (method) => method === 'create' || method === 'createStatic'
307
+ ? Boolean(entry)
308
+ : method === 'createTheme'
309
+ ? themeBindings.length > 0
310
+ : method === 'keyframes' || method === 'viewTransition'
311
+ ? animationBindings.length > 0
312
+ : false;
313
+ const survivesCore = (program) => {
314
+ if (!coreLocal)
315
+ return false;
316
+ let survives = false;
317
+ const visit = (node, parent) => {
318
+ if (survives || !node || typeof node !== 'object')
319
+ return;
320
+ if (Array.isArray(node)) {
321
+ for (const child of node)
322
+ visit(child, parent);
323
+ return;
324
+ }
325
+ if (typeof node.type !== 'string')
326
+ return;
327
+ if (node.type === 'MemberExpression' &&
328
+ node.object.type === 'Identifier' &&
329
+ node.object.name === coreLocal) {
330
+ const method = node.computed
331
+ ? node.property.type === 'Literal'
332
+ ? String(node.property.value)
333
+ : ''
334
+ : node.property.name;
335
+ survives =
336
+ method === 'use'
337
+ ? !releasedUse(node, parent)
338
+ : !removesCall(method);
339
+ return;
340
+ }
341
+ for (const key of Object.keys(node)) {
342
+ if (key === 'parent' || key === 'loc' || key === 'range')
343
+ continue;
344
+ visit(node[key], node);
345
+ }
346
+ };
347
+ visit(program.body, undefined);
348
+ return survives;
349
+ };
350
+ const functionStyle = (value) => {
351
+ if (value?.type !== 'CallExpression')
352
+ return undefined;
353
+ const resolved = styleKey(value.callee);
354
+ if (!resolved)
355
+ return undefined;
356
+ const func = resolved.module.functions?.[resolved.key];
357
+ if (!func || func.variables.length !== value.arguments.length)
358
+ return undefined;
359
+ return {
360
+ className: accessOf(resolved.module, resolved.key),
361
+ declarations: func.variables.map((variable, index) => {
362
+ const argument = sourceCode.getText(value.arguments[index]);
363
+ return `['${variable}' as string]: ${argument}`;
364
+ }),
365
+ };
366
+ };
367
+ const resolveText = (value) => {
368
+ if (!value)
369
+ return undefined;
370
+ const called = functionStyle(value);
371
+ if (called)
372
+ return called.className;
373
+ if (value.type === 'LogicalExpression') {
374
+ const right = resolveText(value.right);
375
+ return right === undefined
376
+ ? undefined
377
+ : `${sourceCode.getText(value.left)} ${value.operator} ${right}`;
378
+ }
379
+ if (value.type === 'ConditionalExpression') {
380
+ const consequent = resolveText(value.consequent);
381
+ const alternate = resolveText(value.alternate);
382
+ return consequent === undefined || alternate === undefined
383
+ ? undefined
384
+ : `${sourceCode.getText(value.test)} ? ${consequent} : ${alternate}`;
385
+ }
386
+ if (value.type === 'Literal' && !value.value)
387
+ return sourceCode.getText(value);
388
+ return classText(value);
389
+ };
390
+ const declarationsOf = (value) => {
391
+ if (!value)
392
+ return [];
393
+ const called = functionStyle(value);
394
+ if (called)
395
+ return called.declarations;
396
+ if (value.type === 'LogicalExpression')
397
+ return declarationsOf(value.right);
398
+ if (value.type === 'ConditionalExpression')
399
+ return [
400
+ ...declarationsOf(value.consequent),
401
+ ...declarationsOf(value.alternate),
402
+ ];
403
+ return [];
404
+ };
159
405
  return {
406
+ Program(program) {
407
+ if (entry) {
408
+ moduleByLocal.set(entry.binding, entry);
409
+ for (const owner of Object.keys(entry.aliases ?? {}))
410
+ moduleByLocal.set(owner, entry);
411
+ }
412
+ for (const node of program.body) {
413
+ if (node.type !== 'ImportDeclaration')
414
+ continue;
415
+ const specifier = String(node.source.value);
416
+ if (specifier === '@plumeria/core') {
417
+ coreImport = node;
418
+ for (const candidate of node.specifiers) {
419
+ if (candidate.type === 'ImportNamespaceSpecifier')
420
+ coreLocal = candidate.local.name;
421
+ else if (candidate.type === 'ImportSpecifier' &&
422
+ (candidate.imported.name ?? candidate.imported.value) === 'css')
423
+ coreLocal = candidate.local.name;
424
+ }
425
+ continue;
426
+ }
427
+ const table = importedFrom(specifier, raw.values);
428
+ if (table)
429
+ for (const candidate of node.specifiers) {
430
+ if (candidate.type !== 'ImportSpecifier')
431
+ continue;
432
+ const imported = candidate.imported.name ?? candidate.imported.value;
433
+ if (imported in table)
434
+ inlineByLocal.set(candidate.local.name, table[imported]);
435
+ }
436
+ const module = importedReleasedModule(specifier);
437
+ if (!module)
438
+ continue;
439
+ const importedBinding = node.specifiers.find((candidate) => (candidate.type === 'ImportSpecifier' &&
440
+ (candidate.imported.name ?? candidate.imported.value) ===
441
+ module.binding) ||
442
+ (candidate.type === 'ImportDefaultSpecifier' &&
443
+ specifier.endsWith('.css')));
444
+ if (!importedBinding)
445
+ continue;
446
+ const local = importedBinding.local.name;
447
+ moduleByLocal.set(local, { ...module, binding: local });
448
+ active = true;
449
+ }
450
+ const migrating = active || themeBindings.length > 0 || animationBindings.length > 0;
451
+ coreNeeded = keptDefinition || !migrating || survivesCore(program);
452
+ if (coreImport &&
453
+ !coreNeeded &&
454
+ !entry &&
455
+ themeBindings.length === 0 &&
456
+ animationBindings.length === 0) {
457
+ context.report({
458
+ node: coreImport,
459
+ messageId: 'core',
460
+ fix: (fixer) => removeStatement(fixer, coreImport),
461
+ });
462
+ }
463
+ },
160
464
  ImportDeclaration(node) {
161
- if ((entry || themeBindings.length > 0 || animationBindings.length > 0) &&
465
+ if (!coreNeeded &&
466
+ (entry || themeBindings.length > 0 || animationBindings.length > 0) &&
162
467
  node.source.value === '@plumeria/core') {
163
468
  context.report({
164
469
  node,
165
470
  messageId: 'core',
166
- fix: (fixer) => fixer.remove(node),
471
+ fix: (fixer) => removeStatement(fixer, node),
167
472
  });
168
473
  return;
169
474
  }
@@ -177,16 +482,28 @@ exports.releaseStyles = {
177
482
  if (removeImportedBindings(node, importedThemes, 'themeImport'))
178
483
  return;
179
484
  }
485
+ const importedStatics = importedStaticBindings(String(node.source.value));
486
+ if (importedStatics) {
487
+ if (removeImportedBindings(node, importedStatics, 'staticImport', (specifier) => onlyUsedByReleasedStyles(specifier.local.name)))
488
+ return;
489
+ }
180
490
  const importedModule = importedReleasedModule(String(node.source.value));
181
491
  if (importedModule) {
182
- active = true;
183
492
  const importedBinding = node.specifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
184
493
  (specifier.imported.name ?? specifier.imported.value) ===
185
494
  importedModule.binding);
186
- activeModule = {
187
- ...importedModule,
188
- binding: importedBinding?.local.name ?? importedModule.binding,
189
- };
495
+ const local = importedBinding?.local.name ?? importedModule.binding;
496
+ if (importedModule.definitionOnly && importedBinding) {
497
+ const retained = node.specifiers.filter((specifier) => specifier !== importedBinding);
498
+ const released = `import ${local} from '${specifierFor(importedModule)}';`;
499
+ context.report({
500
+ node,
501
+ messageId: 'definition',
502
+ fix: (fixer) => fixer.replaceText(node, retained.length === 0
503
+ ? released
504
+ : `${released}\n${importOf(retained, node.source)}`),
505
+ });
506
+ }
190
507
  }
191
508
  },
192
509
  VariableDeclaration(node) {
@@ -212,14 +529,24 @@ exports.releaseStyles = {
212
529
  : path[1] === 'keyframes' || path[1] === 'viewTransition'
213
530
  ? 'animation'
214
531
  : 'static',
215
- fix: (fixer) => fixer.remove(target),
532
+ fix: (fixer) => removeStatement(fixer, target),
216
533
  });
217
534
  return;
218
535
  }
219
536
  if (!entry)
220
537
  return;
221
- if (declaration.id.name !== entry.binding ||
222
- !isCreate(declaration.init))
538
+ if (!isCreate(declaration.init))
539
+ return;
540
+ if (entry.aliases?.[declaration.id.name]) {
541
+ const aliased = node.parent?.type === 'ExportNamedDeclaration' ? node.parent : node;
542
+ context.report({
543
+ node: aliased,
544
+ messageId: 'definition',
545
+ fix: (fixer) => removeStatement(fixer, aliased),
546
+ });
547
+ return;
548
+ }
549
+ if (declaration.id.name !== entry.binding)
223
550
  return;
224
551
  const exported = node.parent?.type === 'ExportNamedDeclaration';
225
552
  const target = exported ? node.parent : node;
@@ -227,10 +554,115 @@ exports.releaseStyles = {
227
554
  const replacement = exported
228
555
  ? `${imported}\nexport { ${entry.binding} };`
229
556
  : imported;
557
+ const anchor = importAnchor();
230
558
  context.report({
231
559
  node: target,
232
560
  messageId: 'definition',
233
- fix: (fixer) => fixer.replaceText(target, replacement),
561
+ fix: (fixer) => anchor >= target.range[0] && anchor <= target.range[1]
562
+ ? fixer.replaceText(target, replacement)
563
+ : [
564
+ fixer.insertTextBeforeRange([anchor, anchor], `${replacement}\n`),
565
+ removeStatement(fixer, target),
566
+ ],
567
+ });
568
+ },
569
+ Identifier(node) {
570
+ const value = inlineByLocal.get(node.name);
571
+ const literal = literalOf(value);
572
+ if (literal === undefined)
573
+ return;
574
+ const parent = node.parent;
575
+ if (parent?.type === 'ImportSpecifier' ||
576
+ parent?.type === 'ExportSpecifier' ||
577
+ (parent?.type === 'MemberExpression' && parent.property === node) ||
578
+ (parent?.type === 'Property' &&
579
+ parent.key === node &&
580
+ !parent.computed))
581
+ return;
582
+ context.report({
583
+ node,
584
+ messageId: 'token',
585
+ fix: (fixer) => fixer.replaceText(node, literal),
586
+ });
587
+ },
588
+ TSTypeReference(node) {
589
+ if (!complete || keptDefinition)
590
+ return;
591
+ const name = node.typeName.type === 'TSQualifiedName'
592
+ ? node.typeName.right.name
593
+ : node.typeName.name;
594
+ if (name !== 'Style')
595
+ return;
596
+ context.report({
597
+ node,
598
+ messageId: 'styleType',
599
+ fix: (fixer) => fixer.replaceText(node, 'string'),
600
+ });
601
+ },
602
+ MemberExpression(node) {
603
+ if (node.object.type === 'Identifier') {
604
+ const table = inlineByLocal.get(node.object.name);
605
+ if (table && typeof table === 'object') {
606
+ const key = node.computed
607
+ ? node.property.type === 'Literal'
608
+ ? String(node.property.value)
609
+ : undefined
610
+ : node.property.name;
611
+ const literal = key === undefined
612
+ ? undefined
613
+ : literalOf(table[key]);
614
+ if (literal !== undefined) {
615
+ context.report({
616
+ node,
617
+ messageId: 'token',
618
+ fix: (fixer) => fixer.replaceText(node, literal),
619
+ });
620
+ return;
621
+ }
622
+ }
623
+ }
624
+ if (!entry?.aliases || node.object.type !== 'Identifier')
625
+ return;
626
+ const alias = entry.aliases[node.object.name];
627
+ if (!alias)
628
+ return;
629
+ const key = node.computed
630
+ ? node.property.type === 'Literal'
631
+ ? String(node.property.value)
632
+ : undefined
633
+ : node.property.name;
634
+ const className = key ? alias[key] : undefined;
635
+ if (!className)
636
+ return;
637
+ const access = IDENTIFIER.test(className)
638
+ ? `.${className}`
639
+ : `['${className}']`;
640
+ context.report({
641
+ node,
642
+ messageId: 'alias',
643
+ fix: (fixer) => fixer.replaceText(node, `${entry.binding}${access}`),
644
+ });
645
+ },
646
+ CallExpression(node) {
647
+ if (node.callee.type !== 'MemberExpression' ||
648
+ node.callee.object.type !== 'Identifier' ||
649
+ node.callee.property.name !== 'use')
650
+ return;
651
+ const reader = node.callee.object.name;
652
+ if (coreLocal ? reader !== coreLocal : moduleByLocal.has(reader))
653
+ return;
654
+ const texts = node.arguments.map(resolveText);
655
+ if (texts.length === 0 || texts.some((text) => text === undefined))
656
+ return;
657
+ const replacement = texts.length === 1 && !node.arguments.some(needsFilter)
658
+ ? texts[0]
659
+ : node.arguments.some(needsFilter)
660
+ ? `[${texts.join(', ')}].filter(Boolean).join(' ')`
661
+ : `[${texts.join(', ')}].join(' ')`;
662
+ context.report({
663
+ node,
664
+ messageId: 'use',
665
+ fix: (fixer) => fixer.replaceText(node, replacement),
234
666
  });
235
667
  },
236
668
  JSXAttribute(node) {
@@ -241,29 +673,49 @@ exports.releaseStyles = {
241
673
  const expression = node.value?.type === 'JSXExpressionContainer'
242
674
  ? node.value.expression
243
675
  : undefined;
244
- const moduleInfo = activeModule;
245
- const functionStyle = (value) => {
246
- if (!moduleInfo ||
247
- value?.type !== 'CallExpression' ||
248
- value.callee.type !== 'MemberExpression' ||
249
- value.callee.object.type !== 'Identifier' ||
250
- value.callee.object.name !== moduleInfo.binding)
676
+ const carried = node.parent.attributes.find((attribute) => attribute !== node &&
677
+ attribute.type === 'JSXAttribute' &&
678
+ attribute.name?.type === 'JSXIdentifier' &&
679
+ attribute.name.name === 'className');
680
+ const takeProp = (valueText) => {
681
+ if (!carried) {
682
+ context.report({
683
+ node: node.name,
684
+ messageId: 'prop',
685
+ fix: (fixer) => fixer.replaceText(node.name, 'className'),
686
+ });
687
+ return;
688
+ }
689
+ const ours = valueText ??
690
+ (node.value ? sourceCode.getText(node.value).slice(1, -1) : '');
691
+ const theirs = carried.value?.type === 'JSXExpressionContainer'
692
+ ? sourceCode.getText(carried.value.expression)
693
+ : carried.value
694
+ ? sourceCode.getText(carried.value)
695
+ : "''";
696
+ context.report({
697
+ node,
698
+ messageId: 'prop',
699
+ fix: (fixer) => [
700
+ fixer.replaceText(carried, `className={[${theirs}, ${ours}].filter(Boolean).join(' ')}`),
701
+ fixer.removeRange([
702
+ Math.min(node.range[0], carried.range[1]),
703
+ node.range[1],
704
+ ]),
705
+ ],
706
+ });
707
+ };
708
+ const mergedClass = (elements) => {
709
+ if (elements.length < 2)
710
+ return undefined;
711
+ const resolved = elements.map(styleKey);
712
+ if (resolved.some((part) => !part))
251
713
  return undefined;
252
- const key = value.callee.computed
253
- ? value.callee.property.type === 'Literal'
254
- ? String(value.callee.property.value)
255
- : undefined
256
- : value.callee.property.name;
257
- const func = key ? moduleInfo.functions?.[key] : undefined;
258
- if (!key || !func || func.variables.length !== value.arguments.length)
714
+ const module = resolved[0].module;
715
+ if (resolved.some((part) => part.module !== module))
259
716
  return undefined;
260
- return {
261
- className: `${moduleInfo.binding}.${key}`,
262
- declarations: func.variables.map((variable, index) => {
263
- const argument = sourceCode.getText(value.arguments[index]);
264
- return `'${variable}': ${argument}`;
265
- }),
266
- };
717
+ const name = module.merges?.[resolved.map((part) => part.key).join('|')];
718
+ return name ? accessOf(module, name) : undefined;
267
719
  };
268
720
  const replaceWithFunctionStyle = (className, declarations) => {
269
721
  const styleAttribute = node.parent.attributes.find((attribute) => attribute.type === 'JSXAttribute' &&
@@ -299,9 +751,7 @@ exports.releaseStyles = {
299
751
  return;
300
752
  }
301
753
  if (expression?.type === 'CallExpression' &&
302
- expression.callee.type === 'MemberExpression' &&
303
- expression.callee.object.type === 'Identifier' &&
304
- expression.callee.object.name === moduleInfo?.binding) {
754
+ moduleOf(expression.callee)) {
305
755
  context.report({
306
756
  node,
307
757
  messageId: 'unsupportedFunctionStyle',
@@ -310,34 +760,70 @@ exports.releaseStyles = {
310
760
  }
311
761
  if (expression?.type === 'ArrayExpression') {
312
762
  const elements = expression.elements.filter(Boolean);
313
- if (elements.length > 0) {
314
- const declarations = [];
315
- const texts = elements.map((element) => {
316
- const resolved = functionStyle(element);
317
- if (!resolved)
318
- return sourceCode.getText(element);
319
- declarations.push(...resolved.declarations);
320
- return resolved.className;
321
- });
322
- const replacement = elements.some(needsFilter)
323
- ? `[${texts.join(', ')}].filter(Boolean).join(' ')`
324
- : `\`${texts.map((text) => `\${${text}}`).join(' ')}\``;
325
- if (declarations.length > 0) {
326
- replaceWithFunctionStyle(replacement, declarations);
763
+ const merged = mergedClass(elements);
764
+ if (merged) {
765
+ if (carried) {
766
+ takeProp(merged);
327
767
  return;
328
768
  }
329
769
  context.report({
330
770
  node: expression,
331
771
  messageId: 'composition',
332
- fix: (fixer) => fixer.replaceText(expression, replacement),
772
+ fix: (fixer) => fixer.replaceText(expression, merged),
333
773
  });
774
+ takeProp();
775
+ return;
776
+ }
777
+ if (elements.length === 0) {
778
+ takeProp();
779
+ return;
780
+ }
781
+ const texts = elements.map(resolveText);
782
+ if (texts.some((text) => text === undefined))
783
+ return;
784
+ const ranked = elements.map((element) => element.type === 'LogicalExpression'
785
+ ? styleKey(element.right)
786
+ : styleKey(element));
787
+ const module = ranked[0]?.module;
788
+ const perSlot = module && ranked.every((part) => part?.module === module)
789
+ ? module.overrides?.[ranked.map((part) => part.key).join('|')]
790
+ : undefined;
791
+ if (perSlot) {
792
+ for (let slot = elements.length - 1; slot >= 0; slot--) {
793
+ const name = perSlot[slot];
794
+ if (!name)
795
+ continue;
796
+ const element = elements[slot];
797
+ const access = accessOf(module, name);
798
+ texts.splice(slot + 1, 0, element.type === 'LogicalExpression'
799
+ ? `${sourceCode.getText(element.left)} ${element.operator} ${access}`
800
+ : access);
801
+ elements.splice(slot + 1, 0, element);
802
+ }
803
+ }
804
+ const declarations = elements.flatMap(declarationsOf);
805
+ const replacement = elements.some(needsFilter)
806
+ ? `[${texts.join(', ')}].filter(Boolean).join(' ')`
807
+ : `[${texts.join(', ')}].join(' ')`;
808
+ if (declarations.length > 0) {
809
+ replaceWithFunctionStyle(replacement, declarations);
810
+ return;
811
+ }
812
+ if (carried) {
813
+ takeProp(replacement);
814
+ return;
334
815
  }
816
+ context.report({
817
+ node: expression,
818
+ messageId: 'composition',
819
+ fix: (fixer) => fixer.replaceText(expression, replacement),
820
+ });
821
+ takeProp();
822
+ return;
335
823
  }
336
- context.report({
337
- node: node.name,
338
- messageId: 'prop',
339
- fix: (fixer) => fixer.replaceText(node.name, 'className'),
340
- });
824
+ if (!complete && expression && resolveText(expression) === undefined)
825
+ return;
826
+ takeProp();
341
827
  },
342
828
  };
343
829
  },