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:
parent
2b3381fa97
commit
1681cefb05
8 changed files with 120 additions and 96 deletions
7
packages/core/src/lib/errors.ts
Normal file
7
packages/core/src/lib/errors.ts
Normal 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.`,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
7
packages/core/src/lib/outputters/errors.ts
Normal file
7
packages/core/src/lib/outputters/errors.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { PenpotExportInternalError } from '../errors'
|
||||
|
||||
export class PenpotExportInvalidAssetsError extends PenpotExportInternalError {
|
||||
constructor() {
|
||||
super('Invalid penpot-export assets')
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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, it’s 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue