0
Fork 0
mirror of https://github.com/penpot/penpot-export.git synced 2025-03-11 23:11:17 -05:00

feat!(core): allow to export JSON files

BREAKING CHANGE: CSS variables (color) are no longer lowercase. Since CSS custom property names are case sensitive, this would break existing consumers.
This commit is contained in:
Roberto Redradix 2023-09-05 14:38:40 +02:00
parent 1fd8a579de
commit 98597dbf4c
8 changed files with 141 additions and 42 deletions

View file

@ -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
}

View file

@ -1,6 +1,7 @@
import { UserConfig, UserFileConfig, Config, FileConfig } from './types'
export { validateUserConfig } from './types'
export type * from './types'
function normalizePenpotExportUserFileConfig(
userConfig: UserFileConfig,

View file

@ -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<typeof assetConfigSchema>
export type ColorsConfig = z.infer<typeof colorsConfigSchema>
export type TypographiesConfig = z.infer<typeof typographiesConfigSchema>
export type PagesConfig = z.infer<typeof pagesConfigSchema>

View file

@ -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)
}

View file

@ -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<object>,
): objects is Array<CSSCustomPropertyDefinition> => {
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)) {

View file

@ -0,0 +1,2 @@
export * from './css'
export * from './json'

View file

@ -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')
}

View file

@ -4,3 +4,14 @@ export interface CSSClassDefinition {
selector: string
cssProps: Record<string, string>
}
export const isCssClassDefinition = (
object: object,
): object is CSSClassDefinition => {
return 'selector' in object
}
export interface CSSCustomPropertyDefinition {
name: string
value: string
}