@allejo/decap-extras 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # @allejo/decap-extras
2
+
3
+ A TypeScript utility library for [Decap CMS](https://decapcms.org/) that does two things:
4
+
5
+ 1. **Widget factory functions** — type-safe helpers for building Decap CMS field configuration objects instead of writing raw object literals by hand.
6
+ 2. **Type inference utilities** — TypeScript utility types that derive content types directly from your CMS config, so you never have to maintain separate type definitions for your CMS content.
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ npm install @allejo/decap-extras
12
+ # or
13
+ pnpm add @allejo/decap-extras
14
+ # or
15
+ yarn add @allejo/decap-extras
16
+ ```
17
+
18
+ This package requires `decap-cms-core` as a peer dependency:
19
+
20
+ ```sh
21
+ npm install decap-cms-core
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Widget factories
27
+
28
+ All factory functions follow the same signature: `widget(label, name, options?)`.
29
+
30
+ ```ts
31
+ import {
32
+ BARE_MARKDOWN,
33
+ boolWidget,
34
+ imageWidget,
35
+ INLINE_MARKDOWN,
36
+ listWidget,
37
+ markdownWidget,
38
+ numberWidget,
39
+ objectWidget,
40
+ optional,
41
+ selectWidget,
42
+ stringWidget,
43
+ textWidget,
44
+ } from '@allejo/decap-extras';
45
+ ```
46
+
47
+ #### Scalar widgets
48
+
49
+ ```ts
50
+ const titleField = stringWidget('Title', 'title');
51
+ const bodyField = textWidget('Body', 'body');
52
+ const countField = numberWidget('Count', 'count');
53
+ const activeField = boolWidget('Active', 'active');
54
+ ```
55
+
56
+ #### Select widget
57
+
58
+ Pass the choices array `as const` to get a narrowed literal union type; omit `as const` to get `string`.
59
+
60
+ ```ts
61
+ const themeField = selectWidget('Theme', 'theme', ['light', 'dark'] as const);
62
+ // TypeScript type: "light" | "dark"
63
+
64
+ const looseField = selectWidget('Size', 'size', ['sm', 'md', 'lg']);
65
+ // TypeScript type: string
66
+ ```
67
+
68
+ #### Object widget
69
+
70
+ ```ts
71
+ const metaField = objectWidget('Meta', 'meta', [
72
+ stringWidget('Title', 'title'),
73
+ stringWidget('Description', 'description'),
74
+ ]);
75
+ ```
76
+
77
+ #### List widget
78
+
79
+ Pass a single field to get `Array<T>`, or an array of fields to get `Array<{ ... }>`.
80
+
81
+ ```ts
82
+ // Single-field list → Array<string>
83
+ const tagsField = listWidget('Tags', 'tags', stringWidget('Tag', 'tag'));
84
+
85
+ // Multi-field list → Array<{ label: string; value: string }>
86
+ const tagObjectsField = listWidget('Tag Objects', 'tagObjects', [
87
+ stringWidget('Label', 'label'),
88
+ stringWidget('Value', 'value'),
89
+ ]);
90
+ ```
91
+
92
+ #### Image widget
93
+
94
+ `imageWidget` is pre-configured with [Cloudinary](https://cloudinary.com/) as the media library, with default transformations (`fetch_format: auto`, `quality: auto`, `width: 300`, `crop: scale`). You can override these via the `options` parameter.
95
+
96
+ ```ts
97
+ const heroField = imageWidget('Hero Image', 'hero');
98
+ ```
99
+
100
+ #### Markdown widget
101
+
102
+ ```ts
103
+ // Full markdown editor
104
+ const contentField = markdownWidget('Content', 'content');
105
+
106
+ // Restricted to bold, italic, lists, links, and quotes — no images or code blocks
107
+ const descriptionField = markdownWidget(
108
+ 'Description',
109
+ 'description',
110
+ BARE_MARKDOWN,
111
+ );
112
+
113
+ // Inline elements only: bold, italic, and links
114
+ const captionField = markdownWidget('Caption', 'caption', INLINE_MARKDOWN);
115
+ ```
116
+
117
+ #### Optional fields
118
+
119
+ Wrap any widget with `optional()` to mark it as not required in both the Decap CMS UI and the derived TypeScript type.
120
+
121
+ ```ts
122
+ const subtitleField = optional(stringWidget('Subtitle', 'subtitle'));
123
+ // TypeScript type: string | undefined
124
+ ```
125
+
126
+ #### Common options
127
+
128
+ Every factory accepts an optional last argument for [common widget options](https://decapcms.org/docs/widgets/#common-widget-options):
129
+
130
+ ```ts
131
+ const slugField = stringWidget('Slug', 'slug', {
132
+ required: true,
133
+ hint: 'URL-friendly identifier, e.g. my-page-title',
134
+ pattern: ['^[a-z0-9-]+$', 'Lowercase letters, numbers, and hyphens only'],
135
+ });
136
+ ```
137
+
138
+ ---
139
+
140
+ ### Type inference
141
+
142
+ Define your CMS config using the widget factories and `as const`, then let `PropsByCollectionAndFile` derive a fully typed content object from it automatically.
143
+
144
+ ```ts
145
+ // cms/config.ts
146
+ import {
147
+ BARE_MARKDOWN,
148
+ boolWidget,
149
+ imageWidget,
150
+ markdownWidget,
151
+ objectWidget,
152
+ optional,
153
+ selectWidget,
154
+ stringWidget,
155
+ } from '@allejo/decap-extras';
156
+
157
+ export const config = {
158
+ collections: [
159
+ {
160
+ label: 'Pages',
161
+ name: 'pages' as const,
162
+ files: [
163
+ {
164
+ label: 'Home',
165
+ name: 'home' as const,
166
+ file: 'content/home.yml',
167
+ fields: [
168
+ stringWidget('Title', 'title'),
169
+ optional(stringWidget('Subtitle', 'subtitle')),
170
+ imageWidget('Hero Image', 'hero'),
171
+ markdownWidget('Body', 'body', BARE_MARKDOWN),
172
+ selectWidget('Theme', 'theme', ['light', 'dark'] as const),
173
+ objectWidget('SEO', 'seo', [
174
+ stringWidget('Meta Title', 'metaTitle'),
175
+ optional(stringWidget('Meta Description', 'metaDescription')),
176
+ ]),
177
+ ],
178
+ },
179
+ ],
180
+ },
181
+ ],
182
+ } as const;
183
+ ```
184
+
185
+ ```ts
186
+ // components/HomePage.ts
187
+ import type { PropsByCollectionAndFile } from '@allejo/decap-extras';
188
+
189
+ import type { config } from '@/cms/config';
190
+
191
+ type HomePageProps = PropsByCollectionAndFile<typeof config, 'pages', 'home'>;
192
+ // Resolves to:
193
+ // {
194
+ // title: string;
195
+ // subtitle?: string;
196
+ // hero: string;
197
+ // body: string;
198
+ // theme: "light" | "dark";
199
+ // seo: {
200
+ // metaTitle: string;
201
+ // metaDescription?: string;
202
+ // };
203
+ // }
204
+
205
+ function render(page: HomePageProps) {
206
+ console.log(page.title); // string
207
+ console.log(page.subtitle); // string | undefined
208
+ console.log(page.theme); // "light" | "dark"
209
+ console.log(page.seo.metaTitle); // string
210
+ }
211
+ ```
212
+
213
+ #### Typing reusable widget functions
214
+
215
+ Use `WidgetTypeFromFactory` to derive a TypeScript type from a widget factory function without duplicating the definition.
216
+
217
+ ```ts
218
+ import type { WidgetTypeFromFactory } from '@allejo/decap-extras';
219
+ import { objectWidget, optional, stringWidget } from '@allejo/decap-extras';
220
+
221
+ function seoWidget() {
222
+ return objectWidget('SEO', 'seo', [
223
+ stringWidget('Meta Title', 'metaTitle'),
224
+ optional(stringWidget('Meta Description', 'metaDescription')),
225
+ ]);
226
+ }
227
+
228
+ type SeoWidget = WidgetTypeFromFactory<typeof seoWidget>;
229
+ // { metaTitle: string; metaDescription?: string }
230
+ ```
231
+
232
+ ---
233
+
234
+ ## API reference
235
+
236
+ ### Widget functions
237
+
238
+ | Function | Decap widget | TypeScript type |
239
+ | -------------------------------------------- | -------------------------------------------------------------- | ------------------------- |
240
+ | `stringWidget(label, name, opts?)` | [`string`](https://decapcms.org/docs/widgets/#string) | `string` |
241
+ | `textWidget(label, name, opts?)` | [`text`](https://decapcms.org/docs/widgets/#text) | `string` |
242
+ | `markdownWidget(label, name, opts?)` | [`richtext`](https://decapcms.org/docs/widgets/#richtext-beta) | `string` |
243
+ | `imageWidget(label, name, opts?)` | [`image`](https://decapcms.org/docs/widgets/#image) | `string` |
244
+ | `numberWidget(label, name, opts?)` | [`number`](https://decapcms.org/docs/widgets/#number) | `number` |
245
+ | `boolWidget(label, name, opts?)` | [`boolean`](https://decapcms.org/docs/widgets/#boolean) | `boolean` |
246
+ | `selectWidget(label, name, choices, opts?)` | [`select`](https://decapcms.org/docs/widgets/#select) | literal union or `string` |
247
+ | `listWidget(label, name, field, opts?)` | [`list`](https://decapcms.org/docs/widgets/#list) | `Array<T>` |
248
+ | `listWidget(label, name, fields[], opts?)` | [`list`](https://decapcms.org/docs/widgets/#list) | `Array<{ ... }>` |
249
+ | `objectWidget(label, name, fields[], opts?)` | [`object`](https://decapcms.org/docs/widgets/#object) | `{ ... }` |
250
+ | `optional(widget)` | — | marks field optional |
251
+
252
+ ### Markdown presets
253
+
254
+ | Constant | Description |
255
+ | ----------------- | ------------------------------------------------------------- |
256
+ | `BARE_MARKDOWN` | Bold, italic, lists, links, quotes. No images or code blocks. |
257
+ | `INLINE_MARKDOWN` | Bold, italic, and links only. No block-level elements. |
258
+
259
+ ### Type utilities
260
+
261
+ | Type | Description |
262
+ | ----------------------------------------- | ------------------------------------------------------------------------------------ |
263
+ | `PropsByCollectionAndFile<TConfig, C, F>` | Derives a typed content object from a CMS config for collection `C` and file `F`. |
264
+ | `WidgetTypeFromFactory<F>` | Returns the TypeScript content type produced by a widget factory function `F`. |
265
+ | `OptionalWidget<T>` | Brands a `CmsField` as optional (added by `optional()`). |
266
+ | `WidgetOpts<T>` | The options type for a given widget field type, with `widget` and `fields` stripped. |
267
+
268
+ ## License
269
+
270
+ MIT
@@ -0,0 +1,345 @@
1
+ import { CmsField, CmsFieldBoolean, CmsFieldFileOrImage, CmsFieldList, CmsFieldMarkdown, CmsFieldNumber, CmsFieldObject, CmsFieldSelect, CmsFieldStringOrText } from "decap-cms-core";
2
+
3
+ //#region src/api.d.ts
4
+ /**
5
+ * Get a widget's options while excluding keys we're handling (i.e., `widget`,
6
+ * `fields`) and respect [global common options](https://decapcms.org/docs/widgets/#String#common-widget-options).
7
+ */
8
+ type WidgetOpts<T extends {
9
+ widget?: string;
10
+ fields?: unknown[];
11
+ }> = Omit<T, 'widget' | 'fields'> & {
12
+ required?: boolean;
13
+ hint?: string;
14
+ pattern?: [string, string];
15
+ };
16
+ type OptionalWidget<T extends CmsField> = T & {
17
+ required: false;
18
+ __optional: true;
19
+ };
20
+ /**
21
+ * Recursively expands a type alias into its fully resolved shape.
22
+ * TypeScript does not eagerly expand type aliases in IDE hover tooltips —
23
+ * intermediate type aliases like `FieldsToObject<...>` are shown as-is rather
24
+ * than their resolved structure. Wrapping a type with this helper forces the
25
+ * IDE to display the fully resolved object shape at every level of nesting.
26
+ * This has no effect on type safety or runtime behavior; it is purely cosmetic.
27
+ *
28
+ * @example
29
+ * // Without this helper, the IDE may show:
30
+ * // type SectionWidget = FieldsToObject<[...]>
31
+ * //
32
+ * // With this helper, the IDE shows:
33
+ * // type SectionWidget = {
34
+ * // images: Array<{ polaroid: { image: string; altText?: string; caption: string } }>;
35
+ * // text: string;
36
+ * // imagePosition: "left" | "right";
37
+ * // }
38
+ */
39
+ type IDE_HACK_ExpandRecursiveType<T> = T extends infer O ? { [K in keyof O]: O[K] extends object ? IDE_HACK_ExpandRecursiveType<O[K]> : O[K] } : never;
40
+ /**
41
+ * Recursively finds the first item in a readonly tuple/array whose `Key`
42
+ * property matches `Value`.
43
+ *
44
+ * Resolves to `never` when no matching item exists.
45
+ *
46
+ * @example
47
+ * type Found = Lookup<
48
+ * [{ name: "home"; file: "home.yml" }, { name: "about"; file: "about.yml" }],
49
+ * "name",
50
+ * "about"
51
+ * >;
52
+ * // { name: "about"; file: "about.yml" }
53
+ *
54
+ * @example
55
+ * type Missing = Lookup<[{ name: "home"; file: "home.yml" }], "name", "contact">;
56
+ * // never
57
+ */
58
+ type Lookup<Array extends readonly unknown[], Key extends string, Value extends string> = Array extends readonly [infer First, ...infer Rest] ? First extends Record<Key, Value> ? First : Lookup<Rest, Key, Value> : never;
59
+ /**
60
+ * Get all the collection definitions as a data type from a given config.
61
+ */
62
+ type CollectionTypes<TConfig extends {
63
+ collections: readonly unknown[];
64
+ }> = TConfig['collections'][number];
65
+ /**
66
+ * Get all the collection names from `CollectionTypes` as a union of string
67
+ * literal types to later use with `Lookup<...>`.
68
+ */
69
+ type CollectionNames<TConfig extends {
70
+ collections: readonly unknown[];
71
+ }> = CollectionTypes<TConfig> extends {
72
+ name: infer N;
73
+ } ? N & string : never;
74
+ /**
75
+ * Get the specific collection structure for a given collection name. For
76
+ * example, `CollectionByName<Config, "pages">` would resolve to the collection
77
+ * definition object for the "Pages" collection in the config,
78
+ * which would look something like,
79
+ *
80
+ * ```
81
+ * {
82
+ * label: "Pages",
83
+ * name: "pages",
84
+ * files: ({
85
+ * label: "Home",
86
+ * name: "home",
87
+ * ...
88
+ * })[]
89
+ * }
90
+ * ```
91
+ */
92
+ type CollectionByName<TConfig extends {
93
+ collections: readonly unknown[];
94
+ }, N extends CollectionNames<TConfig>> = Lookup<TConfig['collections'], 'name', N>;
95
+ /**
96
+ * Get the specific fields defined for a given page. For example,
97
+ * `FileByName<"site/pages/home.yml">` will return the fields defined for the
98
+ * "home.yml" file in the config, which would look something like,
99
+ *
100
+ * ```
101
+ * {
102
+ * label: "Welcome Section",
103
+ * name: "welcomeSection",
104
+ * widget: "object",
105
+ * fields: (...)[]
106
+ * }
107
+ * ```
108
+ */
109
+ type FileByName<C extends {
110
+ files: readonly unknown[];
111
+ }, F extends (C['files'][number] extends {
112
+ name: infer N;
113
+ } ? N & string : never)> = Lookup<C['files'], 'name', F>;
114
+ /**
115
+ * The bare minimum that every file definition in the config must have to be
116
+ * able to look up its fields and translate them to TypeScript types.
117
+ */
118
+ type CmsCollectionFile = {
119
+ name: string;
120
+ file: string;
121
+ fields: readonly CmsField[];
122
+ };
123
+ /**
124
+ * Converts an array of `CmsField` definitions into a TypeScript object type,
125
+ * mapping each field's `name` to its corresponding TypeScript type via
126
+ * {@link TypeScriptTypeForField}.
127
+ *
128
+ * Fields marked as optional (via {@link OptionalWidget}) are emitted as
129
+ * optional keys (`key?`); all other fields are required.
130
+ *
131
+ * @example
132
+ * type Result = FieldsToObject<[
133
+ * { name: "title"; widget: "string" },
134
+ * { name: "ending"; widget: "string"; required: false; __optional: true },
135
+ * ]>;
136
+ * // { ending?: string } & { title: string }
137
+ */
138
+ type FieldsToObject<Fields extends readonly CmsField[]> = { [F in Fields[number] as F extends OptionalWidget<F> ? F['name'] : never]?: TypeScriptTypeForField<F> } & { [F in Fields[number] as F extends OptionalWidget<F> ? never : F['name']]: TypeScriptTypeForField<F> };
139
+ /**
140
+ * Translates the top-level `fields` of a CMS collection file definition into a
141
+ * TypeScript object type using {@link FieldsToObject}.
142
+ *
143
+ * This is the entry point for type generation from a file definition — it
144
+ * extracts `F["fields"]` and delegates to `FieldsToObject` so that the same
145
+ * optional/required splitting logic applies uniformly.
146
+ */
147
+ type FieldsOfFile<F extends CmsCollectionFile> = FieldsToObject<F['fields']>;
148
+ /**
149
+ * Translate from `widget` types to their corresponding TypeScript types. For
150
+ * example, a field with `widget: "string"` will be translated to a TypeScript
151
+ * type of `string`, and a field with `widget: "object"` and nested `fields`
152
+ * will be recursively translated to a nested object type.
153
+ *
154
+ * For `widget: "select"`, if the field's `options` is a readonly tuple of
155
+ * string literals, the type is narrowed to the union of those literals instead
156
+ * of a plain `string`.
157
+ *
158
+ * Note: This is not an exhaustive mapping of all possible widget types, but it
159
+ * can be extended as needed.
160
+ */
161
+ type TypeScriptTypeForField<F extends CmsField> = F['widget'] extends 'string' | 'richtext' | 'text' | 'image' ? string : F['widget'] extends 'number' ? number : F['widget'] extends 'boolean' ? boolean : F['widget'] extends 'select' ? F extends {
162
+ options: readonly (infer O)[];
163
+ } ? O : string : F['widget'] extends 'list' ? F extends {
164
+ fields: readonly CmsField[];
165
+ } ? Array<FieldsToObject<F['fields']>> : F extends {
166
+ field: CmsField;
167
+ } ? Array<TypeScriptTypeForField<F['field']>> : unknown[] : F['widget'] extends 'object' ? F extends {
168
+ fields: readonly CmsField[];
169
+ } ? FieldsToObject<F['fields']> : Record<string, unknown> : unknown;
170
+ /**
171
+ * Get all the file names for a given collection name as a union of string
172
+ * literal types to later use with `Lookup<...>`. For example,
173
+ * `CollectionItemNames<Config, "pages">` would resolve to the union of all
174
+ * file names defined in the "Pages" collection, such as `"home" | "about" | ...`.
175
+ */
176
+ type CollectionItemNames<TConfig extends {
177
+ collections: readonly unknown[];
178
+ }, C extends CollectionNames<TConfig>> = CollectionByName<TConfig, C> extends {
179
+ files: readonly unknown[];
180
+ } ? CollectionByName<TConfig, C>['files'][number] extends {
181
+ name: infer N;
182
+ } ? N & string : never : never;
183
+ /**
184
+ * Based on a collection's name and the respective file name, return a TypeScript
185
+ * object presenting the translated DecapCMS widget types into native TypeScript
186
+ * types.
187
+ *
188
+ * @example
189
+ * import { config } from '@/cms/config';
190
+ * type MyProps = PropsByCollectionAndFile<typeof config, 'blog', 'post'>;
191
+ */
192
+ type PropsByCollectionAndFile<TConfig extends {
193
+ collections: readonly unknown[];
194
+ }, C extends CollectionNames<TConfig>, F extends CollectionItemNames<TConfig, C>> = IDE_HACK_ExpandRecursiveType<CollectionByName<TConfig, C> extends {
195
+ files: infer Files extends readonly unknown[];
196
+ } ? FieldsOfFile<FileByName<{
197
+ files: Files;
198
+ }, F & string>> : never>;
199
+ type WidgetFactory = (...args: any[]) => CmsField;
200
+ /**
201
+ * Given a widget factory function, return the TypeScript type that corresponds
202
+ * to the `widget` type(s) that the factory produces. For example, if a widget
203
+ * factory produces a field with `widget: "object"` and nested `fields`, it will
204
+ * be translated to a nested object type.
205
+ *
206
+ * Note: This relies on the widget factories being defined in a way that their
207
+ * return type is fully inferable by TypeScript.
208
+ */
209
+ type WidgetTypeFromFactory<F extends WidgetFactory> = IDE_HACK_ExpandRecursiveType<TypeScriptTypeForField<ReturnType<F>>>;
210
+ //#endregion
211
+ //#region src/widgets.d.ts
212
+ declare function optional<T extends CmsField>(widget: T): OptionalWidget<T>;
213
+ declare function boolWidget<T extends string>(label: string, name: T, options?: WidgetOpts<CmsFieldBoolean>): {
214
+ readonly default?: boolean;
215
+ readonly required?: boolean;
216
+ readonly hint?: string;
217
+ readonly pattern?: [string, string];
218
+ readonly label: string;
219
+ readonly name: T;
220
+ readonly widget: "boolean";
221
+ };
222
+ declare function imageWidget<T extends string>(label: string, name: T, options?: WidgetOpts<CmsFieldFileOrImage>): {
223
+ readonly media_library: {
224
+ readonly name: "cloudinary";
225
+ readonly config: {
226
+ readonly default_transformations: readonly [readonly [{
227
+ readonly fetch_format: "auto";
228
+ readonly width: 300;
229
+ readonly quality: "auto";
230
+ readonly crop: "scale";
231
+ }]];
232
+ };
233
+ };
234
+ readonly default?: string;
235
+ readonly allow_multiple?: boolean;
236
+ readonly choose_url?: boolean;
237
+ readonly config?: any;
238
+ readonly required?: boolean;
239
+ readonly hint?: string;
240
+ readonly pattern?: [string, string];
241
+ readonly label: string;
242
+ readonly name: T;
243
+ readonly widget: "image";
244
+ };
245
+ declare function listWidget<T extends string, F extends CmsField>(label: string, name: T, fields: F, options?: WidgetOpts<CmsFieldList>): {
246
+ label: string;
247
+ name: T;
248
+ widget: 'list';
249
+ field: F;
250
+ };
251
+ declare function listWidget<T extends string, F extends CmsField[]>(label: string, name: T, fields: F, options?: WidgetOpts<CmsFieldList>): {
252
+ label: string;
253
+ name: T;
254
+ widget: 'list';
255
+ fields: F;
256
+ };
257
+ declare function markdownWidget<T extends string>(label: string, name: T, options?: WidgetOpts<CmsFieldMarkdown>): {
258
+ readonly default?: string;
259
+ readonly minimal?: boolean;
260
+ readonly buttons?: import("decap-cms-core").CmsMarkdownWidgetButton[];
261
+ readonly editor_components?: string[];
262
+ readonly modes?: ("raw" | "rich_text")[];
263
+ readonly editorComponents?: string[];
264
+ readonly required?: boolean;
265
+ readonly hint?: string;
266
+ readonly pattern?: [string, string];
267
+ readonly label: string;
268
+ readonly name: T;
269
+ readonly widget: "string";
270
+ };
271
+ declare function objectWidget<T extends string, F extends CmsField[]>(label: string, name: T, fields: F, options?: WidgetOpts<CmsFieldObject>): {
272
+ readonly fields: F;
273
+ readonly default?: any;
274
+ readonly collapsed?: boolean;
275
+ readonly summary?: string;
276
+ readonly required?: boolean;
277
+ readonly hint?: string;
278
+ readonly pattern?: [string, string];
279
+ readonly label: string;
280
+ readonly name: T;
281
+ readonly widget: "object";
282
+ };
283
+ declare function numberWidget<T extends string>(label: string, name: T, options?: WidgetOpts<CmsFieldNumber>): {
284
+ readonly default?: string | number;
285
+ readonly value_type?: "int" | "float" | string;
286
+ readonly min?: number;
287
+ readonly max?: number;
288
+ readonly step?: number;
289
+ readonly valueType?: "int" | "float" | string;
290
+ readonly required?: boolean;
291
+ readonly hint?: string;
292
+ readonly pattern?: [string, string];
293
+ readonly label: string;
294
+ readonly name: T;
295
+ readonly widget: "number";
296
+ };
297
+ declare function selectWidget<T extends string, O extends string[]>(label: string, name: T, choices: O, options?: WidgetOpts<CmsFieldSelect>): {
298
+ readonly options: O;
299
+ readonly default?: string | string[];
300
+ readonly min?: number;
301
+ readonly max?: number;
302
+ readonly multiple?: boolean;
303
+ readonly required?: boolean;
304
+ readonly hint?: string;
305
+ readonly pattern?: [string, string];
306
+ readonly label: string;
307
+ readonly name: T;
308
+ readonly widget: "select";
309
+ };
310
+ declare function stringWidget<T extends string>(label: string, name: T, options?: WidgetOpts<CmsFieldStringOrText>): {
311
+ readonly default?: string;
312
+ readonly visualEditing?: boolean;
313
+ readonly required?: boolean;
314
+ readonly hint?: string;
315
+ readonly pattern?: [string, string];
316
+ readonly label: string;
317
+ readonly name: T;
318
+ readonly widget: "string";
319
+ };
320
+ declare function textWidget<T extends string>(label: string, name: T, options?: WidgetOpts<CmsFieldStringOrText>): {
321
+ readonly default?: string;
322
+ readonly visualEditing?: boolean;
323
+ readonly required?: boolean;
324
+ readonly hint?: string;
325
+ readonly pattern?: [string, string];
326
+ readonly label: string;
327
+ readonly name: T;
328
+ readonly widget: "text";
329
+ };
330
+ /**
331
+ * Stripped down Markdown widget configuration that does the following:
332
+ *
333
+ * - Allows bold, italic, lists (bullet + numbered), quotes
334
+ * - No images or code block components
335
+ * - Rich Text preview only
336
+ */
337
+ declare const BARE_MARKDOWN: WidgetOpts<CmsFieldMarkdown>;
338
+ /**
339
+ * Stripped down Markdown Widget configuration that allows only inline
340
+ * elements (e.g., no lists, quotes, or headings) and rich text preview only.
341
+ */
342
+ declare const INLINE_MARKDOWN: WidgetOpts<CmsFieldMarkdown>;
343
+ //#endregion
344
+ export { BARE_MARKDOWN, INLINE_MARKDOWN, type OptionalWidget, type PropsByCollectionAndFile, type WidgetOpts, type WidgetTypeFromFactory, boolWidget, imageWidget, listWidget, markdownWidget, numberWidget, objectWidget, optional, selectWidget, stringWidget, textWidget };
345
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/api.ts","../src/widgets.ts"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,128 @@
1
+ //#region src/widgets.ts
2
+ function optional(widget) {
3
+ return {
4
+ ...widget,
5
+ required: false,
6
+ __optional: true
7
+ };
8
+ }
9
+ function boolWidget(label, name, options) {
10
+ return {
11
+ label,
12
+ name,
13
+ widget: "boolean",
14
+ ...options ?? {}
15
+ };
16
+ }
17
+ function imageWidget(label, name, options) {
18
+ return {
19
+ label,
20
+ name,
21
+ widget: "image",
22
+ ...options ?? {},
23
+ media_library: {
24
+ name: "cloudinary",
25
+ config: { default_transformations: [[{
26
+ fetch_format: "auto",
27
+ width: 300,
28
+ quality: "auto",
29
+ crop: "scale"
30
+ }]] }
31
+ }
32
+ };
33
+ }
34
+ function listWidget(label, name, fields, options) {
35
+ return {
36
+ label,
37
+ name,
38
+ widget: "list",
39
+ ...options ?? {},
40
+ ...Array.isArray(fields) ? { fields } : { field: fields }
41
+ };
42
+ }
43
+ function markdownWidget(label, name, options) {
44
+ return {
45
+ label,
46
+ name,
47
+ widget: "richtext",
48
+ ...options ?? {}
49
+ };
50
+ }
51
+ function objectWidget(label, name, fields, options) {
52
+ return {
53
+ label,
54
+ name,
55
+ widget: "object",
56
+ ...options ?? {},
57
+ fields
58
+ };
59
+ }
60
+ function numberWidget(label, name, options) {
61
+ return {
62
+ label,
63
+ name,
64
+ widget: "number",
65
+ ...options ?? {}
66
+ };
67
+ }
68
+ function selectWidget(label, name, choices, options) {
69
+ return {
70
+ label,
71
+ name,
72
+ widget: "select",
73
+ ...options ?? {},
74
+ options: choices ?? []
75
+ };
76
+ }
77
+ function stringWidget(label, name, options) {
78
+ return {
79
+ label,
80
+ name,
81
+ widget: "string",
82
+ ...options ?? {}
83
+ };
84
+ }
85
+ function textWidget(label, name, options) {
86
+ return {
87
+ label,
88
+ name,
89
+ widget: "text",
90
+ ...options ?? {}
91
+ };
92
+ }
93
+ /**
94
+ * Stripped down Markdown widget configuration that does the following:
95
+ *
96
+ * - Allows bold, italic, lists (bullet + numbered), quotes
97
+ * - No images or code block components
98
+ * - Rich Text preview only
99
+ */
100
+ const BARE_MARKDOWN = {
101
+ buttons: [
102
+ "bold",
103
+ "bulleted-list",
104
+ "italic",
105
+ "link",
106
+ "numbered-list",
107
+ "quote"
108
+ ],
109
+ editor_components: [],
110
+ modes: ["rich_text"]
111
+ };
112
+ /**
113
+ * Stripped down Markdown Widget configuration that allows only inline
114
+ * elements (e.g., no lists, quotes, or headings) and rich text preview only.
115
+ */
116
+ const INLINE_MARKDOWN = {
117
+ buttons: [
118
+ "bold",
119
+ "italic",
120
+ "link"
121
+ ],
122
+ editor_components: [],
123
+ modes: ["rich_text"]
124
+ };
125
+
126
+ //#endregion
127
+ export { BARE_MARKDOWN, INLINE_MARKDOWN, boolWidget, imageWidget, listWidget, markdownWidget, numberWidget, objectWidget, optional, selectWidget, stringWidget, textWidget };
128
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/widgets.ts"],"sourcesContent":["import type {\n\tCmsField,\n\tCmsFieldBoolean,\n\tCmsFieldFileOrImage,\n\tCmsFieldList,\n\tCmsFieldMarkdown,\n\tCmsFieldNumber,\n\tCmsFieldObject,\n\tCmsFieldSelect,\n\tCmsFieldStringOrText,\n} from 'decap-cms-core';\n\nimport type { OptionalWidget, WidgetOpts } from './api.js';\n\n// Widget Modifiers\n\nexport function optional<T extends CmsField>(widget: T): OptionalWidget<T> {\n\treturn {\n\t\t...widget,\n\t\trequired: false as const,\n\t\t__optional: true as const,\n\t};\n}\n\n// Primitive Widgets\n\nexport function boolWidget<T extends string>(\n\tlabel: string,\n\tname: T,\n\toptions?: WidgetOpts<CmsFieldBoolean>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'boolean',\n\t\t...(options ?? {}),\n\t} as const satisfies CmsField;\n}\n\nexport function imageWidget<T extends string>(\n\tlabel: string,\n\tname: T,\n\toptions?: WidgetOpts<CmsFieldFileOrImage>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'image',\n\t\t...(options ?? {}),\n\t\tmedia_library: {\n\t\t\tname: 'cloudinary',\n\t\t\tconfig: {\n\t\t\t\tdefault_transformations: [\n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfetch_format: 'auto',\n\t\t\t\t\t\t\twidth: 300,\n\t\t\t\t\t\t\tquality: 'auto',\n\t\t\t\t\t\t\tcrop: 'scale',\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t],\n\t\t\t},\n\t\t},\n\t} as const satisfies CmsField;\n}\n\nexport function listWidget<T extends string, F extends CmsField>(\n\tlabel: string,\n\tname: T,\n\tfields: F,\n\toptions?: WidgetOpts<CmsFieldList>,\n): { label: string; name: T; widget: 'list'; field: F };\nexport function listWidget<T extends string, F extends CmsField[]>(\n\tlabel: string,\n\tname: T,\n\tfields: F,\n\toptions?: WidgetOpts<CmsFieldList>,\n): { label: string; name: T; widget: 'list'; fields: F };\nexport function listWidget<T extends string, F extends CmsField | CmsField[]>(\n\tlabel: string,\n\tname: T,\n\tfields: F,\n\toptions?: WidgetOpts<CmsFieldList>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'list',\n\t\t...(options ?? {}),\n\t\t...(Array.isArray(fields) ? { fields } : { field: fields }),\n\t} as const;\n}\n\nexport function markdownWidget<T extends string>(\n\tlabel: string,\n\tname: T,\n\toptions?: WidgetOpts<CmsFieldMarkdown>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'richtext' as 'string',\n\t\t...(options ?? {}),\n\t} as const satisfies CmsField;\n}\n\nexport function objectWidget<T extends string, F extends CmsField[]>(\n\tlabel: string,\n\tname: T,\n\tfields: F,\n\toptions?: WidgetOpts<CmsFieldObject>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'object',\n\t\t...(options ?? {}),\n\t\tfields,\n\t} as const satisfies CmsField;\n}\n\nexport function numberWidget<T extends string>(\n\tlabel: string,\n\tname: T,\n\toptions?: WidgetOpts<CmsFieldNumber>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'number',\n\t\t...(options ?? {}),\n\t} as const satisfies CmsField;\n}\n\nexport function selectWidget<T extends string, O extends string[]>(\n\tlabel: string,\n\tname: T,\n\tchoices: O,\n\toptions?: WidgetOpts<CmsFieldSelect>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'select',\n\t\t...(options ?? {}),\n\t\toptions: choices ?? [],\n\t} as const satisfies CmsField;\n}\n\nexport function stringWidget<T extends string>(\n\tlabel: string,\n\tname: T,\n\toptions?: WidgetOpts<CmsFieldStringOrText>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'string',\n\t\t...(options ?? {}),\n\t} as const satisfies CmsField;\n}\n\nexport function textWidget<T extends string>(\n\tlabel: string,\n\tname: T,\n\toptions?: WidgetOpts<CmsFieldStringOrText>,\n) {\n\treturn {\n\t\tlabel,\n\t\tname,\n\t\twidget: 'text',\n\t\t...(options ?? {}),\n\t} as const satisfies CmsField;\n}\n\n// Reusable Widget Configurations\n\n/**\n * Stripped down Markdown widget configuration that does the following:\n *\n * - Allows bold, italic, lists (bullet + numbered), quotes\n * - No images or code block components\n * - Rich Text preview only\n */\nexport const BARE_MARKDOWN: WidgetOpts<CmsFieldMarkdown> = {\n\tbuttons: [\n\t\t'bold',\n\t\t'bulleted-list',\n\t\t'italic',\n\t\t'link',\n\t\t'numbered-list',\n\t\t'quote',\n\t],\n\teditor_components: [],\n\tmodes: ['rich_text'],\n};\n\n/**\n * Stripped down Markdown Widget configuration that allows only inline\n * elements (e.g., no lists, quotes, or headings) and rich text preview only.\n */\nexport const INLINE_MARKDOWN: WidgetOpts<CmsFieldMarkdown> = {\n\tbuttons: ['bold', 'italic', 'link'],\n\teditor_components: [],\n\tmodes: ['rich_text'],\n};\n"],"mappings":";AAgBA,SAAgB,SAA6B,QAA8B;CAC1E,OAAO;EACN,GAAG;EACH,UAAU;EACV,YAAY;CACb;AACD;AAIA,SAAgB,WACf,OACA,MACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;CACjB;AACD;AAEA,SAAgB,YACf,OACA,MACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;EAChB,eAAe;GACd,MAAM;GACN,QAAQ,EACP,yBAAyB,CACxB,CACC;IACC,cAAc;IACd,OAAO;IACP,SAAS;IACT,MAAM;GACP,CACD,CACD,EACD;EACD;CACD;AACD;AAcA,SAAgB,WACf,OACA,MACA,QACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;EAChB,GAAI,MAAM,QAAQ,MAAM,IAAI,EAAE,OAAO,IAAI,EAAE,OAAO,OAAO;CAC1D;AACD;AAEA,SAAgB,eACf,OACA,MACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;CACjB;AACD;AAEA,SAAgB,aACf,OACA,MACA,QACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;EAChB;CACD;AACD;AAEA,SAAgB,aACf,OACA,MACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;CACjB;AACD;AAEA,SAAgB,aACf,OACA,MACA,SACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;EAChB,SAAS,WAAW,CAAC;CACtB;AACD;AAEA,SAAgB,aACf,OACA,MACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;CACjB;AACD;AAEA,SAAgB,WACf,OACA,MACA,SACC;CACD,OAAO;EACN;EACA;EACA,QAAQ;EACR,GAAI,WAAW,CAAC;CACjB;AACD;;;;;;;;AAWA,MAAa,gBAA8C;CAC1D,SAAS;EACR;EACA;EACA;EACA;EACA;EACA;CACD;CACA,mBAAmB,CAAC;CACpB,OAAO,CAAC,WAAW;AACpB;;;;;AAMA,MAAa,kBAAgD;CAC5D,SAAS;EAAC;EAAQ;EAAU;CAAM;CAClC,mBAAmB,CAAC;CACpB,OAAO,CAAC,WAAW;AACpB"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@allejo/decap-extras",
3
+ "version": "0.0.0",
4
+ "description": "A TypeScript utility library for Decap CMS with utility types to derive types from Decap CMS configuration files.",
5
+ "keywords": [
6
+ "decapcms"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/allejo/decap-extras"
11
+ },
12
+ "license": "MIT",
13
+ "author": "Vladimir \"allejo\" Jimenez",
14
+ "type": "module",
15
+ "main": "./dist/index.mjs",
16
+ "types": "./dist/index.d.mts",
17
+ "scripts": {
18
+ "build": "tsdown",
19
+ "format": "prettier --write .",
20
+ "test": "vitest run --typecheck"
21
+ },
22
+ "lint-staged": {
23
+ "*.{md,ts}": "prettier --write",
24
+ "*.ts": "eslint --fix"
25
+ },
26
+ "devDependencies": {
27
+ "@allejo/prettier-config": "^1.0.8",
28
+ "@commitlint/cli": "^18.6.0",
29
+ "@commitlint/config-conventional": "^18.6.0",
30
+ "@commitlint/types": "^21.0.1",
31
+ "@types/eslint": "^8.44.7",
32
+ "@types/node": "^24.12.4",
33
+ "decap-cms-app": "^3.12.2",
34
+ "eslint": "^9.21.0",
35
+ "husky": "^9.0.10",
36
+ "lint-staged": "^15.4.3",
37
+ "prettier": "^3.1.1",
38
+ "tsdown": "^0.22.0",
39
+ "typescript": "^5.9.3",
40
+ "vitest": "^3.2.4"
41
+ },
42
+ "peerDependencies": {
43
+ "decap-cms-core": "^3.12.2"
44
+ }
45
+ }