@plumeria/codemod 18.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) zss-in-js contributer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @plumeria/codemod
2
+
3
+ Codemods for migrating Plumeria APIs. No install needed — run it with `npx`.
4
+
5
+ ```sh
6
+ npx @plumeria/codemod rename-prop classStyle sx
7
+ ```
8
+
9
+ ## `rename-prop`
10
+
11
+ Renames the styling prop across your source. The names are arguments, so it
12
+ works for any rename, not just default prop:
13
+
14
+ ```sh
15
+ npx @plumeria/codemod rename-prop <from> <to> [paths...]
16
+ ```
17
+
18
+ | Option | Effect |
19
+ | ------------- | ----------------------------------------------- |
20
+ | `-d, --dry-run` | report what would change without writing |
21
+ | `--no-types` | leave TypeScript declarations untouched |
22
+
23
+ Paths default to the current directory. `node_modules`, `dist`, `build`, `out`,
24
+ `.next` and `coverage` are skipped.
25
+
26
+ ### What it rewrites
27
+
28
+ JSX attributes:
29
+
30
+ ```diff
31
+ - <div classStyle={styles.card} />
32
+ + <div sx={styles.card} />
33
+ ```
34
+
35
+ And property signatures annotated with Plumeria's `Style`, which covers the
36
+ global augmentation in your `plumeria.d.ts` as well as your own component props:
37
+
38
+ ```diff
39
+ interface CardProps {
40
+ - classStyle?: Style;
41
+ + sx?: Style;
42
+ }
43
+ ```
44
+
45
+ The annotation has to be `Style` for the rename to happen, so an unrelated
46
+ interface that happens to use the same key is left alone.
47
+
48
+ ### What it reports instead of rewriting
49
+
50
+ Where the prop is destructured or read off an object, renaming it would change a
51
+ local binding and not just the prop, so those are listed for you to handle:
52
+
53
+ ```
54
+ 2 occurrence(s) need a manual rename — "classStyle" is bound to a local name there:
55
+ src/components/Card.tsx:8:12
56
+ src/components/Card.tsx:14:22
57
+ ```
58
+
59
+ ## After the rename
60
+
61
+ The rewrite only touches your source. Point the rest of the toolchain at the new
62
+ name too:
63
+
64
+ ```js
65
+ // next.config.ts / unplugin options
66
+ withPlumeria(nextConfig, { styleProp: 'sx' });
67
+ ```
68
+
69
+ ```js
70
+ // eslint.config.js
71
+ settings: { plumeria: { styleProp: 'sx' } }
72
+ ```
73
+
74
+ Commit before running, so the rewrite can be reverted with `git checkout`.
package/bin/codemod.js ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../dist/cli.js');
4
+
5
+ main(process.argv.slice(2)).then(
6
+ (code) => {
7
+ process.exitCode = code;
8
+ },
9
+ (error) => {
10
+ console.error(`✖ ${(error && error.message) || error}`);
11
+ process.exitCode = 1;
12
+ },
13
+ );
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(argv: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,238 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.main = main;
40
+ const node_child_process_1 = require("node:child_process");
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const eslint_1 = require("eslint");
43
+ const tsParser = __importStar(require("@typescript-eslint/parser"));
44
+ const rename_prop_1 = require("./transforms/rename-prop");
45
+ const { version } = require('../package.json');
46
+ const TRANSFORMS = ['rename-prop'];
47
+ const SOURCE_FILES = '**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}';
48
+ const IGNORES = [
49
+ '**/dist/**',
50
+ '**/build/**',
51
+ '**/out/**',
52
+ '**/.next/**',
53
+ '**/coverage/**',
54
+ ];
55
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
56
+ const RESERVED = new Set(['className', 'key', 'ref', 'children', 'style']);
57
+ const USAGE = `plumeria-codemod — codemods for migrating Plumeria APIs
58
+
59
+ Usage
60
+ npx @plumeria/codemod rename-prop <from> <to> [paths...]
61
+
62
+ Options
63
+ -d, --dry-run report what would change without writing
64
+ --no-types leave TypeScript declarations untouched
65
+ -h, --help show this message
66
+ -v, --version show the version
67
+
68
+ Examples
69
+ npx @plumeria/codemod rename-prop classStyle sx
70
+ npx @plumeria/codemod rename-prop classStyle sx src app --dry-run
71
+
72
+ Paths default to the current directory. After the rewrite, point the toolchain
73
+ at the new name with \`styleProp\` on your bundler plugin, and with
74
+ \`settings: { plumeria: { styleProp: '<to>' } }\` in your ESLint config.`;
75
+ class UsageError extends Error {
76
+ }
77
+ function parseArgs(argv) {
78
+ if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
79
+ console.log(USAGE);
80
+ return null;
81
+ }
82
+ if (argv.includes('-v') || argv.includes('--version')) {
83
+ console.log(version);
84
+ return null;
85
+ }
86
+ const positional = [];
87
+ let dryRun = false;
88
+ let includeTypes = true;
89
+ for (const arg of argv) {
90
+ switch (arg) {
91
+ case '-d':
92
+ case '--dry-run':
93
+ dryRun = true;
94
+ break;
95
+ case '--no-types':
96
+ includeTypes = false;
97
+ break;
98
+ default:
99
+ if (arg.startsWith('-'))
100
+ throw new UsageError(`unknown option "${arg}"`);
101
+ positional.push(arg);
102
+ }
103
+ }
104
+ const [transform, from, to, ...targets] = positional;
105
+ if (!TRANSFORMS.includes(transform)) {
106
+ throw new UsageError(`unknown transform "${transform}". Available: ${TRANSFORMS.join(', ')}`);
107
+ }
108
+ if (!from || !to) {
109
+ throw new UsageError('rename-prop needs both <from> and <to>');
110
+ }
111
+ if (!IDENTIFIER.test(to)) {
112
+ throw new UsageError(`"${to}" is not a valid identifier`);
113
+ }
114
+ if (from === to) {
115
+ throw new UsageError(`<from> and <to> are both "${from}"`);
116
+ }
117
+ if (RESERVED.has(to)) {
118
+ throw new UsageError(`"${to}" is already used by React`);
119
+ }
120
+ return {
121
+ from,
122
+ to,
123
+ targets: targets.length > 0 ? targets : ['.'],
124
+ dryRun,
125
+ includeTypes,
126
+ };
127
+ }
128
+ function buildConfig(options) {
129
+ const { from, to, includeTypes } = options;
130
+ return [
131
+ { ignores: IGNORES },
132
+ {
133
+ files: [SOURCE_FILES],
134
+ languageOptions: {
135
+ parser: tsParser,
136
+ parserOptions: {
137
+ sourceType: 'module',
138
+ ecmaFeatures: { jsx: true },
139
+ },
140
+ },
141
+ plugins: { codemod: { rules: { 'rename-prop': rename_prop_1.renameProp } } },
142
+ rules: { 'codemod/rename-prop': ['error', { from, to, includeTypes }] },
143
+ },
144
+ ];
145
+ }
146
+ function warnIfDirty() {
147
+ try {
148
+ const status = (0, node_child_process_1.execFileSync)('git', ['status', '--porcelain'], {
149
+ encoding: 'utf8',
150
+ stdio: ['ignore', 'pipe', 'ignore'],
151
+ });
152
+ if (status.trim()) {
153
+ console.warn('! git working tree is not clean. Commit first so the rewrite can be reverted.\n');
154
+ }
155
+ }
156
+ catch {
157
+ }
158
+ }
159
+ async function main(argv) {
160
+ let options;
161
+ try {
162
+ options = parseArgs(argv);
163
+ }
164
+ catch (error) {
165
+ if (!(error instanceof UsageError))
166
+ throw error;
167
+ console.error(`✖ ${error.message}\n`);
168
+ console.error(USAGE);
169
+ return 1;
170
+ }
171
+ if (!options)
172
+ return 0;
173
+ const { from, to, targets, dryRun } = options;
174
+ const baseConfig = {
175
+ overrideConfigFile: true,
176
+ overrideConfig: buildConfig(options),
177
+ errorOnUnmatchedPattern: false,
178
+ };
179
+ const results = await new eslint_1.ESLint(baseConfig).lintFiles(targets);
180
+ const cwd = process.cwd();
181
+ const relative = (filePath) => node_path_1.default.relative(cwd, filePath) || '.';
182
+ const changes = [];
183
+ const manual = [];
184
+ const unparsed = [];
185
+ let total = 0;
186
+ for (const result of results) {
187
+ let count = 0;
188
+ for (const message of result.messages) {
189
+ if (message.fatal) {
190
+ unparsed.push(`${relative(result.filePath)}: ${message.message}`);
191
+ }
192
+ else if (message.fix) {
193
+ count++;
194
+ }
195
+ else {
196
+ manual.push(`${relative(result.filePath)}:${message.line}:${message.column}`);
197
+ }
198
+ }
199
+ if (count > 0) {
200
+ changes.push({ file: relative(result.filePath), count });
201
+ total += count;
202
+ }
203
+ }
204
+ if (total === 0 && manual.length === 0) {
205
+ console.log(`No "${from}" found.`);
206
+ return unparsed.length > 0 ? report(unparsed) : 0;
207
+ }
208
+ if (total === 0) {
209
+ console.log(`Every remaining "${from}" needs a manual rename.`);
210
+ }
211
+ else {
212
+ for (const { file, count } of changes) {
213
+ console.log(` ${file} ${count}`);
214
+ }
215
+ if (dryRun) {
216
+ console.log(`\n${total} occurrence(s) in ${changes.length} file(s) would become "${to}".`);
217
+ console.log('Run without --dry-run to apply.');
218
+ }
219
+ else {
220
+ warnIfDirty();
221
+ const applied = await new eslint_1.ESLint({ ...baseConfig, fix: true }).lintFiles(targets);
222
+ await eslint_1.ESLint.outputFixes(applied);
223
+ console.log(`\n✔ renamed ${total} occurrence(s) of "${from}" to "${to}" in ${changes.length} file(s).`);
224
+ }
225
+ }
226
+ if (manual.length > 0) {
227
+ console.log(`\n${manual.length} occurrence(s) need a manual rename — "${from}" is bound to a local name there:`);
228
+ for (const location of manual)
229
+ console.log(` ${location}`);
230
+ }
231
+ return unparsed.length > 0 ? report(unparsed) : 0;
232
+ }
233
+ function report(unparsed) {
234
+ console.error(`\n✖ ${unparsed.length} file(s) could not be parsed:`);
235
+ for (const failure of unparsed)
236
+ console.error(` ${failure}`);
237
+ return 1;
238
+ }
@@ -0,0 +1,4 @@
1
+ import { renameProp } from './transforms/rename-prop';
2
+ export { renameProp };
3
+ export type { RenamePropOptions } from './transforms/rename-prop';
4
+ export { main } from './cli';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = exports.renameProp = void 0;
4
+ const rename_prop_1 = require("./transforms/rename-prop");
5
+ Object.defineProperty(exports, "renameProp", { enumerable: true, get: function () { return rename_prop_1.renameProp; } });
6
+ var cli_1 = require("./cli");
7
+ Object.defineProperty(exports, "main", { enumerable: true, get: function () { return cli_1.main; } });
@@ -0,0 +1,7 @@
1
+ import type { Rule } from 'eslint';
2
+ export interface RenamePropOptions {
3
+ from: string;
4
+ to: string;
5
+ includeTypes?: boolean;
6
+ }
7
+ export declare const renameProp: Rule.RuleModule;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renameProp = void 0;
4
+ function isStyleAnnotation(node) {
5
+ if (!node)
6
+ return false;
7
+ switch (node.type) {
8
+ case 'TSTypeReference': {
9
+ const name = node.typeName;
10
+ if (name?.type === 'Identifier')
11
+ return name.name === 'Style';
12
+ if (name?.type === 'TSQualifiedName')
13
+ return name.right?.name === 'Style';
14
+ return false;
15
+ }
16
+ case 'TSUnionType':
17
+ case 'TSIntersectionType':
18
+ return node.types.some(isStyleAnnotation);
19
+ default:
20
+ return false;
21
+ }
22
+ }
23
+ exports.renameProp = {
24
+ meta: {
25
+ type: 'suggestion',
26
+ docs: {
27
+ description: 'Rename a JSX prop to a new name',
28
+ },
29
+ fixable: 'code',
30
+ messages: {
31
+ rename: 'Rename "{{from}}" to "{{to}}".',
32
+ manual: '"{{from}}" is forwarded here. Rename it to "{{to}}" by hand — an autofix would change the local binding too.',
33
+ },
34
+ schema: [
35
+ {
36
+ type: 'object',
37
+ properties: {
38
+ from: { type: 'string' },
39
+ to: { type: 'string' },
40
+ includeTypes: { type: 'boolean' },
41
+ },
42
+ required: ['from', 'to'],
43
+ additionalProperties: false,
44
+ },
45
+ ],
46
+ },
47
+ create(context) {
48
+ const { from, to, includeTypes = true, } = context.options[0];
49
+ const rename = (node) => context.report({
50
+ node,
51
+ messageId: 'rename',
52
+ data: { from, to },
53
+ fix: (fixer) => fixer.replaceText(node, to),
54
+ });
55
+ const manual = (node) => context.report({ node, messageId: 'manual', data: { from, to } });
56
+ return {
57
+ JSXAttribute(node) {
58
+ if (node.name.type !== 'JSXIdentifier')
59
+ return;
60
+ if (node.name.name !== from)
61
+ return;
62
+ rename(node.name);
63
+ },
64
+ TSPropertySignature(node) {
65
+ if (!includeTypes)
66
+ return;
67
+ if (node.key.type !== 'Identifier' || node.key.name !== from)
68
+ return;
69
+ if (!isStyleAnnotation(node.typeAnnotation?.typeAnnotation))
70
+ return;
71
+ rename(node.key);
72
+ },
73
+ Property(node) {
74
+ if (node.parent?.type !== 'ObjectPattern')
75
+ return;
76
+ if (node.computed)
77
+ return;
78
+ if (node.key.type !== 'Identifier' || node.key.name !== from)
79
+ return;
80
+ manual(node.key);
81
+ },
82
+ MemberExpression(node) {
83
+ if (node.computed)
84
+ return;
85
+ if (node.property.type !== 'Identifier')
86
+ return;
87
+ if (node.property.name !== from)
88
+ return;
89
+ manual(node.property);
90
+ },
91
+ };
92
+ },
93
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@plumeria/codemod",
3
+ "version": "18.0.0",
4
+ "description": "Codemods for migrating Plumeria APIs",
5
+ "author": "Refirst 11",
6
+ "license": "MIT",
7
+ "funding": "https://github.com/sponsors/refirst11",
8
+ "homepage": "https://plumeria.dev",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/zss-in-js/plumeria.git",
12
+ "directory": "packages/codemod"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/zss-in-js/plumeria/issues"
16
+ },
17
+ "keywords": [
18
+ "plumeria",
19
+ "codemod",
20
+ "migration",
21
+ "eslint",
22
+ "css-in-js"
23
+ ],
24
+ "sideEffects": false,
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "bin": {
28
+ "plumeria-codemod": "bin/codemod.js"
29
+ },
30
+ "files": [
31
+ "dist/",
32
+ "bin/"
33
+ ],
34
+ "scripts": {
35
+ "build": "rimraf dist && pnpm cjs",
36
+ "cjs": "tsc --project tsconfig.cjs.json"
37
+ },
38
+ "dependencies": {
39
+ "@typescript-eslint/parser": "^8.60.0",
40
+ "eslint": "^10.4.1"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }