@plumeria/unplugin 18.1.4 → 18.1.6

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.
Files changed (3) hide show
  1. package/dist/core.js +219 -107
  2. package/dist/core.mjs +219 -107
  3. package/package.json +2 -2
package/dist/core.js CHANGED
@@ -41,6 +41,70 @@ const zss_engine_1 = require("zss-engine");
41
41
  const utils_1 = require("@plumeria/utils");
42
42
  exports.TARGET_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
43
43
  exports.EXTENSION_PATTERN = /\.(ts|tsx|js|jsx)$/;
44
+ const isStaticArgValue = (node) => node.type === 'StringLiteral' ||
45
+ node.type === 'NumericLiteral' ||
46
+ node.type === 'BooleanLiteral' ||
47
+ (node.type === 'TemplateLiteral' && node.expressions.length === 0) ||
48
+ utils_1.t.isIdentifier(node) ||
49
+ utils_1.t.isMemberExpression(node);
50
+ const namedParamsOf = (params) => {
51
+ if (params.length !== 1)
52
+ return undefined;
53
+ const first = params[0];
54
+ const pattern = (first?.pat ?? first);
55
+ if (pattern?.type !== 'ObjectPattern')
56
+ return undefined;
57
+ const named = [];
58
+ for (const prop of pattern.properties ?? []) {
59
+ if (prop.type === 'AssignmentPatternProperty' &&
60
+ utils_1.t.isIdentifier(prop.key) &&
61
+ !prop.value) {
62
+ named.push({ key: prop.key.value, local: prop.key.value });
63
+ }
64
+ else if (prop.type === 'KeyValuePatternProperty' &&
65
+ utils_1.t.isIdentifier(prop.key) &&
66
+ utils_1.t.isIdentifier(prop.value)) {
67
+ named.push({ key: prop.key.value, local: prop.value.value });
68
+ }
69
+ else {
70
+ return undefined;
71
+ }
72
+ }
73
+ return named.length > 0 ? named : undefined;
74
+ };
75
+ const foldDynamicVars = (vars) => {
76
+ const grouped = new Map();
77
+ [...vars]
78
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
79
+ .forEach((entry) => {
80
+ const list = grouped.get(entry.cssVar);
81
+ if (list)
82
+ list.push(entry);
83
+ else
84
+ grouped.set(entry.cssVar, [entry]);
85
+ });
86
+ return [...grouped].map(([cssVar, entries]) => {
87
+ let value = 'undefined';
88
+ entries.forEach((entry) => {
89
+ value = entry.test
90
+ ? `((${entry.test}) ? ${entry.valueExpr} : ${value})`
91
+ : entry.valueExpr;
92
+ });
93
+ return `"${cssVar}": ${value}`;
94
+ });
95
+ };
96
+ const findVarProp = (style, cssVar) => {
97
+ for (const [prop, value] of Object.entries(style)) {
98
+ if (typeof value === 'string' && value.includes(cssVar))
99
+ return prop;
100
+ if (value !== null && typeof value === 'object') {
101
+ const nested = findVarProp(value, cssVar);
102
+ if (nested)
103
+ return nested;
104
+ }
105
+ }
106
+ return undefined;
107
+ };
44
108
  const unpluginFactory = (options = {}, unpluginMeta) => {
45
109
  const filter = (0, pluginutils_1.createFilter)(options.include, options.exclude);
46
110
  const styleProp = options.styleProp ?? utils_1.DEFAULT_STYLE_PROP;
@@ -384,6 +448,7 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
384
448
  }
385
449
  };
386
450
  const replacements = [];
451
+ const dynamicFnCalls = [];
387
452
  const processedDecls = new Set();
388
453
  const idSpans = new Set();
389
454
  const excludedSpans = new Set();
@@ -461,6 +526,7 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
461
526
  if (actualBody && actualBody.type === 'ObjectExpression') {
462
527
  styleFunctions[prop.key.value] = {
463
528
  params,
529
+ named: namedParamsOf(func.params),
464
530
  body: actualBody,
465
531
  };
466
532
  }
@@ -801,7 +867,110 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
801
867
  }
802
868
  return null;
803
869
  };
