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

View file

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

View file

@ -1,23 +1,20 @@
import {
CSSClassDefinition,
CSSCustomPropertyDefinition,
ColorAssets,
FontsSummary,
isCssClassDefinition,
PageComponentAssets,
TypographyAssets,
} from '../../types'
import { camelToKebab } from '../css/syntax'
import { PenpotExportInvalidAssetsError } from '../errors'
import { describeFontsRequirements } from '../fileHeader'
import { OutputterFunction } from '../types'
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/
* 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')
}
const composeFileBody = (
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
) => {
if (areCssCustomPropertiesDefinitions(cssDefinitions)) {
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) =>
serializeScssVariable(customPropertyDefinition),
)
return cssDeclarations.join('\n')
} else {
return cssDefinitions.map(serializeScssMap).join('\n\n')
const serializeScss: OutputterFunction = ({
colors,
typographies,
typographiesSummary,
pageComponents,
}: ColorAssets | TypographyAssets | PageComponentAssets): string => {
if (colors) {
return colors.map(serializeScssVariable).join('\n')
} else if (typographies) {
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 = (
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
throw new PenpotExportInvalidAssetsError()
}
export default serializeScss

View file

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

View file

@ -4,12 +4,6 @@ export interface CSSClassDefinition {
cssProps: Record<string, string>
}
export const isCssClassDefinition = (
object: object,
): object is CSSClassDefinition => {
return 'cssProps' in object
}
export interface CSSCustomPropertyDefinition {
scope: string
name: string
@ -24,3 +18,27 @@ export interface FontsSummary {
googleFonts: 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