From 6612a8716810d385e87eff2ead5112731c5a32b0 Mon Sep 17 00:00:00 2001 From: Sam Brodie Date: Tue, 6 Aug 2024 17:05:19 -0400 Subject: [PATCH] Move headlessConfigSchema from the next package to the core package and tighten checks further --- package-lock.json | 6 +- packages/core/package.json | 13 +- .../core/src/utils/headlessConfigSchema.ts | 157 ++++++++++++++++++ packages/core/src/utils/index.ts | 1 + packages/next/package.json | 3 +- .../next/src/config/headlessConfigSchema.ts | 87 ---------- .../next/src/config/withHeadstartWPConfig.ts | 7 +- 7 files changed, 173 insertions(+), 101 deletions(-) create mode 100644 packages/core/src/utils/headlessConfigSchema.ts delete mode 100644 packages/next/src/config/headlessConfigSchema.ts diff --git a/package-lock.json b/package-lock.json index 5a19e85be..9149178d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20988,7 +20988,8 @@ "react-inspector": "^6.0.1", "schema-dts": "^1.1.2", "swr": "^2.2.5", - "xss": "^1.0.15" + "xss": "^1.0.15", + "yup": "^1.4.0" }, "devDependencies": { "@testing-library/dom": "^10.3.1", @@ -21059,8 +21060,7 @@ "deepmerge": "^4.3.1", "loader-utils": "^3.2.0", "negotiator": "^0.6.3", - "schema-utils": "^4.0.0", - "yup": "^1.4.0" + "schema-utils": "^4.0.0" }, "devDependencies": { "@testing-library/dom": "^10.3.1", diff --git a/packages/core/package.json b/packages/core/package.json index b0e6127c9..3a7561ee9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,25 +65,26 @@ "@testing-library/react": "^16.0.0", "@types/jest": "^29.0.3", "@types/node-fetch": "^2.5.3", + "@types/react": "^18", + "@types/react-dom": "^18", "expect-type": "^0.15.0", + "isomorphic-fetch": "^3.0.0", "jest": "^29.3.1", "msw": "^0.35.0", "ts-jest": "^29.0.3", - "typescript": "^5.5.3", - "isomorphic-fetch": "^3.0.0", "tsc-esm-fix": "^2.20.27", - "@types/react": "^18", - "@types/react-dom": "^18" + "typescript": "^5.5.3" }, "dependencies": { "@justinribeiro/lite-youtube": "^1.3.1", + "deepmerge": "^4.3.1", "html-react-parser": "^3.0.4", "path-to-regexp": "^6.2.0", "react-inspector": "^6.0.1", + "schema-dts": "^1.1.2", "swr": "^2.2.5", "xss": "^1.0.15", - "deepmerge": "^4.3.1", - "schema-dts": "^1.1.2" + "yup": "^1.4.0" }, "peerDependencies": { "react": ">= 17.0.2" diff --git a/packages/core/src/utils/headlessConfigSchema.ts b/packages/core/src/utils/headlessConfigSchema.ts new file mode 100644 index 000000000..d2dd56fa5 --- /dev/null +++ b/packages/core/src/utils/headlessConfigSchema.ts @@ -0,0 +1,157 @@ +import { object, string, mixed, array, boolean, lazy } from 'yup'; +import { CustomPostTypes, CustomTaxonomies } from '../types'; + +// Define the schema for CustomPostType +const customPostTypeSchema = object({ + slug: string().required(), + endpoint: string().required(), + single: string(), + archive: string(), + matchSinglePath: boolean(), +}).noUnknown(); + +// Define the schema for CustomPostTypes (array of CustomPostType) +const customPostTypesSchema = array().of(customPostTypeSchema); + +// Define the schema for CustomTaxonomy +const customTaxonomySchema = object({ + slug: string().required(), + endpoint: string().required(), + rewrite: string(), + restParam: string(), + matchArchivePath: boolean(), +}).noUnknown(); + +// Define the schema for CustomTaxonomies (array of CustomTaxonomy) +const customTaxonomiesSchema = array().of(customTaxonomySchema); + +export const headlessConfigSchema = object({ + host: string(), + locale: string(), + sourceUrl: string(), + hostUrl: string(), + + customPostTypes: mixed< + CustomPostTypes | ((defaultPostTypes: CustomPostTypes) => CustomPostTypes) + >().test( + 'is-custom-post-types', + 'customPostTypes must be an array or a function that returns an array', + (value) => { + if (typeof value === 'function') { + // Test the function by calling it with a dummy array + const result = value([]); + return customPostTypesSchema.isValidSync(result, { + strict: true, + abortEarly: false, + stripUnknown: false, + }); + } + + return customPostTypesSchema.isValidSync(value) || value === undefined; + }, + ), + + customTaxonomies: mixed< + CustomTaxonomies | ((defaultPostTypes: CustomTaxonomies) => CustomTaxonomies) + >().test( + 'is-custom-taxonomies', + 'customTaxonomies must be an array or a function that returns an array', + (value) => { + if (typeof value === 'function') { + // Test the function by calling it with a dummy array + const result = value([]); + + return customTaxonomiesSchema.isValidSync(result, { + strict: true, + abortEarly: false, + stripUnknown: false, + }); + } + + return customTaxonomiesSchema.isValidSync(value) || value === undefined; + }, + ), + + redirectStrategy: string().oneOf(['404', 'none', 'always']), + + useWordPressPlugin: boolean(), + + integrations: object({ + yoastSEO: object({ + enabled: boolean(), + }), + polylang: object({ + enabled: boolean(), + }), + }), + + i18n: object({ + locales: array(string()).required(), + defaultLocale: string().required(), + localeDetection: boolean(), + }).default(undefined), + + preview: object({ + alternativeAuthorizationHeader: boolean(), + usePostLinkForRedirect: boolean(), + }), + + debug: object({ + requests: boolean(), + redirects: boolean(), + devMode: boolean(), + }), + + cache: object({ + ttl: mixed().test( + 'is-valid-ttl', + 'cache.ttl must be a number or a function that returns a number', + (value) => { + if (typeof value === 'function') { + return typeof value() === 'number'; + } + + return typeof value === 'number' || value === undefined; + }, + ), + enabled: mixed().test( + 'is-valid-cache-enabled', + 'cache.enabled must be a boolean or a function that returns a boolean', + (value) => { + if (typeof value === 'function') { + return typeof value() === 'boolean'; + } + + return typeof value === 'boolean' || value === undefined; + }, + ), + beforeSet: mixed() + .test( + 'is-function', + 'cache.beforeSet must be a function', + (value) => typeof value === 'function' || value === undefined, + ) + .optional(), + afterGet: mixed() + .test( + 'is-function', + 'cache.afterGet must be a function', + (value) => typeof value === 'function' || value === undefined, + ) + .optional(), + cacheHandler: object({ + set: mixed().test( + 'is-function', + 'cacheHandler.set must be a function', + (value) => typeof value === 'function', + ), + get: mixed().test( + 'is-function', + 'cacheHandler.get must be a function', + (value) => typeof value === 'function', + ), + }), + }), + + sites: array(lazy(() => headlessConfigSchema.default(undefined))), +}).noUnknown(); diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index afdd1df22..986826213 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -9,3 +9,4 @@ export * from './url'; export * from './log'; export * from './decodeHtmlSpeciaChars'; export * from './getObjectProperty'; +export * from './headlessConfigSchema'; diff --git a/packages/next/package.json b/packages/next/package.json index d50aabdd3..71c7b0930 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -60,8 +60,7 @@ "deepmerge": "^4.3.1", "loader-utils": "^3.2.0", "negotiator": "^0.6.3", - "schema-utils": "^4.0.0", - "yup": "^1.4.0" + "schema-utils": "^4.0.0" }, "devDependencies": { "@testing-library/dom": "^10.3.1", diff --git a/packages/next/src/config/headlessConfigSchema.ts b/packages/next/src/config/headlessConfigSchema.ts deleted file mode 100644 index 6aa48ae5c..000000000 --- a/packages/next/src/config/headlessConfigSchema.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { object, string, mixed, number, array, boolean, lazy } from 'yup'; - -const headlessConfigSchema = object({ - host: string(), - locale: string(), - sourceUrl: string(), - hostUrl: string(), - customPostTypes: lazy((value: any) => { - if (typeof value === 'function') { - return mixed(); - } - - return array().of( - object({ - slug: string().required(), - endpoint: string().required(), - single: string(), - archive: string(), - matchSinglePath: boolean(), - }), - ); - }), - customTaxonomies: lazy((value: any) => { - if (typeof value === 'function') { - return mixed(); - } - - return array().of( - object({ - slug: string().required(), - endpoint: string().required(), - rewrite: string(), - restParam: string(), - matchArchivePath: boolean(), - }), - ); - }), - redirectStrategy: string().oneOf(['404', 'none', 'always']), - useWordPressPlugin: boolean(), - integrations: object({ - yoastSEO: object({ - enabled: boolean(), - }), - polylang: object({ - enabled: boolean(), - }), - }), - i18n: object({ - locales: array(string()).required(), - defaultLocale: string().required(), - localeDetection: boolean(), - }).default(undefined), - preview: object({ - alternativeAuthorizationHeader: boolean(), - usePostLinkForRedirect: boolean(), - }), - debug: object({ - requests: boolean(), - redirects: boolean(), - devMode: boolean(), - }), - cache: object({ - ttl: lazy((value: any) => { - if (typeof value === 'function') { - return mixed(); - } - - return number(); - }), - enabled: lazy((value: any) => { - if (typeof value === 'function') { - return mixed(); - } - - return boolean(); - }), - beforeSet: mixed(), - afterGet: mixed(), - cacheHandler: object({ - set: mixed(), - get: mixed(), - }), - }), - sites: array(lazy(() => headlessConfigSchema.default(undefined))), -}).noUnknown(); - -export default headlessConfigSchema; diff --git a/packages/next/src/config/withHeadstartWPConfig.ts b/packages/next/src/config/withHeadstartWPConfig.ts index 84a47e693..233759db2 100644 --- a/packages/next/src/config/withHeadstartWPConfig.ts +++ b/packages/next/src/config/withHeadstartWPConfig.ts @@ -1,9 +1,8 @@ -import { ConfigError, HeadlessConfig } from '@headstartwp/core'; +import { ConfigError, HeadlessConfig, headlessConfigSchema } from '@headstartwp/core'; import { NextConfig } from 'next'; import fs from 'fs'; import { ValidationError } from 'yup'; import { ModifySourcePlugin, ConcatOperation } from './plugins/ModifySourcePlugin'; -import headlessConfigSchema from './headlessConfigSchema'; const LINARIA_EXTENSION = '.linaria.module.css'; @@ -117,8 +116,10 @@ export function withHeadstartWPConfig( }); } catch (error) { if (error instanceof ValidationError) { - console.error(`Error in the configuration file: ${error.errors.map(String)}`); + // eslint-disable-next-line no-console + console.error(`Error in the configuration file: ${error.errors.join(', ')}`); } else { + // eslint-disable-next-line no-console console.error('Unexpected error in the configuration file:', error); } }