@plumeria/codemod 18.2.20 → 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.
package/README.md CHANGED
@@ -197,6 +197,19 @@ Order is only forced where the two classes actually disagree, measured per
197
197
  at-rule and per selector. Anything left unsettled is reported as
198
198
  `composition-order`.
199
199
 
200
+ A variant picked with a bracket is resolved to the class its key names, whether
201
+ the constant is declared in the same file or imported, and a constant left
202
+ naming nothing is removed with it:
203
+
204
+ ```diff
205
+ - const size = 'small';
206
+ - <div classStyle={styles[size]} />
207
+ + <div className={styles.small} />
208
+ ```
209
+
210
+ Only a `const` holding a string literal is followed. A key computed at runtime is
211
+ reported as `dynamic-style-access` and its definitions stay in Plumeria.
212
+
200
213
  ### Global styles
201
214
 
202
215
  `css.createTheme`, `css.keyframes`, and `css.viewTransition` produce rules that
package/dist/cli.js CHANGED
@@ -331,7 +331,7 @@ async function migrateFromCssModules(options) {
331
331
  console.log(line);
332
332
  return 1;
333
333
  }
334
- function releaseConfig(modules, themes, animations, statics, values, complete) {
334
+ function releaseConfig(modules, themes, animations, statics, values, complete, constants) {
335
335
  return [
336
336
  { ignores: IGNORES },
337
337
  {
@@ -347,7 +347,15 @@ function releaseConfig(modules, themes, animations, statics, values, complete) {
347
347
  rules: {
348
348
  'codemod/release-styles': [
349
349
  'error',
350
- { modules, themes, animations, statics, values, complete },
350
+ {
351
+ modules,
352
+ themes,
353
+ animations,
354
+ statics,
355
+ values,
356
+ complete,
357
+ constants,
358
+ },
351
359
  ],
352
360
  },
353
361
  },
@@ -374,7 +382,7 @@ async function migrateFromPlumeria(options) {
374
382
  }
375
383
  const config = {
376
384
  overrideConfigFile: true,
377
- overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations, planned.statics, planned.values, planned.complete),
385
+ overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations, planned.statics, planned.values, planned.complete, planned.constants),
378
386
  errorOnUnmatchedPattern: false,
379
387
  };
380
388
  const preview = await new eslint_1.ESLint(config).lintFiles(targets);
package/dist/release.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface ReleasePlan {
14
14
  statics: Record<string, string[]>;
15
15
  values: Record<string, Record<string, unknown>>;
16
16
  complete: boolean;
17
+ constants: Record<string, Record<string, string>>;
17
18
  global?: {
18
19
  target: string;
19
20
  css: string;
package/dist/release.js CHANGED
@@ -310,6 +310,54 @@ function planRelease(targets) {
310
310
  catch {
311
311
  }
312
312
  }
313
+ const stringConstants = new Map();
314
+ const exportedConstants = new Map();
315
+ for (const source of sources) {
316
+ let ast;
317
+ try {
318
+ ast = (0, parser_1.parse)(sourceText.get(source), {
319
+ sourceType: 'module',
320
+ ecmaFeatures: { jsx: true },
321
+ });
322
+ }
323
+ catch {
324
+ continue;
325
+ }
326
+ const held = new Map();
327
+ const exported = new Map();
328
+ const duplicated = new Set();
329
+ const read = (statement, isExport) => {
330
+ if (statement?.type !== 'VariableDeclaration' ||
331
+ statement.kind !== 'const')
332
+ return;
333
+ for (const declaration of statement.declarations) {
334
+ if (declaration.id.type !== 'Identifier' ||
335
+ declaration.init?.type !== 'Literal' ||
336
+ typeof declaration.init.value !== 'string')
337
+ continue;
338
+ if (held.has(declaration.id.name))
339
+ duplicated.add(declaration.id.name);
340
+ held.set(declaration.id.name, declaration.init.value);
341
+ if (isExport)
342
+ exported.set(declaration.id.name, declaration.init.value);
343
+ }
344
+ };
345
+ walk(ast.body, (node) => {
346
+ if (node.type === 'ExportNamedDeclaration')
347
+ read(node.declaration, true);
348
+ else
349
+ read(node, false);
350
+ });
351
+ for (const name of duplicated) {
352
+ held.delete(name);
353
+ exported.delete(name);
354
+ }
355
+ if (held.size > 0)
356
+ stringConstants.set(source, held);
357
+ if (exported.size > 0)
358
+ exportedConstants.set(source, exported);
359
+ }
360
+ const resolvedConstants = new Map();
313
361
  const compositions = new Map();
314
362
  for (const source of sources) {
315
363
  let ast;
@@ -324,6 +372,25 @@ function planRelease(targets) {
324
372
  catch {
325
373
  continue;
326
374
  }
375
+ const constants = new Map(stringConstants.get(source) ?? []);
376
+ for (const node of ast.body) {
377
+ if (node.type !== 'ImportDeclaration')
378
+ continue;
379
+ const target = (0, resolve_1.resolveSourcePath)(String(node.source.value), source);
380
+ const available = target ? exportedConstants.get(target) : undefined;
381
+ if (!available)
382
+ continue;
383
+ for (const specifier of node.specifiers) {
384
+ if (specifier.type !== 'ImportSpecifier')
385
+ continue;
386
+ const imported = specifier.imported.name ?? specifier.imported.value;
387
+ const value = available.get(imported);
388
+ if (value !== undefined)
389
+ constants.set(specifier.local.name, value);
390
+ }
391
+ }
392
+ if (constants.size > 0)
393
+ resolvedConstants.set(source, constants);
327
394
  const owners = new Map();
328
395
  for (const binding of createBindings.get(source) ?? [])
329
396
  owners.set(binding, { source, binding });
@@ -354,6 +421,9 @@ function planRelease(targets) {
354
421
  const owner = owners.get(node.object.name);
355
422
  if (!owner)
356
423
  return;
424
+ if (node.property.type === 'Identifier' &&
425
+ constants.has(node.property.name))
426
+ return;
357
427
  note(owner.source, [
358
428
  {
359
429
  line: node.loc.start.line,
@@ -505,6 +575,10 @@ function planRelease(targets) {
505
575
  statics,
506
576
  values,
507
577
  complete: stylesheets.every((sheet) => sheet.reports.length === 0),
578
+ constants: Object.fromEntries([...resolvedConstants].map(([source, held]) => [
579
+ source,
580
+ Object.fromEntries(held),
581
+ ])),
508
582
  global: globalRules.length > 0
509
583
  ? {
510
584
  target: globalPath(targets),
@@ -119,6 +119,25 @@ exports.adoptStyles = {
119
119
  const name = element?.name;
120
120
  return name?.type === 'JSXIdentifier' && /^[a-z]/.test(String(name.name));
121
121
  };
122
+ const renamedText = (element) => {
123
+ const read = styleRead(element);
124
+ const binding = read?.object?.type === 'Identifier'
125
+ ? bindings.get(read.object.name)
126
+ : undefined;
127
+ if (!binding)
128
+ return context.sourceCode.getText(element);
129
+ const written = read.computed && read.property.type === 'Literal'
130
+ ? String(read.property.value)
131
+ : !read.computed && read.property.type === 'Identifier'
132
+ ? read.property.name
133
+ : undefined;
134
+ if (written === undefined)
135
+ return context.sourceCode.getText(element);
136
+ const access = `${binding.local}.${binding.entry.names[written] ?? written}`;
137
+ return element.type === 'LogicalExpression'
138
+ ? `${context.sourceCode.getText(element.left)} ${element.operator} ${access}`
139
+ : access;
140
+ };
122
141
  const styleRead = (node) => node?.type === 'LogicalExpression'
123
142
  ? styleRead(node.right)
124
143
  : node?.type === 'MemberExpression'
@@ -268,14 +287,6 @@ exports.adoptStyles = {
268
287
  : undefined;
269
288
  if (!binding)
270
289
  return;
271
- if (coreLocal && !stylingPosition(node) && !insideUse(node)) {
272
- context.report({
273
- node,
274
- messageId: 'value',
275
- fix: (fixer) => fixer.replaceText(node, `${coreLocal}.use(${context.sourceCode.getText(node)})`),
276
- });
277
- return;
278
- }
279
290
  const written = node.computed && node.property.type === 'Literal'
280
291
  ? String(node.property.value)
281
292
  : !node.computed && node.property.type === 'Identifier'
@@ -285,6 +296,14 @@ exports.adoptStyles = {
285
296
  return;
286
297
  const key = binding.entry.names[written] ?? written;
287
298
  const local = binding.local;
299
+ if (coreLocal && !stylingPosition(node) && !insideUse(node)) {
300
+ context.report({
301
+ node,
302
+ messageId: 'value',
303
+ fix: (fixer) => fixer.replaceText(node, `${coreLocal}.use(${local}.${key})`),
304
+ });
305
+ return;
306
+ }
288
307
  if (key === written && local === node.object.name)
289
308
  return;
290
309
  context.report({
@@ -383,9 +402,7 @@ exports.adoptStyles = {
383
402
  if (joined) {
384
403
  if (!joined.some((element) => bindings.has(styleRead(element)?.object?.name)))
385
404
  return;
386
- const list = joined
387
- .map((element) => context.sourceCode.getText(element))
388
- .join(', ');
405
+ const list = joined.map(renamedText).join(', ');
389
406
  if (!intrinsic(node.parent) && coreLocal) {
390
407
  context.report({
391
408
  node: expression,
@@ -19,6 +19,7 @@ export interface ReleaseStylesOptions {
19
19
  statics?: Record<string, string[]>;
20
20
  values?: Record<string, Record<string, unknown>>;
21
21
  complete?: boolean;
22
+ constants?: Record<string, Record<string, string>>;
22
23
  styleProp?: string;
23
24
  active?: boolean;
24
25
  }
@@ -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';
@@ -82,6 +99,7 @@ exports.releaseStyles = {
82
99
  use: 'Read the class name straight from the CSS Module.',
83
100
  token: 'Inline the value this token resolved to.',
84
101
  styleType: 'A released style is a class name.',
102
+ folded: 'This only named a style key, which the export has resolved.',
85
103
  functionStyle: 'Use the CSS Module class and pass function arguments as custom properties.',
86
104
  unsupportedFunctionStyle: 'This function style call cannot be converted without changing its behavior.',
87
105
  },
@@ -95,6 +113,7 @@ exports.releaseStyles = {
95
113
  statics: { type: 'object' },
96
114
  values: { type: 'object' },
97
115
  complete: { type: 'boolean' },
116
+ constants: { type: 'object' },
98
117
  styleProp: { type: 'string' },
99
118
  active: { type: 'boolean' },
100
119
  },
@@ -117,6 +136,7 @@ exports.releaseStyles = {
117
136
  const styleProp = raw.styleProp ?? 'classStyle';
118
137
  const sourceCode = context.sourceCode;
119
138
  const moduleByLocal = new Map();
139
+ const styleLocals = new Set();
120
140
  let active = Boolean(entry || raw.active || raw.complete);
121
141
  let coreImport;
122
142
  let coreLocal;
@@ -167,10 +187,14 @@ exports.releaseStyles = {
167
187
  bindings.includes(candidate.imported.name ?? candidate.imported.value)));
168
188
  };
169
189
  const removeStatement = (fixer, node) => {
170
- const body = sourceCode.ast.body;
190
+ const body = (Array.isArray(node.parent?.body)
191
+ ? node.parent.body
192
+ : sourceCode.ast.body);
171
193
  const next = body.find((statement) => statement.range[0] >= node.range[1]);
172
194
  if (!next)
173
- return fixer.removeRange([node.range[0], sourceCode.text.length]);
195
+ return body === sourceCode.ast.body
196
+ ? fixer.removeRange([node.range[0], sourceCode.text.length])
197
+ : fixer.remove(node);
174
198
  if (removesStatement(next))
175
199
  return fixer.remove(node);
176
200
  const anchor = importAnchor();
@@ -279,6 +303,7 @@ exports.releaseStyles = {
279
303
  const moduleOf = (node) => node?.type === 'MemberExpression' && node.object.type === 'Identifier'
280
304
  ? moduleByLocal.get(node.object.name)
281
305
  : undefined;
306
+ const constants = raw.constants?.[context.filename] ?? {};
282
307
  const styleKey = (node) => {
283
308
  const module = moduleOf(node);
284
309
  if (!module)
@@ -286,7 +311,9 @@ exports.releaseStyles = {
286
311
  const written = node.computed
287
312
  ? node.property.type === 'Literal'
288
313
  ? String(node.property.value)
289
- : undefined
314
+ : node.property.type === 'Identifier'
315
+ ? constants[node.property.name]
316
+ : undefined
290
317
  : node.property.name;
291
318
  if (written === undefined)
292
319
  return undefined;
@@ -385,6 +412,8 @@ exports.releaseStyles = {
385
412
  }
386
413
  if (value.type === 'Literal' && !value.value)
387
414
  return sourceCode.getText(value);
415
+ if (value.type === 'Identifier' && styleLocals.has(value.name))
416
+ return sourceCode.getText(value);
388
417
  return classText(value);
389
418
  };
390
419
  const declarationsOf = (value) => {
@@ -447,6 +476,13 @@ exports.releaseStyles = {
447
476
  moduleByLocal.set(local, { ...module, binding: local });
448
477
  active = true;
449
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
+ });
450
486
  const migrating = active || themeBindings.length > 0 || animationBindings.length > 0;
451
487
  coreNeeded = keptDefinition || !migrating || survivesCore(program);
452
488
  if (coreImport &&
@@ -585,6 +621,36 @@ exports.releaseStyles = {
585
621
  fix: (fixer) => fixer.replaceText(node, literal),
586
622
  });
587
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
+ },
588
654
  TSTypeReference(node) {
589
655
  if (!complete || keptDefinition)
590
656
  return;
@@ -629,7 +695,9 @@ exports.releaseStyles = {
629
695
  const key = node.computed
630
696
  ? node.property.type === 'Literal'
631
697
  ? String(node.property.value)
632
- : undefined
698
+ : node.property.type === 'Identifier'
699
+ ? constants[node.property.name]
700
+ : undefined
633
701
  : node.property.name;
634
702
  const className = key ? alias[key] : undefined;
635
703
  if (!className)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/codemod",
3
- "version": "18.2.20",
3
+ "version": "18.2.21",
4
4
  "description": "Codemods for migrating Plumeria APIs",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",