@plumeria/codemod 18.2.19 → 18.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,6 +134,7 @@ export function Card({ width }: { width: number }) {
134
134
  /* Card.tsx — after */
135
135
  - import * as css from '@plumeria/core';
136
136
  + import styles from './Card.module.css';
137
+ + export { styles };
137
138
 
138
139
  - export const styles = css.create({
139
140
  - base: { fontSize: 12 },
@@ -143,14 +144,13 @@ export function Card({ width }: { width: number }) {
143
144
  - },
144
145
  - size: (width: number) => ({ width }),
145
146
  - });
146
- + export { styles };
147
147
 
148
148
  export function Card({ width }: { width: number }) {
149
149
  - return <div classStyle={[styles.base, styles.card, styles.size(width)]} />;
150
150
  + return (
151
151
  + <div
152
- + className={`${styles.base} ${styles.card} ${styles.size}`}
153
- + style={{ '--styles-size-width': width }}
152
+ + className={[styles.base, styles.card, styles.size].join(' ')}
153
+ + style={{ ['--styles-size-width' as string]: width }}
154
154
  + />
155
155
  + );
156
156
  }
@@ -162,15 +162,41 @@ equivalent, so each parameter becomes a CSS custom property named
162
162
  values move to the inline `style` prop. If a `style` prop already exists, the
163
163
  custom properties are merged into it.
164
164
 
165
- An array on the styling prop is joined into a template literal. When the array
166
- contains a conditional expression, it uses `.filter(Boolean).join(' ')` instead,
167
- so the falsy branch drops out at runtime:
165
+ An array with no condition in it collapses into one class that `composes` its
166
+ members, so nothing is duplicated:
168
167
 
169
168
  ```diff
169
+ - <div classStyle={[styles.base, styles.card]} />
170
+ + <div className={styles.baseCard} />
171
+ ```
172
+
173
+ Otherwise the array is joined as it stands. `filter` is added only when a
174
+ condition can make a member falsy, so that branch drops out at runtime:
175
+
176
+ ```diff
177
+ - <div classStyle={[styles.base, styles.card, styles.size(width)]} />
178
+ + <div className={[styles.base, styles.card, styles.size].join(' ')} style={{ … }} />
179
+
170
180
  - <div classStyle={[styles.base, active && styles.active]} />
171
181
  + <div className={[styles.base, active && styles.active].filter(Boolean).join(' ')} />
172
182
  ```
173
183
 
184
+ `classStyle={[b, a]}` gives `a` the last word, while a stylesheet gives it to
185
+ whichever rule is written later. The export reproduces the array order by
186
+ emitting the classes in an order that satisfies every call site at once. Where
187
+ two call sites compose the same pair in opposite orders, the one that cannot be
188
+ satisfied carries an extra class holding only the disputed declarations, applied
189
+ under the same condition as the style that has to win:
190
+
191
+ ```diff
192
+ - <div classStyle={[styles.raised, flat && styles.surface]} />
193
+ + <div className={[styles.raised, flat && styles.surface, flat && styles.surfaceOverRaised].filter(Boolean).join(' ')} />
194
+ ```
195
+
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`.
199
+
174
200
  ### Global styles
175
201
 
176
202
  `css.createTheme`, `css.keyframes`, and `css.viewTransition` produce rules that
@@ -195,12 +221,25 @@ src/Card.tsx
195
221
  ```
196
222
 
197
223
  Constructs that cannot be resolved statically — dynamic values, computed keys,
198
- runtime function calls, object spreads, and multiple `css.create` calls in one
199
- file — are reported and left in place. A target `*.module.css` that already
200
- exists is reported as `target-exists` and will not be overwritten.
224
+ runtime function calls, and object spreads — are reported and left in place,
225
+ together with every file whose definitions they still need. A target
226
+ `*.module.css` that already exists is reported as `target-exists` and will not be
227
+ overwritten.
228
+
229
+ Several `css.create` calls in one file are not reported: they are written to the
230
+ same stylesheet, and a key an earlier call claimed is renamed.
201
231
 
202
232
  The exit code is 1 while anything remains, so the command composes with a script.
203
233
 
234
+ The migration is idempotent — the first run converts, every run after it is a
235
+ fixed point:
236
+
237
+ ```
238
+ f(f(x)) = f(x)
239
+ ```
240
+
241
+ Clear a report and run again, and only the newly resolvable files move.
242
+
204
243
  Start with `-d, --dry-run` to preview the export without writing files:
205
244
 
206
245
  ```sh
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) {
334
+ function releaseConfig(modules, themes, animations, statics, values, complete) {
335
335
  return [
336
336
  { ignores: IGNORES },
337
337
  {
@@ -347,7 +347,7 @@ function releaseConfig(modules, themes, animations, statics) {
347
347
  rules: {
348
348
  'codemod/release-styles': [
349
349
  'error',
350
- { modules, themes, animations, statics },
350
+ { modules, themes, animations, statics, values, complete },
351
351
  ],
352
352
  },
353
353
  },
@@ -365,6 +365,8 @@ async function migrateFromPlumeria(options) {
365
365
  return 0;
366
366
  }
367
367
  for (const sheet of planned.stylesheets) {
368
+ if (sheet.reports.length > 0)
369
+ continue;
368
370
  console.log(` ${relative(sheet.source)} -> ${relative(sheet.target)}`);
369
371
  }
370
372
  if (planned.global) {
@@ -372,7 +374,7 @@ async function migrateFromPlumeria(options) {
372
374
  }
373
375
  const config = {
374
376
  overrideConfigFile: true,
375
- overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations, planned.statics),
377
+ overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations, planned.statics, planned.values, planned.complete),
376
378
  errorOnUnmatchedPattern: false,
377
379
  };
378
380
  const preview = await new eslint_1.ESLint(config).lintFiles(targets);
package/dist/migrate.js CHANGED
@@ -83,10 +83,12 @@ function plan(targets) {
83
83
  const converted = (0, css_modules_1.convertStylesheet)(fs.readFileSync(source, 'utf-8'));
84
84
  const target = (0, exports.targetPath)(source);
85
85
  stylesheets.push({ source, target, reports: converted.reports });
86
- modules[path.basename(source)] = {
86
+ modules[source] = {
87
87
  source: `./${path.basename(target, '.ts')}`,
88
+ target,
88
89
  names: converted.names,
89
90
  composes: converted.composes,
91
+ functions: converted.functions,
90
92
  };
91
93
  }
92
94
  return { stylesheets, modules };
package/dist/release.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface ReleasePlan {
12
12
  themes: Record<string, string[]>;
13
13
  animations: Record<string, string[]>;
14
14
  statics: Record<string, string[]>;
15
+ values: Record<string, Record<string, unknown>>;
16
+ complete: boolean;
15
17
  global?: {
16
18
  target: string;
17
19
  css: string;
package/dist/release.js CHANGED
@@ -42,6 +42,8 @@ const path = __importStar(require("node:path"));
42
42
  const parser_1 = require("@typescript-eslint/parser");
43
43
  const resolve_1 = require("./resolve");
44
44
  const from_plumeria_1 = require("./transforms/from-plumeria");
45
+ const GLOBAL_OPEN = '/* Generated from css.createTheme, css.keyframes, and css.viewTransition by @plumeria/codemod */';
46
+ const GLOBAL_CLOSE = '/* End of @plumeria/codemod generated styles */';
45
47
  const EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
46
48
  const IGNORED = new Set([
47
49
  'node_modules',
@@ -75,6 +77,44 @@ const findSources = (targets) => {
75
77
  }
76
78
  return found.sort();
77
79
  };
80
+ const STYLE_PROP = 'classStyle';
81
+ const memberOf = (node) => {
82
+ if (node?.type !== 'MemberExpression' || node.object.type !== 'Identifier')
83
+ return undefined;
84
+ const key = node.computed
85
+ ? node.property.type === 'Literal'
86
+ ? String(node.property.value)
87
+ : undefined
88
+ : node.property.name;
89
+ return key === undefined ? undefined : { object: node.object.name, key };
90
+ };
91
+ const slotOf = (node) => {
92
+ const member = memberOf(node);
93
+ if (member)
94
+ return [member];
95
+ if (node?.type === 'LogicalExpression')
96
+ return slotOf(node.right);
97
+ if (node?.type === 'ConditionalExpression')
98
+ return [...slotOf(node.consequent), ...slotOf(node.alternate)];
99
+ return [];
100
+ };
101
+ const walk = (node, visit) => {
102
+ if (!node || typeof node !== 'object')
103
+ return;
104
+ if (Array.isArray(node)) {
105
+ for (const child of node)
106
+ walk(child, visit);
107
+ return;
108
+ }
109
+ if (typeof node.type !== 'string')
110
+ return;
111
+ visit(node);
112
+ for (const key of Object.keys(node)) {
113
+ if (key === 'parent' || key === 'loc' || key === 'range')
114
+ continue;
115
+ walk(node[key], visit);
116
+ }
117
+ };
78
118
  const releasedPath = (source) => source.replace(/(?:\.styles)?\.[^.]+$/, '.module.css');
79
119
  exports.releasedPath = releasedPath;
80
120
  const globalPath = (targets) => {
@@ -95,12 +135,34 @@ function planRelease(targets) {
95
135
  const themes = {};
96
136
  const animations = {};
97
137
  const statics = {};
98
- const globalRules = [];
99
138
  const sources = findSources(targets);
100
139
  const sourceText = new Map(sources.map((source) => [source, fs.readFileSync(source, 'utf8')]));
101
140
  const themeValues = new Map();
102
141
  const animationValues = new Map();
103
142
  const staticValues = new Map();
143
+ const reportsBySource = new Map();
144
+ const globalBySource = new Map();
145
+ const themeKeys = new Map();
146
+ const animationKeys = new Map();
147
+ const staticKeys = new Map();
148
+ const dependencies = new Map();
149
+ const note = (source, reports) => {
150
+ if (reports.length === 0)
151
+ return;
152
+ reportsBySource.set(source, [
153
+ ...(reportsBySource.get(source) ?? []),
154
+ ...reports,
155
+ ]);
156
+ };
157
+ const addGlobal = (source, css) => {
158
+ const trimmed = css.trim();
159
+ if (trimmed.length === 0)
160
+ return;
161
+ globalBySource.set(source, [
162
+ ...(globalBySource.get(source) ?? []),
163
+ trimmed,
164
+ ]);
165
+ };
104
166
  for (const source of sources) {
105
167
  try {
106
168
  const extracted = (0, from_plumeria_1.extractPlumeriaStatics)(sourceText.get(source));
@@ -116,8 +178,6 @@ function planRelease(targets) {
116
178
  if (Object.keys(extracted.bindings).length === 0)
117
179
  continue;
118
180
  themeValues.set(source, extracted.bindings);
119
- themes[source] = Object.keys(extracted.bindings);
120
- globalRules.push(extracted.globalCss.trim());
121
181
  }
122
182
  catch {
123
183
  }
@@ -152,11 +212,14 @@ function planRelease(targets) {
152
212
  for (const specifier of node.specifiers) {
153
213
  if (specifier.type === 'ImportSpecifier') {
154
214
  const imported = specifier.imported.name ?? specifier.imported.value;
155
- if (available[imported])
215
+ if (available[imported]) {
156
216
  result[specifier.local.name] = available[imported];
217
+ dependencies.set(source, (dependencies.get(source) ?? new Set()).add(target));
218
+ }
157
219
  }
158
220
  else if (specifier.type === 'ImportNamespaceSpecifier') {
159
221
  result[specifier.local.name] = available;
222
+ dependencies.set(source, (dependencies.get(source) ?? new Set()).add(target));
160
223
  }
161
224
  }
162
225
  }
@@ -178,13 +241,26 @@ function planRelease(targets) {
178
241
  }
179
242
  return merged;
180
243
  };
244
+ for (const source of sources) {
245
+ try {
246
+ const extracted = (0, from_plumeria_1.extractPlumeriaThemes)(sourceText.get(source));
247
+ note(source, extracted.reports);
248
+ if (Object.keys(extracted.bindings).length === 0)
249
+ continue;
250
+ themeKeys.set(source, Object.keys(extracted.bindings));
251
+ addGlobal(source, extracted.globalCss);
252
+ }
253
+ catch {
254
+ }
255
+ }
181
256
  for (const source of sources) {
182
257
  try {
183
258
  const extracted = (0, from_plumeria_1.extractPlumeriaStatics)(sourceText.get(source), importedPlumeriaValues(source));
259
+ note(source, extracted.reports);
184
260
  if (Object.keys(extracted.bindings).length === 0)
185
261
  continue;
186
262
  staticValues.set(source, extracted.bindings);
187
- statics[source] = Object.keys(extracted.bindings);
263
+ staticKeys.set(source, Object.keys(extracted.bindings));
188
264
  }
189
265
  catch {
190
266
  }
@@ -195,59 +271,231 @@ function planRelease(targets) {
195
271
  ...importedPlumeriaValues(source),
196
272
  ...(themeValues.get(source) ?? {}),
197
273
  });
274
+ note(source, extracted.reports);
198
275
  if (Object.keys(extracted.bindings).length === 0)
199
276
  continue;
200
277
  animationValues.set(source, extracted.bindings);
201
- animations[source] = [
278
+ animationKeys.set(source, [
202
279
  ...extracted.keyframes,
203
280
  ...extracted.viewTransitions,
204
- ];
205
- globalRules.push(extracted.globalCss.trim());
281
+ ]);
282
+ addGlobal(source, extracted.globalCss);
206
283
  }
207
284
  catch {
208
285
  }
209
286
  }
287
+ const createBindings = new Map();
210
288
  for (const source of sources) {
211
- let converted;
212
289
  try {
213
- converted = (0, from_plumeria_1.convertPlumeriaModule)(sourceText.get(source), importedPlumeriaValues(source));
290
+ const ast = (0, parser_1.parse)(sourceText.get(source), {
291
+ sourceType: 'module',
292
+ ecmaFeatures: { jsx: true },
293
+ });
294
+ const declared = new Set();
295
+ for (const raw of ast.body) {
296
+ const statement = raw.type === 'ExportNamedDeclaration' ? raw.declaration : raw;
297
+ if (statement?.type !== 'VariableDeclaration')
298
+ continue;
299
+ for (const declaration of statement.declarations) {
300
+ if (declaration.id.type === 'Identifier' &&
301
+ declaration.init?.type === 'CallExpression' &&
302
+ declaration.init.callee.type === 'MemberExpression' &&
303
+ declaration.init.callee.property?.name === 'create')
304
+ declared.add(declaration.id.name);
305
+ }
306
+ }
307
+ if (declared.size > 0)
308
+ createBindings.set(source, declared);
309
+ }
310
+ catch {
311
+ }
312
+ }
313
+ const compositions = new Map();
314
+ for (const source of sources) {
315
+ let ast;
316
+ try {
317
+ ast = (0, parser_1.parse)(sourceText.get(source), {
318
+ sourceType: 'module',
319
+ ecmaFeatures: { jsx: true },
320
+ loc: true,
321
+ range: true,
322
+ });
214
323
  }
215
324
  catch {
216
325
  continue;
217
326
  }
218
- if (!converted)
327
+ const owners = new Map();
328
+ for (const binding of createBindings.get(source) ?? [])
329
+ owners.set(binding, { source, binding });
330
+ for (const node of ast.body) {
331
+ if (node.type !== 'ImportDeclaration')
332
+ continue;
333
+ const target = (0, resolve_1.resolveSourcePath)(String(node.source.value), source);
334
+ const declared = target ? createBindings.get(target) : undefined;
335
+ if (!target || !declared)
336
+ continue;
337
+ for (const specifier of node.specifiers) {
338
+ if (specifier.type !== 'ImportSpecifier')
339
+ continue;
340
+ const imported = specifier.imported.name ?? specifier.imported.value;
341
+ if (declared.has(imported))
342
+ owners.set(specifier.local.name, {
343
+ source: target,
344
+ binding: imported,
345
+ });
346
+ }
347
+ }
348
+ walk(ast.body, (node) => {
349
+ if (node.type !== 'MemberExpression' ||
350
+ !node.computed ||
351
+ node.property.type === 'Literal' ||
352
+ node.object.type !== 'Identifier')
353
+ return;
354
+ const owner = owners.get(node.object.name);
355
+ if (!owner)
356
+ return;
357
+ note(owner.source, [
358
+ {
359
+ line: node.loc.start.line,
360
+ column: node.loc.start.column + 1,
361
+ kind: 'dynamic-style-access',
362
+ hint: `\`${node.object.name}\` is read with a computed key, which has no class name until it runs.`,
363
+ },
364
+ ]);
365
+ });
366
+ walk(ast.body, (node) => {
367
+ if (node.type !== 'JSXAttribute' ||
368
+ node.name?.name !== STYLE_PROP ||
369
+ node.value?.type !== 'JSXExpressionContainer')
370
+ return;
371
+ const expression = node.value.expression;
372
+ if (expression?.type !== 'ArrayExpression' ||
373
+ expression.elements.length < 2)
374
+ return;
375
+ const slots = expression.elements.map(slotOf);
376
+ if (slots.some((slot) => slot.length === 0))
377
+ return;
378
+ const owners_ = slots.flat().map((part) => owners.get(part.object));
379
+ if (owners_.some((candidate) => !candidate))
380
+ return;
381
+ const owner = owners_[0].source;
382
+ if (owners_.some((candidate) => candidate.source !== owner))
383
+ return;
384
+ const named = slots.map((slot) => slot.map((part) => ({
385
+ binding: owners.get(part.object).binding,
386
+ key: part.key,
387
+ })));
388
+ const collapsible = expression.elements.every((element) => memberOf(element)) &&
389
+ named.every((slot) => slot.length === 1);
390
+ compositions.set(owner, [
391
+ ...(compositions.get(owner) ?? []),
392
+ {
393
+ parts: collapsible ? named.map((slot) => slot[0]) : [],
394
+ slots: named,
395
+ file: source,
396
+ line: node.loc.start.line,
397
+ column: node.loc.start.column + 1,
398
+ },
399
+ ]);
400
+ });
401
+ }
402
+ const converted = new Map();
403
+ for (const source of sources) {
404
+ let module;
405
+ try {
406
+ module = (0, from_plumeria_1.convertPlumeriaModule)(sourceText.get(source), importedPlumeriaValues(source), compositions.get(source) ?? []);
407
+ }
408
+ catch {
409
+ continue;
410
+ }
411
+ if (!module)
219
412
  continue;
220
413
  const target = (0, exports.releasedPath)(source);
221
- const specifier = `./${path.basename(target)}`;
222
- const reports = [...converted.reports];
414
+ converted.set(source, module);
415
+ note(source, module.reports);
223
416
  if (fs.existsSync(target)) {
224
- reports.push({
225
- line: 0,
226
- column: 0,
227
- kind: 'target-exists',
228
- hint: `${path.basename(target)} already exists and was not overwritten.`,
417
+ note(source, [
418
+ {
419
+ line: 0,
420
+ column: 0,
421
+ kind: 'target-exists',
422
+ hint: `${path.basename(target)} already exists and was not overwritten.`,
423
+ },
424
+ ]);
425
+ }
426
+ }
427
+ const blocked = new Set(reportsBySource.keys());
428
+ for (let changed = true; changed;) {
429
+ changed = false;
430
+ for (const source of blocked) {
431
+ for (const dependency of dependencies.get(source) ?? []) {
432
+ if (blocked.has(dependency))
433
+ continue;
434
+ blocked.add(dependency);
435
+ note(dependency, [
436
+ {
437
+ line: 0,
438
+ column: 0,
439
+ kind: 'blocked-dependency',
440
+ hint: `${path.relative(path.dirname(dependency), source)} still needs these definitions.`,
441
+ },
442
+ ]);
443
+ changed = true;
444
+ }
445
+ }
446
+ }
447
+ const globalRules = [];
448
+ const values = {};
449
+ for (const source of sources) {
450
+ const reports = reportsBySource.get(source) ?? [];
451
+ const module = converted.get(source);
452
+ if (module) {
453
+ stylesheets.push({
454
+ source,
455
+ target: (0, exports.releasedPath)(source),
456
+ css: module.css,
457
+ reports,
229
458
  });
230
459
  }
231
- stylesheets.push({
232
- source,
233
- target,
234
- css: converted.css,
235
- reports,
236
- });
237
- if (reports.length === 0) {
238
- modules[source] = {
239
- source: specifier,
240
- target,
241
- binding: converted.binding,
242
- ...(converted.definitionOnly ? { definitionOnly: true } : {}),
243
- ...(Object.keys(converted.aliases).length > 0
244
- ? { aliases: converted.aliases }
245
- : {}),
246
- ...(Object.keys(converted.functions).length > 0
247
- ? { functions: converted.functions }
248
- : {}),
249
- };
460
+ else if (reports.length > 0) {
461
+ stylesheets.push({ source, target: '', css: '', reports });
250
462
  }
463
+ if (blocked.has(source))
464
+ continue;
465
+ const resolved = {
466
+ ...(staticValues.get(source) ?? {}),
467
+ ...(themeValues.get(source) ?? {}),
468
+ ...(animationValues.get(source) ?? {}),
469
+ };
470
+ if (Object.keys(resolved).length > 0)
471
+ values[source] = resolved;
472
+ globalRules.push(...(globalBySource.get(source) ?? []));
473
+ if (themeKeys.has(source))
474
+ themes[source] = themeKeys.get(source);
475
+ if (animationKeys.has(source))
476
+ animations[source] = animationKeys.get(source);
477
+ if (staticKeys.has(source))
478
+ statics[source] = staticKeys.get(source);
479
+ if (!module)
480
+ continue;
481
+ modules[source] = {
482
+ source: `./${path.basename((0, exports.releasedPath)(source))}`,
483
+ target: (0, exports.releasedPath)(source),
484
+ binding: module.binding,
485
+ ...(module.definitionOnly ? { definitionOnly: true } : {}),
486
+ ...(Object.keys(module.aliases).length > 0
487
+ ? { aliases: module.aliases }
488
+ : {}),
489
+ ...(Object.keys(module.functions).length > 0
490
+ ? { functions: module.functions }
491
+ : {}),
492
+ ...(Object.keys(module.merges).length > 0
493
+ ? { merges: module.merges }
494
+ : {}),
495
+ ...(Object.keys(module.overrides).length > 0
496
+ ? { overrides: module.overrides }
497
+ : {}),
498
+ };
251
499
  }
252
500
  return {
253
501
  stylesheets,
@@ -255,10 +503,12 @@ function planRelease(targets) {
255
503
  themes,
256
504
  animations,
257
505
  statics,
506
+ values,
507
+ complete: stylesheets.every((sheet) => sheet.reports.length === 0),
258
508
  global: globalRules.length > 0
259
509
  ? {
260
510
  target: globalPath(targets),
261
- css: `/* Generated from css.createTheme, css.keyframes, and css.viewTransition by @plumeria/codemod */\n${globalRules.join('\n\n')}\n`,
511
+ css: `${GLOBAL_OPEN}\n${globalRules.join('\n\n')}\n${GLOBAL_CLOSE}\n`,
262
512
  }
263
513
  : undefined,
264
514
  };
@@ -274,6 +524,13 @@ function writeRelease(plan) {
274
524
  const previous = fs.existsSync(plan.global.target)
275
525
  ? fs.readFileSync(plan.global.target, 'utf8')
276
526
  : '';
527
+ const opening = previous.indexOf(GLOBAL_OPEN);
528
+ const closing = previous.lastIndexOf(GLOBAL_CLOSE);
529
+ if (opening !== -1 && closing > opening) {
530
+ const kept = `${previous.slice(0, opening)}${previous.slice(closing + GLOBAL_CLOSE.length)}`;
531
+ fs.writeFileSync(plan.global.target, `${kept.replace(/\n+$/, '\n')}${plan.global.css}`, 'utf8');
532
+ return;
533
+ }
277
534
  const separator = previous.length > 0 && !previous.endsWith('\n') ? '\n\n' : '';
278
535
  fs.writeFileSync(plan.global.target, `${previous}${separator}${plan.global.css}`, 'utf8');
279
536
  }
@@ -1,8 +1,10 @@
1
1
  import type { Rule } from 'eslint';
2
2
  export interface ModuleMap {
3
3
  source: string;
4
+ target?: string;
4
5
  names: Record<string, string>;
5
6
  composes?: Record<string, string[]>;
7
+ functions?: Record<string, string[]>;
6
8
  }
7
9
  export interface AdoptStylesOptions {
8
10
  modules: Record<string, ModuleMap>;