Skip to content

Commit

Permalink
Fix plugin-css, plugin-sass
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Nov 6, 2023
1 parent 7017a11 commit cbda777
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-swans-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cobalt-ui/plugin-css': patch
---

Bugfix: bugs in transform() API introduced in 1.6.0
6 changes: 6 additions & 0 deletions .changeset/violet-seahorses-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@cobalt-ui/plugin-sass': patch
'@cobalt-ui/plugin-css': patch
---

Bugfix: fix TypeScript signature for transform() plugin option
6 changes: 6 additions & 0 deletions .changeset/wet-drinks-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@cobalt-ui/plugin-sass': patch
'@cobalt-ui/plugin-css': patch
---

Bugfix: mismatched versions of plugin-css and plugin-sass (if either were on `latest`) were broken.
114 changes: 69 additions & 45 deletions packages/plugin-css/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
} from '@cobalt-ui/core';
import {indent, isAlias, kebabinate, FG_YELLOW, RESET} from '@cobalt-ui/utils';
import {clampChroma, converter, formatCss, formatHex, formatHex8, formatHsl, formatRgb, parse as parseColor} from 'culori';
import {CustomNameGenerator, DASH_PREFIX_RE, makeNameGenerator} from './utils/generate-token-name.js';
import {CustomNameGenerator, DASH_PREFIX_RE, defaultNameGenerator, makeNameGenerator} from './utils/generate-token-name.js';
import {encode} from './utils/encode.js';
import {formatFontNames} from './utils/format-font-names.js';
import {isTokenMatch} from './utils/is-token-match.js';
Expand Down Expand Up @@ -45,7 +45,7 @@ export interface Options {
/** generate wrapper selectors around token modes */
modeSelectors?: ModeSelector[] | LegacyModeSelectors;
/** handle different token types */
transform?: <T extends ParsedToken>(token: T, mode?: string) => string;
transform?: <T extends ParsedToken>(token: T, mode?: string) => string | undefined | null;
/** @deprecated prefix variable names */
prefix?: string;
/** enable P3 color enhancement? (default: true) */
Expand Down Expand Up @@ -123,9 +123,9 @@ export default function pluginCSS(options?: Options): Plugin {
const selectors: string[] = [];
const colorFormat = options?.colorFormat ?? 'hex';
for (const token of tokens) {
let value: ReturnType<typeof defaultTransformer> | undefined = await options?.transform?.(token);
let value: ReturnType<typeof defaultTransformer> | undefined | null = await options?.transform?.(token);
if (value === undefined || value === null) {
value = defaultTransformer(token, tokens, {colorFormat, generateName});
value = defaultTransformer(token, {prefix, colorFormat, generateName, tokens});
}
switch (token.$type) {
case 'link': {
Expand Down Expand Up @@ -182,9 +182,9 @@ export default function pluginCSS(options?: Options): Plugin {
for (const selector of modeSelector.selectors) {
if (!selectors.includes(selector)) selectors.push(selector);
if (!modeVals[selector]) modeVals[selector] = {};
let modeVal: ReturnType<typeof defaultTransformer> | undefined = await options?.transform?.(token, modeSelector.mode);
let modeVal: ReturnType<typeof defaultTransformer> | undefined | null = await options?.transform?.(token, modeSelector.mode);
if (modeVal === undefined || modeVal === null) {
modeVal = defaultTransformer(token, tokens, {colorFormat, mode: modeSelector.mode, generateName});
modeVal = defaultTransformer(token, {colorFormat, generateName, mode: modeSelector.mode, prefix, tokens});
}
switch (token.$type) {
case 'link': {
Expand Down Expand Up @@ -354,90 +354,101 @@ export function transformStrokeStyle(value: ParsedStrokeStyleToken['$value']): s

export function defaultTransformer(
token: ParsedToken,
tokens: ParsedToken[],
{colorFormat = 'hex', mode, generateName}: {colorFormat: Options['colorFormat']; mode?: string; generateName: ReturnType<typeof makeNameGenerator>},
{
colorFormat = 'hex',
generateName,
mode,
prefix,
tokens,
}: {
colorFormat: Options['colorFormat'];
generateName?: ReturnType<typeof makeNameGenerator>;
mode?: string;
prefix?: string;
tokens?: ParsedToken[];
},
): string | number | Record<string, string> {
switch (token.$type) {
// base tokens
case 'color': {
const {originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal, tokens, generateName);
return varRef(originalVal, {prefix, tokens, generateName});
}
return transformColor(originalVal, colorFormat); // note: use original value because it may have been normalized to hex (which matters if it wasn’t in sRGB gamut to begin with)
}
case 'dimension': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformDimension(value);
}
case 'duration': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformDuration(value);
}
case 'font' as 'fontFamily': // @deprecated (but keep support for now)
case 'fontFamily': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformFontFamily(value);
}
case 'fontWeight': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformFontWeight(value);
}
case 'cubicBezier': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformCubicBezier(value);
}
case 'number': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformNumber(value);
}
case 'link': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformLink(value);
}
case 'strokeStyle': {
const {value, originalVal} = getMode(token, mode);
if (isAlias(originalVal)) {
return varRef(originalVal as string, tokens, generateName);
return varRef(originalVal as string, {prefix, tokens, generateName});
}
return transformStrokeStyle(value);
}
// composite tokens
case 'border': {
const {value, originalVal} = getMode(token, mode);
if (typeof originalVal === 'string') {
return varRef(originalVal, tokens, generateName);
return varRef(originalVal, {prefix, tokens, generateName});
}
const width = isAlias(originalVal.width) ? varRef(originalVal.width, tokens, generateName) : transformDimension(value.width);
const color = isAlias(originalVal.color) ? varRef(originalVal.color, tokens, generateName) : transformColor(originalVal.color, colorFormat);
const style = isAlias(originalVal.style) ? varRef(originalVal.style as string, tokens, generateName) : transformStrokeStyle(value.style);
const width = isAlias(originalVal.width) ? varRef(originalVal.width, {prefix, tokens, generateName}) : transformDimension(value.width);
const color = isAlias(originalVal.color) ? varRef(originalVal.color, {prefix, tokens, generateName}) : transformColor(originalVal.color, colorFormat);
const style = isAlias(originalVal.style) ? varRef(originalVal.style as string, {prefix, tokens, generateName}) : transformStrokeStyle(value.style);
return `${width} ${style} ${color}`;
}
case 'shadow': {
let {value, originalVal} = getMode(token, mode);
if (typeof originalVal === 'string') {
return varRef(originalVal, tokens, generateName);
return varRef(originalVal, {prefix, tokens, generateName});
}

// handle backwards compat for previous versions that didn’t always return array
Expand All @@ -448,56 +459,56 @@ export function defaultTransformer(
.map((shadow, i) => {
const origShadow = originalVal[i]!;
if (typeof origShadow === 'string') {
return varRef(origShadow, tokens, generateName);
return varRef(origShadow, {prefix, tokens, generateName});
}
const offsetX = isAlias(origShadow.offsetX) ? varRef(origShadow.offsetX, tokens, generateName) : transformDimension(shadow.offsetX);
const offsetY = isAlias(origShadow.offsetY) ? varRef(origShadow.offsetY, tokens, generateName) : transformDimension(shadow.offsetY);
const blur = isAlias(origShadow.blur) ? varRef(origShadow.blur, tokens, generateName) : transformDimension(shadow.blur);
const spread = isAlias(origShadow.spread) ? varRef(origShadow.spread, tokens, generateName) : transformDimension(shadow.spread);
const color = isAlias(origShadow.color) ? varRef(origShadow.color, tokens, generateName) : transformColor(origShadow.color, colorFormat);
const offsetX = isAlias(origShadow.offsetX) ? varRef(origShadow.offsetX, {prefix, tokens, generateName}) : transformDimension(shadow.offsetX);
const offsetY = isAlias(origShadow.offsetY) ? varRef(origShadow.offsetY, {prefix, tokens, generateName}) : transformDimension(shadow.offsetY);
const blur = isAlias(origShadow.blur) ? varRef(origShadow.blur, {prefix, tokens, generateName}) : transformDimension(shadow.blur);
const spread = isAlias(origShadow.spread) ? varRef(origShadow.spread, {prefix, tokens, generateName}) : transformDimension(shadow.spread);
const color = isAlias(origShadow.color) ? varRef(origShadow.color, {prefix, tokens, generateName}) : transformColor(origShadow.color, colorFormat);
return `${shadow.inset ? 'inset ' : ''}${offsetX} ${offsetY} ${blur} ${spread} ${color}`;
})
.join(', ');
}
case 'gradient': {
const {value, originalVal} = getMode(token, mode);
if (typeof originalVal === 'string') {
return varRef(originalVal, tokens, generateName);
return varRef(originalVal, {prefix, tokens, generateName});
}
return value
.map((gradient, i) => {
const origGradient = originalVal[i]!;
if (typeof origGradient === 'string') {
return varRef(origGradient, tokens, generateName);
return varRef(origGradient, {prefix, tokens, generateName});
}
const color = isAlias(origGradient.color) ? varRef(origGradient.color, tokens, generateName) : transformColor(origGradient.color, colorFormat);
const stop = isAlias(origGradient.position) ? varRef(origGradient.position as any, tokens, generateName) : `${100 * gradient.position}%`;
const color = isAlias(origGradient.color) ? varRef(origGradient.color, {prefix, tokens, generateName}) : transformColor(origGradient.color, colorFormat);
const stop = isAlias(origGradient.position) ? varRef(origGradient.position as any, {prefix, tokens, generateName}) : `${100 * gradient.position}%`;
return `${color} ${stop}`;
})
.join(', ');
}
case 'transition': {
const {value, originalVal} = getMode(token, mode);
if (typeof originalVal === 'string') {
return varRef(originalVal, tokens, generateName);
return varRef(originalVal, {prefix, tokens, generateName});
}
const duration = isAlias(originalVal.duration) ? varRef(originalVal.duration, tokens, generateName) : transformDuration(value.duration);
const duration = isAlias(originalVal.duration) ? varRef(originalVal.duration, {prefix, tokens, generateName}) : transformDuration(value.duration);
let delay: string | undefined = undefined;
if (value.delay) {
delay = isAlias(originalVal.delay) ? varRef(originalVal.delay, tokens, generateName) : transformDuration(value.delay);
delay = isAlias(originalVal.delay) ? varRef(originalVal.delay, {prefix, tokens, generateName}) : transformDuration(value.delay);
}
const timingFunction = isAlias(originalVal.timingFunction) ? varRef(originalVal.timingFunction as any, tokens, generateName) : transformCubicBezier(value.timingFunction);
const timingFunction = isAlias(originalVal.timingFunction) ? varRef(originalVal.timingFunction as any, {prefix, tokens, generateName}) : transformCubicBezier(value.timingFunction);
return `${duration} ${delay ?? ''} ${timingFunction}`;
}
case 'typography': {
const {value, originalVal} = getMode(token, mode);
if (typeof originalVal === 'string') {
return varRef(originalVal, tokens, generateName);
return varRef(originalVal, {prefix, tokens, generateName});
}
const output: Record<string, string> = {};
for (const [k, v] of Object.entries(value)) {
const formatter = k === 'fontFamily' ? transformFontFamily : (val: any): string => String(val);
output[kebabinate(k)] = isAlias((originalVal as any)[k] as any) ? varRef((originalVal as any)[k], tokens, generateName) : formatter(v as any);
output[kebabinate(k)] = isAlias((originalVal as any)[k] as any) ? varRef((originalVal as any)[k], {prefix, tokens, generateName}) : formatter(v as any);
}
return output;
}
Expand All @@ -518,25 +529,38 @@ function getMode<T extends {id: string; $value: any; $extensions?: any; _origina
return {value: token.$value, originalVal: token._original.$value};
}

/** reference an existing CSS var */
export function varRef(id: string, tokens: ParsedToken[], generateName: ReturnType<typeof makeNameGenerator>, suffix?: string): string {
/**
* Reference an existing CSS var
*
*/
export function varRef(
id: string,
options?: {
prefix?: string;
suffix?: string;
// note: the following properties are optional to preserve backwards-compat without a breaking change
tokens?: ParsedToken[];
generateName?: ReturnType<typeof makeNameGenerator>;
},
): string {
let refID = id;
if (isAlias(id)) {
// unclear if mode is ever appended to id when passed here, but leaving for safety in case
const [rootID, _mode] = id.substring(1, id.length - 1).split('#');
refID = rootID!;
}

const token = tokens.find((t) => t.id === refID);
const token = options?.tokens?.find((t) => t.id === refID);

// eslint-disable-next-line no-console
if (!token) console.warn(`Tried to reference variable with id: ${refID}, no token found`);
if (!token) {
console.warn(`Tried to reference variable with id: ${refID}, no token found`); // eslint-disable-line no-console
}

// suffix is only used internally (one place in plugin-sass), so handle it here rather than clutter the public API in defaultNameGenerator
const normalizedSuffix = suffix ? `-${suffix.replace(DASH_PREFIX_RE, '')}` : '';
const normalizedSuffix = options?.suffix ? `-${options?.suffix.replace(DASH_PREFIX_RE, '')}` : '';
const variableId = refID + normalizedSuffix;

return `var(${generateName(variableId, token)})`;
return `var(${options?.generateName?.(variableId, token) ?? defaultNameGenerator(variableId, options?.prefix)})`;
}

/** @deprecated parse legacy modeSelector */
Expand Down
21 changes: 21 additions & 0 deletions packages/plugin-css/test/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ describe('@cobalt-ui/plugin-css', () => {
expect(fs.readFileSync(new URL('./actual.css', cwd), 'utf8')).toMatchFileSnapshot(fileURLToPath(new URL('./want.css', cwd)));
});

test('transform', async () => {
const cwd = new URL(`./transform/`, import.meta.url);
const tokens = JSON.parse(fs.readFileSync(new URL('./tokens.json', cwd), 'utf8'));
await build(tokens, {
outDir: cwd,
plugins: [
pluginCSS({
filename: 'actual.css',
transform(token) {
if (token.id === 'color.blue.5') {
return '#0969db';
}
},
}),
],
color: {},
tokens: [],
});
expect(fs.readFileSync(new URL('./actual.css', cwd), 'utf8')).toMatchFileSnapshot(fileURLToPath(new URL('./want.css', cwd)));
});

test('p3', async () => {
const cwd = new URL(`./p3/`, import.meta.url);
const tokens = JSON.parse(fs.readFileSync(new URL('./tokens.json', cwd), 'utf8'));
Expand Down
17 changes: 17 additions & 0 deletions packages/plugin-css/test/transform/tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"color": {
"$type": "color",
"blue": {
"0": {"$value": "#ddf4ff"},
"1": {"$value": "#b6e3ff"},
"2": {"$value": "#80ccff"},
"3": {"$value": "#54aeff"},
"4": {"$value": "#218bff"},
"5": {"$value": "#0969da"},
"6": {"$value": "#0550ae"},
"7": {"$value": "#033d8b"},
"8": {"$value": "#0a3069"},
"9": {"$value": "#002155"}
}
}
}
33 changes: 33 additions & 0 deletions packages/plugin-css/test/transform/want.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Design Tokens
* Autogenerated from tokens.json.
* DO NOT EDIT!
*/

:root {
--color-blue-0: #ddf4ff;
--color-blue-1: #b6e3ff;
--color-blue-2: #80ccff;
--color-blue-3: #54aeff;
--color-blue-4: #218bff;
--color-blue-5: #0969db;
--color-blue-6: #0550ae;
--color-blue-7: #033d8b;
--color-blue-8: #0a3069;
--color-blue-9: #002155;
}

@supports (color: color(display-p3 1 1 1)) {
:root {
--color-blue-0: color(display-p3 0.8666666666666667 0.9568627450980393 1);
--color-blue-1: color(display-p3 0.7137254901960784 0.8901960784313725 1);
--color-blue-2: color(display-p3 0.5019607843137255 0.8 1);
--color-blue-3: color(display-p3 0.32941176470588235 0.6823529411764706 1);
--color-blue-4: color(display-p3 0.12941176470588237 0.5450980392156862 1);
--color-blue-5: color(display-p3 0.03529411764705882 0.4117647058823529 0.8588235294117647);
--color-blue-6: color(display-p3 0.0196078431372549 0.3137254901960784 0.6823529411764706);
--color-blue-7: color(display-p3 0.011764705882352941 0.23921568627450981 0.5450980392156862);
--color-blue-8: color(display-p3 0.0392156862745098 0.18823529411764706 0.4117647058823529);
--color-blue-9: color(display-p3 0 0.12941176470588237 0.3333333333333333);
}
}
Loading

0 comments on commit cbda777

Please sign in to comment.