diff --git a/packages/core/src/lib/adapters/inbound/colorsToCssVariables.ts b/packages/core/src/lib/adapters/inbound/colorsToCssVariables.ts index d5b055a..f017666 100644 --- a/packages/core/src/lib/adapters/inbound/colorsToCssVariables.ts +++ b/packages/core/src/lib/adapters/inbound/colorsToCssVariables.ts @@ -1,7 +1,7 @@ -import { textToCssCustomProperyName } from '../../css/helpers' +import { textToCssIdentToken } from '../../css/helpers' import { PenpotApiFile } from '../../api' -import { CSSClassDefinition } from '../../types' +import { CSSCustomPropertyDefinition } from '../../types' const toRgbaCssValue = (hex: string, alpha: number = 1) => { const channels = hex.match(/\w{2}/g) @@ -16,17 +16,17 @@ const toRgbaCssValue = (hex: string, alpha: number = 1) => { export const adaptColorsToCssVariables = ( penpotFile: PenpotApiFile, -): CSSClassDefinition => { +): CSSCustomPropertyDefinition[] => { const colors = Object.values(penpotFile.data.colors ?? {}) const cssPropsEntries = colors.map((color) => { - const objectClassName = textToCssCustomProperyName(color.name) + const objectClassName = textToCssIdentToken(color.name) - return [objectClassName, toRgbaCssValue(color.color, color.opacity)] + return { + name: objectClassName, + value: toRgbaCssValue(color.color, color.opacity), + } }) - return { - selector: ':root', - cssProps: Object.fromEntries(cssPropsEntries), - } + return cssPropsEntries } diff --git a/packages/core/src/lib/config/index.ts b/packages/core/src/lib/config/index.ts index 23f8a18..9431bc8 100644 --- a/packages/core/src/lib/config/index.ts +++ b/packages/core/src/lib/config/index.ts @@ -1,6 +1,7 @@ import { UserConfig, UserFileConfig, Config, FileConfig } from './types' export { validateUserConfig } from './types' +export type * from './types' function normalizePenpotExportUserFileConfig( userConfig: UserFileConfig, diff --git a/packages/core/src/lib/config/types.ts b/packages/core/src/lib/config/types.ts index cf6ac4f..52f00e1 100644 --- a/packages/core/src/lib/config/types.ts +++ b/packages/core/src/lib/config/types.ts @@ -29,19 +29,21 @@ const pageIdSchema = z .uuid({ message: '.pageId must be a valid UUID', }) -const outputSchema = z.string({ +const outputPathSchema = z.string({ required_error: '.output is required', invalid_type_error: '.output must be a string', }) +// FIXME Format should be optional for user config, but required for parsed config +const outputFormatSchema = z.union([z.literal('css'), z.literal('json')]) -const colorsConfigSchema = z.object({ - output: outputSchema, +const assetConfigSchema = z.object({ + output: outputPathSchema, + format: z.optional(outputFormatSchema), }) -const typographiesConfigSchema = z.object({ - output: outputSchema, -}) -const pagesConfigSchema = z.object({ - output: outputSchema, + +const colorsConfigSchema = assetConfigSchema +const typographiesConfigSchema = assetConfigSchema +const pagesConfigSchema = assetConfigSchema.extend({ pageId: pageIdSchema, }) @@ -86,6 +88,7 @@ const userConfigSchema = z.object({ }) // Types +export type AssetConfig = z.infer export type ColorsConfig = z.infer export type TypographiesConfig = z.infer export type PagesConfig = z.infer diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index cbe7020..32c46fc 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -1,12 +1,41 @@ import path from 'node:path' -import Penpot from '../lib/api' -import { validateUserConfig, normalizePenpotExportUserConfig } from './config' -import { writeCssFile } from './outputters/css' + import { adaptTypographiesToCssClassDefinitions } from './adapters/inbound/typographyToCssClasses' import { adaptColorsToCssVariables } from './adapters/inbound/colorsToCssVariables' import { adaptPageComponentsToCssClassDefinitions } from './adapters/inbound/pageComponentsToCssClasses' +import { Penpot } from './api/penpot' + +import { + validateUserConfig, + normalizePenpotExportUserConfig, + AssetConfig, +} from './config' +import { writeJsonFile, writeCssFile } from './outputters' +import { CSSClassDefinition, CSSCustomPropertyDefinition } from './types' + +const processOutput = ({ + outputFormat = 'css', + outputPath, + content, +}: { + outputFormat: AssetConfig['format'] + outputPath: string + content: CSSClassDefinition[] | CSSCustomPropertyDefinition[] +}) => { + if (outputFormat === 'css') { + return writeCssFile(outputPath, content) + } + if (outputFormat === 'json') { + return writeJsonFile(outputPath, content) + } + throw new Error( + 'Unable to process output format. This is an error in penpot-export code, please contact their authors.', + ) +} + export type * from './types' + export default async function penpotExport( userConfig: object, rootProjectPath: string, @@ -27,33 +56,33 @@ export default async function penpotExport( console.log('🎨 Processing Penpot file: %s', penpotFile.name) for (const colorsConfig of fileConfig.colors) { - const cssPath = path.resolve(rootProjectPath, colorsConfig.output) - const cssClassDefinition = adaptColorsToCssVariables(penpotFile) - - writeCssFile(cssPath, [cssClassDefinition]) + processOutput({ + outputFormat: colorsConfig.format, + outputPath: path.resolve(rootProjectPath, colorsConfig.output), + content: adaptColorsToCssVariables(penpotFile), + }) console.log('✅ Colors: %s', colorsConfig.output) } for (const typographiesConfig of fileConfig.typographies) { - const cssPath = path.resolve(rootProjectPath, typographiesConfig.output) - const cssClassDefinitions = - adaptTypographiesToCssClassDefinitions(penpotFile) - - writeCssFile(cssPath, cssClassDefinitions) + processOutput({ + outputFormat: typographiesConfig.format, + outputPath: path.resolve(rootProjectPath, typographiesConfig.output), + content: adaptTypographiesToCssClassDefinitions(penpotFile), + }) console.log('✅ Typographies: %s', typographiesConfig.output) } for (const pagesConfig of fileConfig.pages) { - const cssClassDefinitions = adaptPageComponentsToCssClassDefinitions( - penpotFile, - { pageId: pagesConfig.pageId }, - ) - - const cssPath = path.resolve(rootProjectPath, pagesConfig.output) - - writeCssFile(cssPath, cssClassDefinitions) + processOutput({ + outputFormat: pagesConfig.format, + outputPath: path.resolve(rootProjectPath, pagesConfig.output), + content: adaptPageComponentsToCssClassDefinitions(penpotFile, { + pageId: pagesConfig.pageId, + }), + }) console.log('✅ Page components: %s', pagesConfig.output) } diff --git a/packages/core/src/lib/outputters/css.ts b/packages/core/src/lib/outputters/css.ts index 28c48bb..717b778 100644 --- a/packages/core/src/lib/outputters/css.ts +++ b/packages/core/src/lib/outputters/css.ts @@ -1,7 +1,20 @@ import fs from 'node:fs' import path from 'node:path' + +import { textToCssCustomProperyName } from '../css/helpers' + import { camelToKebab } from '../string' -import { CSSClassDefinition } from '../types' +import { + CSSClassDefinition, + CSSCustomPropertyDefinition, + isCssClassDefinition, +} from '../types' + +const areCssCustomPropertiesDefinitions = ( + objects: Array, +): objects is Array => { + return !objects.every(isCssClassDefinition) +} const serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => { const cssValidProps = Object.keys(cssClassDefinition.cssProps).map( @@ -11,15 +24,32 @@ const serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => { return [`${cssClassDefinition.selector} {`, ...cssValidProps, '}'].join('\n') } -const serializeCss = (cssClassDefinitions: CSSClassDefinition[]): string => { - return cssClassDefinitions.map(serializeCssClass).join('\n\n') +const serializeCssCustomProperties = ( + cssCustomProperties: CSSCustomPropertyDefinition[], +): string => { + const selector = ':root' + const cssDeclarations = cssCustomProperties.map( + ({ name, value }) => ` ${textToCssCustomProperyName(name)}: ${value};`, + ) + + return [`${selector} {`, ...cssDeclarations, '}'].join('\n') +} + +const serializeCss = ( + cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[], +): string => { + if (areCssCustomPropertiesDefinitions(cssDefinitions)) { + return serializeCssCustomProperties(cssDefinitions) + } else { + return cssDefinitions.map(serializeCssClass).join('\n\n') + } } export function writeCssFile( outputPath: string, - cssClassDefinitions: CSSClassDefinition[], + cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[], ) { - const css = serializeCss(cssClassDefinitions) + const css = serializeCss(cssDefinitions) const dirname = path.dirname(outputPath) if (!fs.existsSync(dirname)) { diff --git a/packages/core/src/lib/outputters/index.ts b/packages/core/src/lib/outputters/index.ts new file mode 100644 index 0000000..0ce4ce0 --- /dev/null +++ b/packages/core/src/lib/outputters/index.ts @@ -0,0 +1,2 @@ +export * from './css' +export * from './json' diff --git a/packages/core/src/lib/outputters/json.ts b/packages/core/src/lib/outputters/json.ts new file mode 100644 index 0000000..fdbec07 --- /dev/null +++ b/packages/core/src/lib/outputters/json.ts @@ -0,0 +1,23 @@ +import fs from 'node:fs' +import path from 'node:path' +import { CSSClassDefinition, CSSCustomPropertyDefinition } from '../types' + +const serializeJson = ( + cssClassDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[], +): string => { + return JSON.stringify(cssClassDefinitions, null, 2) +} + +export function writeJsonFile( + outputPath: string, + cssClassDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[], +) { + const json = serializeJson(cssClassDefinitions) + const dirname = path.dirname(outputPath) + + if (!fs.existsSync(dirname)) { + fs.mkdirSync(dirname, { recursive: true }) + } + + fs.writeFileSync(outputPath, json, 'utf-8') +} diff --git a/packages/core/src/lib/types.ts b/packages/core/src/lib/types.ts index b28cce4..decf41a 100644 --- a/packages/core/src/lib/types.ts +++ b/packages/core/src/lib/types.ts @@ -4,3 +4,14 @@ export interface CSSClassDefinition { selector: string cssProps: Record } + +export const isCssClassDefinition = ( + object: object, +): object is CSSClassDefinition => { + return 'selector' in object +} + +export interface CSSCustomPropertyDefinition { + name: string + value: string +}