@plumeria/codemod 19.15.0 → 21.0.0

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
@@ -7,6 +7,7 @@ No install needed — run it with `npx`.
7
7
  npx @plumeria/codemod migrate --from css-modules
8
8
  npx @plumeria/codemod migrate --from plumeria
9
9
  npx @plumeria/codemod rename-prop classStyle sx
10
+ npx @plumeria/codemod rename-api viewTransition viewTransitionClass
10
11
  ```
11
12
 
12
13
  Commit before running, so the rewrite can be reverted with `git checkout`.
@@ -361,3 +362,40 @@ withPlumeria(nextConfig, { styleProp: 'sx' });
361
362
  // eslint.config.js
362
363
  settings: { plumeria: { styleProp: 'sx' } }
363
364
  ```
365
+
366
+ ## `rename-api`
367
+
368
+ Renames a Plumeria API after it has changed its name in a release:
369
+
370
+ ```sh
371
+ npx @plumeria/codemod rename-api <from> <to> [paths...]
372
+ ```
373
+
374
+ | Option | Effect |
375
+ | --------------- | ---------------------------------------- |
376
+ | `-d, --dry-run` | report what would change without writing |
377
+
378
+ `<to>` has to be an API `@plumeria/core` exports, so a typo is reported instead
379
+ of written. Paths default to the current directory, with the same directories
380
+ skipped as `rename-prop`.
381
+
382
+ ### What it rewrites
383
+
384
+ Calls through the namespace or default import of `@plumeria/core`:
385
+
386
+ ```diff
387
+ import * as css from '@plumeria/core';
388
+ - const fade = css.viewTransition({ old: { opacity: 0 } });
389
+ + const fade = css.viewTransitionClass({ old: { opacity: 0 } });
390
+ ```
391
+
392
+ And named imports, together with every reference to them. An alias keeps its
393
+ local name, so only the import changes:
394
+
395
+ ```diff
396
+ - import { viewTransition, viewTransition as vt } from '@plumeria/core';
397
+ - const fade = viewTransition({ old: { opacity: 0 } });
398
+ + import { viewTransitionClass, viewTransitionClass as vt } from '@plumeria/core';
399
+ + const fade = viewTransitionClass({ old: { opacity: 0 } });
400
+ const slide = vt({ new: { opacity: 1 } });
401
+ ```
package/dist/cli.js CHANGED
@@ -42,12 +42,25 @@ const node_path_1 = __importDefault(require("node:path"));
42
42
  const eslint_1 = require("eslint");
43
43
  const tsParser = __importStar(require("@typescript-eslint/parser"));
44
44
  const rename_prop_1 = require("./transforms/rename-prop");
45
+ const rename_api_1 = require("./transforms/rename-api");
45
46
  const adopt_styles_1 = require("./transforms/adopt-styles");
46
47
  const release_styles_1 = require("./transforms/release-styles");
47
48
  const migrate_1 = require("./migrate");
48
49
  const release_1 = require("./release");
49
50
  const { version } = require('../package.json');
50
- const TRANSFORMS = ['rename-prop', 'migrate'];
51
+ const TRANSFORMS = ['rename-prop', 'rename-api', 'migrate'];
52
+ const APIS = [
53
+ 'create',
54
+ 'createTheme',
55
+ 'createStatic',
56
+ 'keyframes',
57
+ 'viewTransitionClass',
58
+ 'positionTry',
59
+ 'varType',
60
+ 'marker',
61
+ 'extended',
62
+ 'use',
63
+ ];
51
64
  const SOURCES = ['css-modules', 'plumeria'];
52
65
  const SOURCE_FILES = '**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}';
53
66
  const IGNORES = [
@@ -63,6 +76,7 @@ const USAGE = `plumeria-codemod — codemods for migrating Plumeria APIs
63
76
 
64
77
  Usage
65
78
  npx @plumeria/codemod rename-prop <from> <to> [paths...]
79
+ npx @plumeria/codemod rename-api <from> <to> [paths...]
66
80
  npx @plumeria/codemod migrate --from css-modules [paths...]
67
81
  npx @plumeria/codemod migrate --from plumeria [paths...]
68
82
 
@@ -76,6 +90,7 @@ Options
76
90
  Examples
77
91
  npx @plumeria/codemod rename-prop classStyle sx
78
92
  npx @plumeria/codemod rename-prop classStyle sx src app --dry-run
93
+ npx @plumeria/codemod rename-api viewTransition viewTransitionClass
79
94
  npx @plumeria/codemod migrate --from css-modules
80
95
  npx @plumeria/codemod migrate --from plumeria --dry-run
81
96
 
@@ -139,14 +154,27 @@ function parseArgs(argv) {
139
154
  };
140
155
  }
141
156
  if (!from || !to) {
142
- throw new UsageError('rename-prop needs both <from> and <to>');
143
- }
144
- if (!IDENTIFIER.test(to)) {
145
- throw new UsageError(`"${to}" is not a valid identifier`);
157
+ throw new UsageError(`${transform} needs both <from> and <to>`);
146
158
  }
147
159
  if (from === to) {
148
160
  throw new UsageError(`<from> and <to> are both "${from}"`);
149
161
  }
162
+ if (transform === 'rename-api') {
163
+ if (!APIS.includes(to)) {
164
+ throw new UsageError(`"${to}" is not a Plumeria API. Available: ${APIS.join(', ')}`);
165
+ }
166
+ return {
167
+ transform: 'rename-api',
168
+ from,
169
+ to,
170
+ targets: targets.length > 0 ? targets : ['.'],
171
+ dryRun,
172
+ includeTypes,
173
+ };
174
+ }
175
+ if (!IDENTIFIER.test(to)) {
176
+ throw new UsageError(`"${to}" is not a valid identifier`);
177
+ }
150
178
  if (RESERVED.has(to)) {
151
179
  throw new UsageError(`"${to}" is already used by React`);
152
180
  }
@@ -160,7 +188,18 @@ function parseArgs(argv) {
160
188
  };
161
189
  }
162
190
  function buildConfig(options) {
163
- const { from, to, includeTypes } = options;
191
+ const { transform, from, to, includeTypes } = options;
192
+ const rule = transform === 'rename-api'
193
+ ? {
194
+ plugins: { codemod: { rules: { 'rename-api': rename_api_1.renameApi } } },
195
+ rules: { 'codemod/rename-api': ['error', { from, to }] },
196
+ }
197
+ : {
198
+ plugins: { codemod: { rules: { 'rename-prop': rename_prop_1.renameProp } } },
199
+ rules: {
200
+ 'codemod/rename-prop': ['error', { from, to, includeTypes }],
201
+ },
202
+ };
164
203
  return [
165
204
  { ignores: IGNORES },
166
205
  {
@@ -172,8 +211,7 @@ function buildConfig(options) {
172
211
  ecmaFeatures: { jsx: true },
173
212
  },
174
213
  },
175
- plugins: { codemod: { rules: { 'rename-prop': rename_prop_1.renameProp } } },
176
- rules: { 'codemod/rename-prop': ['error', { from, to, includeTypes }] },
214
+ ...rule,
177
215
  },
178
216
  ];
179
217
  }
package/dist/release.js CHANGED
@@ -42,7 +42,7 @@ 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 */';
45
+ const GLOBAL_OPEN = '/* Generated from css.createTheme, css.keyframes, and css.viewTransitionClass by @plumeria/codemod */';
46
46
  const GLOBAL_CLOSE = '/* End of @plumeria/codemod generated styles */';
47
47
  const EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
48
48
  const IGNORED = new Set([
@@ -105,6 +105,12 @@ exports.adoptStyles = {
105
105
  (current.parent.name?.name === 'className' ||
106
106
  current.parent.name?.name === styleProp));
107
107
  };
108
+ const lone = (node) => node?.type === 'LogicalExpression' &&
109
+ node.operator === '||' &&
110
+ node.right.type === 'Identifier' &&
111
+ node.right.name === 'undefined'
112
+ ? [node.left]
113
+ : undefined;
108
114
  const joinedElements = (node) => {
109
115
  if (node?.type === 'TemplateLiteral' && node.expressions.length > 1) {
110
116
  const spaced = node.quasis.every((quasi, index) => index === 0 || index === node.quasis.length - 1
@@ -117,7 +123,7 @@ exports.adoptStyles = {
117
123
  node.callee.property?.name !== 'join' ||
118
124
  node.arguments.length !== 1 ||
119
125
  node.arguments[0].value !== ' ')
120
- return undefined;
126
+ return lone(node);
121
127
  const joined = node.callee.object;
122
128
  if (joined?.type === 'ArrayExpression')
123
129
  return joined.elements.filter(Boolean);
@@ -389,11 +389,11 @@ function extractPlumeriaAnimations(source, externalValues = {}) {
389
389
  }
390
390
  for (const declaration of declarations) {
391
391
  if (declaration.id.type !== 'Identifier' ||
392
- !isCall(declaration.init, 'viewTransition'))
392
+ !isCall(declaration.init, 'viewTransitionClass'))
393
393
  continue;
394
394
  const object = evaluate(declaration.init.arguments[0], values);
395
395
  if (!object || typeof object !== 'object') {
396
- report(declaration.init, 'dynamic-view-transition', '`viewTransition` needs a statically resolvable object.');
396
+ report(declaration.init, 'dynamic-view-transition', '`viewTransitionClass` needs a statically resolvable object.');
397
397
  continue;
398
398
  }
399
399
  const hash = (0, zss_engine_1.genBase36Hash)(object, 1, 8);
@@ -406,7 +406,7 @@ function extractPlumeriaAnimations(source, externalValues = {}) {
406
406
  const style = object[part];
407
407
  if (style && typeof style === 'object') {
408
408
  const pseudoPart = part === 'imagePair' ? 'image-pair' : part;
409
- parts.push(emitGlobalObject(`::view-transition-${pseudoPart}(${name})`, style));
409
+ parts.push(emitGlobalObject(`::view-transition-${pseudoPart}(*.${name})`, style));
410
410
  }
411
411
  }
412
412
  if (parts.length > 0)
@@ -76,6 +76,13 @@ const walk = (node, visit) => {
76
76
  const needsFilter = (node) => node.type === 'LogicalExpression' ||
77
77
  node.type === 'ConditionalExpression' ||
78
78
  node.type === 'SpreadElement';
79
+ const mayBeFalsy = (node) => {
80
+ if (node.type === 'LogicalExpression')
81
+ return node.operator === '&&' || mayBeFalsy(node.right);
82
+ if (node.type === 'ConditionalExpression')
83
+ return mayBeFalsy(node.consequent) || mayBeFalsy(node.alternate);
84
+ return node.type === 'Literal' && !node.value;
85
+ };
79
86
  exports.__private = { memberPath, isCreate, needsFilter };
80
87
  exports.releaseStyles = {
81
88
  meta: {
@@ -89,7 +96,7 @@ exports.releaseStyles = {
89
96
  static: '`createStatic` values were inlined into the generated CSS.',
90
97
  theme: '`createTheme` was written to the global stylesheet.',
91
98
  themeImport: 'Remove the createTheme import after inlining its CSS variables.',
92
- animation: '`keyframes` or `viewTransition` was written globally.',
99
+ animation: '`keyframes` or `viewTransitionClass` was written globally.',
93
100
  animationImport: 'Remove the animation import after replacing it with its generated name.',
94
101
  staticImport: 'Remove the createStatic import after inlining its values into the CSS.',
95
102
  alias: 'Read this style from the CSS Module the whole file was exported to.',
@@ -101,6 +108,7 @@ exports.releaseStyles = {
101
108
  styleType: 'A released style is a class name.',
102
109
  folded: 'This only named a style key, which the export has resolved.',
103
110
  read: 'Read this style by the name the export gave it.',
111
+ falsy: 'className takes a string or undefined, so a condition that can be false reads as undefined.',
104
112
  functionStyle: 'Use the CSS Module class and pass function arguments as custom properties.',
105
113
  unsupportedFunctionStyle: 'This function style call cannot be converted without changing its behavior.',
106
114
  },
@@ -162,7 +170,7 @@ exports.releaseStyles = {
162
170
  return Boolean(entry);
163
171
  if (called[1] === 'createTheme')
164
172
  return themeBindings.includes(only.id.name);
165
- if (called[1] === 'keyframes' || called[1] === 'viewTransition')
173
+ if (called[1] === 'keyframes' || called[1] === 'viewTransitionClass')
166
174
  return animationBindings.includes(only.id.name);
167
175
  if (called[1] === 'create')
168
176
  return Boolean(entry &&
@@ -359,7 +367,7 @@ exports.releaseStyles = {
359
367
  ? Boolean(entry)
360
368
  : method === 'createTheme'
361
369
  ? themeBindings.length > 0
362
- : method === 'keyframes' || method === 'viewTransition'
370
+ : method === 'keyframes' || method === 'viewTransitionClass'
363
371
  ? animationBindings.length > 0
364
372
  : false;
365
373
  const survivesCore = (program) => {
@@ -596,14 +604,14 @@ exports.releaseStyles = {
596
604
  ((entry && path[1] === 'createStatic') ||
597
605
  (path[1] === 'createTheme' &&
598
606
  themeBindings.includes(declaration.id.name)) ||
599
- ((path[1] === 'keyframes' || path[1] === 'viewTransition') &&
607
+ ((path[1] === 'keyframes' || path[1] === 'viewTransitionClass') &&
600
608
  animationBindings.includes(declaration.id.name)))) {
601
609
  const target = node.parent?.type === 'ExportNamedDeclaration' ? node.parent : node;
602
610
  context.report({
603
611
  node: target,
604
612
  messageId: path[1] === 'createTheme'
605
613
  ? 'theme'
606
- : path[1] === 'keyframes' || path[1] === 'viewTransition'
614
+ : path[1] === 'keyframes' || path[1] === 'viewTransitionClass'
607
615
  ? 'animation'
608
616
  : 'static',
609
617
  fix: (fixer) => removeStatement(fixer, target),
@@ -923,6 +931,18 @@ exports.releaseStyles = {
923
931
  const text = expression ? resolveText(expression) : undefined;
924
932
  if (!complete && expression && text === undefined)
925
933
  return;
934
+ if (!carried &&
935
+ expression &&
936
+ text !== undefined &&
937
+ mayBeFalsy(expression)) {
938
+ context.report({
939
+ node: expression,
940
+ messageId: 'falsy',
941
+ fix: (fixer) => fixer.replaceText(expression, `(${text}) || undefined`),
942
+ });
943
+ takeProp();
944
+ return;
945
+ }
926
946
  const foldsKey = expression?.type === 'MemberExpression' &&
927
947
  expression.computed &&
928
948
  expression.property.type === 'Identifier' &&
@@ -0,0 +1,6 @@
1
+ import type { Rule } from 'eslint';
2
+ export interface RenameApiOptions {
3
+ from: string;
4
+ to: string;
5
+ }
6
+ export declare const renameApi: Rule.RuleModule;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renameApi = void 0;
4
+ const CORE = '@plumeria/core';
5
+ exports.renameApi = {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: {
9
+ description: 'Rename a Plumeria API to a new name',
10
+ },
11
+ fixable: 'code',
12
+ messages: {
13
+ rename: 'Rename "{{from}}" to "{{to}}".',
14
+ },
15
+ schema: [
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ from: { type: 'string' },
20
+ to: { type: 'string' },
21
+ },
22
+ required: ['from', 'to'],
23
+ additionalProperties: false,
24
+ },
25
+ ],
26
+ },
27
+ create(context) {
28
+ const { from, to } = context.options[0];
29
+ const namespaces = new Set();
30
+ const rename = (node) => context.report({
31
+ node,
32
+ messageId: 'rename',
33
+ data: { from, to },
34
+ fix: (fixer) => fixer.replaceText(node, to),
35
+ });
36
+ return {
37
+ ImportDeclaration(node) {
38
+ if (node.source.value !== CORE)
39
+ return;
40
+ for (const specifier of node.specifiers) {
41
+ if (specifier.type !== 'ImportSpecifier') {
42
+ namespaces.add(specifier.local.name);
43
+ continue;
44
+ }
45
+ if (specifier.imported.type !== 'Identifier')
46
+ continue;
47
+ if (specifier.imported.name !== from)
48
+ continue;
49
+ rename(specifier.imported);
50
+ if (specifier.local.name !== from)
51
+ continue;
52
+ const [variable] = context.sourceCode.getDeclaredVariables(specifier);
53
+ for (const reference of variable?.references ?? []) {
54
+ rename(reference.identifier);
55
+ }
56
+ }
57
+ },
58
+ MemberExpression(node) {
59
+ if (node.computed)
60
+ return;
61
+ if (node.object.type !== 'Identifier')
62
+ return;
63
+ if (!namespaces.has(node.object.name))
64
+ return;
65
+ if (node.property.type !== 'Identifier')
66
+ return;
67
+ if (node.property.name !== from)
68
+ return;
69
+ rename(node.property);
70
+ },
71
+ };
72
+ },
73
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plumeria/codemod",
3
- "version": "19.15.0",
4
- "description": "Codemods for Plumeria: migrate a CSS Modules codebase in, export it back out, and rename the styling prop",
3
+ "version": "21.0.0",
4
+ "description": "Codemods for Plumeria: migrate a CSS Modules codebase in, export it back out, and rename the styling prop or an API",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
7
7
  "homepage": "https://plumeria.dev",
@@ -37,7 +37,7 @@
37
37
  "get-tsconfig": "^4.14.2",
38
38
  "postcss": "8.5.26",
39
39
  "postcss-selector-parser": "^7.1.1",
40
- "zss-engine": "2.10.0"
40
+ "zss-engine": "3.0.1"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public",