@plumeria/codemod 18.2.0 → 18.2.2

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
@@ -1,11 +1,82 @@
1
1
  # @plumeria/codemod
2
2
 
3
- Codemods for migrating Plumeria APIs. No install needed — run it with `npx`.
3
+ Codemods for moving a codebase onto Plumeria, and for migrating Plumeria APIs.
4
+ No install needed — run it with `npx`.
4
5
 
5
6
  ```sh
7
+ npx @plumeria/codemod migrate --from css-modules
6
8
  npx @plumeria/codemod rename-prop classStyle sx
7
9
  ```
8
10
 
11
+ Commit before running, so the rewrite can be reverted with `git checkout`.
12
+
13
+ ## `migrate --from css-modules`
14
+
15
+ Converts every `*.module.css` under the paths into a `*.styles.ts` beside it,
16
+ and rewrites the files importing it — the import, the `className` prop, the
17
+ class names, and `composes`:
18
+
19
+ ```sh
20
+ npx @plumeria/codemod migrate --from css-modules [paths...]
21
+ ```
22
+
23
+ ```css
24
+ /* Card.module.css */
25
+ .base { font-size: 12px }
26
+ .card { composes: base; padding: 16px }
27
+ .card:hover { background: teal }
28
+ .card .card-title { color: red }
29
+ ```
30
+
31
+ ```ts
32
+ /* Card.styles.ts */
33
+ import * as css from '@plumeria/core';
34
+
35
+ export const styles = css.create({
36
+ base: { fontSize: 12 },
37
+ card: {
38
+ ...css.marker('card', ':defined'),
39
+ padding: 16,
40
+ ':hover': { background: 'teal' },
41
+ },
42
+ cardTitle: {
43
+ [css.extended('card', ':defined')]: { color: 'red' },
44
+ },
45
+ });
46
+ ```
47
+
48
+ ```diff
49
+ /* Card.tsx */
50
+ + import '@plumeria/core';
51
+ - import styles from './Card.module.css';
52
+ + import { styles } from './Card.styles';
53
+
54
+ - <div className={styles.card}>
55
+ - <span className={styles['card-title']} />
56
+ + <div classStyle={[styles.base, styles.card]}>
57
+ + <span classStyle={styles.cardTitle} />
58
+ </div>
59
+ ```
60
+
61
+ A descendant rule has no combinator to translate into, so it becomes
62
+ `css.marker` on the parent and `css.extended` on the child. Which class is which
63
+ is read from the stylesheet; no markup is inspected. Class names become
64
+ camel-case keys, and a bare pixel length loses its unit.
65
+
66
+ ### What it reports instead of converting
67
+
68
+ ```
69
+ src/Card.module.css
70
+ 10:1 sibling-combinator .item + .item
71
+ A marker carries no order. Write the relation as a selector key.
72
+ ```
73
+
74
+ Sibling combinators, `:global`, and a `composes` reaching into another file are
75
+ named rather than guessed at, and the original rule is left in place. The exit
76
+ code is 1 while anything remains, so the command composes with a script.
77
+
78
+ Pass `-d, --dry-run` to see the stylesheets and consumer counts without writing.
79
+
9
80
  ## `rename-prop`
10
81
 
11
82
  Renames the styling prop across your source. The names are arguments, so it
@@ -70,5 +141,3 @@ withPlumeria(nextConfig, { styleProp: 'sx' });
70
141
  // eslint.config.js
71
142
  settings: { plumeria: { styleProp: 'sx' } }
72
143
  ```
73
-
74
- Commit before running, so the rewrite can be reverted with `git checkout`.
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
- for (const arg of argv) {
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[];
@@ -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.0",
3
+ "version": "18.2.2",
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",