804
- const buildClassParts = (args, dynamicClassParts = [], existingClass = '') => {
870
+ const resolveDynamicCall = (expr) => {
871
+ if (!utils_1.t.isCallExpression(expr) || !utils_1.t.isMemberExpression(expr.callee))
872
+ return null;
873
+ const callee = expr.callee;
874
+ if (!utils_1.t.isIdentifier(callee.object) || !utils_1.t.isIdentifier(callee.property))
875
+ return null;
876
+ const styleInfo = localCreateStyles[callee.object.value];
877
+ const func = styleInfo?.functions?.[callee.property.value];
878
+ if (!func)
879
+ return null;
880
+ const callArgs = expr.arguments;
881
+ if (callArgs.some((a) => a.spread) || callArgs.length === 0)
882
+ return null;
883
+ const tempStaticTable = { ...mergedStaticTable };
884
+ const runtime = [];
885
+ const resolveObjectArg = (argExpr) => (0, utils_1.objectExpressionToObject)(argExpr, mergedStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
886
+ if (func.named) {
887
+ const argExpr = callArgs[0].expression;
888
+ if (callArgs.length !== 1 || argExpr.type !== 'ObjectExpression') {
889
+ throwCompilationError(`Plumeria: ${getSource(expr)} takes one object argument, because ${callee.property.value} destructures its parameter.`, expr);
890
+ }
891
+ const given = new Map();
892
+ argExpr.properties.forEach((prop) => {
893
+ if (prop.type === 'Identifier') {
894
+ given.set(prop.value, prop);
895
+ }
896
+ else if (prop.type === 'KeyValueProperty' &&
897
+ (utils_1.t.isIdentifier(prop.key) || utils_1.t.isStringLiteral(prop.key))) {
898
+ given.set(String(prop.key.value), prop.value);
899
+ }
900
+ });
901
+ const argObj = resolveObjectArg(argExpr) ?? {};
902
+ func.named.forEach(({ key, local }) => {
903
+ const source = given.get(key);
904
+ if (!source) {
905
+ throwCompilationError(`Plumeria: ${getSource(expr)} leaves "${key}" unset, and a dynamic style function has no value to fall back on.`, expr);
906
+ return;
907
+ }
908
+ if (isStaticArgValue(source) && argObj[key] !== undefined)
909
+ tempStaticTable[local] = argObj[key];
910
+ else
911
+ runtime.push({ param: local, source });
912
+ });
913
+ }
914
+ else if (callArgs.length === 1 &&
915
+ callArgs[0].expression.type === 'ObjectExpression') {
916
+ const argObj = resolveObjectArg(callArgs[0].expression) ?? {};
917
+ func.params.forEach((p) => {
918
+ if (argObj[p] !== undefined)
919
+ tempStaticTable[p] = argObj[p];
920
+ });
921
+ }
922
+ else {
923
+ callArgs.forEach((callArg, i) => {
924
+ const p = func.params[i];
925
+ if (!p)
926
+ return;
927
+ runtime.push({ param: p, source: callArg.expression });
928
+ });
929
+ }
930
+ const cssVars = {};
931
+ if (runtime.length > 0) {
932
+ runtime.forEach(({ param }) => (tempStaticTable[param] = param));
933
+ const probe = (0, utils_1.objectExpressionToObject)(func.body, tempStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
934
+ const hash = (0, zss_engine_1.genBase36Hash)(probe ?? {}, 1, 8);
935
+ runtime.forEach(({ param }) => {
936
+ const cssVar = `--${hash}-${param}`;
937
+ tempStaticTable[param] = `var(${cssVar})`;
938
+ cssVars[param] = cssVar;
939
+ });
940
+ }
941
+ const style = (0, utils_1.objectExpressionToObject)(func.body, tempStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
942
+ if (!style)
943
+ return null;
944
+ const vars = [];
945
+ runtime.forEach(({ param, source }) => {
946
+ const cssVar = cssVars[param];
947
+ const targetProp = cssVar ? findVarProp(style, cssVar) : undefined;
948
+ if (!targetProp)
949
+ return;
950
+ const argStart = source.span.start - baseByteOffset;
951
+ const argEnd = source.span.end - baseByteOffset;
952
+ const argSource = sourceBuffer
953
+ .subarray(argStart, argEnd)
954
+ .toString('utf-8');
955
+ let valueExpr;
956
+ const maybeNumber = Number(argSource);
957
+ if (!isNaN(maybeNumber) && argSource.trim() === String(maybeNumber)) {
958
+ valueExpr = JSON.stringify((0, zss_engine_1.applyCssValue)(maybeNumber, targetProp));
959
+ }
960
+ else if ((argSource.startsWith('"') && argSource.endsWith('"')) ||
961
+ (argSource.startsWith("'") && argSource.endsWith("'"))) {
962
+ valueExpr = JSON.stringify((0, zss_engine_1.applyCssValue)(argSource.slice(1, -1), targetProp));
963
+ }
964
+ else {
965
+ valueExpr = zss_engine_1.exceptionCamelCase.includes(targetProp)
966
+ ? argSource
967
+ : `(typeof (${argSource}) === 'number' ? (${argSource}) + 'px' : (${argSource}))`;
968
+ }
969
+ vars.push({ cssVar, valueExpr });
970
+ });
971
+ return { style, vars };
972
+ };
973
+ const buildClassParts = (args, dynamicClassParts = [], existingClass = '', isStyleProp = false) => {
805
974
  args.forEach((arg) => {
806
975
  const expr = arg.expression;
807
976
  if (utils_1.t.isIdentifier(expr) && localStyleAliases[expr.value]) {
@@ -809,6 +978,7 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
809
978
  }
810
979
  });
811
980
  const conditionals = [];
981
+ const dynamicVars = [];
812
982
  let groupIdCounter = 0;
813
983
  let sourceOrder = 0;
814
984
  const baseChunks = [];
@@ -927,9 +1097,26 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
927
1097
  const off = !scoped && groupKeys.has('') ? `${prefix}off` : '';
928
1098
  return buildDecision(node, prefix, off, { n: 0 }, scoped);
929
1099
  };
930
- const collectConditions = (node, currentTestStrings = []) => {
931
- const staticStyle = resolveStyleObject(node);
932
- if (staticStyle) {
1100
+ const collectConditions = (node, currentTestStrings = [], argOrder) => {
1101
+ let branchStyle = resolveStyleObject(node);
1102
+ if (!branchStyle) {
1103
+ const dynamic = resolveDynamicCall(node);
1104
+ if (dynamic) {
1105
+ if (!isStyleProp) {
1106
+ throwCompilationError(`Plumeria: css.use(${getSource(node)}) does not support dynamic function keys.`, node);
1107
+ }
1108
+ branchStyle = dynamic.style;
1109
+ dynamic.vars.forEach((v) => dynamicVars.push({
1110
+ ...v,
1111
+ test: currentTestStrings.length
1112
+ ? currentTestStrings.join(' && ')
1113
+ : undefined,
1114
+ order: argOrder,
1115
+ }));
1116
+ }
1117
+ }
1118
+ if (branchStyle) {
1119
+ const staticStyle = branchStyle;
933
1120
  if (currentTestStrings.length === 0) {
934
1121
  baseStyle = (0, utils_1.deepMerge)(baseStyle, staticStyle);
935
1122
  baseChunks.push({ order: sourceOrder++, style: staticStyle });
@@ -963,26 +1150,17 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
963
1150
  return true;
964
1151
  }
965
1152
  }
966
- collectConditions(node.consequent, [
967
- ...currentTestStrings,
968
- `(${testSource})`,
969
- ]);
970
- collectConditions(node.alternate, [
971
- ...currentTestStrings,
972
- `!(${testSource})`,
973
- ]);
1153
+ collectConditions(node.consequent, [...currentTestStrings, `(${testSource})`], argOrder);
1154
+ collectConditions(node.alternate, [...currentTestStrings, `!(${testSource})`], argOrder);
974
1155
  return true;
975
1156
  }
976
1157
  else if (node.type === 'BinaryExpression' &&
977
1158
  node.operator === '&&') {
978
- collectConditions(node.right, [
979
- ...currentTestStrings,
980
- `(${getSource(node.left)})`,
981
- ]);
1159
+ collectConditions(node.right, [...currentTestStrings, `(${getSource(node.left)})`], argOrder);
982
1160
  return true;
983
1161
  }
984
1162
  else if (node.type === 'ParenthesisExpression') {
985
- return collectConditions(node.expression, currentTestStrings);
1163
+ return collectConditions(node.expression, currentTestStrings, argOrder);
986
1164
  }
987
1165
  assertResolvable(node);
988
1166
  return false;
@@ -1227,7 +1405,7 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
1227
1405
  }));
1228
1406
  continue;
1229
1407
  }
1230
- const handled = collectConditions(expr);
1408
+ const handled = collectConditions(expr, [], arg.order);
1231
1409
  if (handled)
1232
1410
  continue;
1233
1411
  assertResolvable(expr);
@@ -1240,6 +1418,7 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
1240
1418
  classParts: [...dynamicClassParts],
1241
1419
  isOptimizable,
1242
1420
  baseStyle,
1421
+ dynamicVars,
1243
1422
  };
1244
1423
  }
1245
1424
  const participation = {};
@@ -1488,7 +1667,7 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
1488
1667
  classParts.push(`(${JSON.stringify(results)}[${masterKeyExpr || '""'}] || ${baseConflictClass ? JSON.stringify(baseConflictClass) : '""'})`);
1489
1668
  }
1490
1669
  classParts.push(...dynamicClassParts);
1491
- return { classParts, isOptimizable, baseStyle };
1670
+ return { classParts, isOptimizable, baseStyle, dynamicVars };
1492
1671
  };
1493
1672
  (0, utils_1.traverse)(ast, {
1494
1673
  JSXOpeningElement({ node }) {
@@ -1722,13 +1901,13 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
1722
1901
  if (!node.value || node.value.type !== 'JSXExpressionContainer')
1723
1902
  return;
1724
1903
  const expr = node.value.expression;
1725
- let args = expr.type === 'ArrayExpression'
1904
+ const args = expr.type === 'ArrayExpression'
1726
1905
  ? expr.elements
1727
1906
  .filter((el) => el !== undefined)
1728
1907
  .map((el) => ({ expression: el.expression }))
1729
1908
  : [{ expression: expr }];
1730
1909
  const dynamicClassParts = [];
1731
- const dynamicStyleParts = [];
1910
+ const existingStyleParts = [];
1732
1911
  let attributes = [];
1733
1912
  for (const [, val] of jsxOpeningElementMap) {
1734
1913
  const found = val.attributes
@@ -1782,99 +1961,19 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
1782
1961
  if (innerExpr.type === 'ObjectExpression') {
1783
1962
  const stripped = innerSource.slice(1, -1).trim();
1784
1963
  if (stripped)
1785
- dynamicStyleParts.push(stripped);
1964
+ existingStyleParts.push(stripped);
1786
1965
  }
1787
1966
  else {
1788
1967
  existingStyleExpr = `...(${innerSource})`;
1789
1968
  }
1790
1969
  }
1791
1970
  }
1792
- args = args.filter((arg) => {
1793
- const expr = arg.expression;
1794
- if (!utils_1.t.isCallExpression(expr) || !utils_1.t.isMemberExpression(expr.callee))
1795
- return true;
1796
- const callee = expr.callee;
1797
- if (!utils_1.t.isIdentifier(callee.object) ||
1798
- !utils_1.t.isIdentifier(callee.property))
1799
- return true;
1800
- const varName = callee.object.value;
1801
- const propKey = callee.property.value;
1802
- const styleInfo = localCreateStyles[varName];
1803
- if (styleInfo?.functions?.[propKey]) {
1804
- const func = styleInfo.functions[propKey];
1805
- const callArgs = expr.arguments;
1806
- const hasSpread = callArgs.some((a) => a.spread);
1807
- if (!hasSpread && callArgs.length >= 1) {
1808
- const tempStaticTable = { ...mergedStaticTable };
1809
- const cssVarInfo = {};
1810
- if (callArgs.length === 1 &&
1811
- callArgs[0].expression.type === 'ObjectExpression') {
1812
- const argObj = (0, utils_1.objectExpressionToObject)(callArgs[0].expression, mergedStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
1813
- func.params.forEach((p) => {
1814
- if (argObj[p] !== undefined)
1815
- tempStaticTable[p] = argObj[p];
1816
- });
1817
- }
1818
- else {
1819
- callArgs.forEach((_callArg, i) => {
1820
- const p = func.params[i];
1821
- if (!p)
1822
- return;
1823
- const cssVar = `--${propKey}-${p}`;
1824
- tempStaticTable[p] = `var(${cssVar})`;
1825
- cssVarInfo[p] = { cssVar, propKey: '' };
1826
- });
1827
- }
1828
- const substituted = (0, utils_1.objectExpressionToObject)(func.body, tempStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
1829
- if (substituted) {
1830
- const records = processStyleRecords(substituted);
1831
- const hashes = records.map((r) => r.hash).join(' ');
1832
- if (hashes)
1833
- dynamicClassParts.push(JSON.stringify(hashes));
1834
- if (Object.keys(cssVarInfo).length > 0) {
1835
- Object.entries(cssVarInfo).forEach(([paramName, info]) => {
1836
- const targetProp = Object.keys(substituted).find((k) => typeof substituted[k] === 'string' &&
1837
- substituted[k].includes(info.cssVar));
1838
- if (targetProp) {
1839
- const paramIndex = func.params.indexOf(paramName);
1840
- const srcArg = paramIndex >= 0 && callArgs[paramIndex]
1841
- ? callArgs[paramIndex].expression
1842
- : callArgs[0].expression;
1843
- const argStart = srcArg.span.start - baseByteOffset;
1844
- const argEnd = srcArg.span.end - baseByteOffset;
1845
- const argSource = sourceBuffer
1846
- .subarray(argStart, argEnd)
1847
- .toString('utf-8');
1848
- let valueExpr;
1849
- const maybeNumber = Number(argSource);
1850
- if (!isNaN(maybeNumber) &&
1851
- argSource.trim() === String(maybeNumber)) {
1852
- valueExpr = JSON.stringify((0, zss_engine_1.applyCssValue)(maybeNumber, targetProp));
1853
- }
1854
- else if ((argSource.startsWith('"') &&
1855
- argSource.endsWith('"')) ||
1856
- (argSource.startsWith("'") && argSource.endsWith("'"))) {
1857
- valueExpr = JSON.stringify((0, zss_engine_1.applyCssValue)(argSource.slice(1, -1), targetProp));
1858
- }
1859
- else {
1860
- valueExpr = zss_engine_1.exceptionCamelCase.includes(targetProp)
1861
- ? argSource
1862
- : `(typeof (${argSource}) === 'number' ? (${argSource}) + 'px' : (${argSource}))`;
1863
- }
1864
- dynamicStyleParts.push(`"${info.cssVar}": ${valueExpr}`);
1865
- }
1866
- });
1867
- }
1868
- return false;
1869
- }
1870
- }
1871
- }
1872
- return true;
1873
- });
1874
- const styleAttr = dynamicStyleParts.length > 0 || existingStyleExpr
1875
- ? ` style={{ ${[existingStyleExpr, ...dynamicStyleParts].filter(Boolean).join(', ')} }}`
1971
+ const { classParts, isOptimizable, baseStyle, dynamicVars } = buildClassParts(args, dynamicClassParts, existingClassExpr, true);
1972
+ const dynamicStyleParts = foldDynamicVars(dynamicVars);
1973
+ const styleParts = [...existingStyleParts, ...dynamicStyleParts];
1974
+ const styleAttr = styleParts.length > 0 || existingStyleExpr
1975
+ ? ` style={{ ${[existingStyleExpr, ...styleParts].filter(Boolean).join(', ')} }}`
1876
1976
  : '';
1877
- const { classParts, isOptimizable, baseStyle } = buildClassParts(args, dynamicClassParts, existingClassExpr);
1878
1977
  if (isOptimizable &&
1879
1978
  (args.length > 0 ||
1880
1979
  Object.keys(baseStyle).length > 0 ||
@@ -1902,6 +2001,9 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
1902
2001
  utils_1.t.isIdentifier(callee.property)) {
1903
2002
  const objectName = callee.object.value;
1904
2003
  const propertyName = callee.property.value;
2004
+ if (localCreateStyles[objectName]?.functions?.[propertyName]) {
2005
+ dynamicFnCalls.push(node);
2006
+ }
1905
2007
  const alias = plumeriaAliases[objectName];
1906
2008
  if (alias === 'NAMESPACE' && propertyName === 'use') {
1907
2009
  isUseCall = true;
@@ -1975,6 +2077,16 @@ const unpluginFactory = (options = {}, unpluginMeta) => {
1975
2077
  });
1976
2078
  }
1977
2079
  });
2080
+ dynamicFnCalls.forEach((call) => {
2081
+ const start = call.span.start - baseByteOffset;
2082
+ const end = call.span.end - baseByteOffset;
2083
+ const isResolved = replacements.some((r) => r.start <= start && r.end >= end);
2084
+ if (!isResolved) {
2085
+ throwCompilationError(`Plumeria: ${getSource(call)} is only supported in the ${styleProp} prop. ` +
2086
+ `A dynamic style function resolves to a class name and a CSS variable on the element itself, ` +
2087
+ `so it cannot be passed through another prop or read as a value.`, call);
2088
+ }
2089
+ });
1978
2090
  const buffer = Buffer.from(source);
1979
2091
  let offset = 0;
1980
2092
  const parts = [];
package/dist/core.mjs CHANGED
@@ -5,6 +5,70 @@ import { applyCssValue, genBase36Hash, exceptionCamelCase, camelToKebabCase, isA
5
5
  import { traverse, collectReferenceIdentifiers, getStyleRecords, collectLocalConsts, objectExpressionToObject, t, getRootIdentifier, extractOndemandStyles, deepMerge, scanAll, resolveImportPath, getLeadingCommentLength, optimizer, getFileDependencies, resolveExport, resolveComponentKey, DEFAULT_STYLE_PROP, } from '@plumeria/utils';
6
6
  export const TARGET_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
7
7
  export const EXTENSION_PATTERN = /\.(ts|tsx|js|jsx)$/;
8
+ const isStaticArgValue = (node) => node.type === 'StringLiteral' ||
9
+ node.type === 'NumericLiteral' ||
10
+ node.type === 'BooleanLiteral' ||
11
+ (node.type === 'TemplateLiteral' && node.expressions.length === 0) ||
12
+ t.isIdentifier(node) ||
13
+ t.isMemberExpression(node);
14
+ const namedParamsOf = (params) => {
15
+ if (params.length !== 1)
16
+ return undefined;
17
+ const first = params[0];
18
+ const pattern = (first?.pat ?? first);
19
+ if (pattern?.type !== 'ObjectPattern')
20
+ return undefined;
21
+ const named = [];
22
+ for (const prop of pattern.properties ?? []) {
23
+ if (prop.type === 'AssignmentPatternProperty' &&
24
+ t.isIdentifier(prop.key) &&
25
+ !prop.value) {
26
+ named.push({ key: prop.key.value, local: prop.key.value });
27
+ }
28
+ else if (prop.type === 'KeyValuePatternProperty' &&
29
+ t.isIdentifier(prop.key) &&
30
+ t.isIdentifier(prop.value)) {
31
+ named.push({ key: prop.key.value, local: prop.value.value });
32
+ }
33
+ else {
34
+ return undefined;
35
+ }
36
+ }
37
+ return named.length > 0 ? named : undefined;
38
+ };
39
+ const foldDynamicVars = (vars) => {
40
+ const grouped = new Map();
41
+ [...vars]
42
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
43
+ .forEach((entry) => {
44
+ const list = grouped.get(entry.cssVar);
45
+ if (list)
46
+ list.push(entry);
47
+ else
48
+ grouped.set(entry.cssVar, [entry]);
49
+ });
50
+ return [...grouped].map(([cssVar, entries]) => {
51
+ let value = 'undefined';
52
+ entries.forEach((entry) => {
53
+ value = entry.test
54
+ ? `((${entry.test}) ? ${entry.valueExpr} : ${value})`
55
+ : entry.valueExpr;
56
+ });
57
+ return `"${cssVar}": ${value}`;
58
+ });
59
+ };
60
+ const findVarProp = (style, cssVar) => {
61
+ for (const [prop, value] of Object.entries(style)) {
62
+ if (typeof value === 'string' && value.includes(cssVar))
63
+ return prop;
64
+ if (value !== null && typeof value === 'object') {
65
+ const nested = findVarProp(value, cssVar);
66
+ if (nested)
67
+ return nested;
68
+ }
69
+ }
70
+ return undefined;
71
+ };
8
72
  export const unpluginFactory = (options = {}, unpluginMeta) => {
9
73
  const filter = createFilter(options.include, options.exclude);
10
74
  const styleProp = options.styleProp ?? DEFAULT_STYLE_PROP;
@@ -348,6 +412,7 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
348
412
  }
349
413
  };
350
414
  const replacements = [];
415
+ const dynamicFnCalls = [];
351
416
  const processedDecls = new Set();
352
417
  const idSpans = new Set();
353
418
  const excludedSpans = new Set();
@@ -425,6 +490,7 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
425
490
  if (actualBody && actualBody.type === 'ObjectExpression') {
426
491
  styleFunctions[prop.key.value] = {
427
492
  params,
493
+ named: namedParamsOf(func.params),
428
494
  body: actualBody,
429
495
  };
430
496
  }
@@ -765,7 +831,110 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
765
831
  }
766
832
  return null;
767
833
  };
768
- const buildClassParts = (args, dynamicClassParts = [], existingClass = '') => {
834
+ const resolveDynamicCall = (expr) => {
835
+ if (!t.isCallExpression(expr) || !t.isMemberExpression(expr.callee))
836
+ return null;
837
+ const callee = expr.callee;
838
+ if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property))
839
+ return null;
840
+ const styleInfo = localCreateStyles[callee.object.value];
841
+ const func = styleInfo?.functions?.[callee.property.value];
842
+ if (!func)
843
+ return null;
844
+ const callArgs = expr.arguments;
845
+ if (callArgs.some((a) => a.spread) || callArgs.length === 0)
846
+ return null;
847
+ const tempStaticTable = { ...mergedStaticTable };
848
+ const runtime = [];
849
+ const resolveObjectArg = (argExpr) => objectExpressionToObject(argExpr, mergedStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
850
+ if (func.named) {
851
+ const argExpr = callArgs[0].expression;
852
+ if (callArgs.length !== 1 || argExpr.type !== 'ObjectExpression') {
853
+ throwCompilationError(`Plumeria: ${getSource(expr)} takes one object argument, because ${callee.property.value} destructures its parameter.`, expr);
854
+ }
855
+ const given = new Map();
856
+ argExpr.properties.forEach((prop) => {
857
+ if (prop.type === 'Identifier') {
858
+ given.set(prop.value, prop);
859
+ }
860
+ else if (prop.type === 'KeyValueProperty' &&
861
+ (t.isIdentifier(prop.key) || t.isStringLiteral(prop.key))) {
862
+ given.set(String(prop.key.value), prop.value);
863
+ }
864
+ });
865
+ const argObj = resolveObjectArg(argExpr) ?? {};
866
+ func.named.forEach(({ key, local }) => {
867
+ const source = given.get(key);
868
+ if (!source) {
869
+ throwCompilationError(`Plumeria: ${getSource(expr)} leaves "${key}" unset, and a dynamic style function has no value to fall back on.`, expr);
870
+ return;
871
+ }
872
+ if (isStaticArgValue(source) && argObj[key] !== undefined)
873
+ tempStaticTable[local] = argObj[key];
874
+ else
875
+ runtime.push({ param: local, source });
876
+ });
877
+ }
878
+ else if (callArgs.length === 1 &&
879
+ callArgs[0].expression.type === 'ObjectExpression') {
880
+ const argObj = resolveObjectArg(callArgs[0].expression) ?? {};
881
+ func.params.forEach((p) => {
882
+ if (argObj[p] !== undefined)
883
+ tempStaticTable[p] = argObj[p];
884
+ });
885
+ }
886
+ else {
887
+ callArgs.forEach((callArg, i) => {
888
+ const p = func.params[i];
889
+ if (!p)
890
+ return;
891
+ runtime.push({ param: p, source: callArg.expression });
892
+ });
893
+ }
894
+ const cssVars = {};
895
+ if (runtime.length > 0) {
896
+ runtime.forEach(({ param }) => (tempStaticTable[param] = param));
897
+ const probe = objectExpressionToObject(func.body, tempStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
898
+ const hash = genBase36Hash(probe ?? {}, 1, 8);
899
+ runtime.forEach(({ param }) => {
900
+ const cssVar = `--${hash}-${param}`;
901
+ tempStaticTable[param] = `var(${cssVar})`;
902
+ cssVars[param] = cssVar;
903
+ });
904
+ }
905
+ const style = objectExpressionToObject(func.body, tempStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
906
+ if (!style)
907
+ return null;
908
+ const vars = [];
909
+ runtime.forEach(({ param, source }) => {
910
+ const cssVar = cssVars[param];
911
+ const targetProp = cssVar ? findVarProp(style, cssVar) : undefined;
912
+ if (!targetProp)
913
+ return;
914
+ const argStart = source.span.start - baseByteOffset;
915
+ const argEnd = source.span.end - baseByteOffset;
916
+ const argSource = sourceBuffer
917
+ .subarray(argStart, argEnd)
918
+ .toString('utf-8');
919
+ let valueExpr;
920
+ const maybeNumber = Number(argSource);
921
+ if (!isNaN(maybeNumber) && argSource.trim() === String(maybeNumber)) {
922
+ valueExpr = JSON.stringify(applyCssValue(maybeNumber, targetProp));
923
+ }
924
+ else if ((argSource.startsWith('"') && argSource.endsWith('"')) ||
925
+ (argSource.startsWith("'") && argSource.endsWith("'"))) {
926
+ valueExpr = JSON.stringify(applyCssValue(argSource.slice(1, -1), targetProp));
927
+ }
928
+ else {
929
+ valueExpr = exceptionCamelCase.includes(targetProp)
930
+ ? argSource
931
+ : `(typeof (${argSource}) === 'number' ? (${argSource}) + 'px' : (${argSource}))`;
932
+ }
933
+ vars.push({ cssVar, valueExpr });
934
+ });
935
+ return { style, vars };
936
+ };
937
+ const buildClassParts = (args, dynamicClassParts = [], existingClass = '', isStyleProp = false) => {
769
938
  args.forEach((arg) => {
770
939
  const expr = arg.expression;
771
940
  if (t.isIdentifier(expr) && localStyleAliases[expr.value]) {
@@ -773,6 +942,7 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
773
942
  }
774
943
  });
775
944
  const conditionals = [];
945
+ const dynamicVars = [];
776
946
  let groupIdCounter = 0;
777
947
  let sourceOrder = 0;
778
948
  const baseChunks = [];
@@ -891,9 +1061,26 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
891
1061
  const off = !scoped && groupKeys.has('') ? `${prefix}off` : '';
892
1062
  return buildDecision(node, prefix, off, { n: 0 }, scoped);
893
1063
  };
894
- const collectConditions = (node, currentTestStrings = []) => {
895
- const staticStyle = resolveStyleObject(node);
896
- if (staticStyle) {
1064
+ const collectConditions = (node, currentTestStrings = [], argOrder) => {
1065
+ let branchStyle = resolveStyleObject(node);
1066
+ if (!branchStyle) {
1067
+ const dynamic = resolveDynamicCall(node);
1068
+ if (dynamic) {
1069
+ if (!isStyleProp) {
1070
+ throwCompilationError(`Plumeria: css.use(${getSource(node)}) does not support dynamic function keys.`, node);
1071
+ }
1072
+ branchStyle = dynamic.style;
1073
+ dynamic.vars.forEach((v) => dynamicVars.push({
1074
+ ...v,
1075
+ test: currentTestStrings.length
1076
+ ? currentTestStrings.join(' && ')
1077
+ : undefined,
1078
+ order: argOrder,
1079
+ }));
1080
+ }
1081
+ }
1082
+ if (branchStyle) {
1083
+ const staticStyle = branchStyle;
897
1084
  if (currentTestStrings.length === 0) {
898
1085
  baseStyle = deepMerge(baseStyle, staticStyle);
899
1086
  baseChunks.push({ order: sourceOrder++, style: staticStyle });
@@ -927,26 +1114,17 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
927
1114
  return true;
928
1115
  }
929
1116
  }
930
- collectConditions(node.consequent, [
931
- ...currentTestStrings,
932
- `(${testSource})`,
933
- ]);
934
- collectConditions(node.alternate, [
935
- ...currentTestStrings,
936
- `!(${testSource})`,
937
- ]);
1117
+ collectConditions(node.consequent, [...currentTestStrings, `(${testSource})`], argOrder);
1118
+ collectConditions(node.alternate, [...currentTestStrings, `!(${testSource})`], argOrder);
938
1119
  return true;
939
1120
  }
940
1121
  else if (node.type === 'BinaryExpression' &&
941
1122
  node.operator === '&&') {
942
- collectConditions(node.right, [
943
- ...currentTestStrings,
944
- `(${getSource(node.left)})`,
945
- ]);
1123
+ collectConditions(node.right, [...currentTestStrings, `(${getSource(node.left)})`], argOrder);
946
1124
  return true;
947
1125
  }
948
1126
  else if (node.type === 'ParenthesisExpression') {
949
- return collectConditions(node.expression, currentTestStrings);
1127
+ return collectConditions(node.expression, currentTestStrings, argOrder);
950
1128
  }
951
1129
  assertResolvable(node);
952
1130
  return false;
@@ -1191,7 +1369,7 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
1191
1369
  }));
1192
1370
  continue;
1193
1371
  }
1194
- const handled = collectConditions(expr);
1372
+ const handled = collectConditions(expr, [], arg.order);
1195
1373
  if (handled)
1196
1374
  continue;
1197
1375
  assertResolvable(expr);
@@ -1204,6 +1382,7 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
1204
1382
  classParts: [...dynamicClassParts],
1205
1383
  isOptimizable,
1206
1384
  baseStyle,
1385
+ dynamicVars,
1207
1386
  };
1208
1387
  }
1209
1388
  const participation = {};
@@ -1452,7 +1631,7 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
1452
1631
  classParts.push(`(${JSON.stringify(results)}[${masterKeyExpr || '""'}] || ${baseConflictClass ? JSON.stringify(baseConflictClass) : '""'})`);
1453
1632
  }
1454
1633
  classParts.push(...dynamicClassParts);
1455
- return { classParts, isOptimizable, baseStyle };
1634
+ return { classParts, isOptimizable, baseStyle, dynamicVars };
1456
1635
  };
1457
1636
  traverse(ast, {
1458
1637
  JSXOpeningElement({ node }) {
@@ -1686,13 +1865,13 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
1686
1865
  if (!node.value || node.value.type !== 'JSXExpressionContainer')
1687
1866
  return;
1688
1867
  const expr = node.value.expression;
1689
- let args = expr.type === 'ArrayExpression'
1868
+ const args = expr.type === 'ArrayExpression'
1690
1869
  ? expr.elements
1691
1870
  .filter((el) => el !== undefined)
1692
1871
  .map((el) => ({ expression: el.expression }))
1693
1872
  : [{ expression: expr }];
1694
1873
  const dynamicClassParts = [];
1695
- const dynamicStyleParts = [];
1874
+ const existingStyleParts = [];
1696
1875
  let attributes = [];
1697
1876
  for (const [, val] of jsxOpeningElementMap) {
1698
1877
  const found = val.attributes
@@ -1746,99 +1925,19 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
1746
1925
  if (innerExpr.type === 'ObjectExpression') {
1747
1926
  const stripped = innerSource.slice(1, -1).trim();
1748
1927
  if (stripped)
1749
- dynamicStyleParts.push(stripped);
1928
+ existingStyleParts.push(stripped);
1750
1929
  }
1751
1930
  else {
1752
1931
  existingStyleExpr = `...(${innerSource})`;
1753
1932
  }
1754
1933
  }
1755
1934
  }
1756
- args = args.filter((arg) => {
1757
- const expr = arg.expression;
1758
- if (!t.isCallExpression(expr) || !t.isMemberExpression(expr.callee))
1759
- return true;
1760
- const callee = expr.callee;
1761
- if (!t.isIdentifier(callee.object) ||
1762
- !t.isIdentifier(callee.property))
1763
- return true;
1764
- const varName = callee.object.value;
1765
- const propKey = callee.property.value;
1766
- const styleInfo = localCreateStyles[varName];
1767
- if (styleInfo?.functions?.[propKey]) {
1768
- const func = styleInfo.functions[propKey];
1769
- const callArgs = expr.arguments;
1770
- const hasSpread = callArgs.some((a) => a.spread);
1771
- if (!hasSpread && callArgs.length >= 1) {
1772
- const tempStaticTable = { ...mergedStaticTable };
1773
- const cssVarInfo = {};
1774
- if (callArgs.length === 1 &&
1775
- callArgs[0].expression.type === 'ObjectExpression') {
1776
- const argObj = objectExpressionToObject(callArgs[0].expression, mergedStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
1777
- func.params.forEach((p) => {
1778
- if (argObj[p] !== undefined)
1779
- tempStaticTable[p] = argObj[p];
1780
- });
1781
- }
1782
- else {
1783
- callArgs.forEach((_callArg, i) => {
1784
- const p = func.params[i];
1785
- if (!p)
1786
- return;
1787
- const cssVar = `--${propKey}-${p}`;
1788
- tempStaticTable[p] = `var(${cssVar})`;
1789
- cssVarInfo[p] = { cssVar, propKey: '' };
1790
- });
1791
- }
1792
- const substituted = objectExpressionToObject(func.body, tempStaticTable, mergedKeyframesTable, mergedViewTransitionTable, mergedCreateThemeHashTable, scannedTables.createThemeObjectTable, mergedCreateTable, mergedCreateStaticHashTable, scannedTables.createStaticObjectTable, mergedVariantsTable);
1793
- if (substituted) {
1794
- const records = processStyleRecords(substituted);
1795
- const hashes = records.map((r) => r.hash).join(' ');
1796
- if (hashes)
1797
- dynamicClassParts.push(JSON.stringify(hashes));
1798
- if (Object.keys(cssVarInfo).length > 0) {
1799
- Object.entries(cssVarInfo).forEach(([paramName, info]) => {
1800
- const targetProp = Object.keys(substituted).find((k) => typeof substituted[k] === 'string' &&
1801
- substituted[k].includes(info.cssVar));
1802
- if (targetProp) {
1803
- const paramIndex = func.params.indexOf(paramName);
1804
- const srcArg = paramIndex >= 0 && callArgs[paramIndex]
1805
- ? callArgs[paramIndex].expression
1806
- : callArgs[0].expression;
1807
- const argStart = srcArg.span.start - baseByteOffset;
1808
- const argEnd = srcArg.span.end - baseByteOffset;
1809
- const argSource = sourceBuffer
1810
- .subarray(argStart, argEnd)
1811
- .toString('utf-8');
1812
- let valueExpr;
1813
- const maybeNumber = Number(argSource);
1814
- if (!isNaN(maybeNumber) &&
1815
- argSource.trim() === String(maybeNumber)) {
1816
- valueExpr = JSON.stringify(applyCssValue(maybeNumber, targetProp));
1817
- }
1818
- else if ((argSource.startsWith('"') &&
1819
- argSource.endsWith('"')) ||
1820
- (argSource.startsWith("'") && argSource.endsWith("'"))) {
1821
- valueExpr = JSON.stringify(applyCssValue(argSource.slice(1, -1), targetProp));
1822
- }
1823
- else {
1824
- valueExpr = exceptionCamelCase.includes(targetProp)
1825
- ? argSource
1826
- : `(typeof (${argSource}) === 'number' ? (${argSource}) + 'px' : (${argSource}))`;
1827
- }
1828
- dynamicStyleParts.push(`"${info.cssVar}": ${valueExpr}`);
1829
- }
1830
- });
1831
- }
1832
- return false;
1833
- }
1834
- }
1835
- }
1836
- return true;
1837
- });
1838
- const styleAttr = dynamicStyleParts.length > 0 || existingStyleExpr
1839
- ? ` style={{ ${[existingStyleExpr, ...dynamicStyleParts].filter(Boolean).join(', ')} }}`
1935
+ const { classParts, isOptimizable, baseStyle, dynamicVars } = buildClassParts(args, dynamicClassParts, existingClassExpr, true);
1936
+ const dynamicStyleParts = foldDynamicVars(dynamicVars);
1937
+ const styleParts = [...existingStyleParts, ...dynamicStyleParts];
1938
+ const styleAttr = styleParts.length > 0 || existingStyleExpr
1939
+ ? ` style={{ ${[existingStyleExpr, ...styleParts].filter(Boolean).join(', ')} }}`
1840
1940
  : '';
1841
- const { classParts, isOptimizable, baseStyle } = buildClassParts(args, dynamicClassParts, existingClassExpr);
1842
1941
  if (isOptimizable &&
1843
1942
  (args.length > 0 ||
1844
1943
  Object.keys(baseStyle).length > 0 ||
@@ -1866,6 +1965,9 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
1866
1965
  t.isIdentifier(callee.property)) {
1867
1966
  const objectName = callee.object.value;
1868
1967
  const propertyName = callee.property.value;
1968
+ if (localCreateStyles[objectName]?.functions?.[propertyName]) {
1969
+ dynamicFnCalls.push(node);
1970
+ }
1869
1971
  const alias = plumeriaAliases[objectName];
1870
1972
  if (alias === 'NAMESPACE' && propertyName === 'use') {
1871
1973
  isUseCall = true;
@@ -1939,6 +2041,16 @@ export const unpluginFactory = (options = {}, unpluginMeta) => {
1939
2041
  });
1940
2042
  }
1941
2043
  });
2044
+ dynamicFnCalls.forEach((call) => {
2045
+ const start = call.span.start - baseByteOffset;
2046
+ const end = call.span.end - baseByteOffset;
2047
+ const isResolved = replacements.some((r) => r.start <= start && r.end >= end);
2048
+ if (!isResolved) {
2049
+ throwCompilationError(`Plumeria: ${getSource(call)} is only supported in the ${styleProp} prop. ` +
2050
+ `A dynamic style function resolves to a class name and a CSS variable on the element itself, ` +
2051
+ `so it cannot be passed through another prop or read as a value.`, call);
2052
+ }
2053
+ });
1942
2054
  const buffer = Buffer.from(source);
1943
2055
  let offset = 0;
1944
2056
  const parts = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/unplugin",
3
- "version": "18.1.4",
3
+ "version": "18.1.6",
4
4
  "description": "Universal Plumeria plugin for various build tools",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
@@ -89,7 +89,7 @@
89
89
  "dependencies": {
90
90
  "@rollup/pluginutils": "^5.4.0",
91
91
  "unplugin": "^3.0.0",
92
- "@plumeria/utils": "^18.1.4"
92
+ "@plumeria/utils": "^18.1.6"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@swc/core": "1.15.47",