@plumeria/codemod 18.2.19 → 18.2.20

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.
@@ -1,6 +1,41 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.adoptStyles = void 0;
37
+ const path = __importStar(require("node:path"));
38
+ const resolve_1 = require("../resolve");
4
39
  const CORE = '@plumeria/core';
5
40
  const readOptions = (context) => {
6
41
  const raw = (context.options[0] ?? {});
@@ -22,6 +57,9 @@ exports.adoptStyles = {
22
57
  key: 'The generated key for "{{from}}" is "{{to}}".',
23
58
  prop: 'Styles are carried by the "{{prop}}" prop.',
24
59
  composes: '"{{key}}" composed other classes; pass them as an array.',
60
+ value: 'A style read as a value goes back through `css.use`.',
61
+ joined: 'A joined class list is written as an array.',
62
+ call: 'The custom properties are the arguments of a function style.',
25
63
  },
26
64
  schema: [
27
65
  {
@@ -39,9 +77,125 @@ exports.adoptStyles = {
39
77
  const bindings = new Map();
40
78
  let hasCore = false;
41
79
  let stylesTaken = false;
80
+ let coreLocal;
81
+ let readsValue = false;
82
+ let several = false;
83
+ const stylingPosition = (node) => {
84
+ let current = node;
85
+ while (current.parent &&
86
+ (current.parent.type === 'ArrayExpression' ||
87
+ current.parent.type === 'LogicalExpression' ||
88
+ current.parent.type === 'ConditionalExpression' ||
89
+ current.parent.type === 'JSXExpressionContainer'))
90
+ current = current.parent;
91
+ return (current.parent?.type === 'JSXAttribute' &&
92
+ (current.parent.name?.name === 'className' ||
93
+ current.parent.name?.name === styleProp));
94
+ };
95
+ const joinedElements = (node) => {
96
+ if (node?.type === 'TemplateLiteral' && node.expressions.length > 1) {
97
+ const spaced = node.quasis.every((quasi, index) => index === 0 || index === node.quasis.length - 1
98
+ ? quasi.value.cooked === ''
99
+ : quasi.value.cooked === ' ');
100
+ return spaced ? node.expressions : undefined;
101
+ }
102
+ if (node?.type !== 'CallExpression' ||
103
+ node.callee.type !== 'MemberExpression' ||
104
+ node.callee.property?.name !== 'join' ||
105
+ node.arguments.length !== 1 ||
106
+ node.arguments[0].value !== ' ')
107
+ return undefined;
108
+ const joined = node.callee.object;
109
+ if (joined?.type === 'ArrayExpression')
110
+ return joined.elements.filter(Boolean);
111
+ return joined?.type === 'CallExpression' &&
112
+ joined.callee.type === 'MemberExpression' &&
113
+ joined.callee.property?.name === 'filter' &&
114
+ joined.callee.object?.type === 'ArrayExpression'
115
+ ? joined.callee.object.elements.filter(Boolean)
116
+ : undefined;
117
+ };
118
+ const intrinsic = (element) => {
119
+ const name = element?.name;
120
+ return name?.type === 'JSXIdentifier' && /^[a-z]/.test(String(name.name));
121
+ };
122
+ const styleRead = (node) => node?.type === 'LogicalExpression'
123
+ ? styleRead(node.right)
124
+ : node?.type === 'MemberExpression'
125
+ ? node
126
+ : undefined;
127
+ const insideUse = (node) => {
128
+ let current = node;
129
+ while (current.parent &&
130
+ (current.parent.type === 'LogicalExpression' ||
131
+ current.parent.type === 'ConditionalExpression' ||
132
+ current.parent.type === 'ArrayExpression'))
133
+ current = current.parent;
134
+ return (current.parent?.type === 'CallExpression' &&
135
+ current.parent.callee?.type === 'MemberExpression' &&
136
+ current.parent.callee.property?.name === 'use');
137
+ };
42
138
  return {
43
139
  Program(program) {
44
140
  hasCore = program.body.some((n) => n.type === 'ImportDeclaration' && n.source.value === CORE);
141
+ const locals = new Set();
142
+ let seen = 0;
143
+ for (const statement of program.body) {
144
+ if (statement.type !== 'ImportDeclaration')
145
+ continue;
146
+ if (statement.source.value === CORE) {
147
+ for (const specifier of statement.specifiers)
148
+ if (specifier.type === 'ImportNamespaceSpecifier' ||
149
+ (specifier.type === 'ImportSpecifier' &&
150
+ (specifier.imported.name ?? specifier.imported.value) ===
151
+ 'css'))
152
+ coreLocal = specifier.local.name;
153
+ continue;
154
+ }
155
+ const target = (0, resolve_1.resolveSourcePath)(String(statement.source.value), context.filename);
156
+ if (!target || !modules[target])
157
+ continue;
158
+ for (const specifier of statement.specifiers)
159
+ if (specifier.type === 'ImportDefaultSpecifier') {
160
+ locals.add(specifier.local.name);
161
+ seen += 1;
162
+ }
163
+ }
164
+ several = seen > 1;
165
+ const visit = (node, styling) => {
166
+ if (!node || typeof node !== 'object')
167
+ return;
168
+ if (Array.isArray(node)) {
169
+ for (const child of node)
170
+ visit(child, styling);
171
+ return;
172
+ }
173
+ if (typeof node.type !== 'string')
174
+ return;
175
+ const inside = styling ||
176
+ (node.type === 'JSXAttribute' &&
177
+ (node.name?.name === 'className' ||
178
+ node.name?.name === styleProp));
179
+ if (!inside &&
180
+ node.type === 'MemberExpression' &&
181
+ locals.has(node.object?.name))
182
+ readsValue = true;
183
+ if (node.type === 'JSXAttribute' &&
184
+ node.name?.name === 'className' &&
185
+ node.parent?.name?.type === 'JSXIdentifier' &&
186
+ /^[A-Z]/.test(String(node.parent.name.name)) &&
187
+ node.value?.type === 'JSXExpressionContainer' &&
188
+ joinedElements(node.value.expression))
189
+ readsValue = true;
190
+ for (const key of Object.keys(node)) {
191
+ if (key === 'parent' || key === 'loc' || key === 'range')
192
+ continue;
193
+ visit(node[key], inside);
194
+ }
195
+ };
196
+ visit(program.body, false);
197
+ if (readsValue && !coreLocal)
198
+ coreLocal = 'css';
45
199
  stylesTaken = program.body.some((n) => {
46
200
  if (n.type === 'VariableDeclaration') {
47
201
  return n.declarations.some((d) => d.id?.name === 'styles');
@@ -56,28 +210,55 @@ exports.adoptStyles = {
56
210
  },
57
211
  ImportDeclaration(node) {
58
212
  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)
213
+ const resolved = (0, resolve_1.resolveSourcePath)(specifier, context.filename);
214
+ const named = Object.entries(modules).filter(([key]) => key === specifier ||
215
+ path.basename(key) === path.basename(specifier));
216
+ const entry = (resolved ? modules[resolved] : undefined) ??
217
+ (named.length === 1 ? named[0][1] : undefined);
218
+ if (!entry) {
219
+ const adopted = resolved &&
220
+ Object.values(modules).find((candidate) => candidate.target === resolved);
221
+ if (!adopted)
222
+ return;
223
+ const already = node.specifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
224
+ (specifier.imported.name ?? specifier.imported.value) ===
225
+ 'styles');
226
+ if (already)
227
+ bindings.set(already.local.name, {
228
+ entry: adopted,
229
+ local: already.local.name,
230
+ });
62
231
  return;
232
+ }
233
+ const from = entry.target && path.isAbsolute(context.filename)
234
+ ? (() => {
235
+ const relative = path
236
+ .relative(path.dirname(context.filename), entry.target)
237
+ .split(path.sep)
238
+ .join('/')
239
+ .replace(/\.ts$/, '');
240
+ return relative.startsWith('.') ? relative : `./${relative}`;
241
+ })()
242
+ : entry.source;
63
243
  const local = node.specifiers.find((s) => s.type === 'ImportDefaultSpecifier');
64
244
  if (!local)
65
245
  return;
66
246
  const name = local.local.name;
67
- const renamed = stylesTaken && name !== 'styles' ? name : 'styles';
247
+ const renamed = several || (stylesTaken && name !== 'styles') ? name : 'styles';
68
248
  bindings.set(name, { entry, local: renamed });
69
249
  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}`;
250
+ ? `import { styles } from '${from}';`
251
+ : `import { styles as ${renamed} } from '${from}';`;
252
+ const core = coreLocal
253
+ ? `import * as ${coreLocal} from '${CORE}';`
254
+ : `import '${CORE}';`;
255
+ const replacement = hasCore ? imported : `${core}\n${imported}`;
75
256
  if (!hasCore)
76
257
  hasCore = true;
77
258
  context.report({
78
259
  node,
79
260
  messageId: 'source',
80
- data: { source: entry.source },
261
+ data: { source: from },
81
262
  fix: (fixer) => fixer.replaceText(node, replacement),
82
263
  });
83
264
  },
@@ -87,6 +268,14 @@ exports.adoptStyles = {
87
268
  : undefined;
88
269
  if (!binding)
89
270
  return;
271
+ if (coreLocal && !stylingPosition(node) && !insideUse(node)) {
272
+ context.report({
273
+ node,
274
+ messageId: 'value',
275
+ fix: (fixer) => fixer.replaceText(node, `${coreLocal}.use(${context.sourceCode.getText(node)})`),
276
+ });
277
+ return;
278
+ }
90
279
  const written = node.computed && node.property.type === 'Literal'
91
280
  ? String(node.property.value)
92
281
  : !node.computed && node.property.type === 'Identifier'
@@ -105,6 +294,82 @@ exports.adoptStyles = {
105
294
  fix: (fixer) => fixer.replaceText(node, `${local}.${key}`),
106
295
  });
107
296
  },
297
+ [`JSXAttribute[name.name='${styleProp}']`](node) {
298
+ if (node.value?.type !== 'JSXExpressionContainer')
299
+ return;
300
+ const style = node.parent.attributes.find((attribute) => attribute.type === 'JSXAttribute' &&
301
+ attribute.name?.name === 'style' &&
302
+ attribute.value?.type === 'JSXExpressionContainer' &&
303
+ attribute.value.expression.type === 'ObjectExpression');
304
+ if (!style)
305
+ return;
306
+ const properties = style.value.expression.properties;
307
+ const named = new Map();
308
+ for (const property of properties) {
309
+ if (property.type !== 'Property' || !property.computed)
310
+ continue;
311
+ const key = property.key.type === 'TSAsExpression'
312
+ ? property.key.expression
313
+ : property.key;
314
+ if (key.type === 'Literal' && typeof key.value === 'string')
315
+ named.set(key.value, property);
316
+ }
317
+ if (named.size === 0)
318
+ return;
319
+ const consumed = new Set();
320
+ const call = (read) => {
321
+ const binding = read?.type === 'MemberExpression' &&
322
+ read.object.type === 'Identifier'
323
+ ? bindings.get(read.object.name)
324
+ : undefined;
325
+ if (!binding)
326
+ return undefined;
327
+ const written = !read.computed && read.property.type === 'Identifier'
328
+ ? read.property.name
329
+ : read.computed && read.property.type === 'Literal'
330
+ ? String(read.property.value)
331
+ : undefined;
332
+ const key = written && (binding.entry.names[written] ?? written);
333
+ const variables = key ? binding.entry.functions?.[key] : undefined;
334
+ if (!variables || variables.some((v) => !named.has(v)))
335
+ return undefined;
336
+ for (const variable of variables)
337
+ consumed.add(named.get(variable));
338
+ return `${binding.local}.${key}(${variables
339
+ .map((variable) => context.sourceCode.getText(named.get(variable).value))
340
+ .join(', ')})`;
341
+ };
342
+ const expression = node.value.expression;
343
+ const elements = expression.type === 'ArrayExpression'
344
+ ? expression.elements.filter(Boolean)
345
+ : [expression];
346
+ const texts = elements.map((element) => {
347
+ const called = call(styleRead(element));
348
+ if (!called)
349
+ return context.sourceCode.getText(element);
350
+ return element.type === 'LogicalExpression'
351
+ ? `${context.sourceCode.getText(element.left)} ${element.operator} ${called}`
352
+ : called;
353
+ });
354
+ if (consumed.size === 0)
355
+ return;
356
+ const kept = properties.filter((property) => !consumed.has(property));
357
+ const replacement = expression.type === 'ArrayExpression'
358
+ ? `[${texts.join(', ')}]`
359
+ : texts[0];
360
+ context.report({
361
+ node,
362
+ messageId: 'call',
363
+ fix: (fixer) => [
364
+ fixer.replaceText(expression, replacement),
365
+ kept.length === 0
366
+ ? fixer.removeRange([style.range[0] - 1, style.range[1]])
367
+ : fixer.replaceText(style.value.expression, `{ ${kept
368
+ .map((property) => context.sourceCode.getText(property))
369
+ .join(', ')} }`),
370
+ ],
371
+ });
372
+ },
108
373
  JSXAttribute(node) {
109
374
  if (node.name.type !== 'JSXIdentifier')
110
375
  return;
@@ -114,6 +379,34 @@ exports.adoptStyles = {
114
379
  if (!value || value.type !== 'JSXExpressionContainer')
115
380
  return;
116
381
  const expression = value.expression;
382
+ const joined = joinedElements(expression);
383
+ if (joined) {
384
+ if (!joined.some((element) => bindings.has(styleRead(element)?.object?.name)))
385
+ return;
386
+ const list = joined
387
+ .map((element) => context.sourceCode.getText(element))
388
+ .join(', ');
389
+ if (!intrinsic(node.parent) && coreLocal) {
390
+ context.report({
391
+ node: expression,
392
+ messageId: 'value',
393
+ fix: (fixer) => fixer.replaceText(expression, `${coreLocal}.use(${list})`),
394
+ });
395
+ return;
396
+ }
397
+ context.report({
398
+ node: node.name,
399
+ messageId: 'prop',
400
+ data: { prop: styleProp },
401
+ fix: (fixer) => fixer.replaceText(node.name, styleProp),
402
+ });
403
+ context.report({
404
+ node: expression,
405
+ messageId: 'joined',
406
+ fix: (fixer) => fixer.replaceText(expression, `[${list}]`),
407
+ });
408
+ return;
409
+ }
117
410
  const root = expression.type === 'MemberExpression' &&
118
411
  expression.object.type === 'Identifier'
119
412
  ? expression.object.name
@@ -9,9 +9,13 @@ export interface Converted {
9
9
  code: string;
10
10
  names: Record<string, string>;
11
11
  composes: Record<string, string[]>;
12
+ functions: Record<string, string[]>;
12
13
  reports: Report[];
13
14
  }
14
- type Value = string | number;
15
+ interface Raw {
16
+ raw: string;
17
+ }
18
+ type Value = string | number | Raw;
15
19
  export declare const toProperty: (name: string) => string;
16
20
  export declare const toKey: (className: string) => string;
17
21
  export declare const toValue: (raw: string) => Value;
@@ -41,6 +41,7 @@ const nest = (node, key) => {
41
41
  node.nested.push([key, child]);
42
42
  return child;
43
43
  };
44
+ const isSpecificityBoost = (node) => node.type === 'pseudo' && /^:not\(#[\\_].*\)$/.test(node.toString());
44
45
  const atRuleKeys = (rule) => {
45
46
  const keys = [];
46
47
  let parent = rule.parent;
@@ -57,7 +58,7 @@ const readSelector = (selector) => {
57
58
  hint: 'Only a local class, or one local class under another, is converted.',
58
59
  };
59
60
  (0, postcss_selector_parser_1.default)((root) => {
60
- const nodes = root.first.nodes.filter((n) => n.type !== 'string');
61
+ const nodes = root.first.nodes.filter((n) => n.type !== 'string' && !isSpecificityBoost(n));
61
62
  const parts = [
62
63
  { pseudos: [] },
63
64
  ];
@@ -148,7 +149,16 @@ const serialise = (node, indent) => {
148
149
  lines.push(`${indent}...${marker},`);
149
150
  for (const [prop, value] of node.decls) {
150
151
  const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(prop) ? prop : `'${prop}'`;
151
- lines.push(`${indent}${key}: ${typeof value === 'number' ? value : `'${value}'`},`);
152
+ if (typeof value === 'object' && value.raw === prop) {
153
+ lines.push(`${indent}${prop},`);
154
+ continue;
155
+ }
156
+ const written = typeof value === 'number'
157
+ ? value
158
+ : typeof value === 'object'
159
+ ? value.raw
160
+ : `'${value}'`;
161
+ lines.push(`${indent}${key}: ${written},`);
152
162
  }
153
163
  for (const [key, child] of node.nested) {
154
164
  const written = key.startsWith('css.extended(') ? `[${key}]` : `'${key}'`;
@@ -224,12 +234,56 @@ function convertStylesheet(css) {
224
234
  composes[target.key] = (composes[target.key] ?? []).concat(name.split(/\s+/).map(exports.toKey));
225
235
  return;
226
236
  }
227
- node.decls.push([(0, exports.toProperty)(child.prop), (0, exports.toValue)(child.value)]);
237
+ const property = (0, exports.toProperty)(child.prop);
238
+ const held = node.decls.findIndex(([name]) => name === property);
239
+ if (held !== -1)
240
+ node.decls.splice(held, 1);
241
+ node.decls.push([property, (0, exports.toValue)(child.value)]);
228
242
  });
229
243
  }
230
244
  });
245
+ const declared = new Set();
246
+ root.walkDecls((decl) => {
247
+ if (decl.prop.startsWith('--'))
248
+ declared.add(decl.prop);
249
+ });
250
+ const functions = {};
251
+ const VARIABLE = /^var\((--[A-Za-z0-9_-]+)\)$/;
252
+ const HASHED = /^x[0-9a-z]{7}$/;
253
+ const kebab = (name) => name.replace(/([A-Z])/g, '-$1').toLowerCase();
254
+ const parameterise = (node, key, variables) => {
255
+ const owned = new RegExp(`^--[A-Za-z0-9_-]+-${kebab(key)}-([a-z0-9-]+)$`);
256
+ node.decls = node.decls.map(([prop, value]) => {
257
+ const match = typeof value === 'string' ? VARIABLE.exec(value.trim()) : null;
258
+ const variable = match?.[1];
259
+ if (!variable || declared.has(variable))
260
+ return [prop, value];
261
+ const parameter = owned.exec(variable)?.[1];
262
+ if (!parameter || HASHED.test(variable.slice(2).split('-')[0]))
263
+ return [prop, value];
264
+ if (!variables.includes(variable))
265
+ variables.push(variable);
266
+ return [prop, { raw: (0, exports.toKey)(parameter) }];
267
+ });
268
+ for (const [, child] of node.nested)
269
+ parameterise(child, key, variables);
270
+ };
271
+ for (const [key, node] of keys) {
272
+ const variables = [];
273
+ parameterise(node, key, variables);
274
+ if (variables.length > 0)
275
+ functions[key] = variables;
276
+ }
231
277
  const body = [...keys.entries()]
232
- .map(([key, node]) => ` ${key}: {\n${serialise(node, ' ')}\n },`)
278
+ .map(([key, node]) => {
279
+ const variables = functions[key];
280
+ if (!variables)
281
+ return ` ${key}: {\n${serialise(node, ' ')}\n },`;
282
+ const params = variables
283
+ .map((variable) => `${(0, exports.toKey)(variable.split('-').pop())}: string | number`)
284
+ .join(', ');
285
+ return ` ${key}: (${params}) => ({\n${serialise(node, ' ')}\n }),`;
286
+ })
233
287
  .join('\n');
234
288
  const code = `import * as css from '@plumeria/core';
235
289
 
@@ -237,5 +291,5 @@ export const styles = css.create({
237
291
  ${body}
238
292
  });
239
293
  `;
240
- return { code, names, composes, reports };
294
+ return { code, names, composes, functions, reports };
241
295
  }
@@ -14,9 +14,25 @@ export interface PlumeriaModule {
14
14
  variables: string[];
15
15
  }>;
16
16
  aliases: Record<string, Record<string, string>>;
17
+ merges: Record<string, string>;
18
+ overrides: Record<string, Record<number, string>>;
17
19
  definitionOnly: boolean;
18
20
  reports: PlumeriaReport[];
19
21
  }
22
+ export interface PlumeriaComposition {
23
+ parts: {
24
+ binding: string;
25
+ key: string;
26
+ }[];
27
+ slots: {
28
+ binding: string;
29
+ key: string;
30
+ }[][];
31
+ file: string;
32
+ line: number;
33
+ column: number;
34
+ }
35
+ export declare const compositionSignature: (classNames: string[]) => string;
20
36
  export interface ThemeExtraction {
21
37
  bindings: Record<string, Record<string, string>>;
22
38
  globalCss: string;
@@ -42,4 +58,4 @@ export declare const __private: {
42
58
  export declare function extractPlumeriaThemes(source: string): ThemeExtraction;
43
59
  export declare function extractPlumeriaStatics(source: string, externalValues?: Record<string, unknown>): StaticExtraction;
44
60
  export declare function extractPlumeriaAnimations(source: string, externalValues?: Record<string, unknown>): AnimationExtraction;
45
- export declare function convertPlumeriaModule(source: string, externalValues?: Record<string, unknown>): PlumeriaModule | null;
61
+ export declare function convertPlumeriaModule(source: string, externalValues?: Record<string, unknown>, compositions?: PlumeriaComposition[]): PlumeriaModule | null;