diff --git a/packages/core/package.json b/packages/core/package.json index caf1446..aeaea60 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,7 +15,8 @@ }, "dependencies": { "node-fetch": "2", - "prettier": "^3.0.2" + "prettier": "^3.0.2", + "zod": "^3.22.2" }, "devDependencies": { "@types/node": "^20.5.0", diff --git a/packages/core/src/lib/config/index.ts b/packages/core/src/lib/config/index.ts index d90d35b..772f1a8 100644 --- a/packages/core/src/lib/config/index.ts +++ b/packages/core/src/lib/config/index.ts @@ -8,9 +8,10 @@ function normalizePenpotExportUserFileConfig( ): FileConfig { return { fileId: userConfig.fileId, - colors: 'colors' in userConfig ? userConfig.colors : [], - typographies: 'typographies' in userConfig ? userConfig.typographies : [], - pages: 'pages' in userConfig ? userConfig.pages : [], + colors: userConfig.colors !== undefined ? userConfig.colors : [], + typographies: + userConfig.typographies !== undefined ? userConfig.typographies : [], + pages: userConfig.pages !== undefined ? userConfig.pages : [], } } diff --git a/packages/core/src/lib/config/validator.ts b/packages/core/src/lib/config/validator.ts index c461368..d9da246 100644 --- a/packages/core/src/lib/config/validator.ts +++ b/packages/core/src/lib/config/validator.ts @@ -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 { - constructor() { - super(`Missing or invalid .fileId in penpot export config.`) - } -} +const accessTokenSchema = z.string({ + required_error: '.accessToken is required', + 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 { - constructor(assetType: 'colors' | 'typographies' | 'pages') { - super(`Invalid .${assetType} list in penpot export config.`) - } -} +const userColorsConfigSchema = z.object({ + output: outputSchema, +}) +const userTypographiesConfigSchema = z.object({ + output: outputSchema, +}) +const userPagesConfigSchema = z.object({ + output: outputSchema, + pageId: pageIdSchema, +}) -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 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' +const userFileConfigSchema = z + .object({ + fileId: fileIdSchema, + colors: z.optional(z.array(userColorsConfigSchema)), + typographies: z.optional(z.array(userTypographiesConfigSchema)), + pages: z.optional(z.array(userPagesConfigSchema)), + }) + .refine( + (object) => { + 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', + }, ) - throw new OutputPathConfigError(index) - return true -} +const userConfigSchema = z.object({ + accessToken: accessTokenSchema, + instance: z.optional(instanceSchema), + files: z + .array(userFileConfigSchema) + .nonempty({ message: '.files is required to have at least one item' }), +}) -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 -} +export const validateUserConfig = (userConfig: object): UserConfig => + userConfigSchema.parse(userConfig) diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 7d89c0a..0ae80ed 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -11,12 +11,9 @@ export default async function penpotExport( userConfig: object, rootProjectPath: string, ) { - if (!validateUserConfig(userConfig)) - throw new Error( - 'Error validating user config. This is probably an error in penpot-export code.', - ) + const parsedUserConfig = validateUserConfig(userConfig) - const config = normalizePenpotExportUserConfig(userConfig) + const config = normalizePenpotExportUserConfig(parsedUserConfig) const penpot = new Penpot({ baseUrl: config.instance, accessToken: config.accessToken, diff --git a/packages/core/src/lib/types.ts b/packages/core/src/lib/types.ts index a26ee10..03c44fb 100644 --- a/packages/core/src/lib/types.ts +++ b/packages/core/src/lib/types.ts @@ -11,11 +11,10 @@ import type { export type UserFileConfig = { fileId: string -} & ( - | { colors: ColorsConfig[] } - | { typographies: TypographiesConfig[] } - | { pages: PagesConfig[] } -) + colors?: ColorsConfig[] + typographies?: TypographiesConfig[] + pages?: PagesConfig[] +} export interface UserConfig { instance?: string diff --git a/yarn.lock b/yarn.lock index c81c45c..5882991 100644 --- a/yarn.lock +++ b/yarn.lock @@ -315,3 +315,8 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 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==