@plumeria/codemod 18.2.0 → 18.2.1
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 +91 -3
- package/dist/migrate.d.ts +16 -0
- package/dist/migrate.js +114 -0
- package/dist/transforms/adopt-styles.d.ts +11 -0
- package/dist/transforms/adopt-styles.js +155 -0
- package/dist/transforms/css-modules.d.ts +19 -0
- package/dist/transforms/css-modules.js +241 -0
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -42,8 +42,11 @@ 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 adopt_styles_1 = require("./transforms/adopt-styles");
|
|
46
|
+
const migrate_1 = require("./migrate");
|
|
45
47
|
const { version } = require('../package.json');
|
|
46
|
-
const TRANSFORMS = ['rename-prop'];
|
|
48
|
+
const TRANSFORMS = ['rename-prop', 'migrate'];
|
|
49
|
+
const SOURCES = ['css-modules'];
|
|
47
50
|
const SOURCE_FILES = '**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}';
|
|
48
51
|
const IGNORES = [
|
|
49
52
|
'**/dist/**',
|
|
@@ -58,16 +61,19 @@ const USAGE = `plumeria-codemod — codemods for migrating Plumeria APIs
|
|
|
58
61
|
|
|
59
62
|
Usage
|
|
60
63
|
npx @plumeria/codemod rename-prop <from> <to> [paths...]
|
|
64
|
+
npx @plumeria/codemod migrate --from css-modules [paths...]
|
|
61
65
|
|
|
62
66
|
Options
|
|
63
67
|
-d, --dry-run report what would change without writing
|
|
64
|
-
--no-types leave TypeScript declarations untouched
|
|
68
|
+
--no-types leave TypeScript declarations untouched (rename-prop)
|
|
69
|
+
--from what to migrate from (migrate)
|
|
65
70
|
-h, --help show this message
|
|
66
71
|
-v, --version show the version
|
|
67
72
|
|
|
68
73
|
Examples
|
|
69
74
|
npx @plumeria/codemod rename-prop classStyle sx
|
|
70
75
|
npx @plumeria/codemod rename-prop classStyle sx src app --dry-run
|
|
76
|
+
npx @plumeria/codemod migrate --from css-modules
|
|
71
77
|
|
|
72
78
|
Paths default to the current directory. After the rewrite, point the toolchain
|
|
73
79
|
at the new name with \`styleProp\` on your bundler plugin, and with
|
|
@@ -86,7 +92,9 @@ function parseArgs(argv) {
|
|
|
86
92
|
const positional = [];
|
|
87
93
|
let dryRun = false;
|
|
88
94
|
let includeTypes = true;
|
|
89
|
-
|
|
95
|
+
let source;
|
|
96
|
+
for (let i = 0; i < argv.length; i++) {
|
|
97
|
+
const arg = argv[i];
|
|
90
98
|
switch (arg) {
|
|
91
99
|
case '-d':
|
|
92
100
|
case '--dry-run':
|
|
@@ -95,7 +103,14 @@ function parseArgs(argv) {
|
|
|
95
103
|
case '--no-types':
|
|
96
104
|
includeTypes = false;
|
|
97
105
|
break;
|
|
106
|
+
case '--from':
|
|
107
|
+
source = argv[++i];
|
|
108
|
+
break;
|
|
98
109
|
default:
|
|
110
|
+
if (arg.startsWith('--from=')) {
|
|
111
|
+
source = arg.slice('--from='.length);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
99
114
|
if (arg.startsWith('-'))
|
|
100
115
|
throw new UsageError(`unknown option "${arg}"`);
|
|
101
116
|
positional.push(arg);
|
|
@@ -105,6 +120,20 @@ function parseArgs(argv) {
|
|
|
105
120
|
if (!TRANSFORMS.includes(transform)) {
|
|
106
121
|
throw new UsageError(`unknown transform "${transform}". Available: ${TRANSFORMS.join(', ')}`);
|
|
107
122
|
}
|
|
123
|
+
if (transform === 'migrate') {
|
|
124
|
+
if (!source) {
|
|
125
|
+
throw new UsageError(`migrate needs --from. Available: ${SOURCES.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
if (!SOURCES.includes(source)) {
|
|
128
|
+
throw new UsageError(`unknown source "${source}". Available: ${SOURCES.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
transform: 'migrate',
|
|
132
|
+
source: source,
|
|
133
|
+
targets: positional.slice(1).length > 0 ? positional.slice(1) : ['.'],
|
|
134
|
+
dryRun,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
108
137
|
if (!from || !to) {
|
|
109
138
|
throw new UsageError('rename-prop needs both <from> and <to>');
|
|
110
139
|
}
|
|
@@ -118,6 +147,7 @@ function parseArgs(argv) {
|
|
|
118
147
|
throw new UsageError(`"${to}" is already used by React`);
|
|
119
148
|
}
|
|
120
149
|
return {
|
|
150
|
+
transform: 'rename-prop',
|
|
121
151
|
from,
|
|
122
152
|
to,
|
|
123
153
|
targets: targets.length > 0 ? targets : ['.'],
|
|
@@ -170,6 +200,8 @@ async function main(argv) {
|
|
|
170
200
|
}
|
|
171
201
|
if (!options)
|
|
172
202
|
return 0;
|
|
203
|
+
if (options.transform === 'migrate')
|
|
204
|
+
return migrate(options);
|
|
173
205
|
const { from, to, targets, dryRun } = options;
|
|
174
206
|
const baseConfig = {
|
|
175
207
|
overrideConfigFile: true,
|
|
@@ -236,3 +268,59 @@ function report(unparsed) {
|
|
|
236
268
|
console.error(` ${failure}`);
|
|
237
269
|
return 1;
|
|
238
270
|
}
|
|
271
|
+
function migrateConfig(modules) {
|
|
272
|
+
return [
|
|
273
|
+
{ ignores: IGNORES },
|
|
274
|
+
{
|
|
275
|
+
files: [SOURCE_FILES],
|
|
276
|
+
languageOptions: {
|
|
277
|
+
parser: tsParser,
|
|
278
|
+
parserOptions: {
|
|
279
|
+
sourceType: 'module',
|
|
280
|
+
ecmaFeatures: { jsx: true },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
plugins: { codemod: { rules: { 'adopt-styles': adopt_styles_1.adoptStyles } } },
|
|
284
|
+
rules: { 'codemod/adopt-styles': ['error', { modules }] },
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
async function migrate(options) {
|
|
289
|
+
const { targets, dryRun } = options;
|
|
290
|
+
const cwd = process.cwd();
|
|
291
|
+
const relative = (filePath) => node_path_1.default.relative(cwd, filePath) || '.';
|
|
292
|
+
const { stylesheets, modules } = (0, migrate_1.plan)(targets);
|
|
293
|
+
if (stylesheets.length === 0) {
|
|
294
|
+
console.log('No *.module.css found.');
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
for (const sheet of stylesheets) {
|
|
298
|
+
console.log(` ${relative(sheet.source)} -> ${relative(sheet.target)}`);
|
|
299
|
+
}
|
|
300
|
+
const config = {
|
|
301
|
+
overrideConfigFile: true,
|
|
302
|
+
overrideConfig: migrateConfig(modules),
|
|
303
|
+
errorOnUnmatchedPattern: false,
|
|
304
|
+
};
|
|
305
|
+
if (dryRun) {
|
|
306
|
+
const results = await new eslint_1.ESLint(config).lintFiles(targets);
|
|
307
|
+
const touched = results.filter((r) => r.messages.length > 0);
|
|
308
|
+
console.log(`\n${stylesheets.length} stylesheet(s) would be converted, ${touched.length} consumer(s) rewritten.`);
|
|
309
|
+
console.log('Run without --dry-run to apply.');
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
warnIfDirty();
|
|
313
|
+
(0, migrate_1.write)(targets);
|
|
314
|
+
const applied = await new eslint_1.ESLint({ ...config, fix: true }).lintFiles(targets);
|
|
315
|
+
await eslint_1.ESLint.outputFixes(applied);
|
|
316
|
+
const touched = applied.filter((r) => r.output !== undefined);
|
|
317
|
+
console.log(`\n✔ converted ${stylesheets.length} stylesheet(s) and rewrote ${touched.length} consumer(s).`);
|
|
318
|
+
}
|
|
319
|
+
const lines = (0, migrate_1.formatReports)(stylesheets, cwd);
|
|
320
|
+
if (lines.length === 0)
|
|
321
|
+
return 0;
|
|
322
|
+
console.log('\nLeft in place — these rules were not converted:\n');
|
|
323
|
+
for (const line of lines)
|
|
324
|
+
console.log(line);
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Report } from './transforms/css-modules';
|
|
2
|
+
import type { ModuleMap } from './transforms/adopt-styles';
|
|
3
|
+
export interface Stylesheet {
|
|
4
|
+
source: string;
|
|
5
|
+
target: string;
|
|
6
|
+
reports: Report[];
|
|
7
|
+
}
|
|
8
|
+
export interface Plan {
|
|
9
|
+
stylesheets: Stylesheet[];
|
|
10
|
+
modules: Record<string, ModuleMap>;
|
|
11
|
+
}
|
|
12
|
+
export declare function findStylesheets(targets: string[]): string[];
|
|
13
|
+
export declare const targetPath: (stylesheet: string) => string;
|
|
14
|
+
export declare function plan(targets: string[]): Plan;
|
|
15
|
+
export declare function write(targets: string[]): Plan;
|
|
16
|
+
export declare function formatReports(sheets: Stylesheet[], cwd: string): string[];
|
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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.targetPath = void 0;
|
|
37
|
+
exports.findStylesheets = findStylesheets;
|
|
38
|
+
exports.plan = plan;
|
|
39
|
+
exports.write = write;
|
|
40
|
+
exports.formatReports = formatReports;
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const css_modules_1 = require("./transforms/css-modules");
|
|
44
|
+
const IGNORED = new Set([
|
|
45
|
+
'node_modules',
|
|
46
|
+
'dist',
|
|
47
|
+
'build',
|
|
48
|
+
'out',
|
|
49
|
+
'.next',
|
|
50
|
+
'.git',
|
|
51
|
+
'coverage',
|
|
52
|
+
]);
|
|
53
|
+
function findStylesheets(targets) {
|
|
54
|
+
const found = [];
|
|
55
|
+
const walk = (entry) => {
|
|
56
|
+
const stats = fs.statSync(entry);
|
|
57
|
+
if (stats.isFile()) {
|
|
58
|
+
if (entry.endsWith('.module.css'))
|
|
59
|
+
found.push(path.resolve(entry));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const name of fs.readdirSync(entry)) {
|
|
63
|
+
if (IGNORED.has(name))
|
|
64
|
+
continue;
|
|
65
|
+
walk(path.join(entry, name));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
for (const target of targets) {
|
|
69
|
+
try {
|
|
70
|
+
walk(target);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return found.sort();
|
|
76
|
+
}
|
|
77
|
+
const targetPath = (stylesheet) => stylesheet.replace(/\.module\.css$/, '.styles.ts');
|
|
78
|
+
exports.targetPath = targetPath;
|
|
79
|
+
function plan(targets) {
|
|
80
|
+
const stylesheets = [];
|
|
81
|
+
const modules = {};
|
|
82
|
+
for (const source of findStylesheets(targets)) {
|
|
83
|
+
const converted = (0, css_modules_1.convertStylesheet)(fs.readFileSync(source, 'utf-8'));
|
|
84
|
+
const target = (0, exports.targetPath)(source);
|
|
85
|
+
stylesheets.push({ source, target, reports: converted.reports });
|
|
86
|
+
modules[path.basename(source)] = {
|
|
87
|
+
source: `./${path.basename(target, '.ts')}`,
|
|
88
|
+
names: converted.names,
|
|
89
|
+
composes: converted.composes,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { stylesheets, modules };
|
|
93
|
+
}
|
|
94
|
+
function write(targets) {
|
|
95
|
+
const result = plan(targets);
|
|
96
|
+
for (const sheet of result.stylesheets) {
|
|
97
|
+
const converted = (0, css_modules_1.convertStylesheet)(fs.readFileSync(sheet.source, 'utf-8'));
|
|
98
|
+
fs.writeFileSync(sheet.target, converted.code, 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
function formatReports(sheets, cwd) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
for (const sheet of sheets) {
|
|
105
|
+
if (sheet.reports.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
lines.push(path.relative(cwd, sheet.source) || sheet.source);
|
|
108
|
+
for (const report of sheet.reports) {
|
|
109
|
+
lines.push(` ${report.line}:${report.column} ${report.kind} ${report.source}`);
|
|
110
|
+
lines.push(` ${report.hint}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
export interface ModuleMap {
|
|
3
|
+
source: string;
|
|
4
|
+
names: Record<string, string>;
|
|
5
|
+
composes?: Record<string, string[]>;
|
|
6
|
+
}
|
|
7
|
+
export interface AdoptStylesOptions {
|
|
8
|
+
modules: Record<string, ModuleMap>;
|
|
9
|
+
styleProp?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const adoptStyles: Rule.RuleModule;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adoptStyles = void 0;
|
|
4
|
+
const CORE = '@plumeria/core';
|
|
5
|
+
const readOptions = (context) => {
|
|
6
|
+
const raw = (context.options[0] ?? {});
|
|
7
|
+
return {
|
|
8
|
+
modules: raw.modules ?? {},
|
|
9
|
+
styleProp: raw.styleProp ?? 'classStyle',
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
exports.adoptStyles = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Rewrite a CSS Modules import and its usages onto a generated Plumeria module',
|
|
17
|
+
},
|
|
18
|
+
fixable: 'code',
|
|
19
|
+
messages: {
|
|
20
|
+
source: 'Import the generated styles from "{{source}}" instead of the stylesheet.',
|
|
21
|
+
core: `Add \`import '${CORE}';\` — the compiler collects a file only when it imports the package.`,
|
|
22
|
+
key: 'The generated key for "{{from}}" is "{{to}}".',
|
|
23
|
+
prop: 'Styles are carried by the "{{prop}}" prop.',
|
|
24
|
+
composes: '"{{key}}" composed other classes; pass them as an array.',
|
|
25
|
+
},
|
|
26
|
+
schema: [
|
|
27
|
+
{
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
modules: { type: 'object' },
|
|
31
|
+
styleProp: { type: 'string' },
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
create(context) {
|
|
38
|
+
const { modules, styleProp } = readOptions(context);
|
|
39
|
+
const bindings = new Map();
|
|
40
|
+
let hasCore = false;
|
|
41
|
+
let stylesTaken = false;
|
|
42
|
+
return {
|
|
43
|
+
Program(program) {
|
|
44
|
+
hasCore = program.body.some((n) => n.type === 'ImportDeclaration' && n.source.value === CORE);
|
|
45
|
+
stylesTaken = program.body.some((n) => {
|
|
46
|
+
if (n.type === 'VariableDeclaration') {
|
|
47
|
+
return n.declarations.some((d) => d.id?.name === 'styles');
|
|
48
|
+
}
|
|
49
|
+
if (n.type === 'ImportDeclaration') {
|
|
50
|
+
return n.specifiers.some((sp) => sp.local?.name === 'styles');
|
|
51
|
+
}
|
|
52
|
+
return ((n.type === 'FunctionDeclaration' ||
|
|
53
|
+
n.type === 'ClassDeclaration') &&
|
|
54
|
+
n.id?.name === 'styles');
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
ImportDeclaration(node) {
|
|
58
|
+
const specifier = String(node.source.value);
|
|
59
|
+
const entry = modules[specifier] ??
|
|
60
|
+
Object.entries(modules).find(([key]) => key.includes('/') === false && specifier.endsWith(`/${key}`))?.[1];
|
|
61
|
+
if (!entry)
|
|
62
|
+
return;
|
|
63
|
+
const local = node.specifiers.find((s) => s.type === 'ImportDefaultSpecifier');
|
|
64
|
+
if (!local)
|
|
65
|
+
return;
|
|
66
|
+
const name = local.local.name;
|
|
67
|
+
const renamed = stylesTaken && name !== 'styles' ? name : 'styles';
|
|
68
|
+
bindings.set(name, { entry, local: renamed });
|
|
69
|
+
const imported = renamed === 'styles'
|
|
70
|
+
? `import { styles } from '${entry.source}';`
|
|
71
|
+
: `import { styles as ${renamed} } from '${entry.source}';`;
|
|
72
|
+
const replacement = hasCore
|
|
73
|
+
? imported
|
|
74
|
+
: `import '${CORE}';\n${imported}`;
|
|
75
|
+
if (!hasCore)
|
|
76
|
+
hasCore = true;
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: 'source',
|
|
80
|
+
data: { source: entry.source },
|
|
81
|
+
fix: (fixer) => fixer.replaceText(node, replacement),
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
MemberExpression(node) {
|
|
85
|
+
const binding = node.object.type === 'Identifier'
|
|
86
|
+
? bindings.get(node.object.name)
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!binding)
|
|
89
|
+
return;
|
|
90
|
+
const written = node.computed && node.property.type === 'Literal'
|
|
91
|
+
? String(node.property.value)
|
|
92
|
+
: !node.computed && node.property.type === 'Identifier'
|
|
93
|
+
? node.property.name
|
|
94
|
+
: null;
|
|
95
|
+
if (written === null)
|
|
96
|
+
return;
|
|
97
|
+
const key = binding.entry.names[written] ?? written;
|
|
98
|
+
const local = binding.local;
|
|
99
|
+
if (key === written && local === node.object.name)
|
|
100
|
+
return;
|
|
101
|
+
context.report({
|
|
102
|
+
node: node.property,
|
|
103
|
+
messageId: 'key',
|
|
104
|
+
data: { from: written, to: key },
|
|
105
|
+
fix: (fixer) => fixer.replaceText(node, `${local}.${key}`),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
JSXAttribute(node) {
|
|
109
|
+
if (node.name.type !== 'JSXIdentifier')
|
|
110
|
+
return;
|
|
111
|
+
if (node.name.name !== 'className')
|
|
112
|
+
return;
|
|
113
|
+
const value = node.value;
|
|
114
|
+
if (!value || value.type !== 'JSXExpressionContainer')
|
|
115
|
+
return;
|
|
116
|
+
const expression = value.expression;
|
|
117
|
+
const root = expression.type === 'MemberExpression' &&
|
|
118
|
+
expression.object.type === 'Identifier'
|
|
119
|
+
? expression.object.name
|
|
120
|
+
: null;
|
|
121
|
+
if (!root)
|
|
122
|
+
return;
|
|
123
|
+
const binding = bindings.get(root);
|
|
124
|
+
if (!binding)
|
|
125
|
+
return;
|
|
126
|
+
const { entry, local } = binding;
|
|
127
|
+
context.report({
|
|
128
|
+
node: node.name,
|
|
129
|
+
messageId: 'prop',
|
|
130
|
+
data: { prop: styleProp },
|
|
131
|
+
fix: (fixer) => fixer.replaceText(node.name, styleProp),
|
|
132
|
+
});
|
|
133
|
+
const written = expression.computed && expression.property.type === 'Literal'
|
|
134
|
+
? String(expression.property.value)
|
|
135
|
+
: !expression.computed && expression.property.type === 'Identifier'
|
|
136
|
+
? expression.property.name
|
|
137
|
+
: null;
|
|
138
|
+
const key = written === null ? null : (entry.names[written] ?? written);
|
|
139
|
+
const composed = key ? entry.composes?.[key] : undefined;
|
|
140
|
+
if (!composed || composed.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
const parts = composed
|
|
143
|
+
.concat(key)
|
|
144
|
+
.map((k) => `${local}.${k}`)
|
|
145
|
+
.join(', ');
|
|
146
|
+
context.report({
|
|
147
|
+
node: expression,
|
|
148
|
+
messageId: 'composes',
|
|
149
|
+
data: { key: key },
|
|
150
|
+
fix: (fixer) => fixer.replaceText(expression, `[${parts}]`),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Report {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
kind: string;
|
|
5
|
+
source: string;
|
|
6
|
+
hint: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Converted {
|
|
9
|
+
code: string;
|
|
10
|
+
names: Record<string, string>;
|
|
11
|
+
composes: Record<string, string[]>;
|
|
12
|
+
reports: Report[];
|
|
13
|
+
}
|
|
14
|
+
type Value = string | number;
|
|
15
|
+
export declare const toProperty: (name: string) => string;
|
|
16
|
+
export declare const toKey: (className: string) => string;
|
|
17
|
+
export declare const toValue: (raw: string) => Value;
|
|
18
|
+
export declare function convertStylesheet(css: string): Converted;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.toValue = exports.toKey = exports.toProperty = void 0;
|
|
7
|
+
exports.convertStylesheet = convertStylesheet;
|
|
8
|
+
const postcss_1 = __importDefault(require("postcss"));
|
|
9
|
+
const postcss_selector_parser_1 = __importDefault(require("postcss-selector-parser"));
|
|
10
|
+
const DOC = 'https://plumeria.dev/docs/api-reference/javascript/marker';
|
|
11
|
+
const emptyNode = () => ({ decls: [], nested: [], markers: [] });
|
|
12
|
+
const camel = (name) => name.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
13
|
+
const toProperty = (name) => {
|
|
14
|
+
if (name.startsWith('--'))
|
|
15
|
+
return name;
|
|
16
|
+
if (name.startsWith('-')) {
|
|
17
|
+
const camelised = camel(name.slice(1));
|
|
18
|
+
return camelised.charAt(0).toUpperCase() + camelised.slice(1);
|
|
19
|
+
}
|
|
20
|
+
return camel(name);
|
|
21
|
+
};
|
|
22
|
+
exports.toProperty = toProperty;
|
|
23
|
+
const toKey = (className) => camel(className);
|
|
24
|
+
exports.toKey = toKey;
|
|
25
|
+
const PX = /^-?(\d+\.?\d*|\.\d+)px$/;
|
|
26
|
+
const NUMBER = /^-?(\d+\.?\d*|\.\d+)$/;
|
|
27
|
+
const toValue = (raw) => {
|
|
28
|
+
const value = raw.trim();
|
|
29
|
+
if (PX.test(value))
|
|
30
|
+
return Number(value.slice(0, -2));
|
|
31
|
+
if (NUMBER.test(value))
|
|
32
|
+
return Number(value);
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
exports.toValue = toValue;
|
|
36
|
+
const nest = (node, key) => {
|
|
37
|
+
const found = node.nested.find(([k]) => k === key);
|
|
38
|
+
if (found)
|
|
39
|
+
return found[1];
|
|
40
|
+
const child = emptyNode();
|
|
41
|
+
node.nested.push([key, child]);
|
|
42
|
+
return child;
|
|
43
|
+
};
|
|
44
|
+
const atRuleKeys = (rule) => {
|
|
45
|
+
const keys = [];
|
|
46
|
+
let parent = rule.parent;
|
|
47
|
+
while (parent && parent.type === 'atrule') {
|
|
48
|
+
const at = parent;
|
|
49
|
+
keys.unshift(`@${at.name} ${at.params}`.trim());
|
|
50
|
+
parent = parent.parent;
|
|
51
|
+
}
|
|
52
|
+
return keys;
|
|
53
|
+
};
|
|
54
|
+
const readSelector = (selector) => {
|
|
55
|
+
let result = {
|
|
56
|
+
kind: 'unsupported-selector',
|
|
57
|
+
hint: 'Only a local class, or one local class under another, is converted.',
|
|
58
|
+
};
|
|
59
|
+
(0, postcss_selector_parser_1.default)((root) => {
|
|
60
|
+
const nodes = root.first.nodes.filter((n) => n.type !== 'string');
|
|
61
|
+
const parts = [
|
|
62
|
+
{ pseudos: [] },
|
|
63
|
+
];
|
|
64
|
+
let combinator = null;
|
|
65
|
+
let bad = null;
|
|
66
|
+
for (const node of nodes) {
|
|
67
|
+
if (node.type === 'class') {
|
|
68
|
+
const current = parts[parts.length - 1];
|
|
69
|
+
if (current.className) {
|
|
70
|
+
bad = {
|
|
71
|
+
kind: 'unsupported-selector',
|
|
72
|
+
hint: 'Two classes on one element are written as an array at the call site.',
|
|
73
|
+
};
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
current.className = node.value;
|
|
77
|
+
}
|
|
78
|
+
else if (node.type === 'pseudo') {
|
|
79
|
+
if (node.value === ':global') {
|
|
80
|
+
bad = {
|
|
81
|
+
kind: 'global',
|
|
82
|
+
hint: "A global class is written as a selector key: `':is(.name *)'`.",
|
|
83
|
+
};
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
parts[parts.length - 1].pseudos.push(String(node));
|
|
87
|
+
}
|
|
88
|
+
else if (node.type === 'combinator') {
|
|
89
|
+
const value = node.value.trim();
|
|
90
|
+
if (value === '+' || value === '~') {
|
|
91
|
+
bad = {
|
|
92
|
+
kind: 'sibling-combinator',
|
|
93
|
+
hint: `A marker carries no order. Write the relation as a selector key: ${DOC}`,
|
|
94
|
+
};
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (parts.length > 1) {
|
|
98
|
+
bad = {
|
|
99
|
+
kind: 'unsupported-selector',
|
|
100
|
+
hint: 'Only one level of nesting is converted.',
|
|
101
|
+
};
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
combinator = value === '>' ? '>' : ' ';
|
|
105
|
+
parts.push({ pseudos: [] });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
bad = {
|
|
109
|
+
kind: 'unsupported-selector',
|
|
110
|
+
hint: 'Only a local class, or one local class under another, is converted.',
|
|
111
|
+
};
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (bad) {
|
|
116
|
+
result = bad;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (parts.some((p) => !p.className))
|
|
120
|
+
return;
|
|
121
|
+
const [first, second] = parts;
|
|
122
|
+
if (!second) {
|
|
123
|
+
result = {
|
|
124
|
+
target: {
|
|
125
|
+
key: (0, exports.toKey)(first.className),
|
|
126
|
+
pseudo: first.pseudos.join(''),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
result = {
|
|
132
|
+
target: {
|
|
133
|
+
key: (0, exports.toKey)(second.className),
|
|
134
|
+
pseudo: second.pseudos.join(''),
|
|
135
|
+
ancestor: {
|
|
136
|
+
key: (0, exports.toKey)(first.className),
|
|
137
|
+
pseudo: first.pseudos.join('') || ':defined',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
void combinator;
|
|
142
|
+
}).processSync(selector);
|
|
143
|
+
return result;
|
|
144
|
+
};
|
|
145
|
+
const serialise = (node, indent) => {
|
|
146
|
+
const lines = [];
|
|
147
|
+
for (const marker of node.markers)
|
|
148
|
+
lines.push(`${indent}...${marker},`);
|
|
149
|
+
for (const [prop, value] of node.decls) {
|
|
150
|
+
const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(prop) ? prop : `'${prop}'`;
|
|
151
|
+
lines.push(`${indent}${key}: ${typeof value === 'number' ? value : `'${value}'`},`);
|
|
152
|
+
}
|
|
153
|
+
for (const [key, child] of node.nested) {
|
|
154
|
+
const written = key.startsWith('css.extended(') ? `[${key}]` : `'${key}'`;
|
|
155
|
+
lines.push(`${indent}${written}: {`);
|
|
156
|
+
lines.push(serialise(child, indent + ' '));
|
|
157
|
+
lines.push(`${indent}},`);
|
|
158
|
+
}
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
};
|
|
161
|
+
function convertStylesheet(css) {
|
|
162
|
+
const root = postcss_1.default.parse(css);
|
|
163
|
+
const keys = new Map();
|
|
164
|
+
const names = {};
|
|
165
|
+
const composes = {};
|
|
166
|
+
const reports = [];
|
|
167
|
+
const markerPseudos = new Map();
|
|
168
|
+
const keyOf = (name) => {
|
|
169
|
+
if (!keys.has(name))
|
|
170
|
+
keys.set(name, emptyNode());
|
|
171
|
+
return keys.get(name);
|
|
172
|
+
};
|
|
173
|
+
const report = (node, kind, hint) => {
|
|
174
|
+
const start = node.source?.start;
|
|
175
|
+
reports.push({
|
|
176
|
+
line: start?.line ?? 0,
|
|
177
|
+
column: start?.column ?? 0,
|
|
178
|
+
kind,
|
|
179
|
+
source: node.type === 'rule' ? node.selector : String(node),
|
|
180
|
+
hint,
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
root.walkRules((rule) => {
|
|
184
|
+
for (const selector of rule.selectors) {
|
|
185
|
+
const read = readSelector(selector);
|
|
186
|
+
if (!('target' in read)) {
|
|
187
|
+
report(rule, read.kind, read.hint);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const { target } = read;
|
|
191
|
+
for (const className of selector.match(/\.([A-Za-z0-9_-]+)/g) ?? []) {
|
|
192
|
+
const bare = className.slice(1);
|
|
193
|
+
const key = (0, exports.toKey)(bare);
|
|
194
|
+
const taken = Object.entries(names).find(([other, k]) => k === key && other !== bare);
|
|
195
|
+
if (taken) {
|
|
196
|
+
report(rule, 'key-collision', `\`${bare}\` and \`${taken[0]}\` both become \`${key}\`.`);
|
|
197
|
+
}
|
|
198
|
+
names[bare] = key;
|
|
199
|
+
}
|
|
200
|
+
let node = keyOf(target.key);
|
|
201
|
+
if (target.ancestor) {
|
|
202
|
+
const { key: id, pseudo } = target.ancestor;
|
|
203
|
+
const seen = markerPseudos.get(id) ?? new Set();
|
|
204
|
+
if (!seen.has(pseudo)) {
|
|
205
|
+
seen.add(pseudo);
|
|
206
|
+
markerPseudos.set(id, seen);
|
|
207
|
+
keyOf(id).markers.push(`css.marker('${id}', '${pseudo}')`);
|
|
208
|
+
}
|
|
209
|
+
node = nest(node, `css.extended('${id}', '${pseudo}')`);
|
|
210
|
+
}
|
|
211
|
+
for (const key of atRuleKeys(rule))
|
|
212
|
+
node = nest(node, key);
|
|
213
|
+
if (target.pseudo)
|
|
214
|
+
node = nest(node, target.pseudo);
|
|
215
|
+
rule.each((child) => {
|
|
216
|
+
if (child.type !== 'decl')
|
|
217
|
+
return;
|
|
218
|
+
if (child.prop === 'composes') {
|
|
219
|
+
const [name, from] = child.value.split(/\s+from\s+/);
|
|
220
|
+
if (from) {
|
|
221
|
+
report(child, 'composes-external', 'A class composed from another file is not resolved.');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
composes[target.key] = (composes[target.key] ?? []).concat(name.split(/\s+/).map(exports.toKey));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
node.decls.push([(0, exports.toProperty)(child.prop), (0, exports.toValue)(child.value)]);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
const body = [...keys.entries()]
|
|
232
|
+
.map(([key, node]) => ` ${key}: {\n${serialise(node, ' ')}\n },`)
|
|
233
|
+
.join('\n');
|
|
234
|
+
const code = `import * as css from '@plumeria/core';
|
|
235
|
+
|
|
236
|
+
export const styles = css.create({
|
|
237
|
+
${body}
|
|
238
|
+
});
|
|
239
|
+
`;
|
|
240
|
+
return { code, names, composes, reports };
|
|
241
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumeria/codemod",
|
|
3
|
-
"version": "18.2.
|
|
3
|
+
"version": "18.2.1",
|
|
4
4
|
"description": "Codemods for migrating Plumeria APIs",
|
|
5
5
|
"author": "Refirst 11",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@typescript-eslint/parser": "^8.60.0",
|
|
36
|
-
"eslint": "^10.4.1"
|
|
36
|
+
"eslint": "^10.4.1",
|
|
37
|
+
"postcss": "8.5.25",
|
|
38
|
+
"postcss-selector-parser": "^7.1.1"
|
|
37
39
|
},
|
|
38
40
|
"publishConfig": {
|
|
39
41
|
"access": "public",
|