diff --git a/.changeset/plenty-swans-sit.md b/.changeset/plenty-swans-sit.md new file mode 100644 index 00000000..3747c900 --- /dev/null +++ b/.changeset/plenty-swans-sit.md @@ -0,0 +1,5 @@ +--- +'@cobalt-ui/plugin-css': patch +--- + +Bugfix: bugs in transform() API introduced in 1.6.0 diff --git a/.changeset/violet-seahorses-compete.md b/.changeset/violet-seahorses-compete.md new file mode 100644 index 00000000..6b980dc4 --- /dev/null +++ b/.changeset/violet-seahorses-compete.md @@ -0,0 +1,6 @@ +--- +'@cobalt-ui/plugin-sass': patch +'@cobalt-ui/plugin-css': patch +--- + +Bugfix: fix TypeScript signature for transform() plugin option diff --git a/.changeset/wet-drinks-marry.md b/.changeset/wet-drinks-marry.md new file mode 100644 index 00000000..397c01aa --- /dev/null +++ b/.changeset/wet-drinks-marry.md @@ -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. diff --git a/packages/plugin-css/src/index.ts b/packages/plugin-css/src/index.ts index 706e7d84..709345a9 100644 --- a/packages/plugin-css/src/index.ts +++ b/packages/plugin-css/src/index.ts @@ -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'; @@ -45,7 +45,7 @@ export interface Options { /** generate wrapper selectors around token modes */ modeSelectors?: ModeSelector[] | LegacyModeSelectors; /** handle different token types */ - transform?: (token: T, mode?: string) => string; + transform?: (token: T, mode?: string) => string | undefined | null; /** @deprecated prefix variable names */ prefix?: string; /** enable P3 color enhancement? (default: true) */ @@ -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 | undefined = await options?.transform?.(token); + let value: ReturnType | 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': { @@ -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 | undefined = await options?.transform?.(token, modeSelector.mode); + let modeVal: ReturnType | 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': { @@ -354,29 +354,40 @@ 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}, + { + colorFormat = 'hex', + generateName, + mode, + prefix, + tokens, + }: { + colorFormat: Options['colorFormat']; + generateName?: ReturnType; + mode?: string; + prefix?: string; + tokens?: ParsedToken[]; + }, ): string | number | Record { 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); } @@ -384,42 +395,42 @@ export function defaultTransformer( 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); } @@ -427,17 +438,17 @@ export function defaultTransformer( 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 @@ -448,13 +459,13 @@ 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(', '); @@ -462,16 +473,16 @@ export function defaultTransformer( 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(', '); @@ -479,25 +490,25 @@ export function defaultTransformer( 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 = {}; 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; } @@ -518,8 +529,20 @@ function getMode, 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; + }, +): string { let refID = id; if (isAlias(id)) { // unclear if mode is ever appended to id when passed here, but leaving for safety in case @@ -527,16 +550,17 @@ export function varRef(id: string, tokens: ParsedToken[], generateName: ReturnTy 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 */ diff --git a/packages/plugin-css/test/build.test.ts b/packages/plugin-css/test/build.test.ts index 5463859f..b2056f14 100644 --- a/packages/plugin-css/test/build.test.ts +++ b/packages/plugin-css/test/build.test.ts @@ -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')); diff --git a/packages/plugin-css/test/transform/tokens.json b/packages/plugin-css/test/transform/tokens.json new file mode 100644 index 00000000..cde64103 --- /dev/null +++ b/packages/plugin-css/test/transform/tokens.json @@ -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"} + } + } +} diff --git a/packages/plugin-css/test/transform/want.css b/packages/plugin-css/test/transform/want.css new file mode 100644 index 00000000..93d7c8a8 --- /dev/null +++ b/packages/plugin-css/test/transform/want.css @@ -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); + } +} diff --git a/packages/plugin-sass/src/index.ts b/packages/plugin-sass/src/index.ts index 442fcefb..c573afc2 100644 --- a/packages/plugin-sass/src/index.ts +++ b/packages/plugin-sass/src/index.ts @@ -33,7 +33,7 @@ export interface Options { /** embed files in CSS? */ embedFiles?: boolean; /** handle different token types */ - transform?: (token: T, mode?: string) => string; + transform?: (token: T, mode?: string) => string | undefined | null; /** transform color */ colorFormat?: NonNullable; } @@ -135,9 +135,9 @@ ${cbClose}` output.push(indent(`"${token.id}": (`, 1)); - let value: string | number | undefined; + let value: string | number | undefined | null; if (cssPlugin) { - value = varRef(token.id, tokens, generateName); + value = varRef(token.id, {prefix, tokens, generateName}); } else { value = await options?.transform?.(token); if (value === undefined || value === null) { @@ -149,9 +149,9 @@ ${cbClose}` // modes for (const modeName of Object.keys((token.$extensions && token.$extensions.mode) || {})) { - let modeValue: string | number | undefined; + let modeValue: string | number | undefined | null; if (cssPlugin) { - modeValue = varRef(token.id, tokens, generateName); + modeValue = varRef(token.id, {tokens, generateName}); } else { modeValue = options?.transform?.(token, modeName); if (modeValue === undefined || modeValue === null) { @@ -176,7 +176,7 @@ ${cbClose}` for (const [k, value] of defaultProperties) { const property = k.replace(CAMELCASE_RE, '$1-$2').toLocaleLowerCase(); if (cssPlugin) { - output.push(indent(`"${property}": (${varRef(token.id, tokens, generateName, property)}),`, 3)); + output.push(indent(`"${property}": (${varRef(token.id, {prefix, generateName, suffix: property, tokens})}),`, 3)); } else { output.push(indent(`"${property}": (${Array.isArray(value) ? formatFontFamilyNames(value) : value}),`, 3)); }