0
Fork 0
mirror of https://github.com/penpot/penpot-export.git synced 2025-01-06 14:50:10 -05:00

refactor(core): add better semantics with wrapper internal types

Additionally, throw runtime internal errors when TypeScript assumptions are not fulfilled.
This commit is contained in:
Roberto Redradix 2023-09-12 18:16:46 +02:00
parent 2b3381fa97
commit 1681cefb05
8 changed files with 120 additions and 96 deletions

View file

@ -0,0 +1,7 @@
export class PenpotExportInternalError extends Error {
constructor(message: string) {
super(
`${message}. This is an error in penpot-export code. Please contact their authors.`,
)
}
}

View file

@ -12,6 +12,7 @@ import {
normalizePenpotExportUserConfig, normalizePenpotExportUserConfig,
AssetConfig, AssetConfig,
} from './config' } from './config'
import { PenpotExportInternalError } from './errors'
import { import {
writeTextFile, writeTextFile,
cssOutputter, cssOutputter,
@ -19,22 +20,16 @@ import {
jsonOutputter, jsonOutputter,
OutputterFunction, OutputterFunction,
} from './outputters' } from './outputters'
import { import { PenpotExportAssets } from './types'
CSSClassDefinition,
CSSCustomPropertyDefinition,
FontsSummary,
} from './types'
const processOutput = ({ const processOutput = ({
outputFormat = 'css', outputFormat = 'css',
outputPath, outputPath,
content, assets,
metadata,
}: { }: {
outputFormat: AssetConfig['format'] outputFormat: AssetConfig['format']
outputPath: string outputPath: string
content: CSSClassDefinition[] | CSSCustomPropertyDefinition[] assets: PenpotExportAssets
metadata?: FontsSummary
}) => { }) => {
const outputter: OutputterFunction | null = const outputter: OutputterFunction | null =
outputFormat === 'css' outputFormat === 'css'
@ -46,11 +41,9 @@ const processOutput = ({
: null : null
if (outputter === null) if (outputter === null)
throw new Error( throw new PenpotExportInternalError('Unable to process output format')
'Unable to process output format. This is an error in penpot-export code, please contact their authors.',
)
const textContents = outputter(content, metadata) const textContents = outputter(assets)
return writeTextFile(outputPath, textContents) return writeTextFile(outputPath, textContents)
} }
@ -77,7 +70,9 @@ export default async function penpotExport(
processOutput({ processOutput({
outputFormat: colorsConfig.format, outputFormat: colorsConfig.format,
outputPath: path.resolve(rootProjectPath, colorsConfig.output), outputPath: path.resolve(rootProjectPath, colorsConfig.output),
content: adaptColorsToCssVariables(penpotFile), assets: {
colors: adaptColorsToCssVariables(penpotFile),
},
}) })
console.log('✅ Colors: %s', colorsConfig.output) console.log('✅ Colors: %s', colorsConfig.output)
@ -87,8 +82,10 @@ export default async function penpotExport(
processOutput({ processOutput({
outputFormat: typographiesConfig.format, outputFormat: typographiesConfig.format,
outputPath: path.resolve(rootProjectPath, typographiesConfig.output), outputPath: path.resolve(rootProjectPath, typographiesConfig.output),
content: adaptTypographiesToCssClassDefinitions(penpotFile), assets: {
metadata: summarizeTypographies(penpotFile), typographies: adaptTypographiesToCssClassDefinitions(penpotFile),
typographiesSummary: summarizeTypographies(penpotFile),
},
}) })
console.log('✅ Typographies: %s', typographiesConfig.output) console.log('✅ Typographies: %s', typographiesConfig.output)
@ -98,9 +95,11 @@ export default async function penpotExport(
processOutput({ processOutput({
outputFormat: pagesConfig.format, outputFormat: pagesConfig.format,
outputPath: path.resolve(rootProjectPath, pagesConfig.output), outputPath: path.resolve(rootProjectPath, pagesConfig.output),
content: adaptPageComponentsToCssClassDefinitions(penpotFile, { assets: {
pageId: pagesConfig.pageId, pageComponents: adaptPageComponentsToCssClassDefinitions(penpotFile, {
}), pageId: pagesConfig.pageId,
}),
},
}) })
console.log('✅ Page components: %s', pagesConfig.output) console.log('✅ Page components: %s', pagesConfig.output)

View file

@ -1,10 +1,13 @@
import { import {
CSSClassDefinition, CSSClassDefinition,
CSSCustomPropertyDefinition, CSSCustomPropertyDefinition,
ColorAssets,
FontsSummary, FontsSummary,
isCssClassDefinition, PageComponentAssets,
TypographyAssets,
} from '../../types' } from '../../types'
import { PenpotExportInvalidAssetsError } from '../errors'
import { describeFontsRequirements } from '../fileHeader' import { describeFontsRequirements } from '../fileHeader'
import { OutputterFunction } from '../types' import { OutputterFunction } from '../types'
@ -14,12 +17,6 @@ import {
textToCssCustomPropertyName, textToCssCustomPropertyName,
} from './syntax' } from './syntax'
const areCssCustomPropertiesDefinitions = (
objects: Array<object>,
): objects is Array<CSSCustomPropertyDefinition> => {
return !objects.every(isCssClassDefinition)
}
const serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => { const serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => {
const selector = textToCssClassSelector( const selector = textToCssClassSelector(
`${cssClassDefinition.scope}--${cssClassDefinition.name}`, `${cssClassDefinition.scope}--${cssClassDefinition.name}`,
@ -42,36 +39,40 @@ const serializeCssCustomProperty = (
return `${padding}${key}: ${value};` return `${padding}${key}: ${value};`
} }
const serializeCssCustomPropertiesRoot = (
cssDefinitions: CSSCustomPropertyDefinition[],
) => {
const pad = 2
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) =>
serializeCssCustomProperty(customPropertyDefinition, pad),
)
return [`:root {`, ...cssDeclarations, '}'].join('\n')
}
const composeFileHeader = (fontsSummary: FontsSummary) => { const composeFileHeader = (fontsSummary: FontsSummary) => {
const message = describeFontsRequirements(fontsSummary) const message = describeFontsRequirements(fontsSummary)
return ['/*', ...message.map((line) => ' * ' + line), '*/'].join('\n') return ['/*', ...message.map((line) => ' * ' + line), '*/'].join('\n')
} }
const composeFileBody = ( const serializeCss: OutputterFunction = ({
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[], colors,
) => { typographies,
if (areCssCustomPropertiesDefinitions(cssDefinitions)) { typographiesSummary,
const pad = 2 pageComponents,
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) => }: ColorAssets | TypographyAssets | PageComponentAssets): string => {
serializeCssCustomProperty(customPropertyDefinition, pad), if (colors) {
) return serializeCssCustomPropertiesRoot(colors)
return [`:root {`, ...cssDeclarations, '}'].join('\n') } else if (typographies) {
} else { const body = typographies.map(serializeCssClass).join('\n\n')
return cssDefinitions.map(serializeCssClass).join('\n\n') const header: string = composeFileHeader(typographiesSummary)
return header + '\n\n' + body
} else if (pageComponents) {
return pageComponents.map(serializeCssClass).join('\n\n')
} }
}
const serializeCss: OutputterFunction = ( throw new PenpotExportInvalidAssetsError()
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
fontsSummary?: FontsSummary,
): string => {
const body: string = composeFileBody(cssDefinitions)
if (fontsSummary === undefined) return body
const header: string = composeFileHeader(fontsSummary)
return header + '\n\n' + body
} }
export default serializeCss export default serializeCss

View file

@ -0,0 +1,7 @@
import { PenpotExportInternalError } from '../errors'
export class PenpotExportInvalidAssetsError extends PenpotExportInternalError {
constructor() {
super('Invalid penpot-export assets')
}
}

View file

@ -1,11 +1,18 @@
import { CSSClassDefinition, CSSCustomPropertyDefinition } from '../types' import { ColorAssets, PageComponentAssets, TypographyAssets } from '../types'
import { PenpotExportInvalidAssetsError } from './errors'
import { OutputterFunction } from './types' import { OutputterFunction } from './types'
const serializeJson: OutputterFunction = ( const serializeJson: OutputterFunction = ({
cssClassDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[], colors,
): string => { typographies,
return JSON.stringify(cssClassDefinitions, null, 2) pageComponents,
}: ColorAssets | TypographyAssets | PageComponentAssets): string => {
const assets = colors ?? typographies ?? pageComponents ?? null
if (assets === null) throw new PenpotExportInvalidAssetsError()
return JSON.stringify(assets, null, 2)
} }
export default serializeJson export default serializeJson

View file

@ -1,23 +1,20 @@
import { import {
CSSClassDefinition, CSSClassDefinition,
CSSCustomPropertyDefinition, CSSCustomPropertyDefinition,
ColorAssets,
FontsSummary, FontsSummary,
isCssClassDefinition, PageComponentAssets,
TypographyAssets,
} from '../../types' } from '../../types'
import { camelToKebab } from '../css/syntax' import { camelToKebab } from '../css/syntax'
import { PenpotExportInvalidAssetsError } from '../errors'
import { describeFontsRequirements } from '../fileHeader' import { describeFontsRequirements } from '../fileHeader'
import { OutputterFunction } from '../types' import { OutputterFunction } from '../types'
import { textToScssVariableName } from './syntax' import { textToScssVariableName } from './syntax'
const areCssCustomPropertiesDefinitions = (
objects: Array<object>,
): objects is Array<CSSCustomPropertyDefinition> => {
return !objects.every(isCssClassDefinition)
}
/** /**
* From: https://sass-lang.com/documentation/values/maps/ * From: https://sass-lang.com/documentation/values/maps/
* Most of the time, its a good idea to use quoted strings rather than unquoted strings for map keys. This is because * Most of the time, its a good idea to use quoted strings rather than unquoted strings for map keys. This is because
@ -47,29 +44,24 @@ const composeFileHeader = (fontsSummary: FontsSummary) => {
return message.map((line) => '// ' + line).join('\n') return message.map((line) => '// ' + line).join('\n')
} }
const composeFileBody = ( const serializeScss: OutputterFunction = ({
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[], colors,
) => { typographies,
if (areCssCustomPropertiesDefinitions(cssDefinitions)) { typographiesSummary,
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) => pageComponents,
serializeScssVariable(customPropertyDefinition), }: ColorAssets | TypographyAssets | PageComponentAssets): string => {
) if (colors) {
return cssDeclarations.join('\n') return colors.map(serializeScssVariable).join('\n')
} else { } else if (typographies) {
return cssDefinitions.map(serializeScssMap).join('\n\n') const body = typographies.map(serializeScssMap).join('\n\n')
const header: string = composeFileHeader(typographiesSummary)
return header + '\n\n' + body
} else if (pageComponents) {
return pageComponents.map(serializeScssMap).join('\n\n')
} }
}
const serializeScss: OutputterFunction = ( throw new PenpotExportInvalidAssetsError()
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
fontsSummary?: FontsSummary,
): string => {
const body: string = composeFileBody(cssDefinitions)
if (fontsSummary === undefined) return body
const header: string = composeFileHeader(fontsSummary)
return header + '\n\n' + body
} }
export default serializeScss export default serializeScss

View file

@ -1,10 +1,3 @@
import { import { PenpotExportAssets } from '../types'
CSSClassDefinition,
CSSCustomPropertyDefinition,
FontsSummary,
} from '../types'
export type OutputterFunction = ( export type OutputterFunction = (assets: PenpotExportAssets) => string
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
metadata?: FontsSummary,
) => string

View file

@ -4,12 +4,6 @@ export interface CSSClassDefinition {
cssProps: Record<string, string> cssProps: Record<string, string>
} }
export const isCssClassDefinition = (
object: object,
): object is CSSClassDefinition => {
return 'cssProps' in object
}
export interface CSSCustomPropertyDefinition { export interface CSSCustomPropertyDefinition {
scope: string scope: string
name: string name: string
@ -24,3 +18,27 @@ export interface FontsSummary {
googleFonts: FontsDetails googleFonts: FontsDetails
userCustomFonts: FontsDetails userCustomFonts: FontsDetails
} }
interface BaseAssets {
colors?: never
typographies?: never
typographiesSummary?: never
pageComponents?: never
}
export interface ColorAssets extends Omit<BaseAssets, 'colors'> {
colors: CSSCustomPropertyDefinition[]
}
export interface TypographyAssets
extends Omit<BaseAssets, 'typographies' | 'typographiesSummary'> {
typographies: CSSClassDefinition[]
typographiesSummary: FontsSummary
}
export interface PageComponentAssets
extends Omit<BaseAssets, 'pageComponents'> {
pageComponents: CSSClassDefinition[]
}
export type PenpotExportAssets =
| ColorAssets
| TypographyAssets
| PageComponentAssets