@plumeria/codemod 18.2.20 → 18.2.22

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
@@ -194,8 +194,23 @@ under the same condition as the style that has to win:
194
194
  ```
195
195
 
196
196
  Order is only forced where the two classes actually disagree, measured per
197
- at-rule and per selector. Anything left unsettled is reported as
198
- `composition-order`.
197
+ at-rule and per selector. Where one property covers another — `padding` against
198
+ `padding-top` — Plumeria settles it by specificity rather than order, so the
199
+ shorthand is written first and the array has no say. Anything left unsettled is
200
+ reported as `composition-order`.
201
+
202
+ A variant picked with a bracket is resolved to the class its key names, whether
203
+ the constant is declared in the same file or imported, and a constant left
204
+ naming nothing is removed with it:
205
+
206
+ ```diff
207
+ - const size = 'small';
208
+ - <div classStyle={styles[size]} />
209
+ + <div className={styles.small} />
210
+ ```
211
+
212
+ Only a `const` holding a string literal is followed. A key computed at runtime is
213
+ reported as `dynamic-style-access` and its definitions stay in Plumeria.
199
214
 
200
215
  ### Global styles
201
216
 
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,7 +310,61 @@ 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 seen = new Set();
330
+ const read = (statement, isExport) => {
331
+ if (seen.has(statement))
332
+ return;
333
+ seen.add(statement);
334
+ if (statement?.type !== 'VariableDeclaration' ||
335
+ statement.kind !== 'const')
336
+ return;
337
+ for (const declaration of statement.declarations) {
338
+ if (declaration.id.type !== 'Identifier' ||
339
+ declaration.init?.type !== 'Literal' ||
340
+ typeof declaration.init.value !== 'string')
341
+ continue;
342
+ if (held.has(declaration.id.name))
343
+ duplicated.add(declaration.id.name);
344
+ held.set(declaration.id.name, declaration.init.value);
345
+ if (isExport)
346
+ exported.set(declaration.id.name, declaration.init.value);
347
+ }
348
+ };
349
+ walk(ast.body, (node) => {
350
+ if (node.type === 'ExportNamedDeclaration')
351
+ read(node.declaration, true);
352
+ else
353
+ read(node, false);
354
+ });
355
+ for (const name of duplicated) {
356
+ held.delete(name);
357
+ exported.delete(name);
358
+ }
359
+ if (held.size > 0)
360
+ stringConstants.set(source, held);
361
+ if (exported.size > 0)
362
+ exportedConstants.set(source, exported);
363
+ }
364
+ const resolvedConstants = new Map();
313
365
  const compositions = new Map();
366
+ const foldings = new Map();
367
+ const reached = new Map();
314
368
  for (const source of sources) {
315
369
  let ast;
316
370
  try {
@@ -324,6 +378,25 @@ function planRelease(targets) {
324
378
  catch {
325
379
  continue;
326
380
  }
381
+ const constants = new Map(stringConstants.get(source) ?? []);
382
+ for (const node of ast.body) {
383
+ if (node.type !== 'ImportDeclaration')
384
+ continue;
385
+ const target = (0, resolve_1.resolveSourcePath)(String(node.source.value), source);
386
+ const available = target ? exportedConstants.get(target) : undefined;
387
+ if (!available)
388
+ continue;
389
+ for (const specifier of node.specifiers) {
390
+ if (specifier.type !== 'ImportSpecifier')
391
+ continue;
392
+ const imported = specifier.imported.name ?? specifier.imported.value;
393
+ const value = available.get(imported);
394
+ if (value !== undefined)
395
+ constants.set(specifier.local.name, value);
396
+ }
397
+ }
398
+ if (constants.size > 0)
399
+ resolvedConstants.set(source, constants);
327
400
  const owners = new Map();
328
401
  for (const binding of createBindings.get(source) ?? [])
329
402
  owners.set(binding, { source, binding });
@@ -354,6 +427,9 @@ function planRelease(targets) {
354
427
  const owner = owners.get(node.object.name);
355
428
  if (!owner)
356
429
  return;
430
+ if (node.property.type === 'Identifier' &&
431
+ constants.has(node.property.name))
432
+ return;
357
433
  note(owner.source, [
358
434
  {
359
435
  line: node.loc.start.line,
@@ -379,14 +455,43 @@ function planRelease(targets) {
379
455
  if (owners_.some((candidate) => !candidate))
380
456
  return;
381
457
  const owner = owners_[0].source;
382
- if (owners_.some((candidate) => candidate.source !== owner))
383
- return;
384
458
  const named = slots.map((slot) => slot.map((part) => ({
385
459
  binding: owners.get(part.object).binding,
386
460
  key: part.key,
387
461
  })));
388
462
  const collapsible = expression.elements.every((element) => memberOf(element)) &&
389
463
  named.every((slot) => slot.length === 1);
464
+ if (owners_.some((candidate) => candidate.source !== owner)) {
465
+ if (!collapsible)
466
+ return;
467
+ const parts = slots.map((slot, index) => {
468
+ const held = owners.get(slot[0].object);
469
+ return {
470
+ binding: held.binding,
471
+ key: slot[0].key,
472
+ ...(held.source === owner ? {} : { source: held.source }),
473
+ target: (0, exports.releasedPath)(held.source),
474
+ at: index,
475
+ };
476
+ });
477
+ foldings.set(owner, [
478
+ ...(foldings.get(owner) ?? []),
479
+ {
480
+ signature: parts
481
+ .map((part) => `${part.target}#${part.key}`)
482
+ .join('|'),
483
+ parts: parts.map(({ binding, key, source }) => ({
484
+ binding,
485
+ key,
486
+ ...(source ? { source } : {}),
487
+ })),
488
+ },
489
+ ]);
490
+ for (const part of parts)
491
+ if (part.source)
492
+ reached.set(owner, (reached.get(owner) ?? new Set()).add(part.source));
493
+ return;
494
+ }
390
495
  compositions.set(owner, [
391
496
  ...(compositions.get(owner) ?? []),
392
497
  {
@@ -403,7 +508,22 @@ function planRelease(targets) {
403
508
  for (const source of sources) {
404
509
  let module;
405
510
  try {
406
- module = (0, from_plumeria_1.convertPlumeriaModule)(sourceText.get(source), importedPlumeriaValues(source), compositions.get(source) ?? []);
511
+ module = (0, from_plumeria_1.convertPlumeriaModule)(sourceText.get(source), importedPlumeriaValues(source), compositions.get(source) ?? [], {
512
+ target: (0, exports.releasedPath)(source),
513
+ foldings: foldings.get(source) ?? [],
514
+ foreign: Object.fromEntries([...(reached.get(source) ?? [])].map((other) => [
515
+ other,
516
+ {
517
+ text: sourceText.get(other),
518
+ values: {
519
+ ...importedPlumeriaValues(other),
520
+ ...(staticValues.get(other) ?? {}),
521
+ ...(themeValues.get(other) ?? {}),
522
+ ...(animationValues.get(other) ?? {}),
523
+ },
524
+ },
525
+ ])),
526
+ });
407
527
  }
408
528
  catch {
409
529
  continue;
@@ -505,6 +625,10 @@ function planRelease(targets) {
505
625
  statics,
506
626
  values,
507
627
  complete: stylesheets.every((sheet) => sheet.reports.length === 0),
628
+ constants: Object.fromEntries([...resolvedConstants].map(([source, held]) => [
629
+ source,
630
+ Object.fromEntries(held),
631
+ ])),
508
632
  global: globalRules.length > 0
509
633
  ? {
510
634
  target: globalPath(targets),
@@ -80,6 +80,13 @@ exports.adoptStyles = {
80
80
  let coreLocal;
81
81
  let readsValue = false;
82
82
  let several = false;
83
+ const moduleFor = (specifier) => {
84
+ const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
85
+ if (resolved && modules[resolved])
86
+ return modules[resolved];
87
+ const named = Object.entries(modules).filter(([key]) => key === specifier || path.basename(key) === path.basename(specifier));
88
+ return named.length === 1 ? named[0][1] : undefined;
89
+ };
83
90
  const stylingPosition = (node) => {
84
91
  let current = node;
85
92
  while (current.parent &&
@@ -119,6 +126,25 @@ exports.adoptStyles = {
119
126
  const name = element?.name;
120
127
  return name?.type === 'JSXIdentifier' && /^[a-z]/.test(String(name.name));
121
128
  };
129
+ const renamedText = (element) => {
130
+ const read = styleRead(element);
131
+ const binding = read?.object?.type === 'Identifier'
132
+ ? bindings.get(read.object.name)
133
+ : undefined;
134
+ if (!binding)
135
+ return context.sourceCode.getText(element);
136
+ const written = read.computed && read.property.type === 'Literal'
137
+ ? String(read.property.value)
138
+ : !read.computed && read.property.type === 'Identifier'
139
+ ? read.property.name
140
+ : undefined;
141
+ if (written === undefined)
142
+ return context.sourceCode.getText(element);
143
+ const access = `${binding.local}.${binding.entry.names[written] ?? written}`;
144
+ return element.type === 'LogicalExpression'
145
+ ? `${context.sourceCode.getText(element.left)} ${element.operator} ${access}`
146
+ : access;
147
+ };
122
148
  const styleRead = (node) => node?.type === 'LogicalExpression'
123
149
  ? styleRead(node.right)
124
150
  : node?.type === 'MemberExpression'
@@ -152,8 +178,7 @@ exports.adoptStyles = {
152
178
  coreLocal = specifier.local.name;
153
179
  continue;
154
180
  }
155
- const target = (0, resolve_1.resolveSourcePath)(String(statement.source.value), context.filename);
156
- if (!target || !modules[target])
181
+ if (!moduleFor(String(statement.source.value)))
157
182
  continue;
158
183
  for (const specifier of statement.specifiers)
159
184
  if (specifier.type === 'ImportDefaultSpecifier') {
@@ -210,14 +235,12 @@ exports.adoptStyles = {
210
235
  },
211
236
  ImportDeclaration(node) {
212
237
  const specifier = String(node.source.value);
213
- const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
214
- const named = Object.entries(modules).filter(([key]) => key === specifier ||
215
- path.basename(key) === path.basename(specifier));
216
- const entry = (resolved ? modules[resolved] : undefined) ??
217
- (named.length === 1 ? named[0][1] : undefined);
238
+ const entry = moduleFor(specifier);
218
239
  if (!entry) {
219
- const adopted = resolved &&
220
- Object.values(modules).find((candidate) => candidate.target === resolved);
240
+ const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
241
+ const adopted = Object.values(modules).find((candidate) => candidate.target === resolved ||
242
+ candidate.source === specifier ||
243
+ path.basename(candidate.source) === path.basename(specifier));
221
244
  if (!adopted)
222
245
  return;
223
246
  const already = node.specifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
@@ -268,14 +291,6 @@ exports.adoptStyles = {
268
291
  : undefined;
269
292
  if (!binding)
270
293
  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
294
  const written = node.computed && node.property.type === 'Literal'
280
295
  ? String(node.property.value)
281
296
  : !node.computed && node.property.type === 'Identifier'
@@ -285,6 +300,14 @@ exports.adoptStyles = {
285
300
  return;
286
301
  const key = binding.entry.names[written] ?? written;
287
302
  const local = binding.local;
303
+ if (coreLocal && !stylingPosition(node) && !insideUse(node)) {
304
+ context.report({
305
+ node,
306
+ messageId: 'value',
307
+ fix: (fixer) => fixer.replaceText(node, `${coreLocal}.use(${local}.${key})`),
308
+ });
309
+ return;
310
+ }
288
311
  if (key === written && local === node.object.name)
289
312
  return;
290
313
  context.report({
@@ -383,9 +406,21 @@ exports.adoptStyles = {
383
406
  if (joined) {
384
407
  if (!joined.some((element) => bindings.has(styleRead(element)?.object?.name)))
385
408
  return;
386
- const list = joined
387
- .map((element) => context.sourceCode.getText(element))
388
- .join(', ');
409
+ const styles_ = joined.filter((element) => bindings.has(styleRead(element)?.object?.name));
410
+ const carried = joined.filter((element) => !styles_.includes(element));
411
+ const list = styles_.map(renamedText).join(', ');
412
+ if (carried.length > 0) {
413
+ const kept = carried
414
+ .map((element) => context.sourceCode.getText(element))
415
+ .join(', ');
416
+ context.report({
417
+ node,
418
+ messageId: 'prop',
419
+ data: { prop: styleProp },
420
+ fix: (fixer) => fixer.replaceText(node, `className={[${kept}].filter(Boolean).join(' ')} ${styleProp}={[${list}]}`),
421
+ });
422
+ return;
423
+ }
389
424
  if (!intrinsic(node.parent) && coreLocal) {
390
425
  context.report({
391
426
  node: expression,
@@ -12,6 +12,7 @@ export interface PlumeriaModule {
12
12
  functions: Record<string, {
13
13
  params: string[];
14
14
  variables: string[];
15
+ lengths: boolean[];
15
16
  }>;
16
17
  aliases: Record<string, Record<string, string>>;
17
18
  merges: Record<string, string>;
@@ -32,7 +33,22 @@ export interface PlumeriaComposition {
32
33
  line: number;
33
34
  column: number;
34
35
  }
35
- export declare const compositionSignature: (classNames: string[]) => string;
36
+ export interface PlumeriaFolding {
37
+ signature: string;
38
+ parts: {
39
+ binding: string;
40
+ key: string;
41
+ source?: string;
42
+ }[];
43
+ }
44
+ export interface PlumeriaForeign {
45
+ text: string;
46
+ values: Record<string, unknown>;
47
+ }
48
+ export declare const compositionSignature: (parts: {
49
+ target: string;
50
+ className: string;
51
+ }[]) => string;
36
52
  export interface ThemeExtraction {
37
53
  bindings: Record<string, Record<string, string>>;
38
54
  globalCss: string;
@@ -58,4 +74,8 @@ export declare const __private: {
58
74
  export declare function extractPlumeriaThemes(source: string): ThemeExtraction;
59
75
  export declare function extractPlumeriaStatics(source: string, externalValues?: Record<string, unknown>): StaticExtraction;
60
76
  export declare function extractPlumeriaAnimations(source: string, externalValues?: Record<string, unknown>): AnimationExtraction;
61
- export declare function convertPlumeriaModule(source: string, externalValues?: Record<string, unknown>, compositions?: PlumeriaComposition[]): PlumeriaModule | null;
77
+ export declare function convertPlumeriaModule(source: string, externalValues?: Record<string, unknown>, compositions?: PlumeriaComposition[], options?: {
78
+ target?: string;
79
+ foldings?: PlumeriaFolding[];
80
+ foreign?: Record<string, PlumeriaForeign>;
81
+ }): PlumeriaModule | null;
@@ -7,8 +7,87 @@ exports.extractPlumeriaAnimations = extractPlumeriaAnimations;
7
7
  exports.convertPlumeriaModule = convertPlumeriaModule;
8
8
  const parser_1 = require("@typescript-eslint/parser");
9
9
  const zss_engine_1 = require("zss-engine");
10
- const compositionSignature = (classNames) => classNames.join('|');
10
+ const UNITLESS = new Set(zss_engine_1.exceptionCamelCase.map((property) => (0, zss_engine_1.camelToKebabCase)(property)));
11
+ const compositionSignature = (parts) => parts.map((part) => `${part.target}#${part.className}`).join('|');
11
12
  exports.compositionSignature = compositionSignature;
13
+ const own = (target, classNames) => (0, exports.compositionSignature)(classNames.map((className) => ({ target, className })));
14
+ const DIRECT_SHORTHANDS = {};
15
+ for (const [shorthand, longhands] of Object.entries(zss_engine_1.DIRECT_LONGHANDS)) {
16
+ for (const longhand of longhands) {
17
+ if (!DIRECT_SHORTHANDS[longhand])
18
+ DIRECT_SHORTHANDS[longhand] = [];
19
+ DIRECT_SHORTHANDS[longhand].push(shorthand);
20
+ }
21
+ }
22
+ const depths = new Map();
23
+ const depthOf = (property) => {
24
+ const cached = depths.get(property);
25
+ if (cached !== undefined)
26
+ return cached;
27
+ depths.set(property, 0);
28
+ let depth = 0;
29
+ for (const shorthand of DIRECT_SHORTHANDS[property] || [])
30
+ depth = Math.max(depth, depthOf(shorthand) + 1);
31
+ depths.set(property, depth);
32
+ return depth;
33
+ };
34
+ const coverages = new Map();
35
+ const coverageOf = (property) => {
36
+ const cached = coverages.get(property);
37
+ if (cached)
38
+ return cached;
39
+ const longhands = zss_engine_1.DIRECT_LONGHANDS[property];
40
+ const coverage = longhands
41
+ ? new Set(longhands.flatMap((longhand) => [...coverageOf(longhand)]))
42
+ : new Set([property]);
43
+ coverages.set(property, coverage);
44
+ return coverage;
45
+ };
46
+ const covers = (outer, inner) => {
47
+ if (outer === inner)
48
+ return false;
49
+ const wide = coverageOf(outer);
50
+ const narrow = coverageOf(inner);
51
+ return wide.size > narrow.size && [...narrow].every((leaf) => wide.has(leaf));
52
+ };
53
+ const overlaps = (first, second) => {
54
+ const left = coverageOf(first);
55
+ return [...coverageOf(second)].some((leaf) => left.has(leaf));
56
+ };
57
+ const byRank = (written) => {
58
+ const outranked = (later, earlier) => (later.held ?? []).some((mine) => (earlier.held ?? []).some((theirs) => later.suffix === earlier.suffix &&
59
+ overlaps(mine.property, theirs.property) &&
60
+ mine.rank > theirs.rank));
61
+ const ordered = [];
62
+ const pending = [...written];
63
+ while (pending.length > 0) {
64
+ const index = pending.findIndex((candidate, position) => !pending.some((other, held) => held !== position && outranked(candidate, other)));
65
+ ordered.push(...pending.splice(index === -1 ? 0 : index, 1));
66
+ }
67
+ return ordered;
68
+ };
69
+ const byImplication = (written) => {
70
+ const ordered = [];
71
+ const pending = [...written];
72
+ while (pending.length > 0) {
73
+ const index = pending.findIndex((candidate, position) => !pending.some((other, held) => held !== position &&
74
+ candidate.condition !== undefined &&
75
+ other.condition !== undefined &&
76
+ candidate.condition !== other.condition &&
77
+ (0, zss_engine_1.impliesCondition)(candidate.condition, other.condition)));
78
+ ordered.push(...pending.splice(index === -1 ? 0 : index, 1));
79
+ }
80
+ return ordered.map((entry) => entry.text);
81
+ };
82
+ const byContainment = (declared) => {
83
+ const ordered = [];
84
+ const pending = [...declared];
85
+ while (pending.length > 0) {
86
+ const index = pending.findIndex((candidate, position) => !pending.some((other, held) => held !== position && covers(other.property, candidate.property)));
87
+ ordered.push(...pending.splice(index === -1 ? 0 : index, 1));
88
+ }
89
+ return ordered.map((entry) => entry.text);
90
+ };
12
91
  const toIdent = (value) => value.replace(/[^A-Za-z0-9-]/g, '');
13
92
  const capitalize = (value) => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
14
93
  const camelJoin = (names) => names
@@ -366,7 +445,8 @@ function extractPlumeriaAnimations(source, externalValues = {}) {
366
445
  reports,
367
446
  };
368
447
  }
369
- function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
448
+ function convertPlumeriaModule(source, externalValues = {}, compositions = [], options = {}) {
449
+ const target = options.target ?? '';
370
450
  const ast = (0, parser_1.parse)(source, {
371
451
  sourceType: 'module',
372
452
  ecmaFeatures: { jsx: true },
@@ -435,27 +515,37 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
435
515
  const members = new Map();
436
516
  const footprints = new Map();
437
517
  const track = (className) => {
438
- const map = new Map();
439
- footprints.set(className, map);
440
- return { root: `.${className}`, map };
518
+ const held = [];
519
+ footprints.set(className, held);
520
+ return { root: `.${className}`, held };
441
521
  };
442
- const conflicts = (first, second) => {
522
+ const relate = (first, second) => {
443
523
  const left = footprints.get(first);
444
524
  const right = footprints.get(second);
445
525
  if (!left || !right)
446
- return true;
447
- for (const [bucket, properties] of left) {
448
- const other = right.get(bucket);
449
- if (!other)
450
- continue;
451
- for (const property of properties)
452
- if (other.has(property))
453
- return true;
526
+ return { crossing: true };
527
+ let ranked;
528
+ let crossing = false;
529
+ for (const mine of left) {
530
+ for (const theirs of right) {
531
+ if (mine.suffix !== theirs.suffix)
532
+ continue;
533
+ if (!overlaps(mine.property, theirs.property))
534
+ continue;
535
+ if (mine.rank > theirs.rank)
536
+ ranked ??= [second, first];
537
+ else if (theirs.rank > mine.rank)
538
+ ranked ??= [first, second];
539
+ else
540
+ crossing = true;
541
+ }
454
542
  }
455
- return false;
543
+ return { ranked, crossing };
456
544
  };
545
+ const crosses = (first, second) => relate(first, second).crossing;
457
546
  const taken = new Set();
458
547
  const functions = {};
548
+ const variableUses = new Map();
459
549
  const classNameFor = (owner, key) => {
460
550
  if (!taken.has(key))
461
551
  return key;
@@ -477,7 +567,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
477
567
  return;
478
568
  }
479
569
  const restrictRoot = restrict ? selectorRoot(selector) : 0;
480
- const declarations = [];
570
+ const declared = [];
481
571
  const nested = [];
482
572
  const markers = [];
483
573
  for (const property of style.properties) {
@@ -519,15 +609,24 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
519
609
  .get(bucketOf(wrappers, selector.slice(restrictRoot)))
520
610
  ?.has(cssProperty))
521
611
  continue;
612
+ if (typeof value === 'string')
613
+ for (const [, variable] of value.matchAll(/var\((--[\w-]+)\)/g))
614
+ variableUses.set(variable, (variableUses.get(variable) ?? new Set()).add(cssProperty));
522
615
  if (footprint) {
523
- const bucket = bucketOf(wrappers, selector.slice(footprint.root.length));
524
- const held = footprint.map.get(bucket) ?? new Set();
525
- held.add(cssProperty);
526
- footprint.map.set(bucket, held);
616
+ const suffix = selector.slice(footprint.root.length);
617
+ footprint.held.push({
618
+ bucket: bucketOf(wrappers, suffix),
619
+ suffix,
620
+ property: cssProperty,
621
+ rank: depthOf(cssProperty) + (wrappers.length > 0 ? 1 : 0),
622
+ });
527
623
  }
528
- declarations.push(` ${cssProperty}: ${(0, zss_engine_1.applyCssValue)(value, cssProperty)};`);
624
+ declared.push({
625
+ property: cssProperty,
626
+ text: ` ${cssProperty}: ${(0, zss_engine_1.applyCssValue)(value, cssProperty)};`,
627
+ });
529
628
  }
530
- const pushRule = (target, lines) => {
629
+ const pushRule = (target, lines, held = []) => {
531
630
  if (lines.length === 0)
532
631
  return;
533
632
  let rule = `${target} {\n${lines.join('\n')}\n}`;
@@ -537,10 +636,18 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
537
636
  .map((line) => ` ${line}`)
538
637
  .join('\n')}\n}`;
539
638
  }
540
- rules.push(rule);
639
+ rules.push({
640
+ text: rule,
641
+ condition: wrappers.length === 1 ? wrappers[0] : undefined,
642
+ suffix: target.slice(target.indexOf('.') + 1).replace(/^[\w-]+/, ''),
643
+ held,
644
+ });
541
645
  };
542
646
  if (phase !== 'nested')
543
- pushRule(selector, declarations);
647
+ pushRule(selector, byContainment(declared), declared.map((entry) => ({
648
+ property: entry.property,
649
+ rank: depthOf(entry.property) + (wrappers.length > 0 ? 1 : 0),
650
+ })));
544
651
  if (phase === 'declarations')
545
652
  return;
546
653
  for (const marker of markers) {
@@ -600,10 +707,19 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
600
707
  params.forEach((parameter, index) => {
601
708
  scope.set(parameter, `var(${variables[index]})`);
602
709
  });
603
- functions[className] = { params, variables };
604
710
  keys.push(className);
605
711
  openGroup(className);
606
712
  emit(body, `.${className}`, [], scope, 'both', track(className));
713
+ functions[className] = {
714
+ params,
715
+ variables,
716
+ lengths: variables.map((variable) => {
717
+ const used = variableUses.get(variable);
718
+ return (used !== undefined &&
719
+ used.size > 0 &&
720
+ [...used].every((property) => !UNITLESS.has(property) && !property.startsWith('--')));
721
+ }),
722
+ };
607
723
  continue;
608
724
  }
609
725
  keys.push(className);
@@ -631,6 +747,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
631
747
  const sort = () => {
632
748
  const edges = new Map();
633
749
  const blame = new Map();
750
+ const link = (before, after) => edges.set(before, (edges.get(before) ?? new Set()).add(after));
634
751
  for (const entry of resolved) {
635
752
  if (inlined.has(entry))
636
753
  continue;
@@ -638,9 +755,14 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
638
755
  for (let second = first + 1; second < entry.slots.length; second++) {
639
756
  for (const before of entry.slots[first]) {
640
757
  for (const after of entry.slots[second]) {
641
- if (before === after || !conflicts(before, after))
758
+ if (before === after)
759
+ continue;
760
+ const { ranked, crossing } = relate(before, after);
761
+ if (ranked)
762
+ link(ranked[0], ranked[1]);
763
+ if (!crossing)
642
764
  continue;
643
- edges.set(before, (edges.get(before) ?? new Set()).add(after));
765
+ link(before, after);
644
766
  blame.set(`${before}|${after}`, entry.composition);
645
767
  }
646
768
  }
@@ -693,7 +815,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
693
815
  if (entry.slots.some((slot) => slot.length !== 1))
694
816
  continue;
695
817
  const names = entry.slots.map((slot) => slot[0]);
696
- const signature = (0, exports.compositionSignature)(names);
818
+ const signature = own(target, names);
697
819
  if (overrides[signature])
698
820
  continue;
699
821
  const perSlot = {};
@@ -701,7 +823,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
701
823
  const losers = names
702
824
  .slice(0, slot)
703
825
  .filter((earlier) => earlier !== names[slot] &&
704
- conflicts(earlier, names[slot]) &&
826
+ crosses(earlier, names[slot]) &&
705
827
  order.get(earlier) > order.get(names[slot]));
706
828
  if (losers.length === 0)
707
829
  continue;
@@ -721,17 +843,15 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
721
843
  const theirs = footprints.get(loser);
722
844
  if (!mine || !theirs)
723
845
  continue;
724
- for (const [bucket, properties] of mine) {
725
- const other = theirs.get(bucket);
726
- if (!other)
846
+ for (const entry of mine) {
847
+ const crossed = theirs.some((candidate) => candidate.suffix === entry.suffix &&
848
+ candidate.rank === entry.rank &&
849
+ overlaps(entry.property, candidate.property));
850
+ if (!crossed)
727
851
  continue;
728
- for (const property of properties) {
729
- if (!other.has(property))
730
- continue;
731
- const held = disputed.get(bucket) ?? new Set();
732
- held.add(property);
733
- disputed.set(bucket, held);
734
- }
852
+ const held = disputed.get(entry.bucket) ?? new Set();
853
+ held.add(entry.property);
854
+ disputed.set(entry.bucket, held);
735
855
  }
736
856
  }
737
857
  if (disputed.size === 0)
@@ -747,7 +867,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
747
867
  if (inlined.has(entry))
748
868
  continue;
749
869
  const signature = entry.slots.every((slot) => slot.length === 1) &&
750
- (0, exports.compositionSignature)(entry.slots.map((slot) => slot[0]));
870
+ own(target, entry.slots.map((slot) => slot[0]));
751
871
  let unresolved;
752
872
  for (let first = 0; first < entry.slots.length && !unresolved; first++) {
753
873
  for (let second = first + 1; second < entry.slots.length; second++) {
@@ -755,7 +875,10 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
755
875
  continue;
756
876
  for (const before of entry.slots[first]) {
757
877
  for (const after of entry.slots[second]) {
758
- if (before === after || !conflicts(before, after))
878
+ if (before === after)
879
+ continue;
880
+ const { ranked, crossing } = relate(before, after);
881
+ if (ranked || !crossing)
759
882
  continue;
760
883
  if (order.get(before) < order.get(after))
761
884
  continue;
@@ -774,7 +897,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
774
897
  });
775
898
  }
776
899
  for (const entry of collapsible) {
777
- const signature = (0, exports.compositionSignature)(entry.parts.map((part) => part.className));
900
+ const signature = own(target, entry.parts.map((part) => part.className));
778
901
  if (merges[signature])
779
902
  continue;
780
903
  const merged = classNameFor(binding, camelJoin(entry.parts.map((part) => part.className)));
@@ -787,13 +910,80 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
787
910
  emit(part.value, `.${merged}`, [], values, 'nested');
788
911
  }
789
912
  else {
790
- rules.push(`.${merged} {\n composes: ${entry.parts
791
- .map((part) => part.className)
792
- .join(' ')};\n}`);
913
+ rules.push({
914
+ text: `.${merged} {\n composes: ${entry.parts
915
+ .map((part) => part.className)
916
+ .join(' ')};\n}`,
917
+ });
793
918
  }
794
919
  keys.push(merged);
795
920
  merges[signature] = merged;
796
921
  }
922
+ const parsed = new Map();
923
+ const styleOf = (part) => {
924
+ if (!part.source) {
925
+ const held = members.get(`${part.binding}.${part.key}`);
926
+ return held ? { value: held.value, scope: values } : undefined;
927
+ }
928
+ const held = options.foreign?.[part.source];
929
+ if (!held)
930
+ return undefined;
931
+ let tree = parsed.get(part.source);
932
+ if (!tree) {
933
+ try {
934
+ tree = (0, parser_1.parse)(held.text, {
935
+ sourceType: 'module',
936
+ ecmaFeatures: { jsx: true },
937
+ loc: true,
938
+ range: true,
939
+ });
940
+ }
941
+ catch {
942
+ return undefined;
943
+ }
944
+ parsed.set(part.source, tree);
945
+ }
946
+ for (const raw of tree.body) {
947
+ const statement = declarationOf(raw);
948
+ if (statement?.type !== 'VariableDeclaration')
949
+ continue;
950
+ for (const declaration of statement.declarations) {
951
+ if (declaration.id.name !== part.binding ||
952
+ !isCall(declaration.init, 'create'))
953
+ continue;
954
+ for (const property of declaration.init.arguments[0]?.properties ??
955
+ []) {
956
+ if (property.type !== 'Property')
957
+ continue;
958
+ const name = property.computed
959
+ ? undefined
960
+ : propertyKey(property.key);
961
+ if (name === part.key)
962
+ return {
963
+ value: property.value,
964
+ scope: new Map(Object.entries(held.values)),
965
+ };
966
+ }
967
+ }
968
+ }
969
+ return undefined;
970
+ };
971
+ for (const folding of options.foldings ?? []) {
972
+ if (merges[folding.signature])
973
+ continue;
974
+ const parts = folding.parts.map(styleOf);
975
+ if (parts.some((part) => !part))
976
+ continue;
977
+ const merged = classNameFor(binding, camelJoin(folding.parts.map((part) => part.key)));
978
+ taken.add(merged);
979
+ openGroup(merged);
980
+ for (const part of parts)
981
+ emit(part.value, `.${merged}`, [], part.scope, 'declarations');
982
+ for (const part of parts)
983
+ emit(part.value, `.${merged}`, [], part.scope, 'nested');
984
+ keys.push(merged);
985
+ merges[folding.signature] = merged;
986
+ }
797
987
  const emitted = [
798
988
  ...sorted.ordered,
799
989
  ...[...sorted.stuck],
@@ -804,7 +994,8 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = []) {
804
994
  if (seen.has(name))
805
995
  return [];
806
996
  seen.add(name);
807
- return groups.find((group) => group.name === name)?.rules ?? [];
997
+ const group = groups.find((held) => held.name === name);
998
+ return group ? byImplication(byRank(group.rules)) : [];
808
999
  });
809
1000
  return {
810
1001
  binding,
@@ -8,6 +8,7 @@ export interface ReleaseModule {
8
8
  functions?: Record<string, {
9
9
  params: string[];
10
10
  variables: string[];
11
+ lengths?: boolean[];
11
12
  }>;
12
13
  merges?: Record<string, string>;
13
14
  overrides?: Record<string, Record<number, string>>;
@@ -19,6 +20,7 @@ export interface ReleaseStylesOptions {
19
20
  statics?: Record<string, string[]>;
20
21
  values?: Record<string, Record<string, unknown>>;
21
22
  complete?: boolean;
23
+ constants?: Record<string, Record<string, string>>;
22
24
  styleProp?: string;
23
25
  active?: boolean;
24
26
  }
@@ -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;
@@ -360,7 +387,10 @@ exports.releaseStyles = {
360
387
  className: accessOf(resolved.module, resolved.key),
361
388
  declarations: func.variables.map((variable, index) => {
362
389
  const argument = sourceCode.getText(value.arguments[index]);
363
- return `['${variable}' as string]: ${argument}`;
390
+ const written = func.lengths?.[index]
391
+ ? `typeof ${argument} === 'number' ? \`\${${argument}}px\` : ${argument}`
392
+ : argument;
393
+ return `['${variable}' as string]: ${written}`;
364
394
  }),
365
395
  };
366
396
  };
@@ -385,6 +415,8 @@ exports.releaseStyles = {
385
415
  }
386
416
  if (value.type === 'Literal' && !value.value)
387
417
  return sourceCode.getText(value);
418
+ if (value.type === 'Identifier' && styleLocals.has(value.name))
419
+ return sourceCode.getText(value);
388
420
  return classText(value);
389
421
  };
390
422
  const declarationsOf = (value) => {
@@ -447,6 +479,13 @@ exports.releaseStyles = {
447
479
  moduleByLocal.set(local, { ...module, binding: local });
448
480
  active = true;
449
481
  }
482
+ walk(program.body, (node) => {
483
+ if (node.type !== 'VariableDeclarator' ||
484
+ node.id.type !== 'Identifier' ||
485
+ !styleKey(node.init))
486
+ return;
487
+ styleLocals.add(node.id.name);
488
+ });
450
489
  const migrating = active || themeBindings.length > 0 || animationBindings.length > 0;
451
490
  coreNeeded = keptDefinition || !migrating || survivesCore(program);
452
491
  if (coreImport &&
@@ -585,6 +624,37 @@ exports.releaseStyles = {
585
624
  fix: (fixer) => fixer.replaceText(node, literal),
586
625
  });
587
626
  },
627
+ VariableDeclarator(node) {
628
+ if (!active ||
629
+ keptDefinition ||
630
+ node.id.type !== 'Identifier' ||
631
+ node.init?.type !== 'Literal' ||
632
+ constants[node.id.name] !== node.init.value ||
633
+ node.parent.declarations.length !== 1 ||
634
+ node.parent.parent?.type === 'ExportNamedDeclaration')
635
+ return;
636
+ const scope = sourceCode.getScope(node);
637
+ const variable = scope.variables.find((candidate) => candidate.name === node.id.name);
638
+ if (!variable || variable.references.length === 0)
639
+ return;
640
+ const onlyNamesKeys = variable.references.every((reference) => {
641
+ if (reference.init)
642
+ return true;
643
+ const parent = reference.identifier.parent;
644
+ return (parent?.type === 'MemberExpression' &&
645
+ parent.computed &&
646
+ parent.property === reference.identifier &&
647
+ Boolean(moduleOf(parent)));
648
+ });
649
+ if (!onlyNamesKeys)
650
+ return;
651
+ const target = node.parent;
652
+ context.report({
653
+ node: target,
654
+ messageId: 'folded',
655
+ fix: (fixer) => removeStatement(fixer, target),
656
+ });
657
+ },
588
658
  TSTypeReference(node) {
589
659
  if (!complete || keptDefinition)
590
660
  return;
@@ -629,7 +699,9 @@ exports.releaseStyles = {
629
699
  const key = node.computed
630
700
  ? node.property.type === 'Literal'
631
701
  ? String(node.property.value)
632
- : undefined
702
+ : node.property.type === 'Identifier'
703
+ ? constants[node.property.name]
704
+ : undefined
633
705
  : node.property.name;
634
706
  const className = key ? alias[key] : undefined;
635
707
  if (!className)
@@ -709,13 +781,14 @@ exports.releaseStyles = {
709
781
  if (elements.length < 2)
710
782
  return undefined;
711
783
  const resolved = elements.map(styleKey);
712
- if (resolved.some((part) => !part))
713
- return undefined;
714
- const module = resolved[0].module;
715
- if (resolved.some((part) => part.module !== module))
784
+ if (resolved.some((part) => !part?.module.target))
716
785
  return undefined;
717
- const name = module.merges?.[resolved.map((part) => part.key).join('|')];
718
- return name ? accessOf(module, name) : undefined;
786
+ const host = resolved[0].module;
787
+ const signature = resolved
788
+ .map((part) => `${part.module.target}#${part.key}`)
789
+ .join('|');
790
+ const name = host.merges?.[signature];
791
+ return name ? accessOf(host, name) : undefined;
719
792
  };
720
793
  const replaceWithFunctionStyle = (className, declarations) => {
721
794
  const styleAttribute = node.parent.attributes.find((attribute) => attribute.type === 'JSXAttribute' &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/codemod",
3
- "version": "18.2.20",
3
+ "version": "18.2.22",
4
4
  "description": "Codemods for migrating Plumeria APIs",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",