diff --git a/packages/core/src/lib/adapters/inbound/typographiesToCssClasses.ts b/packages/core/src/lib/adapters/inbound/typographiesToCssClasses.ts deleted file mode 100644 index 599aa4e..0000000 --- a/packages/core/src/lib/adapters/inbound/typographiesToCssClasses.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PenpotApiTypography, CssTextProperty, PenpotApiFile } from '../../api' -import { CSSClassDefinition } from '../../types' - -const getTypographyAssetCssProps = ( - typography: PenpotApiTypography, -): Record => { - return { - lineHeight: typography.lineHeight, - fontStyle: typography.fontStyle, - textTransform: typography.textTransform, - fontWeight: typography.fontWeight, - fontSize: `${typography.fontSize}px`, - letterSpacing: `${typography.letterSpacing}px`, - fontFamily: `"${typography.fontFamily}"`, - } -} - -export const adaptTypographiesToCssClassDefinitions = ( - penpotFile: PenpotApiFile, -): CSSClassDefinition[] => { - const fileName = penpotFile.name - const typographies = Object.values(penpotFile.data.typographies ?? {}) - - const cssClassDefinitions = typographies.map((typography) => { - const cssProps = getTypographyAssetCssProps(typography) - - return { - scope: fileName, - name: typography.name, - cssProps, - } - }) - - return cssClassDefinitions -} diff --git a/packages/core/src/lib/adapters/inbound/typographiesToFontSummary.ts b/packages/core/src/lib/adapters/inbound/typographiesToFontSummary.ts deleted file mode 100644 index 57ae7fc..0000000 --- a/packages/core/src/lib/adapters/inbound/typographiesToFontSummary.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PenpotApiFile } from '../../api' -import { FontsSummary } from '../../types' - -export const summarizeTypographies = ( - penpotFile: PenpotApiFile, -): FontsSummary => { - const typographies = Object.values(penpotFile.data.typographies ?? {}) - - const separator = '|>' - const dedupedFontsKeys = Array.from( - typographies.reduce((set, typography) => { - const { fontId, fontFamily, fontWeight } = typography - const typographyKey = [fontId, fontFamily, fontWeight].join(separator) - - set.add(typographyKey) - return set - }, new Set()), - ) - - const fontsSummary = dedupedFontsKeys.reduce( - (summary, typographyKey) => { - const [fontId, fontFamily, fontWeight] = typographyKey.split(separator) - const fontSource = fontId.startsWith('gfont-') - ? 'googleFonts' - : 'userCustomFonts' - - summary[fontSource][fontFamily] ??= [] - summary[fontSource][fontFamily].push(fontWeight) - - return summary - }, - { googleFonts: {}, userCustomFonts: {} }, - ) - - return fontsSummary -} diff --git a/packages/core/src/lib/adapters/inbound/colorsToCssVariables.ts b/packages/core/src/lib/api/adapters/colors.ts similarity index 68% rename from packages/core/src/lib/adapters/inbound/colorsToCssVariables.ts rename to packages/core/src/lib/api/adapters/colors.ts index cd640eb..45ce861 100644 --- a/packages/core/src/lib/adapters/inbound/colorsToCssVariables.ts +++ b/packages/core/src/lib/api/adapters/colors.ts @@ -1,5 +1,6 @@ -import { PenpotApiFile } from '../../api' -import { CSSCustomPropertyDefinition } from '../../types' +import { CSSCustomPropertyDefinition, ColorAssets } from '../../types' + +import { PenpotApiFile } from '../types' const toHexQuartet = (hexTriplet: string, alpha: number = 1) => { const alphaChannel = Math.trunc(alpha * 0x100) - 1 @@ -20,19 +21,21 @@ const toHexQuartet = (hexTriplet: string, alpha: number = 1) => { return ('#' + RR + GG + BB + AA).toLowerCase() } -export const adaptColorsToCssVariables = ( - penpotFile: PenpotApiFile, -): CSSCustomPropertyDefinition[] => { +const adaptColorsToCssVariables = (penpotFile: PenpotApiFile): ColorAssets => { const fileName = penpotFile.name const colors = Object.values(penpotFile.data.colors ?? {}) - const cssPropsEntries = colors.map((color) => { + const cssPropsEntries = colors.map((color) => { return { - scope: fileName, name: color.name, value: toHexQuartet(color.color, color.opacity), } }) - return cssPropsEntries + return { + scope: fileName, + colors: cssPropsEntries, + } } + +export default adaptColorsToCssVariables diff --git a/packages/core/src/lib/adapters/inbound/pageComponentsToCssClasses.ts b/packages/core/src/lib/api/adapters/pageComponents.ts similarity index 76% rename from packages/core/src/lib/adapters/inbound/pageComponentsToCssClasses.ts rename to packages/core/src/lib/api/adapters/pageComponents.ts index 1b4f8d9..2cdeea4 100644 --- a/packages/core/src/lib/adapters/inbound/pageComponentsToCssClasses.ts +++ b/packages/core/src/lib/api/adapters/pageComponents.ts @@ -1,11 +1,11 @@ +import { CSSClassDefinition, PageComponentAssets } from '../../types' + import { getObjectShapesFromPage, isComponent, pickObjectProps, -} from '../../api/helpers' - -import { PenpotApiFile, PenpotApiObject } from '../../api' -import { CSSClassDefinition } from '../../types' +} from '../helpers' +import { PenpotApiFile, PenpotApiObject } from '../types' const extractObjectCssProps = (object: PenpotApiObject) => { let { textDecoration, ...styles } = object.positionData[0] @@ -33,10 +33,10 @@ const getTextObjectCssProps = (object: PenpotApiObject) => { return pickObjectProps(objectCssProps, textCssProps) } -export const adaptPageComponentsToCssClassDefinitions = ( +const adaptPageComponentsToCssClassDefinitions = ( penpotFile: PenpotApiFile, options: { pageId: string }, -): CSSClassDefinition[] => { +): PageComponentAssets => { const pages = penpotFile.data.pagesIndex ?? {} const page = pages[options.pageId] const pageObjects = Object.values(page.objects) @@ -44,14 +44,13 @@ export const adaptPageComponentsToCssClassDefinitions = ( .filter(isComponent) .map((object) => getObjectShapesFromPage(object, page)) - const cssClassDefinitions = [] + const cssClassDefinitions: CSSClassDefinition[] = [] for (const component of components) { for (const objectId in component.objects) { const object = component.objects[objectId] if (object.type === 'text') { const cssProps = getTextObjectCssProps(object) cssClassDefinitions.push({ - scope: page.name, name: object.name, cssProps, }) @@ -59,5 +58,10 @@ export const adaptPageComponentsToCssClassDefinitions = ( } } - return cssClassDefinitions + return { + scope: page.name, + pageComponents: cssClassDefinitions, + } } + +export default adaptPageComponentsToCssClassDefinitions diff --git a/packages/core/src/lib/api/adapters/typographies.ts b/packages/core/src/lib/api/adapters/typographies.ts new file mode 100644 index 0000000..5bcd526 --- /dev/null +++ b/packages/core/src/lib/api/adapters/typographies.ts @@ -0,0 +1,79 @@ +import { CSSClassDefinition, FontsSummary, TypographyAssets } from '../../types' + +import { PenpotApiTypography, CssTextProperty, PenpotApiFile } from '../types' + +const mapTypographyAssetCssProps = ( + typography: PenpotApiTypography, +): Record => { + return { + lineHeight: typography.lineHeight, + fontStyle: typography.fontStyle, + textTransform: typography.textTransform, + fontWeight: typography.fontWeight, + fontSize: `${typography.fontSize}px`, + letterSpacing: `${typography.letterSpacing}px`, + fontFamily: `"${typography.fontFamily}"`, + } +} + +const adaptTypographiesToCssClassDefinitions = ( + typographies: PenpotApiTypography[], +): CSSClassDefinition[] => { + const cssClassDefinitions = typographies.map( + (typography) => { + const cssProps = mapTypographyAssetCssProps(typography) + + return { + name: typography.name, + cssProps, + } + }, + ) + + return cssClassDefinitions +} + +const summarizeTypographies = ( + typographies: PenpotApiTypography[], +): FontsSummary => { + const separator = '|>' + const dedupedFontsKeys = Array.from( + typographies.reduce((set, typography) => { + const { fontId, fontFamily, fontWeight } = typography + const typographyKey = [fontId, fontFamily, fontWeight].join(separator) + + set.add(typographyKey) + return set + }, new Set()), + ) + + const fontsSummary = dedupedFontsKeys.reduce( + (summary, typographyKey) => { + const [fontId, fontFamily, fontWeight] = typographyKey.split(separator) + const fontSource = fontId.startsWith('gfont-') + ? 'googleFonts' + : 'userCustomFonts' + + summary[fontSource][fontFamily] ??= [] + summary[fontSource][fontFamily].push(fontWeight) + + return summary + }, + { googleFonts: {}, userCustomFonts: {} }, + ) + + return fontsSummary +} + +const adaptTypographies = (penpotFile: PenpotApiFile): TypographyAssets => { + const fileName = penpotFile.name + const typographies = Object.values(penpotFile.data.typographies ?? {}) + + return { + scope: fileName, + typographies: adaptTypographiesToCssClassDefinitions(typographies), + typographiesSummary: summarizeTypographies(typographies), + } +} + +export default adaptTypographies diff --git a/packages/core/src/lib/api/index.ts b/packages/core/src/lib/api/index.ts index ff26f64..3148a38 100644 --- a/packages/core/src/lib/api/index.ts +++ b/packages/core/src/lib/api/index.ts @@ -1,2 +1,6 @@ +export { default as adaptColors } from './adapters/colors' +export { default as adaptTypographies } from './adapters/typographies' +export { default as adaptPageComponents } from './adapters/pageComponents' + export * from './types' export { Penpot as default } from './penpot' diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 066d0a2..7dbf99c 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -1,12 +1,11 @@ import path from 'node:path' -import { adaptTypographiesToCssClassDefinitions } from './adapters/inbound/typographiesToCssClasses' -import { summarizeTypographies } from './adapters/inbound/typographiesToFontSummary' -import { adaptColorsToCssVariables } from './adapters/inbound/colorsToCssVariables' -import { adaptPageComponentsToCssClassDefinitions } from './adapters/inbound/pageComponentsToCssClasses' - -import { Penpot } from './api/penpot' - +import { + default as PenpotApiClient, + adaptColors, + adaptTypographies, + adaptPageComponents, +} from './api' import { parseUserConfig, normalizePenpotExportUserConfig, @@ -20,17 +19,24 @@ import { jsonOutputter, OutputterFunction, } from './outputters' +import { + scopeTypographiesClassNames, + scopePageComponentsClassNames, + TransformerFunction, +} from './transformers' import { PenpotExportAssets } from './types' -const processOutput = ({ - outputFormat = 'css', +function processOutput({ + outputFormat, outputPath, assets, + transform, }: { outputFormat: AssetConfig['format'] outputPath: string - assets: PenpotExportAssets -}) => { + assets: T + transform?: TransformerFunction +}) { const outputter: OutputterFunction | null = outputFormat === 'css' ? cssOutputter @@ -43,7 +49,8 @@ const processOutput = ({ if (outputter === null) throw new PenpotExportInternalError('Unable to process output format') - const textContents = outputter(assets) + const transformedAssets = transform !== undefined ? transform(assets) : assets + const textContents = outputter(transformedAssets) return writeTextFile(outputPath, textContents) } @@ -54,7 +61,7 @@ export default async function penpotExport( const parsedUserConfig = parseUserConfig(userConfig) const config = normalizePenpotExportUserConfig(parsedUserConfig) - const penpot = new Penpot({ + const penpot = new PenpotApiClient({ baseUrl: config.instance, accessToken: config.accessToken, }) @@ -70,9 +77,7 @@ export default async function penpotExport( processOutput({ outputFormat: colorsConfig.format, outputPath: path.resolve(rootProjectPath, colorsConfig.output), - assets: { - colors: adaptColorsToCssVariables(penpotFile), - }, + assets: adaptColors(penpotFile), }) console.log('✅ Colors: %s', colorsConfig.output) @@ -82,10 +87,11 @@ export default async function penpotExport( processOutput({ outputFormat: typographiesConfig.format, outputPath: path.resolve(rootProjectPath, typographiesConfig.output), - assets: { - typographies: adaptTypographiesToCssClassDefinitions(penpotFile), - typographiesSummary: summarizeTypographies(penpotFile), - }, + assets: adaptTypographies(penpotFile), + transform: + typographiesConfig.format === 'css' + ? scopeTypographiesClassNames + : undefined, }) console.log('✅ Typographies: %s', typographiesConfig.output) @@ -95,11 +101,13 @@ export default async function penpotExport( processOutput({ outputFormat: pagesConfig.format, outputPath: path.resolve(rootProjectPath, pagesConfig.output), - assets: { - pageComponents: adaptPageComponentsToCssClassDefinitions(penpotFile, { - pageId: pagesConfig.pageId, - }), - }, + assets: adaptPageComponents(penpotFile, { + pageId: pagesConfig.pageId, + }), + transform: + pagesConfig.format === 'css' + ? scopePageComponentsClassNames + : undefined, }) console.log('✅ Page components: %s', pagesConfig.output) diff --git a/packages/core/src/lib/outputters/css/index.ts b/packages/core/src/lib/outputters/css/index.ts index 05592d1..16392ec 100644 --- a/packages/core/src/lib/outputters/css/index.ts +++ b/packages/core/src/lib/outputters/css/index.ts @@ -18,9 +18,8 @@ import { } from './syntax' const serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => { - const selector = textToCssClassSelector( - `${cssClassDefinition.scope}--${cssClassDefinition.name}`, - ) + const selector = textToCssClassSelector(cssClassDefinition.name) + const cssValidProps = Object.keys(cssClassDefinition.cssProps).map( (key) => ` ${camelToKebab(key)}: ${cssClassDefinition.cssProps[key]};`, ) @@ -56,6 +55,7 @@ const composeFileHeader = (fontsSummary: FontsSummary) => { } const serializeCss: OutputterFunction = ({ + scope, colors, typographies, typographiesSummary, @@ -64,7 +64,7 @@ const serializeCss: OutputterFunction = ({ if (colors) { return serializeCssCustomPropertiesRoot(colors) } else if (typographies) { - const body = typographies.map(serializeCssClass).join('\n\n') + const body: string = typographies.map(serializeCssClass).join('\n\n') const header: string = composeFileHeader(typographiesSummary) return header + '\n\n' + body diff --git a/packages/core/src/lib/outputters/json.ts b/packages/core/src/lib/outputters/json.ts index dfda9ef..2b637e2 100644 --- a/packages/core/src/lib/outputters/json.ts +++ b/packages/core/src/lib/outputters/json.ts @@ -1,18 +1,66 @@ -import { ColorAssets, PageComponentAssets, TypographyAssets } from '../types' +import { + CSSClassDefinition, + CSSCustomPropertyDefinition, + ColorAssets, + PageComponentAssets, + TypographyAssets, +} from '../types' import { PenpotExportInvalidAssetsError } from './errors' import { OutputterFunction } from './types' +interface JSONCustomPropertyDefinition extends CSSCustomPropertyDefinition { + scope: string +} + +interface JSONClassDefinition extends CSSClassDefinition { + scope: string +} + +const appendScopeToCustomProperties = ( + cssCustomPropertiesDefinitions: CSSCustomPropertyDefinition[], + scope: string, +): JSONCustomPropertyDefinition[] => { + return cssCustomPropertiesDefinitions.map( + (customProperty) => { + return { + scope, + ...customProperty, + } + }, + ) +} + +const appendScopeToClasses = ( + cssClassDefinition: CSSClassDefinition[], + scope: string, +): JSONClassDefinition[] => { + return cssClassDefinition.map((classDefinition) => { + return { + scope, + ...classDefinition, + } + }) +} + const serializeJson: OutputterFunction = ({ + scope, colors, typographies, pageComponents, }: ColorAssets | TypographyAssets | PageComponentAssets): string => { - const assets = colors ?? typographies ?? pageComponents ?? null + if (colors) { + const scopedColors = appendScopeToCustomProperties(colors, scope) + return JSON.stringify(scopedColors, null, 2) + } else if (typographies || pageComponents) { + const scopedAssets = appendScopeToClasses( + typographies ?? pageComponents, + scope, + ) + return JSON.stringify(scopedAssets, null, 2) + } - if (assets === null) throw new PenpotExportInvalidAssetsError() - - return JSON.stringify(assets, null, 2) + throw new PenpotExportInvalidAssetsError() } export default serializeJson diff --git a/packages/core/src/lib/transformers/index.ts b/packages/core/src/lib/transformers/index.ts new file mode 100644 index 0000000..fdebde4 --- /dev/null +++ b/packages/core/src/lib/transformers/index.ts @@ -0,0 +1,2 @@ +export * from './scopeClassNames' +export type { TransformerFunction } from './types' diff --git a/packages/core/src/lib/transformers/scopeClassNames.ts b/packages/core/src/lib/transformers/scopeClassNames.ts new file mode 100644 index 0000000..19dc0bf --- /dev/null +++ b/packages/core/src/lib/transformers/scopeClassNames.ts @@ -0,0 +1,40 @@ +import { + CSSClassDefinition, + PageComponentAssets, + TypographyAssets, +} from '../types' + +const scopeClassNames = ( + cssClassDefinition: CSSClassDefinition[], + scope: string, +): CSSClassDefinition[] => { + return cssClassDefinition.map(({ name, cssProps }) => { + return { + name: `${scope}--${name}`, + cssProps, + } + }) +} + +export function scopeTypographiesClassNames( + assets: TypographyAssets, +): TypographyAssets { + const scopedTypographies = scopeClassNames(assets.typographies, assets.scope) + return { + ...assets, + typographies: scopedTypographies, + } +} + +export function scopePageComponentsClassNames( + assets: PageComponentAssets, +): PageComponentAssets { + const scopedPageComponents = scopeClassNames( + assets.pageComponents, + assets.scope, + ) + return { + ...assets, + pageComponents: scopedPageComponents, + } +} diff --git a/packages/core/src/lib/transformers/types.ts b/packages/core/src/lib/transformers/types.ts new file mode 100644 index 0000000..2332773 --- /dev/null +++ b/packages/core/src/lib/transformers/types.ts @@ -0,0 +1,5 @@ +import { ColorAssets, PageComponentAssets, TypographyAssets } from '../types' + +export type TransformerFunction< + T extends ColorAssets | TypographyAssets | PageComponentAssets, +> = (assets: T) => T diff --git a/packages/core/src/lib/types.ts b/packages/core/src/lib/types.ts index 1334193..6144a98 100644 --- a/packages/core/src/lib/types.ts +++ b/packages/core/src/lib/types.ts @@ -1,11 +1,9 @@ export interface CSSClassDefinition { - scope: string name: string cssProps: Record } export interface CSSCustomPropertyDefinition { - scope: string name: string value: string } @@ -20,6 +18,7 @@ export interface FontsSummary { } interface BaseAssets { + scope: string colors?: never typographies?: never typographiesSummary?: never