mirror of
https://github.com/penpot/penpot-export.git
synced 2025-03-12 07:21:20 -05:00
refactor(core): validate user config in a more expressive way
zod unions are exclusive, so the UserConfig type couldn't be kept as a union either for having at least a colors, typographies or pages output. This check has been moved to a runtime zod refine custom validation.
This commit is contained in:
parent
bf6f6a723a
commit
83ba6f93d5
6 changed files with 88 additions and 150 deletions
|
@ -15,7 +15,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
"prettier": "^3.0.2"
|
"prettier": "^3.0.2",
|
||||||
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.5.0",
|
"@types/node": "^20.5.0",
|
||||||
|
|
|
@ -8,9 +8,10 @@ function normalizePenpotExportUserFileConfig(
|
||||||
): FileConfig {
|
): FileConfig {
|
||||||
return {
|
return {
|
||||||
fileId: userConfig.fileId,
|
fileId: userConfig.fileId,
|
||||||
colors: 'colors' in userConfig ? userConfig.colors : [],
|
colors: userConfig.colors !== undefined ? userConfig.colors : [],
|
||||||
typographies: 'typographies' in userConfig ? userConfig.typographies : [],
|
typographies:
|
||||||
pages: 'pages' in userConfig ? userConfig.pages : [],
|
userConfig.typographies !== undefined ? userConfig.typographies : [],
|
||||||
|
pages: userConfig.pages !== undefined ? userConfig.pages : [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,143 +1,78 @@
|
||||||
import { UserFileConfig, UserConfig } from '../types'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { PagesConfig, ColorsConfig, TypographiesConfig } from './types'
|
import { UserConfig } from '../types'
|
||||||
|
|
||||||
class FileIdConfigError extends Error {
|
const accessTokenSchema = z.string({
|
||||||
constructor() {
|
required_error: '.accessToken is required',
|
||||||
super(`Missing or invalid .fileId in penpot export config.`)
|
invalid_type_error: '.accessToken must be a string',
|
||||||
}
|
})
|
||||||
}
|
const instanceSchema = z
|
||||||
|
.string({
|
||||||
|
required_error: '.instance is required',
|
||||||
|
invalid_type_error: '.instance must be a string',
|
||||||
|
})
|
||||||
|
.url({
|
||||||
|
message: '.instance must be a valid URL',
|
||||||
|
})
|
||||||
|
const fileIdSchema = z
|
||||||
|
.string({
|
||||||
|
required_error: '.fileId is required',
|
||||||
|
invalid_type_error: '.fileId must be a string',
|
||||||
|
})
|
||||||
|
.uuid({
|
||||||
|
message: '.fileId must be a valid UUID',
|
||||||
|
})
|
||||||
|
const pageIdSchema = z
|
||||||
|
.string({
|
||||||
|
required_error: '.pageId is required',
|
||||||
|
invalid_type_error: '.pageId must be a string',
|
||||||
|
})
|
||||||
|
.uuid({
|
||||||
|
message: '.pageId must be a valid UUID',
|
||||||
|
})
|
||||||
|
const outputSchema = z.string({
|
||||||
|
required_error: '.output is required',
|
||||||
|
invalid_type_error: '.output must be a string',
|
||||||
|
})
|
||||||
|
|
||||||
class AssetListConfigError extends Error {
|
const userColorsConfigSchema = z.object({
|
||||||
constructor(assetType: 'colors' | 'typographies' | 'pages') {
|
output: outputSchema,
|
||||||
super(`Invalid .${assetType} list in penpot export config.`)
|
})
|
||||||
}
|
const userTypographiesConfigSchema = z.object({
|
||||||
}
|
output: outputSchema,
|
||||||
|
})
|
||||||
|
const userPagesConfigSchema = z.object({
|
||||||
|
output: outputSchema,
|
||||||
|
pageId: pageIdSchema,
|
||||||
|
})
|
||||||
|
|
||||||
class OutputPathConfigError extends Error {
|
const userFileConfigSchema = z
|
||||||
constructor(index: number) {
|
.object({
|
||||||
super(`Missing or invalid .path in penpot export config at index ${index}.`)
|
fileId: fileIdSchema,
|
||||||
}
|
colors: z.optional(z.array(userColorsConfigSchema)),
|
||||||
}
|
typographies: z.optional(z.array(userTypographiesConfigSchema)),
|
||||||
|
pages: z.optional(z.array(userPagesConfigSchema)),
|
||||||
class PageIdConfigError extends Error {
|
})
|
||||||
constructor(index: number) {
|
.refine(
|
||||||
super(
|
(object) => {
|
||||||
`Missing or invalid .pageId in penpot export config at index ${index}.`,
|
if ('colors' in object) return true
|
||||||
|
if ('typographies' in object) return true
|
||||||
|
if ('pages' in object) return true
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Each file in .files is required to have at least one of .colors, .typographies or .pages properties',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUserColorsConfig(
|
const userConfigSchema = z.object({
|
||||||
colorsConfig: object,
|
accessToken: accessTokenSchema,
|
||||||
index: number,
|
instance: z.optional(instanceSchema),
|
||||||
): colorsConfig is ColorsConfig {
|
files: z
|
||||||
if (!('output' in colorsConfig) || typeof colorsConfig.output !== 'string')
|
.array(userFileConfigSchema)
|
||||||
throw new OutputPathConfigError(index)
|
.nonempty({ message: '.files is required to have at least one item' }),
|
||||||
|
})
|
||||||
|
|
||||||
return true
|
export const validateUserConfig = (userConfig: object): UserConfig =>
|
||||||
}
|
userConfigSchema.parse(userConfig)
|
||||||
|
|
||||||
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 && userConfig.instance !== undefined) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,12 +11,9 @@ export default async function penpotExport(
|
||||||
userConfig: object,
|
userConfig: object,
|
||||||
rootProjectPath: string,
|
rootProjectPath: string,
|
||||||
) {
|
) {
|
||||||
if (!validateUserConfig(userConfig))
|
const parsedUserConfig = validateUserConfig(userConfig)
|
||||||
throw new Error(
|
|
||||||
'Error validating user config. This is probably an error in penpot-export code.',
|
|
||||||
)
|
|
||||||
|
|
||||||
const config = normalizePenpotExportUserConfig(userConfig)
|
const config = normalizePenpotExportUserConfig(parsedUserConfig)
|
||||||
const penpot = new Penpot({
|
const penpot = new Penpot({
|
||||||
baseUrl: config.instance,
|
baseUrl: config.instance,
|
||||||
accessToken: config.accessToken,
|
accessToken: config.accessToken,
|
||||||
|
|
|
@ -11,11 +11,10 @@ import type {
|
||||||
|
|
||||||
export type UserFileConfig = {
|
export type UserFileConfig = {
|
||||||
fileId: string
|
fileId: string
|
||||||
} & (
|
colors?: ColorsConfig[]
|
||||||
| { colors: ColorsConfig[] }
|
typographies?: TypographiesConfig[]
|
||||||
| { typographies: TypographiesConfig[] }
|
pages?: PagesConfig[]
|
||||||
| { pages: PagesConfig[] }
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export interface UserConfig {
|
export interface UserConfig {
|
||||||
instance?: string
|
instance?: string
|
||||||
|
|
|
@ -315,3 +315,8 @@ yn@3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||||
|
|
||||||
|
zod@^3.22.2:
|
||||||
|
version "3.22.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b"
|
||||||
|
integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==
|
||||||
|
|
Loading…
Add table
Reference in a new issue