Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bezier-tokens): Implementing composition tokens and removing duplicate CSS #1779

Merged
merged 10 commits into from
Dec 12, 2023
184 changes: 115 additions & 69 deletions packages/bezier-tokens/scripts/build-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { minimatch } from 'minimatch'
import StyleDictionary, {
type Config,
type Options,
type Platform,
} from 'style-dictionary'
import StyleDictionary, { type Platform } from 'style-dictionary'

import {
customJsCjs,
customJsEsm,
} from './lib/format'
import {
customCubicBezier,
customFontFamily,
customFontRem,
} from './lib/transform'
import { CSSTransforms } from './lib/transform'

const CustomTransforms = [...Object.values(CSSTransforms)]

const TokenBuilder = StyleDictionary.registerTransform(customCubicBezier)
.registerTransform(customFontFamily)
.registerTransform(customFontRem)
const TokenBuilder = CustomTransforms.reduce(
(builder, transform) => builder.registerTransform(transform),
StyleDictionary,
)
Comment on lines +12 to +15
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom transformer를 하나의 객체에 담아서 register 과정을 간소화했습니다.

.registerFormat(customJsCjs)
.registerFormat(customJsEsm)

Expand All @@ -26,9 +21,7 @@ function defineWebPlatform({ options, ...rest }: Platform): Platform {
transforms: [
'attribute/cti',
'name/cti/kebab',
customCubicBezier.name,
customFontFamily.name,
customFontRem.name,
...CustomTransforms.map((transform) => transform.name),
],
basePxFontSize: 10,
options: {
Expand All @@ -39,93 +32,146 @@ function defineWebPlatform({ options, ...rest }: Platform): Platform {
}
}

interface DefineConfigOptions {
source: string[]
reference?: string[]
destination: string
options?: Options & {
cssSelector: string
}
}
const PATH = {
GLOBAL: 'src/global/*.json',
SEMANTIC_COMMON: 'src/semantic/*.json',
SEMANTIC_LIGHT: 'src/semantic/light-theme/*.json',
SEMANTIC_DARK: 'src/semantic/dark-theme/*.json',
} as const

function defineConfig({
source,
reference = [],
destination,
options,
}: DefineConfigOptions): Config {
return {
source: [...source, ...reference],
function main() {
TokenBuilder.extend({
source: [PATH.GLOBAL, PATH.SEMANTIC_COMMON, PATH.SEMANTIC_LIGHT],
platforms: {
'web/cjs': defineWebPlatform({
'web/cjs/global': defineWebPlatform({
buildPath: 'dist/cjs/',
files: [
{
destination: 'global.js',
format: customJsCjs.name,
filter: ({ filePath }) =>
[PATH.GLOBAL].some((src) => minimatch(filePath, src)),
},
],
}),
'web/esm/global': defineWebPlatform({
buildPath: 'dist/esm/',
files: [
{
destination: 'global.mjs',
format: customJsEsm.name,
filter: ({ filePath }) =>
[PATH.GLOBAL].some((src) => minimatch(filePath, src)),
},
],
}),
'web/css/global': defineWebPlatform({
buildPath: 'dist/css/',
files: [
{
destination: 'global.css',
format: 'css/variables',
filter: ({ filePath }) =>
[PATH.GLOBAL, PATH.SEMANTIC_COMMON].some((src) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:where(:root, :host) selector 하위에 한번에 Global, 공통 Semantic token을 빌드하는 로직

minimatch(filePath, src),
),
options: {
selector: ':where(:root, :host)',
outputReferences: true,
},
},
],
}),
'web/cjs/light-theme': defineWebPlatform({
buildPath: 'dist/cjs/',
files: [
{
destination: `${destination}.js`,
destination: 'lightTheme.js',
format: customJsCjs.name,
filter: ({ filePath }) =>
source.some((src) => minimatch(filePath, src)),
[PATH.SEMANTIC_COMMON, PATH.SEMANTIC_LIGHT].some((src) =>
minimatch(filePath, src),
),
},
],
}),
'web/esm': defineWebPlatform({
'web/esm/light-theme': defineWebPlatform({
buildPath: 'dist/esm/',
files: [
{
destination: `${destination}.mjs`,
destination: 'lightTheme.mjs',
format: customJsEsm.name,
filter: ({ filePath }) =>
source.some((src) => minimatch(filePath, src)),
[PATH.SEMANTIC_COMMON, PATH.SEMANTIC_LIGHT].some((src) =>
minimatch(filePath, src),
),
},
],
}),
'web/css': defineWebPlatform({
'web/css/light-theme': defineWebPlatform({
buildPath: 'dist/css/',
files: [
{
destination: `${destination}.css`,
destination: 'light-theme.css',
format: 'css/variables',
filter: ({ filePath }) =>
source.some((src) => minimatch(filePath, src)),
[PATH.SEMANTIC_LIGHT].some((src) => minimatch(filePath, src)),
options: {
selector: options?.cssSelector,
selector: ':where(:root, :host), [data-bezier-theme="light"]',
outputReferences: true,
},
},
],
}),
},
}
}
}).buildAllPlatforms()

function main() {
[
TokenBuilder.extend(
defineConfig({
source: ['src/global/*.json'],
destination: 'global',
options: { cssSelector: ':where(:root, :host)' },
TokenBuilder.extend({
source: [PATH.GLOBAL, PATH.SEMANTIC_COMMON, PATH.SEMANTIC_DARK],
platforms: {
'web/cjs/dark-theme': defineWebPlatform({
buildPath: 'dist/cjs/',
files: [
{
destination: 'darkTheme.js',
format: customJsCjs.name,
filter: ({ filePath }) =>
[PATH.SEMANTIC_COMMON, PATH.SEMANTIC_DARK].some((src) =>
minimatch(filePath, src),
),
},
],
}),
),
TokenBuilder.extend(
defineConfig({
source: ['src/semantic/*.json', 'src/semantic/light-theme/*.json'],
reference: ['src/global/*.json'],
destination: 'lightTheme',
options: {
cssSelector: ':where(:root, :host), [data-bezier-theme="light"]',
},
'web/esm/dark-theme': defineWebPlatform({
buildPath: 'dist/esm/',
files: [
{
destination: 'darkTheme.mjs',
format: customJsEsm.name,
filter: ({ filePath }) =>
[PATH.SEMANTIC_COMMON, PATH.SEMANTIC_DARK].some((src) =>
minimatch(filePath, src),
),
},
],
}),
),
TokenBuilder.extend(
defineConfig({
source: ['src/semantic/*.json', 'src/semantic/dark-theme/*.json'],
reference: ['src/global/*.json'],
destination: 'darkTheme',
options: { cssSelector: '[data-bezier-theme="dark"]' },
'web/css/dark-theme': defineWebPlatform({
buildPath: 'dist/css/',
files: [
{
destination: 'darkTheme.css',
format: 'css/variables',
filter: ({ filePath }) =>
[PATH.SEMANTIC_DARK].some((src) => minimatch(filePath, src)),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테마별 CSS에선 JS와 다르게 공통 Semantic token을 필터링하는 로직

options: {
selector: '[data-bezier-theme="dark"]',
outputReferences: true,
},
},
],
}),
),
].forEach((builder) => builder.buildAllPlatforms())
},
}).buildAllPlatforms()
}

main()
117 changes: 87 additions & 30 deletions packages/bezier-tokens/scripts/lib/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,94 @@ import type {
Transform,
} from 'style-dictionary'

import { extractNumber } from './utils'
import {
extractNumber,
toCSSDimension,
} from './utils'

type CustomTransform = Named<Transform<unknown>>
type Transforms = Record<string, CustomTransform>

export const customFontRem: CustomTransform = {
name: 'custom/font/rem',
type: 'value',
transitive: true,
matcher: ({ attributes: { category, type } = {} }) =>
category === 'font' && (type === 'size' || type === 'line-height'),
transformer: ({ value }: { value: string }, options) =>
`${parseFloat(extractNumber(value) ?? '') / ((options && options.basePxFontSize) || 16)}rem`,
}
export const CSSTransforms = {
fontRem: {
name: 'custom/css/font/rem',
type: 'value',
transitive: true,
matcher: (token) =>
token.attributes?.category === 'font' && token.type === 'dimension',
transformer: ({ value }: { value: string }, options) => {
const extractedNumber = extractNumber(value)
const isNegative = value.trim().startsWith('-')
const numberValue =
parseFloat(extractedNumber ?? '') /
((options && options.basePxFontSize) || 16)
return `${isNegative ? -numberValue : numberValue}rem`
},
},
fontFamily: {
name: 'custom/css/font/family',
type: 'value',
transitive: true,
matcher: (token) => token.type === 'fontFamily',
transformer: ({ value }: { value: string[] }) =>
/**
* @see {@link https://stackoverflow.com/questions/13751412/why-would-font-names-need-quotes}
*/
value.map((fontFamily) => `'${fontFamily}'`).join(', '),
},
cubicBezier: {
name: 'custom/css/cubicBezier',
type: 'value',
transitive: true,
matcher: (token) => token.type === 'cubicBezier',
transformer: ({
value: [x1, y1, x2, y2],
}: {
value: [number, number, number, number]
}) => `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`,
},
shadow: {
name: 'custom/css/shadow',
type: 'value',
transitive: true,
matcher: (token) => token.type === 'shadow',
transformer: ({ value }) => {
function transform(shadow?: {
offsetX?: string
offsetY?: string
blur?: string
spread?: string
color: string
type: 'dropShadow' | 'innerShadow'
}) {
if (typeof shadow !== 'object') {
return shadow
}
const { offsetX, offsetY, blur, spread, color, type } = shadow
return `${type === 'innerShadow' ? 'inset ' : ''}${offsetX ? toCSSDimension(offsetX) : 0
} ${offsetY ? toCSSDimension(offsetY) : 0} ${blur ? toCSSDimension(blur) : 0
}${spread ? ` ${toCSSDimension(spread)} ` : ' '}${color || 'rgba(0, 0, 0, 1)'
}`.trim()
}

export const customFontFamily: CustomTransform = {
name: 'custom/font/family',
type: 'value',
transitive: true,
matcher: ({ original: { $type } }) => $type === 'fontFamily',
transformer: ({ value }: { value: string[] }) =>
/**
* @see {@link https://stackoverflow.com/questions/13751412/why-would-font-names-need-quotes}
*/
value.map((fontFamily) => `'${fontFamily}'`).join(', '),
}

export const customCubicBezier: CustomTransform = {
name: 'custom/cubicBezier',
type: 'value',
transitive: true,
matcher: ({ original: { $type } }) => $type === 'cubicBezier',
transformer: ({ value: [x1, y1, x2, y2] }: { value: [number, number, number, number] }) =>
`cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`,
}
return Array.isArray(value)
? value.map((v) => transform(v)).join(', ')
: transform(value)
},
},
transition: {
name: 'custom/css/transition',
type: 'value',
transitive: true,
matcher: (token) => token.type === 'transition',
transformer: ({
value: { duration, timingFunction, delay },
}: {
value: {
duration: string
timingFunction?: string
delay?: string
}
}) => `${`${duration} ` || ''}${timingFunction || ''}${delay || ''}`.trim(),
},
} satisfies Transforms
2 changes: 2 additions & 0 deletions packages/bezier-tokens/scripts/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const toCamelCase = (str: string) =>
str.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase())

export const extractNumber = (str: string) => str.match(/\d+/g)?.join('')

export const toCSSDimension = (value: string) => (/^0[a-zA-Z]+$/.test(value) ? 0 : value)
Loading