@riducms/plugin 0.2.1 → 0.2.3
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 +20 -0
- package/package.json +4 -4
- package/src/admin.ts +73 -35
- package/src/authoring/v1.ts +2 -2
- package/src/authoring.ts +16 -0
- package/src/editor/registry.ts +15 -39
- package/src/index.ts +2 -2
- package/src/plugin-registry.ts +38 -66
- package/src/plugin.ts +2 -18
package/README.md
CHANGED
|
@@ -259,6 +259,26 @@ tree, and `identity` identifies the existing item. Ridu finds its current positi
|
|
|
259
259
|
ordinary fields. Edits immediately enter the parent form; this does not save the document.
|
|
260
260
|
Removed/malformed items show recovery UI rather than another item's fields.
|
|
261
261
|
|
|
262
|
+
For a Go-configured `BlockAdmin.NameField`, `schemaHeader` renders just the editorial
|
|
263
|
+
name input and its feedback:
|
|
264
|
+
|
|
265
|
+
```svelte
|
|
266
|
+
{#if authoring.schemaHeader !== undefined}
|
|
267
|
+
{@render authoring.schemaHeader({
|
|
268
|
+
treeKey: "widgets",
|
|
269
|
+
identity: selectedIdentity,
|
|
270
|
+
onChange: ({ field, value }) => updateWidgetField(selectedIdentity, field, value),
|
|
271
|
+
})}
|
|
272
|
+
{/if}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Here `updateWidgetField` belongs to your plugin: apply the permitted field change
|
|
276
|
+
to the latest matching item through your editor's history, then serialize with
|
|
277
|
+
`field.set`. The header does not directly change the parent form. Ridu resolves the
|
|
278
|
+
configured child, checks its access and visibility, and disables the card header
|
|
279
|
+
while a draft for that tree and identity is open. The ordinary content renderer
|
|
280
|
+
omits the name field's body placement; draft drawers include it in their own header.
|
|
281
|
+
|
|
262
282
|
For **Apply/Cancel**, use a temporary embedded draft:
|
|
263
283
|
|
|
264
284
|
1. Call `beginSchemaDraft({ treeKey, identity })` for an existing item, or
|
package/package.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"url": "https://github.com/riducms/ridu/issues"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@riducms/protocol": "0.2.
|
|
7
|
-
"@riducms/translations": "0.2.
|
|
8
|
-
"@riducms/ui": "0.2.
|
|
6
|
+
"@riducms/protocol": "0.2.3",
|
|
7
|
+
"@riducms/translations": "0.2.3",
|
|
8
|
+
"@riducms/ui": "0.2.3"
|
|
9
9
|
},
|
|
10
10
|
"description": "Public TypeScript contracts for statically registered Ridu admin plugins.",
|
|
11
11
|
"exports": {
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"test": "bun test"
|
|
40
40
|
},
|
|
41
41
|
"type": "module",
|
|
42
|
-
"version": "0.2.
|
|
42
|
+
"version": "0.2.3"
|
|
43
43
|
}
|
package/src/admin.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { bindSchemaManifest } from "@riducms/protocol";
|
|
2
2
|
import { resolveBlockTypes } from "@riducms/protocol";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
resolvePluginFields,
|
|
5
|
+
validateManifestPluginPairs,
|
|
6
|
+
createPluginFieldValidator,
|
|
7
|
+
type ResolvedPluginField,
|
|
8
|
+
} from "./plugin-registry";
|
|
4
9
|
import { isRegisteredRowLabel, type RegisteredRowLabel } from "./local-row-label";
|
|
5
10
|
import { localEditorReference } from "./editor/registry";
|
|
6
11
|
import type { SchemaField } from "@riducms/protocol";
|
|
7
12
|
import type { SchemaManifest } from "@riducms/protocol";
|
|
8
|
-
import type
|
|
13
|
+
import { en, type TranslationLanguage } from "@riducms/translations";
|
|
9
14
|
import type { PluginMessageCatalog } from "./i18n";
|
|
10
|
-
import { resolveAdminExtensions, type AdminContributions, type AdminPlugin } from "./plugin";
|
|
11
15
|
import {
|
|
12
|
-
|
|
16
|
+
resolveAdminExtensions,
|
|
17
|
+
type AdminContributions,
|
|
18
|
+
type AdminPlugin,
|
|
19
|
+
type ResolvedAdminExtensions,
|
|
20
|
+
} from "./plugin";
|
|
21
|
+
import {
|
|
22
|
+
validateFieldEditorSelection,
|
|
13
23
|
validateFieldEditorRegistrations,
|
|
14
24
|
type FieldEditorConfig,
|
|
15
25
|
} from "./editor/registry";
|
|
@@ -50,9 +60,8 @@ export interface AdminConfig extends AdminContributions, FieldEditorConfig {
|
|
|
50
60
|
* These checks also run when building and starting the admin.
|
|
51
61
|
*
|
|
52
62
|
* @param config The installed plugins, custom components, and interface translations.
|
|
53
|
-
* @returns The same configuration object
|
|
63
|
+
* @returns The same typed configuration object. The admin
|
|
54
64
|
* reads it when starting; calling this function does not mount any components.
|
|
55
|
-
* @throws If registrations, keys, or replacement locations conflict.
|
|
56
65
|
* @example
|
|
57
66
|
* ```ts
|
|
58
67
|
* import { defineAdmin } from '@riducms/plugin/admin';
|
|
@@ -66,18 +75,39 @@ export interface AdminConfig extends AdminContributions, FieldEditorConfig {
|
|
|
66
75
|
* ```
|
|
67
76
|
*/
|
|
68
77
|
export function defineAdmin(config: AdminConfig): AdminConfig {
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Static configuration owned by one mounted admin. Changes require remount/HMR. */
|
|
82
|
+
export interface ResolvedAdminConfig {
|
|
83
|
+
readonly plugins: readonly AdminPlugin[];
|
|
84
|
+
readonly fields: readonly ResolvedPluginField[];
|
|
85
|
+
readonly editors: NonNullable<AdminConfig["fields"]>;
|
|
86
|
+
readonly rowLabels: NonNullable<AdminConfig["rowLabels"]>;
|
|
87
|
+
readonly languages: readonly TranslationLanguage[];
|
|
88
|
+
readonly extensions: Readonly<ResolvedAdminExtensions>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Normalize and validate static registrations once, independently of schema refresh. */
|
|
92
|
+
export function resolveAdminConfig(config: AdminConfig = {}): ResolvedAdminConfig {
|
|
69
93
|
if ("fieldPlugins" in config)
|
|
70
94
|
throw new Error(
|
|
71
95
|
"Application fieldPlugins are not supported; use a field-owned Editor component or a paired advanced field plugin."
|
|
72
96
|
);
|
|
73
|
-
validatePluginRegistrations(config.plugins ?? []);
|
|
74
97
|
validateFieldEditorRegistrations(config);
|
|
75
98
|
for (const [reference, label] of Object.entries(config.rowLabels ?? {})) {
|
|
76
99
|
if (!localEditorReference.test(reference) || !isRegisteredRowLabel(label))
|
|
77
100
|
throw new Error(`Row label ${reference} must use an app:name reference and defineRowLabel.`);
|
|
78
101
|
}
|
|
79
|
-
|
|
80
|
-
return
|
|
102
|
+
const plugins = Object.freeze([...(config.plugins ?? [])]);
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
plugins,
|
|
105
|
+
fields: resolvePluginFields(plugins),
|
|
106
|
+
editors: Object.freeze({ ...config.fields }),
|
|
107
|
+
rowLabels: Object.freeze({ ...config.rowLabels }),
|
|
108
|
+
languages: Object.freeze(config.languages?.length ? [...config.languages] : [en]),
|
|
109
|
+
extensions: Object.freeze(resolveAdminExtensions(plugins, config)),
|
|
110
|
+
});
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
/**
|
|
@@ -87,40 +117,47 @@ export function defineAdmin(config: AdminConfig): AdminConfig {
|
|
|
87
117
|
* resources the current user cannot access, so absence there does not prove a typo.
|
|
88
118
|
* Throws with the affected registration or field when a selection/config is invalid.
|
|
89
119
|
*/
|
|
90
|
-
export function
|
|
91
|
-
config:
|
|
120
|
+
export function validateAdminManifest(
|
|
121
|
+
config: ResolvedAdminConfig,
|
|
92
122
|
manifest: Pick<SchemaManifest, "collections" | "globals" | "blocks"> &
|
|
93
123
|
Partial<Pick<SchemaManifest, "plugins">>,
|
|
94
124
|
options: { completeManifest?: boolean } = {}
|
|
95
125
|
): void {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
validateManifestPluginPairs(
|
|
127
|
+
config.plugins,
|
|
128
|
+
config.fields,
|
|
129
|
+
manifest,
|
|
130
|
+
options.completeManifest === true
|
|
131
|
+
);
|
|
132
|
+
const validatePluginField = createPluginFieldValidator(config.fields);
|
|
133
|
+
const failures: string[] = [];
|
|
99
134
|
bindSchemaManifest(manifest);
|
|
100
135
|
const inspect = (fields: readonly SchemaField[], owner: string) => {
|
|
101
136
|
for (const field of fields) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
137
|
+
try {
|
|
138
|
+
validateFieldEditorSelection(config.editors, field);
|
|
139
|
+
validatePluginField(field);
|
|
140
|
+
const selection = field.nested?.rowLabelComponent;
|
|
141
|
+
if (selection?.reference !== undefined) {
|
|
142
|
+
const reference = selection.reference;
|
|
143
|
+
if (
|
|
144
|
+
!localEditorReference.test(reference) ||
|
|
145
|
+
selection.plugin !== undefined ||
|
|
146
|
+
selection.component !== undefined ||
|
|
147
|
+
(field.type !== "array" && field.type !== "blocks")
|
|
148
|
+
)
|
|
149
|
+
throw new Error("invalid local row label selection.");
|
|
150
|
+
const label = config.rowLabels?.[reference as `app:${string}`];
|
|
151
|
+
if (label === undefined)
|
|
152
|
+
throw new Error(
|
|
153
|
+
`row label ${reference} is not registered in admin/src/admin.config.ts.`
|
|
154
|
+
);
|
|
118
155
|
label.decode(field);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
`${owner}.${field.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
122
|
-
);
|
|
123
156
|
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
failures.push(
|
|
159
|
+
`${owner}.${field.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
160
|
+
);
|
|
124
161
|
}
|
|
125
162
|
for (const tree of field.plugin?.embeddedTrees ?? [])
|
|
126
163
|
for (const item of tree.cases)
|
|
@@ -131,11 +168,12 @@ export function validateAdminConfig(
|
|
|
131
168
|
};
|
|
132
169
|
for (const collection of manifest.collections) inspect(collection.fields, collection.slug);
|
|
133
170
|
for (const global of manifest.globals ?? []) inspect(global.fields, global.slug);
|
|
171
|
+
if (failures.length) throw new Error(failures.join("\n"));
|
|
134
172
|
// Runtime manifests are permission-filtered; only build checks can prove target absence.
|
|
135
173
|
if (!options.completeManifest) return;
|
|
136
174
|
const collections = new Map(manifest.collections.map((item) => [item.slug, item]));
|
|
137
175
|
const globals = new Set((manifest.globals ?? []).map((item) => item.slug));
|
|
138
|
-
const extensions =
|
|
176
|
+
const extensions = config.extensions;
|
|
139
177
|
for (const cell of extensions.listCells) {
|
|
140
178
|
const collection = collections.get(cell.collection);
|
|
141
179
|
if (!collection?.fields.some((field) => field.name === cell.field || field.path === cell.field))
|
package/src/authoring/v1.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdminPlugin } from "../plugin";
|
|
2
|
-
import {
|
|
2
|
+
import { resolvePluginFields } from "../plugin-registry";
|
|
3
3
|
|
|
4
4
|
export { definePluginField, defineFieldComponent } from "../field";
|
|
5
5
|
export type {
|
|
@@ -66,6 +66,6 @@ export function defineAdminPlugin<const Plugin extends Omit<AdminPlugin, "apiVer
|
|
|
66
66
|
apiVersion: authoringAPIVersion,
|
|
67
67
|
});
|
|
68
68
|
const definition: Omit<AdminPlugin, "apiVersion"> = plugin;
|
|
69
|
-
|
|
69
|
+
resolvePluginFields([{ ...definition, apiVersion: authoringAPIVersion }]);
|
|
70
70
|
return result;
|
|
71
71
|
}
|
package/src/authoring.ts
CHANGED
|
@@ -84,6 +84,16 @@ export interface EmbeddedSchemaFormScope {
|
|
|
84
84
|
readOnly?: boolean;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** A configured editorial-name control for one existing embedded item. */
|
|
88
|
+
export interface EmbeddedSchemaHeaderProps extends EmbeddedSchemaFormScope {
|
|
89
|
+
/**
|
|
90
|
+
* Receives a permitted change to the Go-configured name field. Apply it to the
|
|
91
|
+
* latest matching item through your editor's own history/serialization path.
|
|
92
|
+
* The host does not write the parent form or save the document itself.
|
|
93
|
+
*/
|
|
94
|
+
onChange: (change: { field: string; value: string }) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
/** Select a Go-declared embedded variant when creating or copying an item's field data. */
|
|
88
98
|
export interface EmbeddedSchemaVariantScope {
|
|
89
99
|
/** The embedded tree key declared by the Go field. */
|
|
@@ -159,6 +169,12 @@ export interface FieldAuthoringHost {
|
|
|
159
169
|
* if the user needs Apply/Cancel. Neither approach saves the document itself.
|
|
160
170
|
*/
|
|
161
171
|
schemaForm?: Snippet<[EmbeddedSchemaFormScope]>;
|
|
172
|
+
/**
|
|
173
|
+
* Render the configured name input and field feedback, without the card chrome.
|
|
174
|
+
* Uses the item's schema, access, visibility and stable identity. An open draft
|
|
175
|
+
* locks the card input; an item without a name field renders no control.
|
|
176
|
+
*/
|
|
177
|
+
schemaHeader?: Snippet<[EmbeddedSchemaHeaderProps]>;
|
|
162
178
|
/**
|
|
163
179
|
* Start a temporary Apply/Cancel form. Pass `{ treeKey, identity }` to edit an
|
|
164
180
|
* existing item, or `{ treeKey, caseTag, variantSlug }` to prepare a new one using
|
package/src/editor/registry.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { bindSchemaManifest } from "@riducms/protocol";
|
|
2
|
-
import { resolveBlockTypes } from "@riducms/protocol";
|
|
3
1
|
import type { Component } from "svelte";
|
|
4
|
-
import type { SchemaField
|
|
2
|
+
import type { SchemaField } from "@riducms/protocol";
|
|
5
3
|
|
|
6
4
|
import type { FieldEditorProps, FieldEditorType } from "./types";
|
|
7
5
|
import { decodeComponentConfig } from "../component-config";
|
|
@@ -134,41 +132,19 @@ export function validateFieldEditorRegistrations(config: FieldEditorConfig): voi
|
|
|
134
132
|
}
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
/**
|
|
138
|
-
export function
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
/** Check one local selection using the same decoder as the mounted editor. */
|
|
136
|
+
export function validateFieldEditorSelection(
|
|
137
|
+
editors: NonNullable<FieldEditorConfig["fields"]>,
|
|
138
|
+
field: SchemaField
|
|
141
139
|
): void {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
throw new Error(`Malformed editor reference ${reference}; expected app:name.`);
|
|
153
|
-
if (editor === undefined)
|
|
154
|
-
throw new Error(
|
|
155
|
-
`Editor ${reference} is not registered. Register it in admin/src/admin.config.ts or change the field component in Go.`
|
|
156
|
-
);
|
|
157
|
-
editor.decode(field);
|
|
158
|
-
} catch (error) {
|
|
159
|
-
failures.push(
|
|
160
|
-
`${owner}.${field.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
for (const tree of field.plugin?.embeddedTrees ?? [])
|
|
165
|
-
for (const item of tree.cases)
|
|
166
|
-
for (const variant of resolveBlockTypes(item)) inspect(variant.fields, owner);
|
|
167
|
-
if (field.nested !== undefined) inspect(field.nested.fields, owner);
|
|
168
|
-
for (const block of resolveBlockTypes(field.blocks) ?? []) inspect(block.fields, owner);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
for (const collection of manifest.collections) inspect(collection.fields, collection.slug);
|
|
172
|
-
for (const global of manifest.globals ?? []) inspect(global.fields, global.slug);
|
|
173
|
-
if (failures.length !== 0) throw new Error(failures.join("\n"));
|
|
140
|
+
const reference = field.admin.editor?.reference;
|
|
141
|
+
if (reference === undefined) return;
|
|
142
|
+
if (!localEditorReference.test(reference))
|
|
143
|
+
throw new Error(`Malformed editor reference ${reference}; expected app:name.`);
|
|
144
|
+
const editor = editors[reference as `app:${string}`];
|
|
145
|
+
if (editor === undefined)
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Editor ${reference} is not registered. Register it in admin/src/admin.config.ts or change the field component in Go.`
|
|
148
|
+
);
|
|
149
|
+
editor.decode(field);
|
|
174
150
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
EmbeddedSchemaFormScope,
|
|
3
|
+
EmbeddedSchemaHeaderProps,
|
|
3
4
|
EmbeddedSchemaVariantScope,
|
|
4
5
|
EmbeddedSchemaDraft,
|
|
5
6
|
EmbeddedSchemaDraftEditorProps,
|
|
@@ -36,7 +37,7 @@ export {
|
|
|
36
37
|
export {
|
|
37
38
|
ADMIN_PLUGIN_API_VERSION,
|
|
38
39
|
resolveAdminExtensions,
|
|
39
|
-
|
|
40
|
+
assertAdminPluginPairs,
|
|
40
41
|
type AdminPlugin,
|
|
41
42
|
type AdminContributions,
|
|
42
43
|
type AdminDashboardPanel,
|
|
@@ -79,7 +80,6 @@ export {
|
|
|
79
80
|
type AdminRoute,
|
|
80
81
|
type BackendAdminPlugin,
|
|
81
82
|
type ResolvedAdminExtensions,
|
|
82
|
-
type ResolvedAdminPluginPairs,
|
|
83
83
|
} from "./plugin";
|
|
84
84
|
|
|
85
85
|
export { resolvePluginFields, type ResolvedPluginField } from "./plugin-registry";
|
package/src/plugin-registry.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { bindSchemaManifest } from "@riducms/protocol";
|
|
2
|
-
import { resolveBlockTypes } from "@riducms/protocol";
|
|
3
1
|
import { ADMIN_PLUGIN_API_VERSION, type SchemaField, type SchemaManifest } from "@riducms/protocol";
|
|
4
2
|
import { assertPluginFieldRegistration, type RegisteredPluginField } from "./field";
|
|
5
|
-
import type
|
|
3
|
+
import { assertAdminPluginPairs, type AdminPlugin } from "./plugin";
|
|
6
4
|
|
|
7
5
|
/** A registered field editor with the names Ridu uses to select it. Returned by registry resolution. */
|
|
8
6
|
export interface ResolvedPluginField {
|
|
@@ -15,10 +13,6 @@ export interface ResolvedPluginField {
|
|
|
15
13
|
readonly registration: RegisteredPluginField;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
export function validatePluginRegistrations(plugins: readonly AdminPlugin[]): void {
|
|
19
|
-
resolvePluginFields(plugins);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
16
|
/**
|
|
23
17
|
* Collect field registrations, checking plugin IDs, field-type ownership, named
|
|
24
18
|
* components and API versions. Throws on malformed/duplicate registrations.
|
|
@@ -87,13 +81,13 @@ export function resolvePluginFields(
|
|
|
87
81
|
}
|
|
88
82
|
|
|
89
83
|
/** Canonical manifest, actual executable registrations, and the same decoders as the host. */
|
|
90
|
-
export function
|
|
84
|
+
export function validateManifestPluginPairs(
|
|
91
85
|
plugins: readonly AdminPlugin[],
|
|
86
|
+
registrations: readonly ResolvedPluginField[],
|
|
92
87
|
manifest: Pick<SchemaManifest, "collections" | "globals" | "blocks"> &
|
|
93
88
|
Partial<Pick<SchemaManifest, "plugins">>,
|
|
94
89
|
complete: boolean
|
|
95
90
|
): void {
|
|
96
|
-
const registrations = resolvePluginFields(plugins);
|
|
97
91
|
// Startup may receive only public or access-filtered schemas. Build/check own
|
|
98
92
|
// completeness; selected fields below still validate their exact renderer.
|
|
99
93
|
if (complete && manifest.plugins !== undefined)
|
|
@@ -108,38 +102,22 @@ export function validatePluginManifest(
|
|
|
108
102
|
`Named renderer ${item.owner}:${item.key} selects undeclared field type ${item.registration.fieldType}.`
|
|
109
103
|
);
|
|
110
104
|
}
|
|
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
105
|
if (manifest.plugins !== undefined) {
|
|
120
106
|
for (const backend of manifest.plugins) {
|
|
121
107
|
if (backend.admin === undefined) continue;
|
|
122
108
|
const plugin = plugins.find((candidate) => candidate.key === backend.key);
|
|
123
109
|
if (plugin === undefined)
|
|
124
110
|
throw new Error(`Missing admin registration for backend plugin ${backend.key}.`);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
);
|
|
111
|
+
assertAdminPluginPairs([
|
|
112
|
+
{
|
|
113
|
+
admin: plugin,
|
|
114
|
+
backend: {
|
|
115
|
+
...backend.admin,
|
|
116
|
+
key: backend.key,
|
|
117
|
+
fieldTypes: (backend.fieldTypes ?? []).map((field) => field.key),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
143
121
|
}
|
|
144
122
|
if (complete)
|
|
145
123
|
for (const plugin of plugins) {
|
|
@@ -151,38 +129,32 @@ export function validatePluginManifest(
|
|
|
151
129
|
throw new Error(`Admin plugin ${plugin.key} has no paired Go admin descriptor.`);
|
|
152
130
|
}
|
|
153
131
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Resolve a selection against static registrations; the caller owns schema traversal. */
|
|
135
|
+
export function createPluginFieldValidator(registrations: readonly ResolvedPluginField[]) {
|
|
136
|
+
const fields = new Map(
|
|
137
|
+
registrations.filter((item) => item.componentKey === undefined).map((item) => [item.key, item])
|
|
138
|
+
);
|
|
139
|
+
const components = new Map(
|
|
140
|
+
registrations
|
|
141
|
+
.filter((item) => item.componentKey !== undefined)
|
|
142
|
+
.map((item) => [`${item.owner}:${item.key}`, item])
|
|
143
|
+
);
|
|
144
|
+
return (field: SchemaField): void => {
|
|
145
|
+
const selection = field.admin.component;
|
|
146
|
+
const selected =
|
|
147
|
+
selection !== undefined
|
|
148
|
+
? components.get(`${selection.plugin}:${selection.component}`)
|
|
149
|
+
: field.type === "plugin"
|
|
150
|
+
? fields.get(field.plugin?.key ?? "")
|
|
151
|
+
: undefined;
|
|
152
|
+
if (selection !== undefined || field.type === "plugin") {
|
|
153
|
+
if (selected === undefined)
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Missing renderer for ${selection !== undefined ? `${selection.plugin}:${selection.component}` : field.plugin?.key}.`
|
|
176
156
|
);
|
|
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);
|
|
157
|
+
selected.registration.decodeConfig(field);
|
|
183
158
|
}
|
|
184
159
|
};
|
|
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
160
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FieldType } from "@riducms/protocol";
|
|
2
2
|
import type { RegisteredPluginField } from "./field";
|
|
3
|
-
import { resolvePluginFields, type ResolvedPluginField } from "./plugin-registry";
|
|
4
3
|
import { ADMIN_PLUGIN_API_VERSION } from "@riducms/protocol";
|
|
5
4
|
import type {
|
|
6
5
|
OperationCapabilities,
|
|
@@ -402,9 +401,7 @@ export interface AdminPluginPair {
|
|
|
402
401
|
}
|
|
403
402
|
|
|
404
403
|
/** Checked plugin registrations collected for Ridu's admin runtime. Normally consumed by generated code. */
|
|
405
|
-
export interface
|
|
406
|
-
plugins: readonly AdminPlugin[];
|
|
407
|
-
fields: readonly ResolvedPluginField[];
|
|
404
|
+
export interface ResolvedAdminExtensions {
|
|
408
405
|
rowLabels: readonly RowLabelPlugin[];
|
|
409
406
|
routes: readonly AdminRoute[];
|
|
410
407
|
dashboard: readonly AdminDashboardPanel[];
|
|
@@ -423,19 +420,14 @@ export interface ResolvedAdminPluginPairs {
|
|
|
423
420
|
applicationMessages: PluginMessageCatalog | undefined;
|
|
424
421
|
}
|
|
425
422
|
|
|
426
|
-
export type ResolvedAdminExtensions = Omit<ResolvedAdminPluginPairs, "plugins" | "fields">;
|
|
427
|
-
|
|
428
423
|
/**
|
|
429
424
|
* Check that installed Go/admin plugin packages agree on identity, API version,
|
|
430
425
|
* pairing version, routes, assets and field types. Ridu's generated file calls this;
|
|
431
426
|
* authors should fix mismatched packages rather than editing generated metadata.
|
|
432
427
|
* Missing imports fail during compilation; incompatible pairs throw here.
|
|
433
428
|
*/
|
|
434
|
-
export function
|
|
435
|
-
pairs: readonly AdminPluginPair[]
|
|
436
|
-
): ResolvedAdminPluginPairs {
|
|
429
|
+
export function assertAdminPluginPairs(pairs: readonly AdminPluginPair[]): void {
|
|
437
430
|
const keys = new Set<string>();
|
|
438
|
-
const plugins: AdminPlugin[] = [];
|
|
439
431
|
|
|
440
432
|
for (const { admin, backend } of pairs) {
|
|
441
433
|
const identity = `${backend.package}#${backend.export}`;
|
|
@@ -473,15 +465,7 @@ export function resolveAdminPluginPairs(
|
|
|
473
465
|
Object.keys(admin.fields ?? {}).sort()
|
|
474
466
|
);
|
|
475
467
|
keys.add(backend.key);
|
|
476
|
-
plugins.push(admin);
|
|
477
468
|
}
|
|
478
|
-
const extensions = resolveAdminExtensions(plugins);
|
|
479
|
-
|
|
480
|
-
return {
|
|
481
|
-
plugins: Object.freeze(plugins),
|
|
482
|
-
fields: resolvePluginFields(plugins),
|
|
483
|
-
...extensions,
|
|
484
|
-
};
|
|
485
469
|
}
|
|
486
470
|
|
|
487
471
|
/**
|