@plumeria/codemod 18.2.26 → 18.2.28

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.
@@ -0,0 +1,14 @@
1
+ export declare const depthOf: (property: string) => number;
2
+ export declare const rankOf: (property: string, conditional: boolean) => number;
3
+ export declare const coverageOf: (property: string) => Set<string>;
4
+ export declare const overlaps: (first: string, second: string) => boolean;
5
+ export interface Held {
6
+ property: string;
7
+ suffix: string;
8
+ rank: number;
9
+ place: number;
10
+ }
11
+ export declare const unrepresentable: (left: Held[], right: Held[]) => {
12
+ earlier: Held;
13
+ later: Held;
14
+ } | undefined;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unrepresentable = exports.overlaps = exports.coverageOf = exports.rankOf = exports.depthOf = void 0;
4
+ const zss_engine_1 = require("zss-engine");
5
+ const SHORTHANDS_OF = {};
6
+ for (const [shorthand, longhands] of Object.entries(zss_engine_1.DIRECT_LONGHANDS))
7
+ for (const longhand of longhands)
8
+ (SHORTHANDS_OF[longhand] ??= []).push(shorthand);
9
+ const depths = new Map();
10
+ const depthOf = (property) => {
11
+ const cached = depths.get(property);
12
+ if (cached !== undefined)
13
+ return cached;
14
+ depths.set(property, 0);
15
+ let depth = 0;
16
+ for (const shorthand of SHORTHANDS_OF[property] ?? [])
17
+ depth = Math.max(depth, (0, exports.depthOf)(shorthand) + 1);
18
+ depths.set(property, depth);
19
+ return depth;
20
+ };
21
+ exports.depthOf = depthOf;
22
+ const rankOf = (property, conditional) => property.startsWith('--') ? 0 : 1 + (conditional ? 1 : 0) + (0, exports.depthOf)(property);
23
+ exports.rankOf = rankOf;
24
+ const coverages = new Map();
25
+ const coverageOf = (property) => {
26
+ const cached = coverages.get(property);
27
+ if (cached)
28
+ return cached;
29
+ const longhands = zss_engine_1.DIRECT_LONGHANDS[property];
30
+ const coverage = longhands
31
+ ? new Set(longhands.flatMap((longhand) => [...(0, exports.coverageOf)(longhand)]))
32
+ : new Set([property]);
33
+ coverages.set(property, coverage);
34
+ return coverage;
35
+ };
36
+ exports.coverageOf = coverageOf;
37
+ const overlaps = (first, second) => {
38
+ const left = (0, exports.coverageOf)(first);
39
+ return [...(0, exports.coverageOf)(second)].some((leaf) => left.has(leaf));
40
+ };
41
+ exports.overlaps = overlaps;
42
+ const unrepresentable = (left, right) => {
43
+ for (const mine of left)
44
+ for (const theirs of right) {
45
+ if (mine.suffix !== theirs.suffix)
46
+ continue;
47
+ if (mine.place === theirs.place)
48
+ continue;
49
+ if (!(0, exports.overlaps)(mine.property, theirs.property))
50
+ continue;
51
+ const [earlier, later] = mine.place < theirs.place ? [mine, theirs] : [theirs, mine];
52
+ if (earlier.rank > later.rank)
53
+ return { earlier, later };
54
+ }
55
+ return undefined;
56
+ };
57
+ exports.unrepresentable = unrepresentable;
package/dist/cli.js CHANGED
@@ -301,6 +301,36 @@ async function migrateFromCssModules(options) {
301
301
  console.log('No *.module.css found.');
302
302
  return 0;
303
303
  }
304
+ const held = new Map();
305
+ const detected = await new eslint_1.ESLint({
306
+ overrideConfigFile: true,
307
+ overrideConfig: migrateConfig(modules),
308
+ errorOnUnmatchedPattern: false,
309
+ }).lintFiles(targets);
310
+ for (const result of detected)
311
+ for (const message of result.messages) {
312
+ if (message.messageId !== 'missing' && message.messageId !== 'cascade')
313
+ continue;
314
+ const [sheet, rest] = message.message.split(' :: ');
315
+ if (!modules[sheet])
316
+ continue;
317
+ const named = held.get(sheet) ?? new Set();
318
+ named.add(rest);
319
+ held.set(sheet, named);
320
+ }
321
+ for (const [sheet, named] of held) {
322
+ delete modules[sheet];
323
+ const listed = [...named].map((name) => `\`${name}\``).join(', ');
324
+ stylesheets
325
+ .find((s) => s.source === sheet)
326
+ ?.reports.push({
327
+ line: 0,
328
+ column: 0,
329
+ kind: 'held-for-consumer',
330
+ source: '',
331
+ hint: `${listed} ${named.size === 1 ? 'is' : 'are'} read by a consumer the migration cannot answer for, so nothing was written and no consumer was moved. Settle the rules above and run again.`,
332
+ });
333
+ }
304
334
  for (const sheet of stylesheets) {
305
335
  if (!modules[sheet.source])
306
336
  continue;
@@ -320,7 +350,7 @@ async function migrateFromCssModules(options) {
320
350
  }
321
351
  else {
322
352
  warnIfDirty();
323
- (0, migrate_1.write)(targets);
353
+ (0, migrate_1.write)(targets, new Set(held.keys()));
324
354
  const applied = await new eslint_1.ESLint({ ...config, fix: true }).lintFiles(targets);
325
355
  await eslint_1.ESLint.outputFixes(applied);
326
356
  const touched = applied.filter((r) => r.output !== undefined);
package/dist/migrate.d.ts CHANGED
@@ -12,5 +12,5 @@ export interface Plan {
12
12
  export declare function findStylesheets(targets: string[]): string[];
13
13
  export declare const targetPath: (stylesheet: string) => string;
14
14
  export declare function plan(targets: string[]): Plan;
15
- export declare function write(targets: string[]): Plan;
15
+ export declare function write(targets: string[], skip?: Set<string>): Plan;
16
16
  export declare function formatReports(sheets: Stylesheet[], cwd: string): string[];
package/dist/migrate.js CHANGED
@@ -103,18 +103,25 @@ function plan(targets) {
103
103
  modules[source] = {
104
104
  source: `./${path.basename(target, '.ts')}`,
105
105
  target,
106
+ stylesheet: source,
106
107
  names: converted.names,
108
+ order: converted.order,
109
+ held: converted.held,
107
110
  composes: converted.composes,
108
111
  functions: converted.functions,
112
+ tags: converted.tags,
113
+ unconvertible: converted.unconvertible,
109
114
  };
110
115
  }
111
116
  return { stylesheets, modules };
112
117
  }
113
- function write(targets) {
118
+ function write(targets, skip) {
114
119
  const result = plan(targets);
115
120
  for (const sheet of result.stylesheets) {
116
121
  if (!result.modules[sheet.source])
117
122
  continue;
123
+ if (skip?.has(sheet.source))
124
+ continue;
118
125
  const converted = (0, css_modules_1.convertStylesheet)(fs.readFileSync(sheet.source, 'utf-8'));
119
126
  fs.writeFileSync(sheet.target, converted.code, 'utf-8');
120
127
  }
@@ -3,8 +3,19 @@ export interface ModuleMap {
3
3
  source: string;
4
4
  target?: string;
5
5
  names: Record<string, string>;
6
+ order?: Record<string, number>;
7
+ held?: Record<string, import('../cascade').Held[]>;
6
8
  composes?: Record<string, string[]>;
7
9
  functions?: Record<string, string[]>;
10
+ tags?: {
11
+ key: string;
12
+ tag: string;
13
+ under: string;
14
+ direct?: boolean;
15
+ order?: number;
16
+ }[];
17
+ stylesheet?: string;
18
+ unconvertible?: string[];
8
19
  }
9
20
  export interface AdoptStylesOptions {
10
21
  modules: Record<string, ModuleMap>;
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.adoptStyles = void 0;
37
37
  const path = __importStar(require("node:path"));
38
38
  const resolve_1 = require("../resolve");
39
+ const cascade_1 = require("../cascade");
39
40
  const CORE = '@plumeria/core';
40
41
  const readOptions = (context) => {
41
42
  const raw = (context.options[0] ?? {});
@@ -61,6 +62,9 @@ exports.adoptStyles = {
61
62
  value: 'A style read as a value goes back through `css.use`.',
62
63
  joined: 'A joined class list is written as an array.',
63
64
  call: 'The custom properties are the arguments of a function style.',
65
+ tag: 'A rule reached this <{{tag}}> through a class; carry it as "{{key}}".',
66
+ missing: '{{stylesheet}} :: {{name}}',
67
+ cascade: '{{stylesheet}} :: {{name}}',
64
68
  },
65
69
  schema: [
66
70
  {
@@ -76,6 +80,7 @@ exports.adoptStyles = {
76
80
  create(context) {
77
81
  const { modules, styleProp } = readOptions(context);
78
82
  const bindings = new Map();
83
+ const tagged = new Set();
79
84
  let hasCore = false;
80
85
  let stylesTaken = false;
81
86
  let coreLocal;
@@ -182,6 +187,61 @@ exports.adoptStyles = {
182
187
  current.parent.callee?.type === 'MemberExpression' &&
183
188
  current.parent.callee.property?.name === 'use');
184
189
  };
190
+ const at = (element) => {
191
+ const read = styleRead(element);
192
+ const binding = bindings.get(read?.object?.name);
193
+ if (!binding)
194
+ return null;
195
+ const written = read.computed && read.property.type === 'Literal'
196
+ ? String(read.property.value)
197
+ : !read.computed && read.property.type === 'Identifier'
198
+ ? read.property.name
199
+ : null;
200
+ if (written === null)
201
+ return null;
202
+ const key = binding.entry.names[written] ?? written;
203
+ const place = binding.entry.order?.[key];
204
+ return place === undefined
205
+ ? null
206
+ : {
207
+ module: binding.entry,
208
+ place,
209
+ key,
210
+ entry: binding.entry,
211
+ };
212
+ };
213
+ const inStylesheetOrder = (elements) => {
214
+ const sorted = [...elements];
215
+ const groups = new Map();
216
+ elements.forEach((element, index) => {
217
+ const found = at(element);
218
+ if (!found)
219
+ return;
220
+ const slots = groups.get(found.module) ?? [];
221
+ slots.push(index);
222
+ groups.set(found.module, slots);
223
+ });
224
+ for (const slots of groups.values()) {
225
+ const ordered = slots
226
+ .map((index) => elements[index])
227
+ .sort((a, b) => at(a).place - at(b).place);
228
+ slots.forEach((index, at_) => (sorted[index] = ordered[at_]));
229
+ }
230
+ return sorted;
231
+ };
232
+ const unsettled = (held) => held.flatMap((mine, index) => held.slice(index + 1).flatMap((theirs) => {
233
+ if (mine.entry !== theirs.entry)
234
+ return [];
235
+ const left = mine.entry.held?.[mine.key];
236
+ const right = theirs.entry.held?.[theirs.key];
237
+ if (!left || !right)
238
+ return [];
239
+ return (0, cascade_1.unrepresentable)(left, right) ? [mine, theirs] : [];
240
+ }));
241
+ const readsOf = (elements) => elements.flatMap((element) => {
242
+ const found = at(element);
243
+ return found ? [{ entry: found.entry, key: found.key }] : [];
244
+ });
185
245
  return {
186
246
  Program(program) {
187
247
  hasCore = program.body.some((n) => n.type === 'ImportDeclaration' && n.source.value === CORE);
@@ -328,6 +388,16 @@ exports.adoptStyles = {
328
388
  });
329
389
  return;
330
390
  }
391
+ const entry = binding.entry;
392
+ if (entry.stylesheet &&
393
+ (entry.unconvertible?.includes(written) || !(written in entry.names))) {
394
+ context.report({
395
+ node: node.property,
396
+ messageId: 'missing',
397
+ data: { stylesheet: entry.stylesheet, name: written },
398
+ });
399
+ return;
400
+ }
331
401
  const key = binding.entry.names[written] ?? written;
332
402
  if (coreLocal && !stylingPosition(node) && !insideUse(node)) {
333
403
  context.report({
@@ -422,6 +492,131 @@ exports.adoptStyles = {
422
492
  ],
423
493
  });
424
494
  },
495
+ JSXElement(node) {
496
+ const scoped = [];
497
+ for (const attribute of node.openingElement?.attributes ?? []) {
498
+ if (attribute.type !== 'JSXAttribute')
499
+ continue;
500
+ const name = attribute.name?.name;
501
+ if (name !== 'className' && name !== styleProp)
502
+ continue;
503
+ if (attribute.value?.type !== 'JSXExpressionContainer')
504
+ continue;
505
+ const reads = [];
506
+ const gather = (expression) => {
507
+ if (!expression || typeof expression !== 'object')
508
+ return;
509
+ if (expression.type === 'MemberExpression') {
510
+ reads.push(expression);
511
+ return;
512
+ }
513
+ if (expression.type === 'ArrayExpression')
514
+ expression.elements.forEach(gather);
515
+ else if (expression.type === 'TemplateLiteral')
516
+ expression.expressions.forEach(gather);
517
+ else if (expression.type === 'LogicalExpression') {
518
+ gather(expression.left);
519
+ gather(expression.right);
520
+ }
521
+ else if (expression.type === 'ConditionalExpression') {
522
+ gather(expression.consequent);
523
+ gather(expression.alternate);
524
+ }
525
+ };
526
+ gather(attribute.value.expression);
527
+ for (const read of reads) {
528
+ const name_ = read.object?.name;
529
+ const binding = bindings.get(name_) ??
530
+ [...bindings.values()].find((b) => b.local === name_);
531
+ if (!binding)
532
+ continue;
533
+ const written = read.property?.name ?? read.property?.value;
534
+ if (written == null)
535
+ continue;
536
+ const key = binding.entry.names[written] ?? written;
537
+ for (const pair of binding.entry.tags ?? [])
538
+ if (pair.under === key)
539
+ scoped.push({
540
+ tag: pair.tag,
541
+ key: pair.key,
542
+ local: binding.local,
543
+ direct: pair.direct === true,
544
+ order: pair.order ?? 0,
545
+ });
546
+ }
547
+ }
548
+ if (!scoped.length)
549
+ return;
550
+ const walk = (children, depth) => {
551
+ for (const child of children ?? []) {
552
+ if (child.type !== 'JSXElement')
553
+ continue;
554
+ const opening = child.openingElement;
555
+ const named = opening?.name?.type === 'JSXIdentifier'
556
+ ? opening.name.name
557
+ : null;
558
+ const attributes = opening?.attributes ?? [];
559
+ const already = attributes.find((attribute) => attribute.type === 'JSXAttribute' &&
560
+ attribute.name?.name === styleProp &&
561
+ attribute.value?.type === 'JSXExpressionContainer');
562
+ const pending = attributes.some((attribute) => attribute.type === 'JSXAttribute' &&
563
+ attribute.name?.name === 'className');
564
+ const written = already?.value.expression.type === 'ArrayExpression'
565
+ ? already.value.expression.elements.filter(Boolean)
566
+ : [];
567
+ const carried = new Set(already
568
+ ? already.value.expression.type === 'ArrayExpression'
569
+ ? written.map((element) => context.sourceCode.getText(element))
570
+ : [context.sourceCode.getText(already.value.expression)]
571
+ : []);
572
+ for (const pair of scoped) {
573
+ if (named !== pair.tag || pending)
574
+ continue;
575
+ if (pair.direct && depth > 1)
576
+ continue;
577
+ const read = `${pair.local}.${pair.key}`;
578
+ if (carried.has(read))
579
+ continue;
580
+ if (tagged.has(opening))
581
+ continue;
582
+ tagged.add(opening);
583
+ const reads = new Map();
584
+ for (const { entry, local } of bindings.values())
585
+ for (const other of entry.tags ?? [])
586
+ reads.set(`${local}.${other.key}`, other.order ?? 0);
587
+ const placed = written.filter((element) => reads.has(context.sourceCode.getText(element)));
588
+ const before = placed.find((element) => (reads.get(context.sourceCode.getText(element)) ?? 0) >
589
+ pair.order);
590
+ const after = before ? undefined : placed[placed.length - 1];
591
+ context.report({
592
+ node: opening.name,
593
+ messageId: 'tag',
594
+ data: { tag: pair.tag, key: pair.key },
595
+ fix: (fixer) => {
596
+ if (!already)
597
+ return fixer.insertTextAfter(opening.name, ` ${styleProp}={${read}}`);
598
+ const expression = already.value.expression;
599
+ if (expression.type !== 'ArrayExpression') {
600
+ const held = context.sourceCode.getText(expression);
601
+ return fixer.replaceText(expression, reads.has(held)
602
+ ? `[${held}, ${read}]`
603
+ : `[${read}, ${held}]`);
604
+ }
605
+ if (before)
606
+ return fixer.insertTextBefore(before, `${read}, `);
607
+ if (after)
608
+ return fixer.insertTextAfter(after, `, ${read}`);
609
+ return written.length === 0
610
+ ? fixer.replaceText(expression, `[${read}]`)
611
+ : fixer.insertTextBefore(written[0], `${read}, `);
612
+ },
613
+ });
614
+ }
615
+ walk(child.children, depth + 1);
616
+ }
617
+ };
618
+ walk(node.children, 1);
619
+ },
425
620
  JSXAttribute(node) {
426
621
  if (node.name.type !== 'JSXIdentifier')
427
622
  return;
@@ -437,7 +632,18 @@ exports.adoptStyles = {
437
632
  return;
438
633
  const styles_ = joined.filter((element) => bindings.has(styleRead(element)?.object?.name));
439
634
  const carried = joined.filter((element) => !styles_.includes(element));
440
- const list = styles_.map(renamedText).join(', ');
635
+ const beyond = unsettled(readsOf(styles_));
636
+ if (beyond.length > 0) {
637
+ for (const { entry, key } of beyond)
638
+ if (entry.stylesheet)
639
+ context.report({
640
+ node,
641
+ messageId: 'cascade',
642
+ data: { stylesheet: entry.stylesheet, name: key },
643
+ });
644
+ return;
645
+ }
646
+ const list = inStylesheetOrder(styles_).map(renamedText).join(', ');
441
647
  if (carried.length > 0) {
442
648
  const kept = carried
443
649
  .map((element) => context.sourceCode.getText(element))
@@ -496,8 +702,20 @@ exports.adoptStyles = {
496
702
  const composed = key ? entry.composes?.[key] : undefined;
497
703
  if (!composed || composed.length === 0)
498
704
  return;
705
+ const beyond = unsettled(composed.concat(key).map((k) => ({ entry, key: k })));
706
+ if (beyond.length > 0) {
707
+ for (const held of beyond)
708
+ if (held.entry.stylesheet)
709
+ context.report({
710
+ node: expression,
711
+ messageId: 'cascade',
712
+ data: { stylesheet: held.entry.stylesheet, name: held.key },
713
+ });
714
+ return;
715
+ }
499
716
  const parts = composed
500
717
  .concat(key)
718
+ .sort((a, b) => (entry.order?.[a] ?? 0) - (entry.order?.[b] ?? 0))
501
719
  .map((k) => `${local}.${k}`)
502
720
  .join(', ');
503
721
  context.report({
@@ -1,3 +1,4 @@
1
+ import { type Held } from '../cascade';
1
2
  export interface Report {
2
3
  line: number;
3
4
  column: number;
@@ -10,8 +11,19 @@ export interface Converted {
10
11
  names: Record<string, string>;
11
12
  composes: Record<string, string[]>;
12
13
  functions: Record<string, string[]>;
14
+ tags: Tags[];
15
+ held: Record<string, Held[]>;
16
+ order: Record<string, number>;
17
+ unconvertible: string[];
13
18
  reports: Report[];
14
19
  }
20
+ export interface Tags {
21
+ key: string;
22
+ tag: string;
23
+ under: string;
24
+ direct: boolean;
25
+ order: number;
26
+ }
15
27
  interface Raw {
16
28
  raw: string;
17
29
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.toValue = exports.toKey = exports.toProperty = void 0;
7
7
  exports.convertStylesheet = convertStylesheet;
8
8
  const postcss_1 = __importDefault(require("postcss"));
9
+ const cascade_1 = require("../cascade");
9
10
  const postcss_selector_parser_1 = __importDefault(require("postcss-selector-parser"));
10
11
  const DOC = 'https://plumeria.dev/docs/api-reference/javascript/marker';
11
12
  const emptyNode = () => ({ decls: [], nested: [], markers: [] });
@@ -33,6 +34,7 @@ const toValue = (raw) => {
33
34
  return value;
34
35
  };
35
36
  exports.toValue = toValue;
37
+ const last = (names) => names[names.length - 1];
36
38
  const nest = (node, key) => {
37
39
  const found = node.nested.find(([k]) => k === key);
38
40
  if (found)
@@ -59,51 +61,43 @@ const readSelector = (selector) => {
59
61
  };
60
62
  (0, postcss_selector_parser_1.default)((root) => {
61
63
  const nodes = root.first.nodes.filter((n) => n.type !== 'string' && !isSpecificityBoost(n));
62
- const parts = [
63
- { pseudos: [] },
64
- ];
64
+ const parts = [{ classNames: [], pseudos: [] }];
65
65
  let combinator = null;
66
66
  let bad = null;
67
67
  for (const node of nodes) {
68
68
  if (node.type === 'class') {
69
- const current = parts[parts.length - 1];
70
- if (current.className) {
71
- bad = {
72
- kind: 'unsupported-selector',
73
- hint: 'Two classes on one element are written as an array at the call site.',
74
- };
75
- break;
76
- }
77
- current.className = node.value;
69
+ parts[parts.length - 1].classNames.push(node.value);
78
70
  }
79
71
  else if (node.type === 'pseudo') {
80
72
  if (node.value === ':global') {
81
- bad = {
82
- kind: 'global',
83
- hint: "A global class is written as a selector key: `':is(.name *)'`.",
84
- };
85
- break;
73
+ const inner = String(node)
74
+ .slice(':global'.length)
75
+ .replace(/^\(|\)$/g, '');
76
+ if (!inner) {
77
+ bad = {
78
+ kind: 'global',
79
+ hint: "A global class is written as a selector key: `':is(.name *)'`.",
80
+ };
81
+ break;
82
+ }
83
+ parts[parts.length - 1].global = inner;
84
+ continue;
86
85
  }
87
86
  parts[parts.length - 1].pseudos.push(String(node));
88
87
  }
88
+ else if (node.type === 'tag') {
89
+ parts[parts.length - 1].tag = node.value;
90
+ }
91
+ else if (node.type === 'attribute') {
92
+ const attr = node;
93
+ if (attr.value != null && attr.quoteMark === "'")
94
+ attr.quoteMark = '"';
95
+ parts[parts.length - 1].pseudos.push(`:is(${String(node).trim()})`);
96
+ }
89
97
  else if (node.type === 'combinator') {
90
98
  const value = node.value.trim();
91
- if (value === '+' || value === '~') {
92
- bad = {
93
- kind: 'sibling-combinator',
94
- hint: `A marker carries no order. Write the relation as a selector key: ${DOC}`,
95
- };
96
- break;
97
- }
98
- if (parts.length > 1) {
99
- bad = {
100
- kind: 'unsupported-selector',
101
- hint: 'Only one level of nesting is converted.',
102
- };
103
- break;
104
- }
105
- combinator = value === '>' ? '>' : ' ';
106
- parts.push({ pseudos: [] });
99
+ combinator = value === '>' ? '>' : value === ' ' ? ' ' : value;
100
+ parts.push({ classNames: [], pseudos: [], after: combinator });
107
101
  }
108
102
  else {
109
103
  bad = {
@@ -117,26 +111,64 @@ const readSelector = (selector) => {
117
111
  result = bad;
118
112
  return;
119
113
  }
120
- if (parts.some((p) => !p.className))
114
+ const globals = parts.filter((p) => p.global && !p.classNames.length);
115
+ if (globals.length) {
116
+ const rest = parts.filter((p) => !(p.global && !p.classNames.length));
117
+ if (rest.length !== 1 || !rest[0].classNames.length)
118
+ return;
119
+ const ancestors = globals.map((p) => `${p.global} `).join('');
120
+ result = {
121
+ target: {
122
+ key: (0, exports.toKey)(last(rest[0].classNames)),
123
+ pseudo: `:is(${ancestors}*)${rest[0].pseudos.join('')}`,
124
+ },
125
+ };
126
+ return;
127
+ }
128
+ if (parts.some((p) => p.after === '+' || p.after === '~')) {
129
+ result = {
130
+ kind: 'sibling-combinator',
131
+ hint: `A container query reaches descendants, never siblings, so no marker states this. Whether an ordinal such as \`:not(:first-child)\` is equivalent depends on the markup — write the relation as a selector key: ${DOC}`,
132
+ };
121
133
  return;
122
- const [first, second] = parts;
123
- if (!second) {
134
+ }
135
+ const tagged = parts[parts.length - 1];
136
+ if (parts.length > 1 &&
137
+ tagged.tag &&
138
+ !tagged.classNames.length &&
139
+ parts.slice(0, -1).every((p) => p.classNames.length)) {
140
+ const above = parts.slice(0, -1);
141
+ const relation = tagged.after === '>' ? 'child-' : '';
124
142
  result = {
125
143
  target: {
126
- key: (0, exports.toKey)(first.className),
127
- pseudo: first.pseudos.join(''),
144
+ key: (0, exports.toKey)(`${last(above[above.length - 1].classNames)}-${relation}${tagged.tag}`),
145
+ pseudo: tagged.pseudos.join(''),
146
+ tag: tagged.tag,
147
+ direct: tagged.after === '>',
148
+ ancestors: above.map((part) => ({
149
+ key: (0, exports.toKey)(last(part.classNames)),
150
+ pseudo: part.pseudos.join('') || ':defined',
151
+ })),
128
152
  },
129
153
  };
130
154
  return;
131
155
  }
156
+ if (parts.some((p) => !p.classNames.length))
157
+ return;
158
+ const target = parts[parts.length - 1];
159
+ const above = parts.slice(0, -1);
132
160
  result = {
133
161
  target: {
134
- key: (0, exports.toKey)(second.className),
135
- pseudo: second.pseudos.join(''),
136
- ancestor: {
137
- key: (0, exports.toKey)(first.className),
138
- pseudo: first.pseudos.join('') || ':defined',
139
- },
162
+ key: (0, exports.toKey)(last(target.classNames)),
163
+ pseudo: target.pseudos.join(''),
164
+ ...(above.length
165
+ ? {
166
+ ancestors: above.map((part) => ({
167
+ key: (0, exports.toKey)(last(part.classNames)),
168
+ pseudo: part.pseudos.join('') || ':defined',
169
+ })),
170
+ }
171
+ : {}),
140
172
  },
141
173
  };
142
174
  void combinator;
@@ -172,15 +204,26 @@ function convertStylesheet(css) {
172
204
  const root = postcss_1.default.parse(css);
173
205
  const keys = new Map();
174
206
  const names = {};
207
+ const candidates = {};
175
208
  const composes = {};
176
209
  const reports = [];
177
210
  const markerPseudos = new Map();
211
+ const tags = [];
212
+ const unconvertible = new Set();
213
+ const order = {};
214
+ const held = {};
215
+ let wrote = 0;
216
+ let seen = 0;
217
+ const written = [];
178
218
  const keyOf = (name) => {
179
219
  if (!keys.has(name))
180
220
  keys.set(name, emptyNode());
181
221
  return keys.get(name);
182
222
  };
183
223
  const report = (node, kind, hint) => {
224
+ const rule = node.type === 'rule' ? node : node.parent;
225
+ for (const found of rule?.selector?.match(/\.([A-Za-z0-9_-]+)/g) ?? [])
226
+ unconvertible.add(found.slice(1));
184
227
  const start = node.source?.start;
185
228
  reports.push({
186
229
  line: start?.line ?? 0,
@@ -201,23 +244,48 @@ function convertStylesheet(css) {
201
244
  for (const className of selector.match(/\.([A-Za-z0-9_-]+)/g) ?? []) {
202
245
  const bare = className.slice(1);
203
246
  const key = (0, exports.toKey)(bare);
204
- const taken = Object.entries(names).find(([other, k]) => k === key && other !== bare);
247
+ const taken = Object.entries(candidates).find(([other, k]) => k === key && other !== bare);
205
248
  if (taken) {
206
249
  report(rule, 'key-collision', `\`${bare}\` and \`${taken[0]}\` both become \`${key}\`.`);
207
250
  }
208
- names[bare] = key;
251
+ candidates[bare] = key;
209
252
  }
210
253
  let node = keyOf(target.key);
211
- if (target.ancestor) {
212
- const { key: id, pseudo } = target.ancestor;
213
- const seen = markerPseudos.get(id) ?? new Set();
214
- if (!seen.has(pseudo)) {
215
- seen.add(pseudo);
216
- markerPseudos.set(id, seen);
217
- keyOf(id).markers.push(`css.marker('${id}', '${pseudo}')`);
254
+ if (target.tag && target.ancestors) {
255
+ const under = target.ancestors[target.ancestors.length - 1].key;
256
+ if (!tags.some((t) => t.key === target.key &&
257
+ t.tag === target.tag &&
258
+ t.direct === (target.direct === true)))
259
+ tags.push({
260
+ key: target.key,
261
+ tag: target.tag,
262
+ under,
263
+ direct: target.direct === true,
264
+ order: tags.length,
265
+ });
266
+ }
267
+ if (target.ancestors) {
268
+ let gate = null;
269
+ for (const { key: id, pseudo } of target.ancestors) {
270
+ const signature = `${gate ?? ''}>${pseudo}`;
271
+ const seen = markerPseudos.get(id) ?? new Set();
272
+ if (!seen.has(signature)) {
273
+ seen.add(signature);
274
+ markerPseudos.set(id, seen);
275
+ const holder = gate ? nest(keyOf(id), gate) : keyOf(id);
276
+ holder.markers.push(`css.marker('${id}', '${pseudo}')`);
277
+ }
278
+ gate = `css.extended('${id}', '${pseudo}')`;
218
279
  }
219
- node = nest(node, `css.extended('${id}', '${pseudo}')`);
280
+ node = nest(node, gate);
220
281
  }
282
+ const place = seen++;
283
+ order[target.key] = place;
284
+ const props = new Set();
285
+ const where = `${atRuleKeys(rule).join('|')}::${target.pseudo}`;
286
+ written.push({ key: target.key, place, props });
287
+ const conditional = atRuleKeys(rule).length > 0;
288
+ const mine = (held[target.key] ??= []);
221
289
  for (const key of atRuleKeys(rule))
222
290
  node = nest(node, key);
223
291
  if (target.pseudo)
@@ -228,13 +296,20 @@ function convertStylesheet(css) {
228
296
  if (child.prop === 'composes') {
229
297
  const [name, from] = child.value.split(/\s+from\s+/);
230
298
  if (from) {
231
- report(child, 'composes-external', 'A class composed from another file is not resolved.');
299
+ report(child, 'composes-external', 'A class composed from another file is not resolved. Convert that stylesheet too, then pass both at the call site as an array.');
232
300
  return;
233
301
  }
234
302
  composes[target.key] = (composes[target.key] ?? []).concat(name.split(/\s+/).map(exports.toKey));
235
303
  return;
236
304
  }
237
305
  const property = (0, exports.toProperty)(child.prop);
306
+ props.add(`${where}::${property}`);
307
+ mine.push({
308
+ property: child.prop,
309
+ suffix: target.pseudo,
310
+ rank: (0, cascade_1.rankOf)(child.prop, conditional),
311
+ place: wrote++,
312
+ });
238
313
  const held = node.decls.findIndex(([name]) => name === property);
239
314
  if (held !== -1)
240
315
  node.decls.splice(held, 1);
@@ -291,5 +366,75 @@ export const styles = css.create({
291
366
  ${body}
292
367
  });
293
368
  `;
294
- return { code, names, composes, functions, reports };
369
+ for (const [key, declarations] of Object.entries(held)) {
370
+ const beaten = (0, cascade_1.unrepresentable)(declarations, declarations);
371
+ if (beaten) {
372
+ reports.push({
373
+ line: 0,
374
+ column: 0,
375
+ kind: 'rank-order',
376
+ source: '',
377
+ hint: `${beaten.earlier.property === beaten.later.property
378
+ ? `\`${key}\` sets \`${beaten.later.property}\` after setting it where Plumeria ranks it higher — an at-rule is one step up.`
379
+ : `\`${key}\` sets \`${beaten.later.property}\` after \`${beaten.earlier.property}\`, which Plumeria ranks above it.`} CSS gives the property to the later declaration and Plumeria cannot, and one key has no order to settle that with. Write the higher-ranked declaration last.`,
380
+ });
381
+ for (const [name, candidate] of Object.entries(candidates))
382
+ if (candidate === key)
383
+ unconvertible.add(name);
384
+ }
385
+ }
386
+ for (const entry of written) {
387
+ const earlier = written.filter((other) => other.key === entry.key && other.place < entry.place);
388
+ if (earlier.length === 0)
389
+ continue;
390
+ for (const between of written) {
391
+ if (between.key === entry.key)
392
+ continue;
393
+ if (between.place > entry.place)
394
+ continue;
395
+ const above = new Set(earlier
396
+ .filter((other) => other.place < between.place)
397
+ .flatMap((other) => [...other.props]));
398
+ const shared = [...between.props].filter((prop) => above.has(prop));
399
+ if (shared.length === 0)
400
+ continue;
401
+ reports.push({
402
+ line: 0,
403
+ column: 0,
404
+ kind: 'split-order',
405
+ source: '',
406
+ hint: `\`${entry.key}\` is written on both sides of \`${between.key}\` and they share a property, so one place in the array cannot say which wins. Write the class once.`,
407
+ });
408
+ for (const [bare, key] of Object.entries(candidates))
409
+ if (key === entry.key || key === between.key)
410
+ unconvertible.add(bare);
411
+ }
412
+ }
413
+ for (const pair of tags) {
414
+ const taken = Object.entries(candidates).find(([, key]) => key === pair.key);
415
+ if (!taken)
416
+ continue;
417
+ reports.push({
418
+ line: 0,
419
+ column: 0,
420
+ kind: 'key-collision',
421
+ source: '',
422
+ hint: `\`${taken[0]}\` and the rule reaching \`${pair.tag}\` both become \`${pair.key}\`.`,
423
+ });
424
+ unconvertible.add(taken[0]);
425
+ }
426
+ for (const [bare, key] of Object.entries(candidates))
427
+ if (keys.has(key))
428
+ names[bare] = key;
429
+ return {
430
+ code,
431
+ names,
432
+ order,
433
+ held,
434
+ composes,
435
+ functions,
436
+ tags,
437
+ unconvertible: [...unconvertible],
438
+ reports,
439
+ };
295
440
  }
@@ -6,57 +6,22 @@ exports.extractPlumeriaStatics = extractPlumeriaStatics;
6
6
  exports.extractPlumeriaAnimations = extractPlumeriaAnimations;
7
7
  exports.convertPlumeriaModule = convertPlumeriaModule;
8
8
  const parser_1 = require("@typescript-eslint/parser");
9
+ const cascade_1 = require("../cascade");
9
10
  const zss_engine_1 = require("zss-engine");
10
11
  const UNITLESS = new Set(zss_engine_1.exceptionCamelCase.map((property) => (0, zss_engine_1.camelToKebabCase)(property)));
11
12
  const compositionSignature = (parts) => parts.map((part) => `${part.target}#${part.className}`).join('|');
12
13
  exports.compositionSignature = compositionSignature;
13
14
  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
15
  const covers = (outer, inner) => {
47
16
  if (outer === inner)
48
17
  return false;
49
- const wide = coverageOf(outer);
50
- const narrow = coverageOf(inner);
18
+ const wide = (0, cascade_1.coverageOf)(outer);
19
+ const narrow = (0, cascade_1.coverageOf)(inner);
51
20
  return wide.size > narrow.size && [...narrow].every((leaf) => wide.has(leaf));
52
21
  };
53
- const overlaps = (first, second) => {
54
- const left = coverageOf(first);
55
- return [...coverageOf(second)].some((leaf) => left.has(leaf));
56
- };
57
22
  const byRank = (written) => {
58
23
  const outranked = (later, earlier) => (later.held ?? []).some((mine) => (earlier.held ?? []).some((theirs) => later.suffix === earlier.suffix &&
59
- overlaps(mine.property, theirs.property) &&
24
+ (0, cascade_1.overlaps)(mine.property, theirs.property) &&
60
25
  mine.rank > theirs.rank));
61
26
  const ordered = [];
62
27
  const pending = [...written];
@@ -530,7 +495,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = [], o
530
495
  for (const theirs of right) {
531
496
  if (mine.suffix !== theirs.suffix)
532
497
  continue;
533
- if (!overlaps(mine.property, theirs.property))
498
+ if (!(0, cascade_1.overlaps)(mine.property, theirs.property))
534
499
  continue;
535
500
  if (mine.rank > theirs.rank)
536
501
  ranked ??= [second, first];
@@ -618,7 +583,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = [], o
618
583
  bucket: bucketOf(wrappers, suffix),
619
584
  suffix,
620
585
  property: cssProperty,
621
- rank: depthOf(cssProperty) + (wrappers.length > 0 ? 1 : 0),
586
+ rank: (0, cascade_1.depthOf)(cssProperty) + (wrappers.length > 0 ? 1 : 0),
622
587
  });
623
588
  }
624
589
  declared.push({
@@ -646,7 +611,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = [], o
646
611
  if (phase !== 'nested')
647
612
  pushRule(selector, byContainment(declared), declared.map((entry) => ({
648
613
  property: entry.property,
649
- rank: depthOf(entry.property) + (wrappers.length > 0 ? 1 : 0),
614
+ rank: (0, cascade_1.depthOf)(entry.property) + (wrappers.length > 0 ? 1 : 0),
650
615
  })));
651
616
  if (phase === 'declarations')
652
617
  return;
@@ -846,7 +811,7 @@ function convertPlumeriaModule(source, externalValues = {}, compositions = [], o
846
811
  for (const entry of mine) {
847
812
  const crossed = theirs.some((candidate) => candidate.suffix === entry.suffix &&
848
813
  candidate.rank === entry.rank &&
849
- overlaps(entry.property, candidate.property));
814
+ (0, cascade_1.overlaps)(entry.property, candidate.property));
850
815
  if (!crossed)
851
816
  continue;
852
817
  const held = disputed.get(entry.bucket) ?? new Set();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plumeria/codemod",
3
- "version": "18.2.26",
4
- "description": "Codemods for migrating Plumeria APIs",
3
+ "version": "18.2.28",
4
+ "description": "Codemods for Plumeria: migrate a CSS Modules codebase in, export it back out, and rename the styling prop",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/refirst11",
@@ -18,6 +18,7 @@
18
18
  "plumeria",
19
19
  "codemod",
20
20
  "migration",
21
+ "css-modules",
21
22
  "eslint",
22
23
  "css-in-js"
23
24
  ],