@riducms/plugin 0.1.5 → 0.2.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 +346 -32
- package/package.json +9 -4
- package/src/admin.ts +171 -0
- package/src/authoring/v1.ts +71 -0
- package/src/authoring.ts +160 -2
- package/src/component-config.ts +51 -0
- package/src/editor/field.svelte +21 -0
- package/src/editor/field.ts +9 -0
- package/src/editor/index.ts +3 -0
- package/src/editor/registry.ts +174 -0
- package/src/editor/types.ts +104 -0
- package/src/field.ts +596 -16
- package/src/form.ts +33 -4
- package/src/i18n.ts +29 -4
- package/src/index.ts +19 -7
- package/src/local-row-label.ts +87 -0
- package/src/plugin-registry.ts +188 -0
- package/src/plugin.ts +265 -74
- package/src/row-label.ts +15 -4
package/src/i18n.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AdminI18n,
|
|
3
3
|
PluginMessageCatalog,
|
|
4
4
|
PluginTranslationKey,
|
|
5
|
+
ExtensionTranslationKey,
|
|
5
6
|
TranslationMessage,
|
|
6
7
|
} from "@riducms/translations";
|
|
7
8
|
import { validatePluginMessageCatalog } from "@riducms/translations";
|
|
@@ -67,11 +68,21 @@ type CheckedAdminMessages<Input extends AdminMessagesShape> =
|
|
|
67
68
|
export interface DefineAdminMessagesInput<
|
|
68
69
|
Fallback extends Readonly<Record<string, TranslationMessage>>,
|
|
69
70
|
> {
|
|
71
|
+
/** Default text for every message key. Keys here do not include the plugin/app namespace. */
|
|
70
72
|
fallback: Fallback;
|
|
73
|
+
/** Catalogs by language code; each supplied language must translate every fallback key. */
|
|
71
74
|
translations?: Readonly<Record<string, ExactCatalog<Fallback>>>;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Define interface text for a plugin or application. Every supplied translation
|
|
79
|
+
* must have the fallback catalog's keys, message kinds and `{placeholder}` names.
|
|
80
|
+
* The helper checks and freezes the catalogs; literals also receive TypeScript checks.
|
|
81
|
+
*
|
|
82
|
+
* For plugin key `"notes"`, fallback key `"copy"` is read as
|
|
83
|
+
* `i18n.t("plugin.notes:copy")`. In `defineAdmin({ messages })`, it is `"app:copy"`.
|
|
84
|
+
* These are admin interface translations, not translations of saved content.
|
|
85
|
+
*/
|
|
75
86
|
export function defineAdminMessages<const Input extends AdminMessagesShape>(
|
|
76
87
|
input: Input & CheckedAdminMessages<Input>
|
|
77
88
|
): DefineAdminMessagesInput<Input["fallback"]> {
|
|
@@ -79,6 +90,7 @@ export function defineAdminMessages<const Input extends AdminMessagesShape>(
|
|
|
79
90
|
return freezeAdminMessages(input) as DefineAdminMessagesInput<Input["fallback"]>;
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
/** Check a catalog at runtime, throwing with its owner key on invalid messages/translations. */
|
|
82
94
|
export function validateAdminMessages(key: string, messages: PluginMessageCatalog) {
|
|
83
95
|
validatePluginMessageCatalog(key, messages);
|
|
84
96
|
}
|
|
@@ -91,7 +103,20 @@ export function freezeAdminMessages(messages: PluginMessageCatalog): PluginMessa
|
|
|
91
103
|
return Object.freeze(messages);
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
|
|
106
|
+
// Separate named exports let TypeScript show their docs at call sites; these remain the same functions.
|
|
107
|
+
const [readAdminI18n, provideAdminI18n] = createContext<AdminI18n>();
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Read Ridu's translation context during Svelte component setup. Field/extension
|
|
111
|
+
* components already receive `i18n` in props; nested components can use this getter.
|
|
112
|
+
* Throws outside a provider, such as a standalone component test without context.
|
|
113
|
+
*/
|
|
114
|
+
export const getAdminI18n = readAdminI18n;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Provide translations to child components during Svelte component setup.
|
|
118
|
+
* Ridu normally does this. Use it when hosting components independently, for
|
|
119
|
+
* example in a test that needs to supply its own AdminI18n instance.
|
|
120
|
+
*/
|
|
121
|
+
export const setAdminI18n = provideAdminI18n;
|
|
122
|
+
export type { AdminI18n, PluginMessageCatalog, PluginTranslationKey, ExtensionTranslationKey };
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
export type {
|
|
2
|
+
EmbeddedSchemaFormScope,
|
|
3
|
+
EmbeddedSchemaVariantScope,
|
|
4
|
+
EmbeddedSchemaDraft,
|
|
5
|
+
EmbeddedSchemaDraftEditorProps,
|
|
2
6
|
FieldAuthoringHost,
|
|
3
7
|
FieldDocument,
|
|
4
8
|
FieldReferenceBrowserProps,
|
|
5
9
|
FieldReferenceFilter,
|
|
6
10
|
} from "./authoring";
|
|
7
|
-
export
|
|
11
|
+
export type {
|
|
12
|
+
PluginFieldBinding,
|
|
13
|
+
PluginFieldProps,
|
|
14
|
+
PluginForm,
|
|
15
|
+
RegisteredPluginField,
|
|
16
|
+
PluginFieldRegistration,
|
|
17
|
+
} from "./field";
|
|
8
18
|
export {
|
|
9
19
|
defineRowLabelPlugin,
|
|
10
20
|
type RowLabelComponentProps,
|
|
@@ -12,7 +22,7 @@ export {
|
|
|
12
22
|
type RowLabelSnapshot,
|
|
13
23
|
type RowLabelValue,
|
|
14
24
|
} from "./row-label";
|
|
15
|
-
export type {
|
|
25
|
+
export type { FieldDocumentForm, FieldFormResource, FieldLiveValidation } from "./form";
|
|
16
26
|
export {
|
|
17
27
|
defineAdminMessages,
|
|
18
28
|
getAdminI18n,
|
|
@@ -25,10 +35,10 @@ export {
|
|
|
25
35
|
} from "./i18n";
|
|
26
36
|
export {
|
|
27
37
|
ADMIN_PLUGIN_API_VERSION,
|
|
28
|
-
|
|
29
|
-
resolveAdminPluginExtensions,
|
|
38
|
+
resolveAdminExtensions,
|
|
30
39
|
resolveAdminPluginPairs,
|
|
31
40
|
type AdminPlugin,
|
|
41
|
+
type AdminContributions,
|
|
32
42
|
type AdminDashboardPanel,
|
|
33
43
|
type AdminDashboardPanelProps,
|
|
34
44
|
type AdminExtensionProps,
|
|
@@ -64,10 +74,12 @@ export {
|
|
|
64
74
|
type AdminExtensionNotificationTone,
|
|
65
75
|
type AdminListCell,
|
|
66
76
|
type AdminListCellProps,
|
|
67
|
-
type
|
|
77
|
+
type AdminRouteNavigation,
|
|
68
78
|
type AdminPluginPair,
|
|
69
|
-
type
|
|
79
|
+
type AdminRoute,
|
|
70
80
|
type BackendAdminPlugin,
|
|
71
|
-
type
|
|
81
|
+
type ResolvedAdminExtensions,
|
|
72
82
|
type ResolvedAdminPluginPairs,
|
|
73
83
|
} from "./plugin";
|
|
84
|
+
|
|
85
|
+
export { resolvePluginFields, type ResolvedPluginField } from "./plugin-registry";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { SchemaField } from "@riducms/protocol";
|
|
3
|
+
import type { RowLabelComponentProps } from "./row-label";
|
|
4
|
+
import { decodeComponentConfig } from "./component-config";
|
|
5
|
+
|
|
6
|
+
/** Props for an application-local array/block heading; row data is read-only and config is decoded. */
|
|
7
|
+
export type RowLabelProps<Config = undefined> = Omit<RowLabelComponentProps, "config"> &
|
|
8
|
+
([Config] extends [undefined] ? { config?: never } : { config: Config });
|
|
9
|
+
/** Heading component plus a synchronous settings decoder, required when Config is not undefined. */
|
|
10
|
+
export type RowLabelDefinition<Config = undefined> = {
|
|
11
|
+
component: Component<RowLabelProps<NoInfer<Config>>>;
|
|
12
|
+
} & (0 extends 1 & Config
|
|
13
|
+
? { decodeConfig: (value: unknown) => Config }
|
|
14
|
+
: [Config] extends [never]
|
|
15
|
+
? { decodeConfig: (value: unknown) => Config }
|
|
16
|
+
: [Config] extends [undefined]
|
|
17
|
+
? { decodeConfig?: (value: unknown) => Config }
|
|
18
|
+
: { decodeConfig: (value: unknown) => Config });
|
|
19
|
+
const registration = Symbol("ridu-row-label");
|
|
20
|
+
const registered = new WeakSet<RegisteredRowLabel>();
|
|
21
|
+
/** Helper result stored in `defineAdmin({ rowLabels: ... })`; do not construct it manually. */
|
|
22
|
+
export interface RegisteredRowLabel {
|
|
23
|
+
readonly [registration]: true;
|
|
24
|
+
readonly component: Component<never>;
|
|
25
|
+
decode(field: SchemaField): unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register a custom heading for an array or block row in your application.
|
|
30
|
+
*
|
|
31
|
+
* Put the result under an `app:name` key in `defineAdmin`'s `rowLabels`. Select
|
|
32
|
+
* that key in Go with `field.Admin{RowLabel: field.Component("app:name")}`.
|
|
33
|
+
* Keep `RowLabelPath` for the plain-text name used by row actions and screen readers.
|
|
34
|
+
*
|
|
35
|
+
* `component` is required and receives `RowLabelProps`: `row` contains current,
|
|
36
|
+
* unsaved values, and `rowNumber` starts at one. Display a summary here; use a
|
|
37
|
+
* field component for controls that edit values.
|
|
38
|
+
*
|
|
39
|
+
* Add `decodeConfig` when the Go `field.Component` call supplies settings. Return
|
|
40
|
+
* checked settings synchronously, or throw a useful error. A decoder requires a
|
|
41
|
+
* Go configuration object, even for empty settings; without one, omit Go settings.
|
|
42
|
+
*
|
|
43
|
+
* @param definition The heading component and optional settings decoder.
|
|
44
|
+
* @returns A frozen registration for `defineAdmin`'s `rowLabels` map. It does not
|
|
45
|
+
* choose a Go field or mount the component until the admin renders that field.
|
|
46
|
+
* @throws If the component is invalid. `ridu check` also checks Go selections and settings.
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { defineAdmin, defineRowLabel } from '@riducms/plugin/admin';
|
|
50
|
+
* import { generatedAdminPlugins } from './ridu.plugins.generated';
|
|
51
|
+
* import LinkRowLabel from './components/link-row-label.svelte';
|
|
52
|
+
*
|
|
53
|
+
* export default defineAdmin({
|
|
54
|
+
* plugins: generatedAdminPlugins,
|
|
55
|
+
* rowLabels: {
|
|
56
|
+
* 'app:linkSummary': defineRowLabel({ component: LinkRowLabel })
|
|
57
|
+
* }
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function defineRowLabel<Config = undefined>(
|
|
62
|
+
definition: RowLabelDefinition<Config>
|
|
63
|
+
): RegisteredRowLabel {
|
|
64
|
+
if (typeof definition.component !== "function")
|
|
65
|
+
throw new Error("A row label requires a Svelte component.");
|
|
66
|
+
const { component, decodeConfig } = definition;
|
|
67
|
+
const label = Object.freeze({
|
|
68
|
+
[registration]: true as const,
|
|
69
|
+
component,
|
|
70
|
+
decode(field: SchemaField): unknown {
|
|
71
|
+
const selection = field.nested?.rowLabelComponent;
|
|
72
|
+
try {
|
|
73
|
+
return decodeComponentConfig(selection, decodeConfig);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Row label ${selection?.reference} config for ${field.path} is invalid: ${error instanceof Error ? error.message : String(error)}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
registered.add(label);
|
|
82
|
+
return label;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isRegisteredRowLabel(value: RegisteredRowLabel): boolean {
|
|
86
|
+
return registered.has(value);
|
|
87
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { bindSchemaManifest } from "@riducms/protocol";
|
|
2
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
3
|
+
import { ADMIN_PLUGIN_API_VERSION, type SchemaField, type SchemaManifest } from "@riducms/protocol";
|
|
4
|
+
import { assertPluginFieldRegistration, type RegisteredPluginField } from "./field";
|
|
5
|
+
import type { AdminPlugin } from "./plugin";
|
|
6
|
+
|
|
7
|
+
/** A registered field editor with the names Ridu uses to select it. Returned by registry resolution. */
|
|
8
|
+
export interface ResolvedPluginField {
|
|
9
|
+
/** Owning Go/admin plugin key. */
|
|
10
|
+
readonly owner: string;
|
|
11
|
+
/** Field-type key for a default editor, or component name for an explicitly selected editor. */
|
|
12
|
+
readonly key: string;
|
|
13
|
+
/** Present only for editors registered in `components` and selected by Go `AdminComponent`. */
|
|
14
|
+
readonly componentKey?: string;
|
|
15
|
+
readonly registration: RegisteredPluginField;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validatePluginRegistrations(plugins: readonly AdminPlugin[]): void {
|
|
19
|
+
resolvePluginFields(plugins);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Collect field registrations, checking plugin IDs, field-type ownership, named
|
|
24
|
+
* components and API versions. Throws on malformed/duplicate registrations.
|
|
25
|
+
* Used by Ridu's runtime/check tooling; authors normally call the registration
|
|
26
|
+
* helpers and retain their generated plugin list instead of invoking this directly.
|
|
27
|
+
*/
|
|
28
|
+
export function resolvePluginFields(
|
|
29
|
+
plugins: readonly AdminPlugin[]
|
|
30
|
+
): readonly ResolvedPluginField[] {
|
|
31
|
+
const owners = new Set<string>();
|
|
32
|
+
const identities = new Set<string>();
|
|
33
|
+
const resolved: ResolvedPluginField[] = [];
|
|
34
|
+
for (const plugin of plugins) {
|
|
35
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(plugin.key) || owners.has(plugin.key))
|
|
36
|
+
throw new Error(`Invalid or duplicate admin plugin key ${plugin.key}.`);
|
|
37
|
+
if (plugin.apiVersion !== ADMIN_PLUGIN_API_VERSION)
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Admin plugin ${plugin.key} uses API ${plugin.apiVersion}; this admin supports ${ADMIN_PLUGIN_API_VERSION}.`
|
|
40
|
+
);
|
|
41
|
+
if (!Number.isSafeInteger(plugin.pairingVersion) || plugin.pairingVersion < 1)
|
|
42
|
+
throw new Error(`Admin plugin ${plugin.key} requires a positive pairingVersion.`);
|
|
43
|
+
owners.add(plugin.key);
|
|
44
|
+
for (const [kind, entries] of [
|
|
45
|
+
["fields", plugin.fields],
|
|
46
|
+
["components", plugin.components],
|
|
47
|
+
] as const) {
|
|
48
|
+
if (entries === undefined) continue;
|
|
49
|
+
if (
|
|
50
|
+
typeof entries !== "object" ||
|
|
51
|
+
entries === null ||
|
|
52
|
+
Array.isArray(entries) ||
|
|
53
|
+
(Object.getPrototypeOf(entries) !== Object.prototype &&
|
|
54
|
+
Object.getPrototypeOf(entries) !== null)
|
|
55
|
+
)
|
|
56
|
+
throw new Error(`Plugin ${plugin.key} ${kind} must be a keyed registration object.`);
|
|
57
|
+
for (const [key, registration] of Object.entries(entries)) {
|
|
58
|
+
if (
|
|
59
|
+
!(
|
|
60
|
+
kind === "fields" ? /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/ : /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
|
|
61
|
+
).test(key)
|
|
62
|
+
)
|
|
63
|
+
throw new Error(`Invalid plugin ${kind} key ${key}.`);
|
|
64
|
+
assertPluginFieldRegistration(registration);
|
|
65
|
+
if (
|
|
66
|
+
kind === "fields"
|
|
67
|
+
? registration.type !== "plugin" || registration.fieldType !== undefined
|
|
68
|
+
: registration.type === "plugin" && registration.fieldType === undefined
|
|
69
|
+
)
|
|
70
|
+
throw new Error(`Plugin ${plugin.key} ${kind}.${key} uses the wrong registration kind.`);
|
|
71
|
+
const identity = kind === "fields" ? `field:${key}` : `component:${plugin.key}:${key}`;
|
|
72
|
+
if (identities.has(identity))
|
|
73
|
+
throw new Error(`Duplicate admin field renderer ${identity}.`);
|
|
74
|
+
identities.add(identity);
|
|
75
|
+
resolved.push(
|
|
76
|
+
Object.freeze({
|
|
77
|
+
owner: plugin.key,
|
|
78
|
+
key,
|
|
79
|
+
registration,
|
|
80
|
+
...(kind === "components" ? { componentKey: key } : {}),
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze(resolved);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Canonical manifest, actual executable registrations, and the same decoders as the host. */
|
|
90
|
+
export function validatePluginManifest(
|
|
91
|
+
plugins: readonly AdminPlugin[],
|
|
92
|
+
manifest: Pick<SchemaManifest, "collections" | "globals" | "blocks"> &
|
|
93
|
+
Partial<Pick<SchemaManifest, "plugins">>,
|
|
94
|
+
complete: boolean
|
|
95
|
+
): void {
|
|
96
|
+
const registrations = resolvePluginFields(plugins);
|
|
97
|
+
// Startup may receive only public or access-filtered schemas. Build/check own
|
|
98
|
+
// completeness; selected fields below still validate their exact renderer.
|
|
99
|
+
if (complete && manifest.plugins !== undefined)
|
|
100
|
+
for (const item of registrations) {
|
|
101
|
+
if (
|
|
102
|
+
item.registration.fieldType !== undefined &&
|
|
103
|
+
!manifest.plugins.some((plugin) =>
|
|
104
|
+
plugin.fieldTypes?.some((field) => field.key === item.registration.fieldType)
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Named renderer ${item.owner}:${item.key} selects undeclared field type ${item.registration.fieldType}.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const fields = new Map(
|
|
112
|
+
registrations.filter((item) => item.componentKey === undefined).map((item) => [item.key, item])
|
|
113
|
+
);
|
|
114
|
+
const components = new Map(
|
|
115
|
+
registrations
|
|
116
|
+
.filter((item) => item.componentKey !== undefined)
|
|
117
|
+
.map((item) => [`${item.owner}:${item.key}`, item])
|
|
118
|
+
);
|
|
119
|
+
if (manifest.plugins !== undefined) {
|
|
120
|
+
for (const backend of manifest.plugins) {
|
|
121
|
+
if (backend.admin === undefined) continue;
|
|
122
|
+
const plugin = plugins.find((candidate) => candidate.key === backend.key);
|
|
123
|
+
if (plugin === undefined)
|
|
124
|
+
throw new Error(`Missing admin registration for backend plugin ${backend.key}.`);
|
|
125
|
+
if (
|
|
126
|
+
plugin.apiVersion !== backend.admin.apiVersion ||
|
|
127
|
+
plugin.pairingVersion !== backend.admin.pairingVersion
|
|
128
|
+
)
|
|
129
|
+
throw new Error(`Backend/admin compatibility mismatch for plugin ${backend.key}.`);
|
|
130
|
+
if (
|
|
131
|
+
JSON.stringify(backend.admin.routes ?? []) !==
|
|
132
|
+
JSON.stringify((plugin.routes ?? []).map((route) => route.path)) ||
|
|
133
|
+
JSON.stringify(backend.admin.assets ?? []) !== JSON.stringify(plugin.assets ?? [])
|
|
134
|
+
)
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Plugin ${backend.key} routes/assets do not match the canonical Go descriptor.`
|
|
137
|
+
);
|
|
138
|
+
const expected = (backend.fieldTypes ?? []).map((field) => field.key).sort();
|
|
139
|
+
if (JSON.stringify(expected) !== JSON.stringify(Object.keys(plugin.fields ?? {}).sort()))
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Plugin ${backend.key} field types do not match the canonical Go descriptor (${expected.join(", ")}).`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (complete)
|
|
145
|
+
for (const plugin of plugins) {
|
|
146
|
+
if (
|
|
147
|
+
!manifest.plugins.some(
|
|
148
|
+
(backend) => backend.key === plugin.key && backend.admin !== undefined
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
throw new Error(`Admin plugin ${plugin.key} has no paired Go admin descriptor.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const failures: string[] = [];
|
|
155
|
+
bindSchemaManifest(manifest);
|
|
156
|
+
const inspect = (items: readonly SchemaField[], owner: string) => {
|
|
157
|
+
for (const field of items) {
|
|
158
|
+
try {
|
|
159
|
+
const selection = field.admin.component;
|
|
160
|
+
const selected =
|
|
161
|
+
selection !== undefined
|
|
162
|
+
? components.get(`${selection.plugin}:${selection.component}`)
|
|
163
|
+
: field.type === "plugin"
|
|
164
|
+
? fields.get(field.plugin?.key ?? "")
|
|
165
|
+
: undefined;
|
|
166
|
+
if (selection !== undefined || field.type === "plugin") {
|
|
167
|
+
if (selected === undefined)
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Missing renderer for ${selection !== undefined ? `${selection.plugin}:${selection.component}` : field.plugin?.key}.`
|
|
170
|
+
);
|
|
171
|
+
selected.registration.decodeConfig(field);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
failures.push(
|
|
175
|
+
`${owner}.${field.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (field.nested !== undefined) inspect(field.nested.fields, owner);
|
|
179
|
+
for (const block of resolveBlockTypes(field.blocks) ?? []) inspect(block.fields, owner);
|
|
180
|
+
for (const tree of field.plugin?.embeddedTrees ?? [])
|
|
181
|
+
for (const branch of tree.cases)
|
|
182
|
+
for (const block of resolveBlockTypes(branch)) inspect(block.fields, owner);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
for (const collection of manifest.collections) inspect(collection.fields, collection.slug);
|
|
186
|
+
for (const global of manifest.globals ?? []) inspect(global.fields, global.slug);
|
|
187
|
+
if (failures.length) throw new Error(failures.join("\n"));
|
|
188
|
+
}
|