@plumeria/codemod 18.2.18 → 18.2.19

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/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) {
334
+ function releaseConfig(modules, themes, animations, statics) {
335
335
  return [
336
336
  { ignores: IGNORES },
337
337
  {
@@ -345,7 +345,10 @@ function releaseConfig(modules, themes, animations) {
345
345
  },
346
346
  plugins: { codemod: { rules: { 'release-styles': release_styles_1.releaseStyles } } },
347
347
  rules: {
348
- 'codemod/release-styles': ['error', { modules, themes, animations }],
348
+ 'codemod/release-styles': [
349
+ 'error',
350
+ { modules, themes, animations, statics },
351
+ ],
349
352
  },
350
353
  },
351
354
  ];
@@ -369,7 +372,7 @@ async function migrateFromPlumeria(options) {
369
372
  }
370
373
  const config = {
371
374
  overrideConfigFile: true,
372
- overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations),
375
+ overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations, planned.statics),
373
376
  errorOnUnmatchedPattern: false,
374
377
  };
375
378
  const preview = await new eslint_1.ESLint(config).lintFiles(targets);
package/dist/release.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface ReleasePlan {
11
11
  modules: Record<string, ReleaseModule>;
12
12
  themes: Record<string, string[]>;
13
13
  animations: Record<string, string[]>;
14
+ statics: Record<string, string[]>;
14
15
  global?: {
15
16
  target: string;
16
17
  css: string;
package/dist/release.js CHANGED
@@ -40,6 +40,7 @@ exports.formatReleaseReports = formatReleaseReports;
40
40
  const fs = __importStar(require("node:fs"));
41
41
  const path = __importStar(require("node:path"));
42
42
  const parser_1 = require("@typescript-eslint/parser");
43
+ const resolve_1 = require("./resolve");
43
44
  const from_plumeria_1 = require("./transforms/from-plumeria");
44
45
  const EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
45
46
  const IGNORED = new Set([
@@ -74,7 +75,7 @@ const findSources = (targets) => {
74
75
  }
75
76
  return found.sort();
76
77
  };
77
- const releasedPath = (source) => source.replace(/\.[^.]+$/, '.module.css');
78
+ const releasedPath = (source) => source.replace(/(?:\.styles)?\.[^.]+$/, '.module.css');
78
79
  exports.releasedPath = releasedPath;
79
80
  const globalPath = (targets) => {
80
81
  const first = path.resolve(targets[0]);
@@ -93,11 +94,22 @@ function planRelease(targets) {
93
94
  const modules = {};
94
95
  const themes = {};
95
96
  const animations = {};
97
+ const statics = {};
96
98
  const globalRules = [];
97
99
  const sources = findSources(targets);
98
100
  const sourceText = new Map(sources.map((source) => [source, fs.readFileSync(source, 'utf8')]));
99
101
  const themeValues = new Map();
100
102
  const animationValues = new Map();
103
+ const staticValues = new Map();
104
+ for (const source of sources) {
105
+ try {
106
+ const extracted = (0, from_plumeria_1.extractPlumeriaStatics)(sourceText.get(source));
107
+ if (Object.keys(extracted.bindings).length > 0)
108
+ staticValues.set(source, extracted.bindings);
109
+ }
110
+ catch {
111
+ }
112
+ }
101
113
  for (const source of sources) {
102
114
  try {
103
115
  const extracted = (0, from_plumeria_1.extractPlumeriaThemes)(sourceText.get(source));
@@ -121,11 +133,6 @@ function planRelease(targets) {
121
133
  catch {
122
134
  }
123
135
  }
124
- const sourceForImport = (owner, specifier, candidates) => {
125
- const resolved = path.resolve(path.dirname(owner), specifier);
126
- const stem = resolved.replace(/\.[^.]+$/, '');
127
- return [...candidates].find((candidate) => candidate.replace(/\.[^.]+$/, '') === stem);
128
- };
129
136
  const importedValues = (source, availableBySource) => {
130
137
  const result = {};
131
138
  const ast = (0, parser_1.parse)(sourceText.get(source), {
@@ -134,13 +141,14 @@ function planRelease(targets) {
134
141
  });
135
142
  for (const node of ast.body) {
136
143
  if (node.type !== 'ImportDeclaration' ||
137
- typeof node.source.value !== 'string' ||
138
- !node.source.value.startsWith('.'))
144
+ typeof node.source.value !== 'string')
139
145
  continue;
140
- const target = sourceForImport(source, node.source.value, availableBySource.keys());
146
+ const target = (0, resolve_1.resolveSourcePath)(node.source.value, source);
141
147
  if (!target)
142
148
  continue;
143
149
  const available = availableBySource.get(target);
150
+ if (!available)
151
+ continue;
144
152
  for (const specifier of node.specifiers) {
145
153
  if (specifier.type === 'ImportSpecifier') {
146
154
  const imported = specifier.imported.name ?? specifier.imported.value;
@@ -154,22 +162,33 @@ function planRelease(targets) {
154
162
  }
155
163
  return result;
156
164
  };
157
- const importedThemes = (source) => importedValues(source, themeValues);
158
- const importedAnimations = (source) => importedValues(source, animationValues);
159
165
  const importedPlumeriaValues = (source) => {
160
- const merged = { ...importedThemes(source) };
161
- for (const [key, value] of Object.entries(importedAnimations(source))) {
162
- const previous = merged[key];
163
- merged[key] =
164
- previous &&
165
- typeof previous === 'object' &&
166
- value &&
167
- typeof value === 'object'
168
- ? { ...previous, ...value }
169
- : value;
166
+ const merged = {};
167
+ for (const table of [staticValues, themeValues, animationValues]) {
168
+ for (const [key, value] of Object.entries(importedValues(source, table))) {
169
+ const previous = merged[key];
170
+ merged[key] =
171
+ previous &&
172
+ typeof previous === 'object' &&
173
+ value &&
174
+ typeof value === 'object'
175
+ ? { ...previous, ...value }
176
+ : value;
177
+ }
170
178
  }
171
179
  return merged;
172
180
  };
181
+ for (const source of sources) {
182
+ try {
183
+ const extracted = (0, from_plumeria_1.extractPlumeriaStatics)(sourceText.get(source), importedPlumeriaValues(source));
184
+ if (Object.keys(extracted.bindings).length === 0)
185
+ continue;
186
+ staticValues.set(source, extracted.bindings);
187
+ statics[source] = Object.keys(extracted.bindings);
188
+ }
189
+ catch {
190
+ }
191
+ }
173
192
  for (const source of sources) {
174
193
  try {
175
194
  const extracted = (0, from_plumeria_1.extractPlumeriaAnimations)(sourceText.get(source), {
@@ -218,7 +237,12 @@ function planRelease(targets) {
218
237
  if (reports.length === 0) {
219
238
  modules[source] = {
220
239
  source: specifier,
240
+ target,
221
241
  binding: converted.binding,
242
+ ...(converted.definitionOnly ? { definitionOnly: true } : {}),
243
+ ...(Object.keys(converted.aliases).length > 0
244
+ ? { aliases: converted.aliases }
245
+ : {}),
222
246
  ...(Object.keys(converted.functions).length > 0
223
247
  ? { functions: converted.functions }
224
248
  : {}),
@@ -230,6 +254,7 @@ function planRelease(targets) {
230
254
  modules,
231
255
  themes,
232
256
  animations,
257
+ statics,
233
258
  global: globalRules.length > 0
234
259
  ? {
235
260
  target: globalPath(targets),
@@ -0,0 +1,2 @@
1
+ export declare const resetTsconfigCache: () => void;
2
+ export declare function resolveSourcePath(specifier: string, importer: string): string | null;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resetTsconfigCache = void 0;
37
+ exports.resolveSourcePath = resolveSourcePath;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const get_tsconfig_1 = require("get-tsconfig");
41
+ const EXTENSIONS = [
42
+ '.ts',
43
+ '.tsx',
44
+ '.mts',
45
+ '.cts',
46
+ '.js',
47
+ '.jsx',
48
+ '.mjs',
49
+ '.cjs',
50
+ ];
51
+ const CANDIDATES = [
52
+ ...EXTENSIONS,
53
+ ...EXTENSIONS.map((extension) => `${path.sep}index${extension}`),
54
+ ];
55
+ const REWRITTEN = {
56
+ '.js': ['.ts', '.tsx'],
57
+ '.jsx': ['.tsx'],
58
+ '.mjs': ['.mts'],
59
+ '.cjs': ['.cts'],
60
+ };
61
+ let cachedMatcher;
62
+ const pathsMatcher = () => {
63
+ if (cachedMatcher === undefined) {
64
+ const tsconfig = (0, get_tsconfig_1.getTsconfig)(process.cwd());
65
+ cachedMatcher = tsconfig ? (0, get_tsconfig_1.createPathsMatcher)(tsconfig) : null;
66
+ }
67
+ return cachedMatcher;
68
+ };
69
+ const resetTsconfigCache = () => {
70
+ cachedMatcher = undefined;
71
+ };
72
+ exports.resetTsconfigCache = resetTsconfigCache;
73
+ const isFile = (candidate) => fs.statSync(candidate, { throwIfNoEntry: false })?.isFile() ?? false;
74
+ const resolveFile = (base) => {
75
+ if (isFile(base))
76
+ return base;
77
+ const extension = path.extname(base);
78
+ for (const rewritten of REWRITTEN[extension] ?? []) {
79
+ const candidate = `${base.slice(0, -extension.length)}${rewritten}`;
80
+ if (isFile(candidate))
81
+ return candidate;
82
+ }
83
+ for (const candidate of CANDIDATES) {
84
+ if (isFile(base + candidate))
85
+ return base + candidate;
86
+ }
87
+ return null;
88
+ };
89
+ function resolveSourcePath(specifier, importer) {
90
+ if (specifier === '@plumeria/core')
91
+ return null;
92
+ if (specifier.startsWith('.')) {
93
+ return resolveFile(path.resolve(path.dirname(importer), specifier));
94
+ }
95
+ const matcher = pathsMatcher();
96
+ if (!matcher)
97
+ return null;
98
+ for (const candidate of matcher(specifier)) {
99
+ const resolved = resolveFile(candidate);
100
+ if (resolved)
101
+ return resolved;
102
+ }
103
+ return null;
104
+ }
@@ -13,6 +13,8 @@ export interface PlumeriaModule {
13
13
  params: string[];
14
14
  variables: string[];
15
15
  }>;
16
+ aliases: Record<string, Record<string, string>>;
17
+ definitionOnly: boolean;
16
18
  reports: PlumeriaReport[];
17
19
  }
18
20
  export interface ThemeExtraction {
@@ -20,6 +22,10 @@ export interface ThemeExtraction {
20
22
  globalCss: string;
21
23
  reports: PlumeriaReport[];
22
24
  }
25
+ export interface StaticExtraction {
26
+ bindings: Record<string, Record<string, unknown>>;
27
+ reports: PlumeriaReport[];
28
+ }
23
29
  export interface AnimationExtraction {
24
30
  bindings: Record<string, string>;
25
31
  keyframes: string[];
@@ -31,7 +37,9 @@ export declare const __private: {
31
37
  propertyKey: (node: any) => string | undefined;
32
38
  memberPath: (node: any) => string[] | undefined;
33
39
  evaluate: (node: any, values: Map<string, unknown>) => unknown;
40
+ bindsElsewhere: (ast: any, name: string, declared: any) => boolean;
34
41
  };
35
42
  export declare function extractPlumeriaThemes(source: string): ThemeExtraction;
43
+ export declare function extractPlumeriaStatics(source: string, externalValues?: Record<string, unknown>): StaticExtraction;
36
44
  export declare function extractPlumeriaAnimations(source: string, externalValues?: Record<string, unknown>): AnimationExtraction;
37
45
  export declare function convertPlumeriaModule(source: string, externalValues?: Record<string, unknown>): PlumeriaModule | null;
@@ -2,10 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.__private = void 0;
4
4
  exports.extractPlumeriaThemes = extractPlumeriaThemes;
5
+ exports.extractPlumeriaStatics = extractPlumeriaStatics;
5
6
  exports.extractPlumeriaAnimations = extractPlumeriaAnimations;
6
7
  exports.convertPlumeriaModule = convertPlumeriaModule;
7
8
  const parser_1 = require("@typescript-eslint/parser");
8
9
  const zss_engine_1 = require("zss-engine");
10
+ const toIdent = (value) => value.replace(/[^A-Za-z0-9-]/g, '');
11
+ const markerVariable = (id, pseudo) => {
12
+ const hash = (0, zss_engine_1.genBase36Hash)({ [id]: pseudo }, 1, 8);
13
+ return `--${hash}-${toIdent(id)}-${toIdent(pseudo.split('(')[0])}`;
14
+ };
9
15
  const propertyKey = (node) => {
10
16
  if (!node)
11
17
  return undefined;
@@ -35,9 +41,27 @@ const lookup = (path, values) => {
35
41
  }
36
42
  return value;
37
43
  };
44
+ const markerArguments = (node, method, values) => {
45
+ if (node?.type !== 'CallExpression')
46
+ return undefined;
47
+ const path = memberPath(node.callee);
48
+ if (path?.length !== 2 || path[1] !== method)
49
+ return undefined;
50
+ const id = evaluate(node.arguments[0], values);
51
+ const pseudo = evaluate(node.arguments[1], values);
52
+ return typeof id === 'string' && typeof pseudo === 'string'
53
+ ? { id, pseudo }
54
+ : undefined;
55
+ };
38
56
  const evaluate = (node, values) => {
39
57
  if (!node)
40
58
  return undefined;
59
+ if (node.type === 'CallExpression') {
60
+ const extended = markerArguments(node, 'extended', values);
61
+ return extended
62
+ ? `@container style(${markerVariable(extended.id, extended.pseudo)}: 1)`
63
+ : undefined;
64
+ }
41
65
  if (node.type === 'Literal')
42
66
  return node.value;
43
67
  if (node.type === 'Identifier')
@@ -78,7 +102,29 @@ const evaluate = (node, values) => {
78
102
  }
79
103
  return undefined;
80
104
  };
81
- exports.__private = { propertyKey, memberPath, evaluate };
105
+ const bindsElsewhere = (ast, name, declared) => {
106
+ const skipped = new Set(['loc', 'range', 'parent']);
107
+ const visit = (node, parent) => {
108
+ if (!node || typeof node !== 'object' || node === declared)
109
+ return false;
110
+ if (Array.isArray(node))
111
+ return node.some((child) => visit(child, parent));
112
+ if (typeof node.type !== 'string')
113
+ return false;
114
+ if (node.type === 'Identifier' && node.name === name) {
115
+ if (parent?.type === 'ExportSpecifier')
116
+ return false;
117
+ if (parent?.type === 'MemberExpression' && parent.property === node)
118
+ return !!parent.computed;
119
+ if (parent?.type === 'Property' && parent.key === node)
120
+ return !!parent.computed;
121
+ return true;
122
+ }
123
+ return Object.keys(node).some((key) => !skipped.has(key) && visit(node[key], node));
124
+ };
125
+ return visit(ast, undefined);
126
+ };
127
+ exports.__private = { propertyKey, memberPath, evaluate, bindsElsewhere };
82
128
  const isCall = (node, method) => {
83
129
  if (node?.type !== 'CallExpression')
84
130
  return false;
@@ -192,6 +238,41 @@ const emitGlobalObject = (selector, object) => {
192
238
  const content = [...declarations, ...children].join('\n');
193
239
  return `${selector} {\n${content}\n}`;
194
240
  };
241
+ function extractPlumeriaStatics(source, externalValues = {}) {
242
+ const ast = (0, parser_1.parse)(source, {
243
+ sourceType: 'module',
244
+ ecmaFeatures: { jsx: true },
245
+ loc: true,
246
+ range: true,
247
+ });
248
+ const values = new Map(Object.entries(externalValues));
249
+ const bindings = {};
250
+ const reports = [];
251
+ for (const raw of ast.body) {
252
+ const statement = declarationOf(raw);
253
+ if (statement?.type !== 'VariableDeclaration')
254
+ continue;
255
+ for (const declaration of statement.declarations) {
256
+ if (declaration.id.type !== 'Identifier' ||
257
+ !isCall(declaration.init, 'createStatic'))
258
+ continue;
259
+ const value = evaluate(declaration.init.arguments[0], values);
260
+ if (value && typeof value === 'object') {
261
+ values.set(declaration.id.name, value);
262
+ bindings[declaration.id.name] = value;
263
+ }
264
+ else {
265
+ reports.push({
266
+ line: declaration.init.loc.start.line,
267
+ column: declaration.init.loc.start.column + 1,
268
+ kind: 'dynamic-create-static',
269
+ hint: '`createStatic` must contain values that can be written into CSS.',
270
+ });
271
+ }
272
+ }
273
+ }
274
+ return { bindings, reports };
275
+ }
195
276
  function extractPlumeriaAnimations(source, externalValues = {}) {
196
277
  const ast = (0, parser_1.parse)(source, {
197
278
  sourceType: 'module',
@@ -311,7 +392,7 @@ function convertPlumeriaModule(source, externalValues = {}) {
311
392
  if (isCall(declaration.init, 'create')) {
312
393
  const object = declaration.init.arguments[0];
313
394
  if (object?.type === 'ObjectExpression') {
314
- creates.push({ binding: declaration.id.name, object });
395
+ creates.push({ binding: declaration.id.name, object, declaration });
315
396
  }
316
397
  else {
317
398
  report(declaration.init, 'dynamic-create', '`css.create` must receive an object literal.');
@@ -321,13 +402,22 @@ function convertPlumeriaModule(source, externalValues = {}) {
321
402
  }
322
403
  if (creates.length === 0)
323
404
  return null;
324
- if (creates.length > 1) {
325
- report(creates[1].object, 'multiple-create', 'Move each `css.create` call to its own module before exporting to CSS Modules.');
326
- }
327
- const { binding, object } = creates[0];
405
+ const { binding } = creates[0];
328
406
  const rules = [];
329
407
  const keys = [];
408
+ const aliases = {};
409
+ const taken = new Set();
330
410
  const functions = {};
411
+ const classNameFor = (owner, key) => {
412
+ if (!taken.has(key))
413
+ return key;
414
+ const namespaced = `${owner}-${key}`;
415
+ let candidate = namespaced;
416
+ let suffix = 2;
417
+ while (taken.has(candidate))
418
+ candidate = `${namespaced}-${suffix++}`;
419
+ return candidate;
420
+ };
331
421
  const emit = (style, selector, wrappers = [], scope = values) => {
332
422
  if (style?.type !== 'ObjectExpression') {
333
423
  report(style, 'dynamic-style', 'Only static style objects are exported.');
@@ -335,7 +425,18 @@ function convertPlumeriaModule(source, externalValues = {}) {
335
425
  }
336
426
  const declarations = [];
337
427
  const nested = [];
428
+ const markers = [];
338
429
  for (const property of style.properties) {
430
+ if (property.type === 'SpreadElement') {
431
+ const marker = markerArguments(property.argument, 'marker', scope);
432
+ if (marker) {
433
+ markers.push({
434
+ pseudo: marker.pseudo,
435
+ variable: markerVariable(marker.id, marker.pseudo),
436
+ });
437
+ continue;
438
+ }
439
+ }
339
440
  if (property.type !== 'Property' || property.kind !== 'init') {
340
441
  report(property, 'spread-style', 'Spread styles must be expanded before exporting to CSS Modules.');
341
442
  continue;
@@ -361,8 +462,10 @@ function convertPlumeriaModule(source, externalValues = {}) {
361
462
  const cssProperty = (0, zss_engine_1.camelToKebabCase)(rawKey);
362
463
  declarations.push(` ${cssProperty}: ${(0, zss_engine_1.applyCssValue)(value, cssProperty)};`);
363
464
  }
364
- if (declarations.length > 0) {
365
- let rule = `${selector} {\n${declarations.join('\n')}\n}`;
465
+ const pushRule = (target, lines) => {
466
+ if (lines.length === 0)
467
+ return;
468
+ let rule = `${target} {\n${lines.join('\n')}\n}`;
366
469
  for (const wrapper of [...wrappers].reverse()) {
367
470
  rule = `${wrapper} {\n${rule
368
471
  .split('\n')
@@ -370,6 +473,10 @@ function convertPlumeriaModule(source, externalValues = {}) {
370
473
  .join('\n')}\n}`;
371
474
  }
372
475
  rules.push(rule);
476
+ };
477
+ pushRule(selector, declarations);
478
+ for (const marker of markers) {
479
+ pushRule(`${selector}${marker.pseudo}`, [` ${marker.variable}: 1;`]);
373
480
  }
374
481
  for (const child of nested) {
375
482
  if (child.key.startsWith('@')) {
@@ -380,50 +487,60 @@ function convertPlumeriaModule(source, externalValues = {}) {
380
487
  }
381
488
  }
382
489
  };
383
- for (const property of object.properties) {
384
- if (property.type !== 'Property' || property.kind !== 'init') {
385
- report(property, 'spread-create', 'Top-level spreads cannot name a CSS Module class.');
386
- continue;
387
- }
388
- const key = property.computed
389
- ? evaluate(property.key, values)
390
- : propertyKey(property.key);
391
- if (typeof key !== 'string') {
392
- report(property.key, 'dynamic-style-key', 'Style names must be statically known.');
393
- continue;
394
- }
395
- if (property.value.type === 'ArrowFunctionExpression' ||
396
- property.value.type === 'FunctionExpression') {
397
- const params = [];
398
- let valid = true;
399
- for (const parameter of property.value.params) {
400
- if (parameter.type !== 'Identifier') {
401
- valid = false;
402
- break;
403
- }
404
- params.push(parameter.name);
490
+ for (const create of creates) {
491
+ const owner = create.binding;
492
+ const alias = {};
493
+ for (const property of create.object.properties) {
494
+ if (property.type !== 'Property' || property.kind !== 'init') {
495
+ report(property, 'spread-create', 'Top-level spreads cannot name a CSS Module class.');
496
+ continue;
405
497
  }
406
- let body = property.value.body;
407
- if (body.type === 'BlockStatement') {
408
- const returned = body.body.find((statement) => statement.type === 'ReturnStatement');
409
- body = returned?.argument;
498
+ const key = property.computed
499
+ ? evaluate(property.key, values)
500
+ : propertyKey(property.key);
501
+ if (typeof key !== 'string') {
502
+ report(property.key, 'dynamic-style-key', 'Style names must be statically known.');
503
+ continue;
410
504
  }
411
- if (!valid || body?.type !== 'ObjectExpression') {
412
- report(property.value, 'function-style', 'Function style keys need identifier parameters and an object return value.');
505
+ const className = classNameFor(owner, key);
506
+ taken.add(className);
507
+ if (owner !== binding)
508
+ alias[key] = className;
509
+ if (property.value.type === 'ArrowFunctionExpression' ||
510
+ property.value.type === 'FunctionExpression') {
511
+ const params = [];
512
+ let valid = true;
513
+ for (const parameter of property.value.params) {
514
+ if (parameter.type !== 'Identifier') {
515
+ valid = false;
516
+ break;
517
+ }
518
+ params.push(parameter.name);
519
+ }
520
+ let body = property.value.body;
521
+ if (body.type === 'BlockStatement') {
522
+ const returned = body.body.find((statement) => statement.type === 'ReturnStatement');
523
+ body = returned?.argument;
524
+ }
525
+ if (!valid || body?.type !== 'ObjectExpression') {
526
+ report(property.value, 'function-style', 'Function style keys need identifier parameters and an object return value.');
527
+ continue;
528
+ }
529
+ const variables = params.map((parameter) => `--${(0, zss_engine_1.camelToKebabCase)(owner)}-${(0, zss_engine_1.camelToKebabCase)(key)}-${(0, zss_engine_1.camelToKebabCase)(parameter)}`);
530
+ const scope = new Map(values);
531
+ params.forEach((parameter, index) => {
532
+ scope.set(parameter, `var(${variables[index]})`);
533
+ });
534
+ functions[className] = { params, variables };
535
+ keys.push(className);
536
+ emit(body, `.${className}`, [], scope);
413
537
  continue;
414
538
  }
415
- const variables = params.map((parameter) => `--${(0, zss_engine_1.camelToKebabCase)(binding)}-${(0, zss_engine_1.camelToKebabCase)(key)}-${(0, zss_engine_1.camelToKebabCase)(parameter)}`);
416
- const scope = new Map(values);
417
- params.forEach((parameter, index) => {
418
- scope.set(parameter, `var(${variables[index]})`);
419
- });
420
- functions[key] = { params, variables };
421
- keys.push(key);
422
- emit(body, `.${key}`, [], scope);
423
- continue;
539
+ keys.push(className);
540
+ emit(property.value, `.${className}`);
424
541
  }
425
- keys.push(key);
426
- emit(property.value, `.${key}`);
542
+ if (owner !== binding)
543
+ aliases[owner] = alias;
427
544
  }
428
545
  return {
429
546
  binding,
@@ -431,6 +548,8 @@ function convertPlumeriaModule(source, externalValues = {}) {
431
548
  globalCss: `${themes.globalCss}${animations.globalCss}`,
432
549
  keys,
433
550
  functions,
551
+ aliases,
552
+ definitionOnly: creates.every((create) => !bindsElsewhere(ast, create.binding, create.declaration.id)),
434
553
  reports,
435
554
  };
436
555
  }
@@ -1,7 +1,10 @@
1
1
  import type { Rule } from 'eslint';
2
2
  export interface ReleaseModule {
3
3
  source: string;
4
+ target?: string;
4
5
  binding: string;
6
+ definitionOnly?: boolean;
7
+ aliases?: Record<string, Record<string, string>>;
5
8
  functions?: Record<string, {
6
9
  params: string[];
7
10
  variables: string[];
@@ -11,6 +14,7 @@ export interface ReleaseStylesOptions {
11
14
  modules: Record<string, ReleaseModule>;
12
15
  themes?: Record<string, string[]>;
13
16
  animations?: Record<string, string[]>;
17
+ statics?: Record<string, string[]>;
14
18
  styleProp?: string;
15
19
  active?: boolean;
16
20
  }
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.releaseStyles = exports.__private = void 0;
37
37
  const path = __importStar(require("node:path"));
38
+ const resolve_1 = require("../resolve");
38
39
  const memberPath = (node) => {
39
40
  if (node?.type === 'Identifier')
40
41
  return [node.name];
@@ -54,6 +55,7 @@ const isCreate = (node) => {
54
55
  const path = memberPath(node.callee);
55
56
  return path?.length === 2 && path[1] === 'create';
56
57
  };
58
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
57
59
  const needsFilter = (node) => node.type === 'LogicalExpression' ||
58
60
  node.type === 'ConditionalExpression' ||
59
61
  node.type === 'SpreadElement';
@@ -72,6 +74,8 @@ exports.releaseStyles = {
72
74
  themeImport: 'Remove the createTheme import after inlining its CSS variables.',
73
75
  animation: '`keyframes` or `viewTransition` was written globally.',
74
76
  animationImport: 'Remove the animation import after replacing it with its generated name.',
77
+ staticImport: 'Remove the createStatic import after inlining its values into the CSS.',
78
+ alias: 'Read this style from the CSS Module the whole file was exported to.',
75
79
  core: 'Remove the Plumeria import after exporting the styles.',
76
80
  prop: 'Use className after exporting this style to CSS Modules.',
77
81
  composition: 'Join the composed CSS Module class names.',
@@ -85,6 +89,7 @@ exports.releaseStyles = {
85
89
  modules: { type: 'object' },
86
90
  themes: { type: 'object' },
87
91
  animations: { type: 'object' },
92
+ statics: { type: 'object' },
88
93
  styleProp: { type: 'string' },
89
94
  active: { type: 'boolean' },
90
95
  },
@@ -94,43 +99,80 @@ exports.releaseStyles = {
94
99
  },
95
100
  create(context) {
96
101
  const raw = (context.options[0] ?? {});
97
- const entry = raw.modules?.[context.filename];
102
+ const declared = raw.modules?.[context.filename];
103
+ const entry = declared?.definitionOnly ? undefined : declared;
98
104
  const themeBindings = raw.themes?.[context.filename] ?? [];
99
105
  const animationBindings = raw.animations?.[context.filename] ?? [];
100
106
  const styleProp = raw.styleProp ?? 'classStyle';
101
107
  const sourceCode = context.sourceCode;
102
108
  let active = Boolean(entry || raw.active);
103
109
  let activeModule = entry;
104
- const importedReleasedModule = (specifier) => {
105
- if (!specifier.startsWith('.'))
106
- return undefined;
107
- const resolved = path.resolve(path.dirname(context.filename), specifier);
108
- const withoutExtension = resolved.replace(/\.[^.]+$/, '');
109
- const source = Object.keys(raw.modules ?? {}).find((candidate) => candidate.replace(/\.[^.]+$/, '') === withoutExtension);
110
- return source ? raw.modules?.[source] : undefined;
110
+ const specifierFor = (module) => {
111
+ if (!module.target)
112
+ return module.source;
113
+ const relative = path
114
+ .relative(path.dirname(context.filename), module.target)
115
+ .split(path.sep)
116
+ .join('/');
117
+ return relative.startsWith('.') ? relative : `./${relative}`;
111
118
  };
112
- const importedThemeBindings = (specifier) => {
113
- if (!specifier.startsWith('.'))
114
- return undefined;
115
- const resolved = path.resolve(path.dirname(context.filename), specifier);
116
- const withoutExtension = resolved.replace(/\.[^.]+$/, '');
117
- const source = Object.keys(raw.themes ?? {}).find((candidate) => candidate.replace(/\.[^.]+$/, '') === withoutExtension);
118
- return source ? raw.themes?.[source] : undefined;
119
+ const importOf = (specifiers, source) => {
120
+ const defaultImport = specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
121
+ const namedImports = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier');
122
+ const clauses = [];
123
+ if (defaultImport)
124
+ clauses.push(sourceCode.getText(defaultImport));
125
+ if (namedImports.length > 0) {
126
+ clauses.push(`{ ${namedImports
127
+ .map((specifier) => sourceCode.getText(specifier))
128
+ .join(', ')} }`);
129
+ }
130
+ return `import ${clauses.join(', ')} from ${sourceCode.getText(source)};`;
119
131
  };
120
- const importedAnimationBindings = (specifier) => {
121
- if (!specifier.startsWith('.'))
132
+ const importedFrom = (specifier, table) => {
133
+ const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
134
+ return resolved ? table?.[resolved] : undefined;
135
+ };
136
+ const importedReleasedModule = (specifier) => importedFrom(specifier, raw.modules);
137
+ const importedThemeBindings = (specifier) => importedFrom(specifier, raw.themes);
138
+ const importedAnimationBindings = (specifier) => importedFrom(specifier, raw.animations);
139
+ const importedStaticBindings = (specifier) => importedFrom(specifier, raw.statics);
140
+ const releasedRange = () => {
141
+ if (!entry)
122
142
  return undefined;
123
- const resolved = path.resolve(path.dirname(context.filename), specifier);
124
- const withoutExtension = resolved.replace(/\.[^.]+$/, '');
125
- const source = Object.keys(raw.animations ?? {}).find((candidate) => candidate.replace(/\.[^.]+$/, '') === withoutExtension);
126
- return source ? raw.animations?.[source] : undefined;
143
+ for (const statement of sourceCode.ast.body) {
144
+ const declaration = statement.type === 'ExportNamedDeclaration'
145
+ ? statement.declaration
146
+ : statement;
147
+ if (declaration?.type !== 'VariableDeclaration')
148
+ continue;
149
+ for (const candidate of declaration.declarations) {
150
+ if (candidate.id.type === 'Identifier' &&
151
+ candidate.id.name === entry.binding &&
152
+ isCreate(candidate.init))
153
+ return statement.range;
154
+ }
155
+ }
156
+ return undefined;
127
157
  };
128
- const removeImportedBindings = (node, bindings, messageId) => {
158
+ const onlyUsedByReleasedStyles = (local) => {
159
+ const range = releasedRange();
160
+ if (!range)
161
+ return false;
162
+ const scope = sourceCode.getScope(sourceCode.ast);
163
+ const moduleScope = scope.type === 'global' ? (scope.childScopes[0] ?? scope) : scope;
164
+ const variable = moduleScope.variables.find((candidate) => candidate.name === local);
165
+ if (!variable || variable.references.length === 0)
166
+ return false;
167
+ return variable.references.every((reference) => reference.identifier.range[0] >= range[0] &&
168
+ reference.identifier.range[1] <= range[1]);
169
+ };
170
+ const removeImportedBindings = (node, bindings, messageId, dead) => {
129
171
  const removed = node.specifiers.filter((specifier) => {
130
172
  if (specifier.type !== 'ImportSpecifier')
131
173
  return false;
132
174
  const name = specifier.imported.name ?? specifier.imported.value;
133
- return bindings.includes(name);
175
+ return bindings.includes(name) && (!dead || dead(specifier));
134
176
  });
135
177
  if (removed.length === 0)
136
178
  return false;
@@ -138,21 +180,9 @@ exports.releaseStyles = {
138
180
  context.report({
139
181
  node,
140
182
  messageId,
141
- fix: (fixer) => {
142
- if (retained.length === 0)
143
- return fixer.remove(node);
144
- const defaultImport = retained.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
145
- const namedImports = retained.filter((specifier) => specifier.type === 'ImportSpecifier');
146
- const clauses = [];
147
- if (defaultImport)
148
- clauses.push(sourceCode.getText(defaultImport));
149
- if (namedImports.length > 0) {
150
- clauses.push(`{ ${namedImports
151
- .map((specifier) => sourceCode.getText(specifier))
152
- .join(', ')} }`);
153
- }
154
- return fixer.replaceText(node, `import ${clauses.join(', ')} from ${sourceCode.getText(node.source)};`);
155
- },
183
+ fix: (fixer) => retained.length === 0
184
+ ? fixer.remove(node)
185
+ : fixer.replaceText(node, importOf(retained, node.source)),
156
186
  });
157
187
  return true;
158
188
  };
@@ -177,16 +207,30 @@ exports.releaseStyles = {
177
207
  if (removeImportedBindings(node, importedThemes, 'themeImport'))
178
208
  return;
179
209
  }
210
+ const importedStatics = importedStaticBindings(String(node.source.value));
211
+ if (importedStatics) {
212
+ if (removeImportedBindings(node, importedStatics, 'staticImport', (specifier) => onlyUsedByReleasedStyles(specifier.local.name)))
213
+ return;
214
+ }
180
215
  const importedModule = importedReleasedModule(String(node.source.value));
181
216
  if (importedModule) {
182
217
  active = true;
183
218
  const importedBinding = node.specifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
184
219
  (specifier.imported.name ?? specifier.imported.value) ===
185
220
  importedModule.binding);
186
- activeModule = {
187
- ...importedModule,
188
- binding: importedBinding?.local.name ?? importedModule.binding,
189
- };
221
+ const local = importedBinding?.local.name ?? importedModule.binding;
222
+ activeModule = { ...importedModule, binding: local };
223
+ if (importedModule.definitionOnly && importedBinding) {
224
+ const retained = node.specifiers.filter((specifier) => specifier !== importedBinding);
225
+ const released = `import ${local} from '${specifierFor(importedModule)}';`;
226
+ context.report({
227
+ node,
228
+ messageId: 'definition',
229
+ fix: (fixer) => fixer.replaceText(node, retained.length === 0
230
+ ? released
231
+ : `${released}\n${importOf(retained, node.source)}`),
232
+ });
233
+ }
190
234
  }
191
235
  },
192
236
  VariableDeclaration(node) {
@@ -218,8 +262,18 @@ exports.releaseStyles = {
218
262
  }
219
263
  if (!entry)
220
264
  return;
221
- if (declaration.id.name !== entry.binding ||
222
- !isCreate(declaration.init))
265
+ if (!isCreate(declaration.init))
266
+ return;
267
+ if (entry.aliases?.[declaration.id.name]) {
268
+ const aliased = node.parent?.type === 'ExportNamedDeclaration' ? node.parent : node;
269
+ context.report({
270
+ node: aliased,
271
+ messageId: 'definition',
272
+ fix: (fixer) => fixer.remove(aliased),
273
+ });
274
+ return;
275
+ }
276
+ if (declaration.id.name !== entry.binding)
223
277
  return;
224
278
  const exported = node.parent?.type === 'ExportNamedDeclaration';
225
279
  const target = exported ? node.parent : node;
@@ -233,6 +287,29 @@ exports.releaseStyles = {
233
287
  fix: (fixer) => fixer.replaceText(target, replacement),
234
288
  });
235
289
  },
290
+ MemberExpression(node) {
291
+ if (!entry?.aliases || node.object.type !== 'Identifier')
292
+ return;
293
+ const alias = entry.aliases[node.object.name];
294
+ if (!alias)
295
+ return;
296
+ const key = node.computed
297
+ ? node.property.type === 'Literal'
298
+ ? String(node.property.value)
299
+ : undefined
300
+ : node.property.name;
301
+ const className = key ? alias[key] : undefined;
302
+ if (!className)
303
+ return;
304
+ const access = IDENTIFIER.test(className)
305
+ ? `.${className}`
306
+ : `['${className}']`;
307
+ context.report({
308
+ node,
309
+ messageId: 'alias',
310
+ fix: (fixer) => fixer.replaceText(node, `${entry.binding}${access}`),
311
+ });
312
+ },
236
313
  JSXAttribute(node) {
237
314
  if (!active)
238
315
  return;
@@ -242,23 +319,34 @@ exports.releaseStyles = {
242
319
  ? node.value.expression
243
320
  : undefined;
244
321
  const moduleInfo = activeModule;
245
- const functionStyle = (value) => {
246
- if (!moduleInfo ||
247
- value?.type !== 'CallExpression' ||
248
- value.callee.type !== 'MemberExpression' ||
249
- value.callee.object.type !== 'Identifier' ||
250
- value.callee.object.name !== moduleInfo.binding)
322
+ const readsModule = (callee) => Boolean(moduleInfo &&
323
+ callee?.type === 'MemberExpression' &&
324
+ callee.object.type === 'Identifier' &&
325
+ (callee.object.name === moduleInfo.binding ||
326
+ moduleInfo.aliases?.[callee.object.name]));
327
+ const styleKey = (callee) => {
328
+ if (!readsModule(callee))
251
329
  return undefined;
252
- const key = value.callee.computed
253
- ? value.callee.property.type === 'Literal'
254
- ? String(value.callee.property.value)
330
+ const raw = callee.computed
331
+ ? callee.property.type === 'Literal'
332
+ ? String(callee.property.value)
255
333
  : undefined
256
- : value.callee.property.name;
334
+ : callee.property.name;
335
+ if (raw === undefined)
336
+ return undefined;
337
+ const alias = moduleInfo.aliases?.[callee.object.name];
338
+ return alias ? alias[raw] : raw;
339
+ };
340
+ const functionStyle = (value) => {
341
+ if (!moduleInfo || value?.type !== 'CallExpression')
342
+ return undefined;
343
+ const key = styleKey(value.callee);
257
344
  const func = key ? moduleInfo.functions?.[key] : undefined;
258
345
  if (!key || !func || func.variables.length !== value.arguments.length)
259
346
  return undefined;
347
+ const access = IDENTIFIER.test(key) ? `.${key}` : `['${key}']`;
260
348
  return {
261
- className: `${moduleInfo.binding}.${key}`,
349
+ className: `${moduleInfo.binding}${access}`,
262
350
  declarations: func.variables.map((variable, index) => {
263
351
  const argument = sourceCode.getText(value.arguments[index]);
264
352
  return `'${variable}': ${argument}`;
@@ -299,9 +387,7 @@ exports.releaseStyles = {
299
387
  return;
300
388
  }
301
389
  if (expression?.type === 'CallExpression' &&
302
- expression.callee.type === 'MemberExpression' &&
303
- expression.callee.object.type === 'Identifier' &&
304
- expression.callee.object.name === moduleInfo?.binding) {
390
+ readsModule(expression.callee)) {
305
391
  context.report({
306
392
  node,
307
393
  messageId: 'unsupportedFunctionStyle',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/codemod",
3
- "version": "18.2.18",
3
+ "version": "18.2.19",
4
4
  "description": "Codemods for migrating Plumeria APIs",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
@@ -34,6 +34,7 @@
34
34
  "dependencies": {
35
35
  "@typescript-eslint/parser": "^8.60.0",
36
36
  "eslint": "^10.4.1",
37
+ "get-tsconfig": "^4.14.2",
37
38
  "postcss": "8.5.25",
38
39
  "postcss-selector-parser": "^7.1.1",
39
40
  "zss-engine": "2.6.0"