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:
parent
1fd8a579de
commit
98597dbf4c
8 changed files with 141 additions and 42 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { UserConfig, UserFileConfig, Config, FileConfig } from './types'
|
||||
|
||||
export { validateUserConfig } from './types'
|
||||
export type * from './types'
|
||||
|
||||
function normalizePenpotExportUserFileConfig(
|
||||
userConfig: UserFileConfig,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
2
packages/core/src/lib/outputters/index.ts
Normal file
2
packages/core/src/lib/outputters/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './css'
|
||||
export * from './json'
|
23
packages/core/src/lib/outputters/json.ts
Normal file
23
packages/core/src/lib/outputters/json.ts
Normal 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')
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue