refactor: monorepo setup with lerna (#243)
This commit is contained in:
committed by
GitHub
parent
dac29fbf3c
commit
504d95c34f
4
packages/core/src/constants/collectionViews.ts
Normal file
4
packages/core/src/constants/collectionViews.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const VIEW_STYLE_LIST = 'VIEW_STYLE_LIST';
|
||||
export const VIEW_STYLE_GRID = 'VIEW_STYLE_GRID';
|
||||
|
||||
export type CollectionViewStyle = typeof VIEW_STYLE_LIST | typeof VIEW_STYLE_GRID;
|
4
packages/core/src/constants/commitProps.ts
Normal file
4
packages/core/src/constants/commitProps.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const COMMIT_AUTHOR = 'commit_author';
|
||||
export const COMMIT_DATE = 'commit_date';
|
||||
|
||||
export type CollectionProps = typeof COMMIT_AUTHOR | typeof COMMIT_DATE;
|
389
packages/core/src/constants/configSchema.tsx
Normal file
389
packages/core/src/constants/configSchema.tsx
Normal file
@ -0,0 +1,389 @@
|
||||
import AJV from 'ajv';
|
||||
import select from 'ajv-keywords/dist/keywords/select';
|
||||
import uniqueItemProperties from 'ajv-keywords/dist/keywords/uniqueItemProperties';
|
||||
import instanceOf from 'ajv-keywords/dist/keywords/instanceof';
|
||||
import prohibited from 'ajv-keywords/dist/keywords/prohibited';
|
||||
import ajvErrors from 'ajv-errors';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { formatExtensions, frontmatterFormats, extensionFormatters } from '../formats/formats';
|
||||
import { getWidgets } from '../lib/registry';
|
||||
import { I18N_STRUCTURE, I18N_FIELD } from '../lib/i18n';
|
||||
|
||||
import type { ErrorObject } from 'ajv';
|
||||
import type { Config } from '../interface';
|
||||
|
||||
const localeType = { type: 'string', minLength: 2, maxLength: 10, pattern: '^[a-zA-Z-_]+$' };
|
||||
|
||||
const i18n = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
structure: { type: 'string', enum: Object.values(I18N_STRUCTURE) },
|
||||
locales: {
|
||||
type: 'array',
|
||||
minItems: 2,
|
||||
items: localeType,
|
||||
uniqueItems: true,
|
||||
},
|
||||
defaultLocale: localeType,
|
||||
},
|
||||
};
|
||||
|
||||
const i18nRoot = {
|
||||
...i18n,
|
||||
required: ['structure', 'locales'],
|
||||
};
|
||||
|
||||
const i18nCollection = {
|
||||
oneOf: [{ type: 'boolean' }, i18n],
|
||||
};
|
||||
|
||||
const i18nField = {
|
||||
oneOf: [{ type: 'boolean' }, { type: 'string', enum: Object.values(I18N_FIELD) }],
|
||||
};
|
||||
|
||||
/**
|
||||
* Config for fields in both file and folder collections.
|
||||
*/
|
||||
function fieldsConfig() {
|
||||
const id = uuid();
|
||||
return {
|
||||
$id: `fields_${id}`,
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
// ------- Each field: -------
|
||||
$id: `field_${id}`,
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
label: { type: 'string' },
|
||||
widget: { type: 'string' },
|
||||
required: { type: 'boolean' },
|
||||
i18n: i18nField,
|
||||
hint: { type: 'string' },
|
||||
pattern: {
|
||||
type: 'array',
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
items: [{ oneOf: [{ type: 'string' }, { instanceof: 'RegExp' }] }, { type: 'string' }],
|
||||
},
|
||||
field: { $ref: `field_${id}` },
|
||||
fields: { $ref: `fields_${id}` },
|
||||
types: { $ref: `fields_${id}` },
|
||||
},
|
||||
select: { $data: '0/widget' },
|
||||
selectCases: {
|
||||
...getWidgetSchemas(),
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
uniqueItemProperties: ['name'],
|
||||
};
|
||||
}
|
||||
|
||||
const viewFilters = {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: { type: 'string' },
|
||||
field: { type: 'string' },
|
||||
pattern: {
|
||||
oneOf: [
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['label', 'field', 'pattern'],
|
||||
},
|
||||
};
|
||||
|
||||
const viewGroups = {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: { type: 'string' },
|
||||
field: { type: 'string' },
|
||||
pattern: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['label', 'field'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The schema had to be wrapped in a function to
|
||||
* fix a circular dependency problem for WebPack,
|
||||
* where the imports get resolved asynchronously.
|
||||
*/
|
||||
function getConfigSchema() {
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
backend: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', examples: ['test-repo'] },
|
||||
auth_scope: {
|
||||
type: 'string',
|
||||
examples: ['repo', 'public_repo'],
|
||||
enum: ['repo', 'public_repo'],
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
local_backend: {
|
||||
oneOf: [
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', examples: ['http://localhost:8081/api/v1'] },
|
||||
allowed_hosts: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
locale: { type: 'string', examples: ['en', 'fr', 'de'] },
|
||||
i18n: i18nRoot,
|
||||
site_url: { type: 'string', examples: ['https://example.com'] },
|
||||
display_url: { type: 'string', examples: ['https://example.com'] },
|
||||
logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] },
|
||||
media_folder: { type: 'string', examples: ['assets/uploads'] },
|
||||
public_folder: { type: 'string', examples: ['/uploads'] },
|
||||
media_folder_relative: { type: 'boolean' },
|
||||
media_library: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', examples: ['uploadcare'] },
|
||||
config: { type: 'object' },
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
slug: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
encoding: { type: 'string', enum: ['unicode', 'ascii'] },
|
||||
clean_accents: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
collections: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
// ------- Each collection: -------
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
label: { type: 'string' },
|
||||
label_singular: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
folder: { type: 'string' },
|
||||
files: {
|
||||
type: 'array',
|
||||
items: {
|
||||
// ------- Each file: -------
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
label: { type: 'string' },
|
||||
label_singular: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
file: { type: 'string' },
|
||||
editor: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
preview: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
fields: fieldsConfig(),
|
||||
},
|
||||
required: ['name', 'label', 'file', 'fields'],
|
||||
},
|
||||
uniqueItemProperties: ['name'],
|
||||
},
|
||||
identifier_field: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
slug: { type: 'string' },
|
||||
path: { type: 'string' },
|
||||
create: { type: 'boolean' },
|
||||
publish: { type: 'boolean' },
|
||||
hide: { type: 'boolean' },
|
||||
editor: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
preview: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
format: { type: 'string', enum: Object.keys(formatExtensions) },
|
||||
extension: { type: 'string' },
|
||||
frontmatter_delimiter: {
|
||||
type: ['string', 'array'],
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
fields: fieldsConfig(),
|
||||
sortable_fields: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
default: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
field: {
|
||||
type: 'string',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['field'],
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['fields'],
|
||||
},
|
||||
view_filters: viewFilters,
|
||||
view_groups: viewGroups,
|
||||
nested: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
depth: { type: 'number', minimum: 1, maximum: 1000 },
|
||||
summary: { type: 'string' },
|
||||
},
|
||||
required: ['depth'],
|
||||
},
|
||||
i18n: i18nCollection,
|
||||
},
|
||||
required: ['name', 'label'],
|
||||
oneOf: [{ required: ['files'] }, { required: ['folder', 'fields'] }],
|
||||
if: { required: ['extension'] },
|
||||
then: {
|
||||
// Cannot infer format from extension.
|
||||
if: {
|
||||
properties: {
|
||||
extension: { enum: Object.keys(extensionFormatters) },
|
||||
},
|
||||
},
|
||||
else: { required: ['format'] },
|
||||
},
|
||||
dependencies: {
|
||||
frontmatter_delimiter: {
|
||||
properties: {
|
||||
format: { enum: frontmatterFormats },
|
||||
},
|
||||
required: ['format'],
|
||||
},
|
||||
},
|
||||
},
|
||||
uniqueItemProperties: ['name'],
|
||||
},
|
||||
editor: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
preview: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['backend', 'collections'],
|
||||
anyOf: [{ required: ['media_folder'] }, { required: ['media_library'] }],
|
||||
};
|
||||
}
|
||||
|
||||
function getWidgetSchemas() {
|
||||
const schemas = getWidgets().reduce((acc, widget) => {
|
||||
acc[widget.name] = widget.schema ?? {};
|
||||
return acc;
|
||||
}, {} as Record<string, unknown>);
|
||||
return { ...schemas };
|
||||
}
|
||||
|
||||
class ConfigError extends Error {
|
||||
constructor(errors: ErrorObject<string, Record<string, unknown>, unknown>[]) {
|
||||
const message = errors
|
||||
.map(({ message, schemaPath }) => {
|
||||
const dotPath = schemaPath
|
||||
.slice(1)
|
||||
.split('/')
|
||||
.map(seg => (seg.match(/^\d+$/) ? `[${seg}]` : `.${seg}`))
|
||||
.join('')
|
||||
.slice(1);
|
||||
return `${dotPath ? `'${dotPath}'` : 'config'} ${message}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
super(message);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `validateConfig` is a pure function. It does not mutate
|
||||
* the config that is passed in.
|
||||
*/
|
||||
export default function validateConfig(config: Config) {
|
||||
const ajv = new AJV({ allErrors: true, allowUnionTypes: true, $data: true });
|
||||
uniqueItemProperties(ajv);
|
||||
select(ajv);
|
||||
instanceOf(ajv);
|
||||
prohibited(ajv);
|
||||
ajvErrors(ajv);
|
||||
|
||||
const valid = ajv.validate(getConfigSchema(), config);
|
||||
if (!valid) {
|
||||
const errors = ajv.errors?.map(e => {
|
||||
switch (e.keyword) {
|
||||
// TODO: remove after https://github.com/ajv-validator/ajv-keywords/pull/123 is merged
|
||||
case 'uniqueItemProperties': {
|
||||
const path = e.schemaPath || '';
|
||||
let newError = e;
|
||||
if (path.endsWith('/fields')) {
|
||||
newError = { ...e, message: 'fields names must be unique' };
|
||||
} else if (path.endsWith('/files')) {
|
||||
newError = { ...e, message: 'files names must be unique' };
|
||||
} else if (path.endsWith('/collections')) {
|
||||
newError = { ...e, message: 'collections names must be unique' };
|
||||
}
|
||||
return newError;
|
||||
}
|
||||
case 'instanceof': {
|
||||
const path = e.schemaPath || '';
|
||||
let newError = e;
|
||||
if (/fields\/\d+\/pattern\/\d+/.test(path)) {
|
||||
newError = {
|
||||
...e,
|
||||
message: 'should be a regular expression',
|
||||
};
|
||||
}
|
||||
return newError;
|
||||
}
|
||||
default:
|
||||
return e;
|
||||
}
|
||||
});
|
||||
console.error('Config Errors', errors);
|
||||
throw new ConfigError(errors ?? []);
|
||||
}
|
||||
}
|
89
packages/core/src/constants/fieldInference.tsx
Normal file
89
packages/core/src/constants/fieldInference.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const IDENTIFIER_FIELDS = ['title', 'path'] as const;
|
||||
|
||||
export const SORTABLE_FIELDS = ['title', 'date', 'author', 'description'] as const;
|
||||
|
||||
export interface InferredField {
|
||||
type: string;
|
||||
secondaryTypes: string[];
|
||||
synonyms: string[];
|
||||
defaultPreview: (value: string | boolean | number) => JSX.Element | ReactNode;
|
||||
fallbackToFirstField: boolean;
|
||||
showError: boolean;
|
||||
}
|
||||
|
||||
export const INFERABLE_FIELDS: Record<string, InferredField> = {
|
||||
title: {
|
||||
type: 'string',
|
||||
secondaryTypes: [],
|
||||
synonyms: ['title', 'name', 'label', 'headline', 'header'],
|
||||
defaultPreview: value => <h1>{value}</h1>, // eslint-disable-line react/display-name
|
||||
fallbackToFirstField: true,
|
||||
showError: true,
|
||||
},
|
||||
shortTitle: {
|
||||
type: 'string',
|
||||
secondaryTypes: [],
|
||||
synonyms: ['short_title', 'shortTitle', 'short'],
|
||||
defaultPreview: value => <h2>{value}</h2>, // eslint-disable-line react/display-name
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
author: {
|
||||
type: 'string',
|
||||
secondaryTypes: [],
|
||||
synonyms: ['author', 'name', 'by', 'byline', 'owner'],
|
||||
defaultPreview: value => <strong>{value}</strong>, // eslint-disable-line react/display-name
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
date: {
|
||||
type: 'datetime',
|
||||
secondaryTypes: ['date'],
|
||||
synonyms: ['date', 'publishDate', 'publish_date'],
|
||||
defaultPreview: value => value,
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
secondaryTypes: ['text', 'markdown'],
|
||||
synonyms: [
|
||||
'shortDescription',
|
||||
'short_description',
|
||||
'shortdescription',
|
||||
'description',
|
||||
'intro',
|
||||
'introduction',
|
||||
'brief',
|
||||
'content',
|
||||
'biography',
|
||||
'bio',
|
||||
'summary',
|
||||
],
|
||||
defaultPreview: value => value,
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
image: {
|
||||
type: 'image',
|
||||
secondaryTypes: [],
|
||||
synonyms: [
|
||||
'image',
|
||||
'thumbnail',
|
||||
'thumb',
|
||||
'picture',
|
||||
'avatar',
|
||||
'photo',
|
||||
'cover',
|
||||
'hero',
|
||||
'logo',
|
||||
],
|
||||
defaultPreview: value => value,
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
};
|
3
packages/core/src/constants/files.ts
Normal file
3
packages/core/src/constants/files.ts
Normal file
@ -0,0 +1,3 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export const IMAGE_EXTENSION_REGEX =
|
||||
/(\.apng|\.avif|\.gif|\.jpg|\.jpeg|\.jfif|\.pjpeg|\.pjp|\.png|\.svg|\.webp)$/g;
|
8
packages/core/src/constants/validationErrorTypes.ts
Normal file
8
packages/core/src/constants/validationErrorTypes.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const ValidationErrorTypes = {
|
||||
PRESENCE: 'PRESENCE',
|
||||
PATTERN: 'PATTERN',
|
||||
RANGE: 'RANGE',
|
||||
CUSTOM: 'CUSTOM',
|
||||
} as const;
|
||||
|
||||
export default ValidationErrorTypes;
|
Reference in New Issue
Block a user