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 { PenpotApiFile } from '../../api'
|
||||||
import { CSSClassDefinition } from '../../types'
|
import { CSSCustomPropertyDefinition } from '../../types'
|
||||||
|
|
||||||
const toRgbaCssValue = (hex: string, alpha: number = 1) => {
|
const toRgbaCssValue = (hex: string, alpha: number = 1) => {
|
||||||
const channels = hex.match(/\w{2}/g)
|
const channels = hex.match(/\w{2}/g)
|
||||||
|
@ -16,17 +16,17 @@ const toRgbaCssValue = (hex: string, alpha: number = 1) => {
|
||||||
|
|
||||||
export const adaptColorsToCssVariables = (
|
export const adaptColorsToCssVariables = (
|
||||||
penpotFile: PenpotApiFile,
|
penpotFile: PenpotApiFile,
|
||||||
): CSSClassDefinition => {
|
): CSSCustomPropertyDefinition[] => {
|
||||||
const colors = Object.values(penpotFile.data.colors ?? {})
|
const colors = Object.values(penpotFile.data.colors ?? {})
|
||||||
|
|
||||||
const cssPropsEntries = colors.map((color) => {
|
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 {
|
return cssPropsEntries
|
||||||
selector: ':root',
|
|
||||||
cssProps: Object.fromEntries(cssPropsEntries),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { UserConfig, UserFileConfig, Config, FileConfig } from './types'
|
import { UserConfig, UserFileConfig, Config, FileConfig } from './types'
|
||||||
|
|
||||||
export { validateUserConfig } from './types'
|
export { validateUserConfig } from './types'
|
||||||
|
export type * from './types'
|
||||||
|
|
||||||
function normalizePenpotExportUserFileConfig(
|
function normalizePenpotExportUserFileConfig(
|
||||||
userConfig: UserFileConfig,
|
userConfig: UserFileConfig,
|
||||||
|
|
|
@ -29,19 +29,21 @@ const pageIdSchema = z
|
||||||
.uuid({
|
.uuid({
|
||||||
message: '.pageId must be a valid UUID',
|
message: '.pageId must be a valid UUID',
|
||||||
})
|
})
|
||||||
const outputSchema = z.string({
|
const outputPathSchema = z.string({
|
||||||
required_error: '.output is required',
|
required_error: '.output is required',
|
||||||
invalid_type_error: '.output must be a string',
|
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({
|
const assetConfigSchema = z.object({
|
||||||
output: outputSchema,
|
output: outputPathSchema,
|
||||||
|
format: z.optional(outputFormatSchema),
|
||||||
})
|
})
|
||||||
const typographiesConfigSchema = z.object({
|
|
||||||
output: outputSchema,
|
const colorsConfigSchema = assetConfigSchema
|
||||||
})
|
const typographiesConfigSchema = assetConfigSchema
|
||||||
const pagesConfigSchema = z.object({
|
const pagesConfigSchema = assetConfigSchema.extend({
|
||||||
output: outputSchema,
|
|
||||||
pageId: pageIdSchema,
|
pageId: pageIdSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -86,6 +88,7 @@ const userConfigSchema = z.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
export type AssetConfig = z.infer<typeof assetConfigSchema>
|
||||||
export type ColorsConfig = z.infer<typeof colorsConfigSchema>
|
export type ColorsConfig = z.infer<typeof colorsConfigSchema>
|
||||||
export type TypographiesConfig = z.infer<typeof typographiesConfigSchema>
|
export type TypographiesConfig = z.infer<typeof typographiesConfigSchema>
|
||||||
export type PagesConfig = z.infer<typeof pagesConfigSchema>
|
export type PagesConfig = z.infer<typeof pagesConfigSchema>
|
||||||
|
|
|
@ -1,12 +1,41 @@
|
||||||
import path from 'node:path'
|
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 { adaptTypographiesToCssClassDefinitions } from './adapters/inbound/typographyToCssClasses'
|
||||||
import { adaptColorsToCssVariables } from './adapters/inbound/colorsToCssVariables'
|
import { adaptColorsToCssVariables } from './adapters/inbound/colorsToCssVariables'
|
||||||
import { adaptPageComponentsToCssClassDefinitions } from './adapters/inbound/pageComponentsToCssClasses'
|
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 type * from './types'
|
||||||
|
|
||||||
export default async function penpotExport(
|
export default async function penpotExport(
|
||||||
userConfig: object,
|
userConfig: object,
|
||||||
rootProjectPath: string,
|
rootProjectPath: string,
|
||||||
|
@ -27,33 +56,33 @@ export default async function penpotExport(
|
||||||
console.log('🎨 Processing Penpot file: %s', penpotFile.name)
|
console.log('🎨 Processing Penpot file: %s', penpotFile.name)
|
||||||
|
|
||||||
for (const colorsConfig of fileConfig.colors) {
|
for (const colorsConfig of fileConfig.colors) {
|
||||||
const cssPath = path.resolve(rootProjectPath, colorsConfig.output)
|
processOutput({
|
||||||
const cssClassDefinition = adaptColorsToCssVariables(penpotFile)
|
outputFormat: colorsConfig.format,
|
||||||
|
outputPath: path.resolve(rootProjectPath, colorsConfig.output),
|
||||||
writeCssFile(cssPath, [cssClassDefinition])
|
content: adaptColorsToCssVariables(penpotFile),
|
||||||
|
})
|
||||||
|
|
||||||
console.log('✅ Colors: %s', colorsConfig.output)
|
console.log('✅ Colors: %s', colorsConfig.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const typographiesConfig of fileConfig.typographies) {
|
for (const typographiesConfig of fileConfig.typographies) {
|
||||||
const cssPath = path.resolve(rootProjectPath, typographiesConfig.output)
|
processOutput({
|
||||||
const cssClassDefinitions =
|
outputFormat: typographiesConfig.format,
|
||||||
adaptTypographiesToCssClassDefinitions(penpotFile)
|
outputPath: path.resolve(rootProjectPath, typographiesConfig.output),
|
||||||
|
content: adaptTypographiesToCssClassDefinitions(penpotFile),
|
||||||
writeCssFile(cssPath, cssClassDefinitions)
|
})
|
||||||
|
|
||||||
console.log('✅ Typographies: %s', typographiesConfig.output)
|
console.log('✅ Typographies: %s', typographiesConfig.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pagesConfig of fileConfig.pages) {
|
for (const pagesConfig of fileConfig.pages) {
|
||||||
const cssClassDefinitions = adaptPageComponentsToCssClassDefinitions(
|
processOutput({
|
||||||
penpotFile,
|
outputFormat: pagesConfig.format,
|
||||||
{ pageId: pagesConfig.pageId },
|
outputPath: path.resolve(rootProjectPath, pagesConfig.output),
|
||||||
)
|
content: adaptPageComponentsToCssClassDefinitions(penpotFile, {
|
||||||
|
pageId: pagesConfig.pageId,
|
||||||
const cssPath = path.resolve(rootProjectPath, pagesConfig.output)
|
}),
|
||||||
|
})
|
||||||
writeCssFile(cssPath, cssClassDefinitions)
|
|
||||||
|
|
||||||
console.log('✅ Page components: %s', pagesConfig.output)
|
console.log('✅ Page components: %s', pagesConfig.output)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { textToCssCustomProperyName } from '../css/helpers'
|
||||||
|
|
||||||
import { camelToKebab } from '../string'
|
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 serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => {
|
||||||
const cssValidProps = Object.keys(cssClassDefinition.cssProps).map(
|
const cssValidProps = Object.keys(cssClassDefinition.cssProps).map(
|
||||||
|
@ -11,15 +24,32 @@ const serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => {
|
||||||
return [`${cssClassDefinition.selector} {`, ...cssValidProps, '}'].join('\n')
|
return [`${cssClassDefinition.selector} {`, ...cssValidProps, '}'].join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializeCss = (cssClassDefinitions: CSSClassDefinition[]): string => {
|
const serializeCssCustomProperties = (
|
||||||
return cssClassDefinitions.map(serializeCssClass).join('\n\n')
|
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(
|
export function writeCssFile(
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
cssClassDefinitions: CSSClassDefinition[],
|
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
||||||
) {
|
) {
|
||||||
const css = serializeCss(cssClassDefinitions)
|
const css = serializeCss(cssDefinitions)
|
||||||
const dirname = path.dirname(outputPath)
|
const dirname = path.dirname(outputPath)
|
||||||
|
|
||||||
if (!fs.existsSync(dirname)) {
|
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
|
selector: string
|
||||||
cssProps: Record<string, 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