mirror of
https://github.com/penpot/penpot-export.git
synced 2025-01-22 14:38:57 -05:00
chore(cli): split validating user config from normalizing app config
Also, be more exhaustive with types and errors
This commit is contained in:
parent
990260bf81
commit
d2540b3765
6 changed files with 195 additions and 163 deletions
|
@ -1,5 +1,9 @@
|
|||
require('dotenv').config()
|
||||
|
||||
if (typeof process.env.PENPOT_ACCESS_TOKEN !== 'string') {
|
||||
throw new Error('Missing PENPOT_ACCESS_TOKEN environment variable')
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('penpot-css-export').UserConfig}
|
||||
*/
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
import { Config, PagesConfig, FileConfig } from './types'
|
||||
|
||||
const createBaseFileConfig = (): FileConfig => ({
|
||||
fileId: '',
|
||||
colors: [],
|
||||
typographies: [],
|
||||
pages: [],
|
||||
})
|
||||
|
||||
const createBasePagesConfig = (): PagesConfig => ({
|
||||
output: '',
|
||||
pageId: '',
|
||||
})
|
||||
|
||||
class FileIdConfigError extends Error {
|
||||
constructor() {
|
||||
super(`Missing or invalid .fileId in penpot export config.`)
|
||||
}
|
||||
}
|
||||
|
||||
class OutputPathConfigError extends Error {
|
||||
constructor(index: number) {
|
||||
super(`Missing or invalid .path in penpot export config at index ${index}.`)
|
||||
}
|
||||
}
|
||||
|
||||
class PageIdConfigError extends Error {
|
||||
constructor(index: number) {
|
||||
super(
|
||||
`Missing or invalid .pageId in penpot export config at index ${index}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function validateAndNormalizePenpotExportFileConfig(
|
||||
userConfig: object,
|
||||
): FileConfig {
|
||||
const normalizedConfig = createBaseFileConfig()
|
||||
|
||||
if ('fileId' in userConfig && typeof userConfig.fileId === 'string') {
|
||||
normalizedConfig.fileId = userConfig.fileId
|
||||
} else throw new FileIdConfigError()
|
||||
|
||||
if ('colors' in userConfig && Array.isArray(userConfig.colors)) {
|
||||
normalizedConfig.colors = userConfig.colors.map(
|
||||
(colorsConfig: object, index) => {
|
||||
if (
|
||||
'output' in colorsConfig &&
|
||||
typeof colorsConfig.output === 'string'
|
||||
) {
|
||||
return {
|
||||
output: colorsConfig.output,
|
||||
}
|
||||
} else throw new OutputPathConfigError(index)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if ('typographies' in userConfig && Array.isArray(userConfig.typographies)) {
|
||||
normalizedConfig.typographies = userConfig.typographies.map(
|
||||
(typographiesConfig: object, index) => {
|
||||
if (
|
||||
'output' in typographiesConfig &&
|
||||
typeof typographiesConfig.output === 'string'
|
||||
) {
|
||||
return {
|
||||
output: typographiesConfig.output,
|
||||
}
|
||||
} else throw new OutputPathConfigError(index)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if ('pages' in userConfig && Array.isArray(userConfig.pages)) {
|
||||
normalizedConfig.pages = userConfig.pages.map(
|
||||
(pageConfig: object, index) => {
|
||||
const normalizedPageConfig = createBasePagesConfig()
|
||||
|
||||
if ('output' in pageConfig && typeof pageConfig.output === 'string') {
|
||||
normalizedPageConfig.output = pageConfig.output
|
||||
} else throw new OutputPathConfigError(index)
|
||||
|
||||
if ('pageId' in pageConfig && typeof pageConfig.pageId === 'string') {
|
||||
normalizedPageConfig.pageId = pageConfig.pageId
|
||||
} else throw new PageIdConfigError(index)
|
||||
|
||||
return normalizedPageConfig
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return normalizedConfig
|
||||
}
|
||||
|
||||
const createBaseConfig = (): Config => ({
|
||||
instance: 'https://design.penpot.app',
|
||||
accessToken: '',
|
||||
files: [],
|
||||
})
|
||||
|
||||
class AccessTokenConfigError extends Error {
|
||||
constructor() {
|
||||
super('Missing or invalid .accessToken in penpot export config.')
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidInstanceUrlConfigError extends Error {
|
||||
constructor() {
|
||||
super('Invalid .instance URL in penpot export config.')
|
||||
}
|
||||
}
|
||||
|
||||
class FilesConfigError extends Error {
|
||||
constructor() {
|
||||
super('Missing or invalid .files in penpot export config.')
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAndNormalizePenpotExportConfig(
|
||||
userConfig: object,
|
||||
): Config {
|
||||
let normalizedConfig: Config = createBaseConfig()
|
||||
|
||||
if (
|
||||
'accessToken' in userConfig &&
|
||||
typeof userConfig.accessToken === 'string'
|
||||
) {
|
||||
normalizedConfig.accessToken = userConfig.accessToken
|
||||
} else throw new AccessTokenConfigError()
|
||||
|
||||
if ('instance' in userConfig) {
|
||||
if (typeof userConfig.instance === 'string') {
|
||||
try {
|
||||
new URL(userConfig.instance)
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError)
|
||||
throw new InvalidInstanceUrlConfigError()
|
||||
throw error
|
||||
}
|
||||
|
||||
normalizedConfig.instance = userConfig.instance.endsWith('/')
|
||||
? userConfig.instance.slice(0, -1)
|
||||
: userConfig.instance
|
||||
} else throw new InvalidInstanceUrlConfigError()
|
||||
}
|
||||
|
||||
if (
|
||||
'files' in userConfig &&
|
||||
Array.isArray(userConfig.files) &&
|
||||
userConfig.files.length > 0
|
||||
) {
|
||||
normalizedConfig.files = userConfig.files.map(
|
||||
validateAndNormalizePenpotExportFileConfig,
|
||||
)
|
||||
} else throw new FilesConfigError()
|
||||
|
||||
return normalizedConfig
|
||||
}
|
26
packages/penpot-css-export/src/lib/config/index.ts
Normal file
26
packages/penpot-css-export/src/lib/config/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Config, UserConfig, FileConfig, UserFileConfig } from '../types'
|
||||
|
||||
export { validateUserConfig } from './validator'
|
||||
|
||||
function normalizePenpotExportUserFileConfig(
|
||||
userConfig: UserFileConfig,
|
||||
): FileConfig {
|
||||
return {
|
||||
fileId: userConfig.fileId,
|
||||
colors: 'colors' in userConfig ? userConfig.colors : [],
|
||||
typographies: 'typographies' in userConfig ? userConfig.typographies : [],
|
||||
pages: 'pages' in userConfig ? userConfig.pages : [],
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePenpotExportUserConfig(
|
||||
userConfig: UserConfig,
|
||||
): Config {
|
||||
const instance = userConfig.instance ?? 'https://design.penpot.app'
|
||||
|
||||
return {
|
||||
instance: instance.endsWith('/') ? instance.slice(0, -1) : instance,
|
||||
accessToken: userConfig.accessToken,
|
||||
files: userConfig.files.map(normalizePenpotExportUserFileConfig),
|
||||
}
|
||||
}
|
155
packages/penpot-css-export/src/lib/config/validator.ts
Normal file
155
packages/penpot-css-export/src/lib/config/validator.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { PagesConfig, ColorsConfig, TypographiesConfig } from '../types'
|
||||
|
||||
class FileIdConfigError extends Error {
|
||||
constructor() {
|
||||
super(`Missing or invalid .fileId in penpot export config.`)
|
||||
}
|
||||
}
|
||||
|
||||
class AssetListConfigError extends Error {
|
||||
constructor(assetType: 'colors' | 'typographies' | 'pages') {
|
||||
super(`Invalid .${assetType} list in penpot export config.`)
|
||||
}
|
||||
}
|
||||
|
||||
class OutputPathConfigError extends Error {
|
||||
constructor(index: number) {
|
||||
super(`Missing or invalid .path in penpot export config at index ${index}.`)
|
||||
}
|
||||
}
|
||||
|
||||
class PageIdConfigError extends Error {
|
||||
constructor(index: number) {
|
||||
super(
|
||||
`Missing or invalid .pageId in penpot export config at index ${index}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export type UserFileConfig = {
|
||||
fileId: string
|
||||
} & (
|
||||
| { colors: ColorsConfig[] }
|
||||
| { typographies: TypographiesConfig[] }
|
||||
| { pages: PagesConfig[] }
|
||||
)
|
||||
|
||||
export interface UserConfig {
|
||||
instance?: string
|
||||
accessToken: string
|
||||
files: UserFileConfig[]
|
||||
}
|
||||
|
||||
function validateUserColorsConfig(
|
||||
colorsConfig: object,
|
||||
index: number,
|
||||
): colorsConfig is ColorsConfig {
|
||||
if (!('output' in colorsConfig) || typeof colorsConfig.output !== 'string')
|
||||
throw new OutputPathConfigError(index)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function validateUserTypographiesConfig(
|
||||
typographiesConfig: object,
|
||||
index: number,
|
||||
): typographiesConfig is TypographiesConfig {
|
||||
if (
|
||||
!('output' in typographiesConfig) ||
|
||||
typeof typographiesConfig.output !== 'string'
|
||||
)
|
||||
throw new OutputPathConfigError(index)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function validateUserPagesConfig(
|
||||
pagesConfig: object,
|
||||
index: number,
|
||||
): pagesConfig is PagesConfig {
|
||||
if (!('output' in pagesConfig) || typeof pagesConfig.output !== 'string')
|
||||
throw new OutputPathConfigError(index)
|
||||
|
||||
if (!('pageId' in pagesConfig) || typeof pagesConfig.pageId !== 'string')
|
||||
throw new PageIdConfigError(index)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function validateUserFileConfig(
|
||||
userConfig: object,
|
||||
): userConfig is UserFileConfig {
|
||||
if (!('fileId' in userConfig) || typeof userConfig.fileId !== 'string')
|
||||
throw new FileIdConfigError()
|
||||
|
||||
if ('colors' in userConfig) {
|
||||
if (!Array.isArray(userConfig.colors))
|
||||
throw new AssetListConfigError('colors')
|
||||
userConfig.colors.every(validateUserColorsConfig)
|
||||
}
|
||||
|
||||
if ('typographies' in userConfig) {
|
||||
if (!Array.isArray(userConfig.typographies))
|
||||
throw new AssetListConfigError('typographies')
|
||||
userConfig.typographies.every(validateUserTypographiesConfig)
|
||||
}
|
||||
|
||||
if ('pages' in userConfig) {
|
||||
if (!Array.isArray(userConfig.pages))
|
||||
throw new AssetListConfigError('pages')
|
||||
userConfig.pages.every(validateUserPagesConfig)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
class AccessTokenConfigError extends Error {
|
||||
constructor() {
|
||||
super('Missing or invalid .accessToken in penpot export config.')
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidInstanceUrlConfigError extends Error {
|
||||
constructor() {
|
||||
super('Invalid .instance URL in penpot export config.')
|
||||
}
|
||||
}
|
||||
|
||||
class FilesConfigError extends Error {
|
||||
constructor() {
|
||||
super('Missing or invalid .files in penpot export config.')
|
||||
}
|
||||
}
|
||||
|
||||
export function validateUserConfig(
|
||||
userConfig: object,
|
||||
): userConfig is UserConfig {
|
||||
if (
|
||||
!('accessToken' in userConfig) ||
|
||||
typeof userConfig.accessToken !== 'string'
|
||||
)
|
||||
throw new AccessTokenConfigError()
|
||||
|
||||
if ('instance' in userConfig) {
|
||||
if (typeof userConfig.instance !== 'string')
|
||||
throw new InvalidInstanceUrlConfigError()
|
||||
|
||||
try {
|
||||
new URL(userConfig.instance)
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError) throw new InvalidInstanceUrlConfigError()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!('files' in userConfig) ||
|
||||
!Array.isArray(userConfig.files) ||
|
||||
userConfig.files.length === 0
|
||||
)
|
||||
throw new FilesConfigError()
|
||||
|
||||
userConfig.files.every(validateUserFileConfig)
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Penpot from '../lib/api'
|
||||
import { validateAndNormalizePenpotExportConfig } from './config'
|
||||
import { CSSClassDefinition, Config } from './types'
|
||||
import { validateUserConfig, normalizePenpotExportUserConfig } from './config'
|
||||
import { CSSClassDefinition } from './types'
|
||||
import {
|
||||
textToCssClassSelector,
|
||||
textToCssCustomProperyName,
|
||||
|
@ -13,7 +13,12 @@ export async function generateCssFromConfig(
|
|||
userConfig: object,
|
||||
rootProjectPath: string,
|
||||
) {
|
||||
const config = validateAndNormalizePenpotExportConfig(userConfig)
|
||||
if (!validateUserConfig(userConfig))
|
||||
throw new Error(
|
||||
'Error validating user config. This is probably an error in penpot-css-export code.',
|
||||
)
|
||||
|
||||
const config = normalizePenpotExportUserConfig(userConfig)
|
||||
const penpot = new Penpot({
|
||||
baseUrl: config.instance,
|
||||
accessToken: config.accessToken,
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface ColorsConfig {
|
|||
output: string
|
||||
}
|
||||
|
||||
export type FileUserConfig = {
|
||||
export type UserFileConfig = {
|
||||
fileId: string
|
||||
} & (
|
||||
| { colors: ColorsConfig[] }
|
||||
|
@ -22,7 +22,7 @@ export type FileUserConfig = {
|
|||
export interface UserConfig {
|
||||
instance?: string
|
||||
accessToken: string
|
||||
files: FileUserConfig[]
|
||||
files: UserFileConfig[]
|
||||
}
|
||||
|
||||
export interface FileConfig {
|
||||
|
|
Loading…
Add table
Reference in a new issue