@wp-typia/block-types 0.2.4 → 0.3.1

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 CHANGED
@@ -18,8 +18,11 @@ Shared WordPress block semantic types derived from Gutenberg source and unoffici
18
18
  - `@wp-typia/block-types/block-editor/style-attributes`
19
19
  - `@wp-typia/block-types/block-editor/typography`
20
20
  - `@wp-typia/block-types/blocks`
21
+ - `@wp-typia/block-types/blocks/bindings`
22
+ - `@wp-typia/block-types/blocks/compatibility`
21
23
  - `@wp-typia/block-types/blocks/registration`
22
24
  - `@wp-typia/block-types/blocks/supports`
25
+ - `@wp-typia/block-types/blocks/variations`
23
26
 
24
27
  ## Current policy
25
28
 
@@ -43,6 +46,12 @@ Shared WordPress block semantic types derived from Gutenberg source and unoffici
43
46
  migration-facing `BlockInstance`
44
47
  - additive stable Core coverage for drop caps, spacing sizes, layout gaps,
45
48
  duotone, per-side border widths, and `js` / `locking`
49
+ - a WordPress block API compatibility matrix for Supports, Variations, and
50
+ Bindings features that codegen and diagnostics can share
51
+ - typed Block Variations authoring helpers with static JavaScript registration
52
+ source generation
53
+ - typed Block Bindings source helpers with PHP/editor source generation and
54
+ `metadata.bindings` type helpers
46
55
 
47
56
  ## Registration facade
48
57
 
@@ -71,6 +80,80 @@ TypeScript installs surface the requirement explicitly.
71
80
  Compatibility should track that floor unless the generated project dependency
72
81
  matrix changes in the same release.
73
82
 
83
+ ## WordPress block API compatibility
84
+
85
+ `@wp-typia/block-types/blocks/compatibility` exposes the shared compatibility
86
+ foundation used by future block Supports, Variations, and Bindings helpers.
87
+ The matrix records documented WordPress version floors, runtime surfaces, derived
88
+ attributes, fallback hints, and source URLs for feature checks.
89
+
90
+ ```ts
91
+ import { createWordPressBlockApiCompatibilityManifest } from '@wp-typia/block-types/blocks/compatibility';
92
+
93
+ const manifest = createWordPressBlockApiCompatibilityManifest(
94
+ [
95
+ { area: 'blockSupports', feature: 'allowedBlocks' },
96
+ { area: 'blockBindings', feature: 'editorRegistration' },
97
+ ],
98
+ {
99
+ minVersion: '6.7',
100
+ strict: true,
101
+ allowUnknownFutureKeys: false,
102
+ },
103
+ );
104
+ ```
105
+
106
+ Strict mode marks known unsupported features as errors and recommends skipping
107
+ generation. Non-strict mode downgrades them to warnings and recommends guarded
108
+ generation. Unknown future keys are guarded by default, or passed through only
109
+ when `allowUnknownFutureKeys` is enabled.
110
+
111
+ ## Diagnostic output policy
112
+
113
+ Supports, Variations, and Bindings helpers keep diagnostics structured and
114
+ silent by default. Strict diagnostics still throw grouped errors, but non-strict
115
+ warnings do not write to `console.warn` unless a consumer opts into visible
116
+ output.
117
+
118
+ Use `onDiagnostic` when callers need callback-driven diagnostics for tests,
119
+ custom reporting, or UI integration:
120
+
121
+ ```ts
122
+ const diagnostics: Array<{ message: string; severity: 'warning' | 'error' }> =
123
+ [];
124
+
125
+ defineSupports(
126
+ {
127
+ allowedBlocks: true,
128
+ minWordPress: '6.8',
129
+ strict: false,
130
+ },
131
+ {
132
+ onDiagnostic: (diagnostic) => {
133
+ diagnostics.push(diagnostic);
134
+ },
135
+ },
136
+ );
137
+ ```
138
+
139
+ Use `logger` when callers want formatted warning messages without taking over
140
+ the structured callback path. Passing `console` restores console warning output.
141
+ If both `onDiagnostic` and `logger` are provided, `onDiagnostic` handles the
142
+ diagnostic and the logger is not called.
143
+
144
+ ```ts
145
+ defineSupports(
146
+ {
147
+ allowedBlocks: true,
148
+ minWordPress: '6.8',
149
+ strict: false,
150
+ },
151
+ {
152
+ logger: console,
153
+ },
154
+ );
155
+ ```
156
+
74
157
  ## Validation coverage
75
158
 
76
159
  `@wp-typia/block-types` now validates itself with a mixed strategy that matches
@@ -88,35 +171,73 @@ breakage in scaffold or runtime packages.
88
171
 
89
172
  ## WordPress style support helpers
90
173
 
91
- The package now exposes two complementary surfaces:
174
+ The package now exposes three complementary surfaces:
92
175
 
93
- - `@wp-typia/block-types/blocks/supports` for typed `block.json` `supports`
176
+ - `@wp-typia/block-types/blocks/supports` for typed `block.json` `supports`,
177
+ the `defineSupports()` helper, and `SupportAttributes<typeof supports>`
94
178
  - `@wp-typia/block-types/block-editor/style-attributes` for the attribute and `style` shapes WordPress injects when those supports are enabled
179
+ - `@wp-typia/block-types/blocks/compatibility` for version-aware diagnostics
180
+ when a support key requires a newer WordPress floor
95
181
 
96
182
  Example:
97
183
 
98
184
  ```ts
99
- import type { BlockStyleSupportAttributes } from '@wp-typia/block-types/block-editor/style-attributes';
100
- import type { BlockSupports } from '@wp-typia/block-types/blocks/supports';
185
+ import {
186
+ defineSupports,
187
+ type SupportAttributes,
188
+ } from '@wp-typia/block-types/blocks/supports';
101
189
 
102
- const supports: BlockSupports = {
103
- color: { text: true, background: true, gradients: true, enableAlpha: true },
190
+ const supports = defineSupports({
191
+ minWordPress: '6.6',
192
+ color: { text: true, background: true },
104
193
  spacing: {
105
194
  padding: true,
106
- units: ['px', 'rem'],
107
- spacingSizes: [{ name: 'Large', slug: 'large', size: '2rem' }],
195
+ margin: true,
196
+ blockGap: true,
197
+ },
198
+ typography: {
199
+ fontSize: true,
200
+ lineHeight: true,
201
+ letterSpacing: true,
202
+ textAlign: ['left', 'center'],
203
+ },
204
+ layout: {
205
+ default: {
206
+ type: 'constrained',
207
+ },
108
208
  },
109
- typography: { fontSize: true, dropCap: true },
110
- js: true,
111
- locking: true,
209
+ anchor: true,
210
+ html: false,
211
+ });
212
+
213
+ type OwnAttributes = {
214
+ content: string;
215
+ density: 'compact' | 'balanced' | 'airy';
112
216
  };
113
217
 
114
- type MyBlockStyleAttrs = Pick<
115
- BlockStyleSupportAttributes,
116
- 'backgroundColor' | 'fontSize' | 'style' | 'textColor'
117
- >;
218
+ type BlockAttributes = OwnAttributes & SupportAttributes<typeof supports>;
118
219
  ```
119
220
 
221
+ `defineSupports()` returns a plain object that can be written directly to
222
+ `block.json.supports`; inline helper controls such as `minWordPress`, `strict`,
223
+ and `allowUnknownFutureKeys` are stripped from the returned metadata. The helper
224
+ stores its compatibility manifest on a non-enumerable symbol so generated JSON
225
+ stays clean while tests and codegen can still inspect diagnostics through
226
+ `getDefinedSupportsCompatibilityManifest()`.
227
+
228
+ Strict mode is enabled by default. Known support keys that require a newer
229
+ WordPress version throw, while `strict: false` reports warnings through
230
+ `onDiagnostic` and keeps the metadata pass-through. Unknown future top-level
231
+ support keys are rejected unless `allowUnknownFutureKeys` is enabled.
232
+
233
+ `SupportAttributes<typeof supports>` is intentionally conservative where
234
+ Gutenberg behavior is broad. Enabling color, spacing, typography, border,
235
+ dimensions, background, filter duotone, position, or shadow includes the shared
236
+ `style` attribute shape. Typography also exposes slug attributes such as
237
+ `fontSize` and `fontFamily` when typography support is enabled; the compatibility
238
+ matrix tracks version-gated typography keys like `textAlign` separately from
239
+ longstanding keys such as `dropCap`.
240
+
120
241
  Stable Core coverage now also includes support/style helpers for layout
121
242
  `rowGap` / `columnGap`, color `duotone`, per-side border widths, and other
122
243
  recently stabilized support keys.
@@ -126,6 +247,179 @@ for blocks that compute style output on the server. This follows Gutenberg's
126
247
  current experimental surface and should not be treated as a long-term
127
248
  stability guarantee.
128
249
 
250
+ ## WordPress block variation helpers
251
+
252
+ `@wp-typia/block-types/blocks/variations` provides a type-first layer over the
253
+ native Block Variations API. `defineVariation()` returns a plain variation
254
+ metadata object suitable for `registerBlockVariation(...)`; the target block name
255
+ and compatibility manifest are stored on a non-enumerable symbol so generated
256
+ registration code can still recover the block target.
257
+
258
+ Static registration source generation serializes JSON-compatible variation
259
+ metadata. Function-based `isActive` callbacks are accepted by the type helper,
260
+ but `createStaticBlockVariationRegistrationSource()` rejects them; use a dynamic
261
+ registration path aligned with the `blockVariations.phpVariationCallback`
262
+ compatibility entry when callbacks must stay executable.
263
+
264
+ ```ts
265
+ import {
266
+ createStaticBlockVariationRegistrationSource,
267
+ defineVariation,
268
+ defineVariations,
269
+ } from '@wp-typia/block-types/blocks/variations';
270
+
271
+ type HeadingVariationAttributes = {
272
+ className?: string;
273
+ level?: number;
274
+ };
275
+
276
+ const paragraphVariation = defineVariation('core/paragraph', {
277
+ name: 'example-balanced-paragraph',
278
+ title: 'Balanced Paragraph',
279
+ attributes: {
280
+ className: 'is-style-example-balanced',
281
+ },
282
+ scope: ['inserter', 'transform'],
283
+ isActive: ['className'],
284
+ });
285
+
286
+ const headingVariation = defineVariation<HeadingVariationAttributes>(
287
+ 'core/heading',
288
+ {
289
+ name: 'example-balanced-heading',
290
+ title: 'Balanced Heading',
291
+ attributes: {
292
+ className: 'is-style-example-heading',
293
+ level: 2,
294
+ },
295
+ scope: ['inserter', 'transform'],
296
+ isActive: ['className', 'level'],
297
+ },
298
+ );
299
+
300
+ const variations = defineVariations([
301
+ paragraphVariation,
302
+ headingVariation,
303
+ ] as const);
304
+
305
+ const registrationSource =
306
+ createStaticBlockVariationRegistrationSource(variations);
307
+ ```
308
+
309
+ Custom block variations use the same helper. Pass the custom block name and a
310
+ local attribute type when the variation should be checked against project-owned
311
+ metadata:
312
+
313
+ ```ts
314
+ type TestimonialAttributes = {
315
+ className?: string;
316
+ layout?: 'quote' | 'card';
317
+ };
318
+
319
+ export const featuredTestimonialVariation =
320
+ defineVariation<TestimonialAttributes>('acme/testimonial', {
321
+ name: 'acme-featured-testimonial',
322
+ title: 'Featured Testimonial',
323
+ attributes: {
324
+ className: 'is-style-acme-featured',
325
+ layout: 'card',
326
+ },
327
+ isActive: ['className', 'layout'],
328
+ });
329
+ ```
330
+
331
+ `defineVariation()` warns when active-state detection is missing or points at an
332
+ attribute not present in the variation metadata. Use `allowMissingIsActive: true`
333
+ for intentionally passive/default-style variations. `defineVariations()` detects
334
+ duplicate variation names and shared active discriminators for the same target
335
+ block.
336
+
337
+ Static code generation currently targets editor-side
338
+ `registerBlockVariation(...)` calls. Function-based `isActive` callbacks are
339
+ accepted by the type helper, but static source generation rejects them because
340
+ they cannot be represented safely as JSON-backed registration metadata. PHP or
341
+ dynamic variation registration remains scoped to a future helper built on the
342
+ existing `blockVariations.phpVariationCallback` compatibility entry.
343
+
344
+ ## WordPress block binding helpers
345
+
346
+ `@wp-typia/block-types/blocks/bindings` provides a type-first layer for the
347
+ Block Bindings API. `defineBindingSource()` returns a plain source metadata
348
+ object, while compatibility diagnostics and codegen metadata are stored on a
349
+ non-enumerable symbol.
350
+
351
+ The helper tracks the documented WordPress floors for server registration
352
+ (`6.5`), editor registration (`6.7`), and editor field lists/custom bindable
353
+ attribute filters (`6.9`). Strict mode throws for unsupported combinations;
354
+ `strict: false` reports diagnostics and omits unsupported editor-only output.
355
+
356
+ ```ts
357
+ import {
358
+ createEditorBindingSourceRegistrationSource,
359
+ createPhpBindingSourceRegistrationSource,
360
+ defineBindableAttributes,
361
+ defineBindingSource,
362
+ defineBlockMetadataBindings,
363
+ type Binding,
364
+ } from '@wp-typia/block-types/blocks/bindings';
365
+
366
+ type ProfileCardAttributes = {
367
+ imageUrl?: string;
368
+ };
369
+
370
+ const profileSource = defineBindingSource({
371
+ name: 'example/profile-data',
372
+ label: 'Profile Data',
373
+ getValueCallback: 'example_get_profile_binding_value',
374
+ usesContext: ['postId'],
375
+ minWordPress: {
376
+ server: '6.5',
377
+ editor: '6.7',
378
+ fieldsList: '6.9',
379
+ supportedAttributesFilter: '6.9',
380
+ },
381
+ args: {
382
+ field: 'image_url' as 'display_name' | 'image_url',
383
+ },
384
+ fields: [
385
+ {
386
+ name: 'display_name',
387
+ label: 'Display name',
388
+ args: { field: 'display_name' },
389
+ type: 'string',
390
+ },
391
+ {
392
+ name: 'image_url',
393
+ label: 'Image URL',
394
+ args: { field: 'image_url' },
395
+ type: 'string',
396
+ },
397
+ ],
398
+ bindableAttributes: [
399
+ defineBindableAttributes<ProfileCardAttributes>('example/profile-card', [
400
+ 'imageUrl',
401
+ ] as const),
402
+ ],
403
+ });
404
+
405
+ const metadata = defineBlockMetadataBindings({
406
+ imageUrl: {
407
+ source: profileSource.name,
408
+ args: { field: 'image_url' },
409
+ } satisfies Binding<typeof profileSource, { field: 'image_url' }>,
410
+ });
411
+
412
+ const phpSource = createPhpBindingSourceRegistrationSource(profileSource);
413
+ const editorSource = createEditorBindingSourceRegistrationSource(profileSource);
414
+ ```
415
+
416
+ The generated PHP registers `register_block_bindings_source()` on `init` and,
417
+ when custom bindable attributes are declared, adds the
418
+ `block_bindings_supported_attributes_{$block_type}` filter behind a WordPress
419
+ 6.9-compatible guard. The generated editor source registers
420
+ `registerBlockBindingsSource()` and emits `getFieldsList()` only when the
421
+ source compatibility manifest marks field lists as supported.
422
+
129
423
  ## Typia pipeline notes
130
424
 
131
425
  - `CssColorValue` and `MinHeightValue` are richer DX aliases that currently rely on template literal types.
@@ -0,0 +1,13 @@
1
+ import { type DefinedBindingSource } from "./bindings-core.js";
2
+ export interface CreatePhpBindingSourceRegistrationSourceOptions {
3
+ readonly functionName?: string;
4
+ readonly hook?: string;
5
+ readonly includeOpeningTag?: boolean;
6
+ readonly textDomain?: string;
7
+ }
8
+ export interface CreateEditorBindingSourceRegistrationSourceOptions {
9
+ readonly functionName?: string;
10
+ readonly importSource?: string;
11
+ }
12
+ export declare function createPhpBindingSourceRegistrationSource(sources: DefinedBindingSource | readonly DefinedBindingSource[], options?: CreatePhpBindingSourceRegistrationSourceOptions): string;
13
+ export declare function createEditorBindingSourceRegistrationSource(sources: DefinedBindingSource | readonly DefinedBindingSource[], options?: CreateEditorBindingSourceRegistrationSourceOptions): string;
@@ -0,0 +1,210 @@
1
+ import { createBindingSourceRegistrationPlan, } from "./bindings-core.js";
2
+ import { normalizeStaticRegistrationValue } from "./shared/static-registration.js";
3
+ function getBindingEvaluation(metadata, feature) {
4
+ return metadata.manifest.evaluations.find((evaluation) => evaluation.area === "blockBindings" && evaluation.feature === feature);
5
+ }
6
+ function shouldGenerateFeature(metadata, feature) {
7
+ const evaluation = getBindingEvaluation(metadata, feature);
8
+ return evaluation?.action === "generate";
9
+ }
10
+ function asSourceList(sources) {
11
+ if (Array.isArray(sources)) {
12
+ return sources;
13
+ }
14
+ return [sources];
15
+ }
16
+ function escapePhpSingleQuotedString(value) {
17
+ return value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'");
18
+ }
19
+ function phpString(value) {
20
+ return `'${escapePhpSingleQuotedString(value)}'`;
21
+ }
22
+ function phpStringArray(values) {
23
+ return values.length === 0
24
+ ? "array()"
25
+ : `array( ${values.map((value) => phpString(value)).join(", ")} )`;
26
+ }
27
+ const PHP_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
28
+ function sanitizePhpIdentifier(value) {
29
+ if (PHP_IDENTIFIER_PATTERN.test(value)) {
30
+ return value;
31
+ }
32
+ const sanitized = value.replace(/[^A-Za-z0-9_]/gu, "_").replace(/_+/gu, "_");
33
+ return /^[A-Za-z_]/u.test(sanitized) ? sanitized : `wp_typia_${sanitized}`;
34
+ }
35
+ function createPhpIdentifierHash(value) {
36
+ let hash = 0x811c9dc5;
37
+ for (const character of value) {
38
+ hash ^= character.codePointAt(0) ?? 0;
39
+ hash = Math.imul(hash, 0x01000193) >>> 0;
40
+ }
41
+ return hash.toString(36);
42
+ }
43
+ function createPhpSourceProperties(source, options) {
44
+ const properties = [];
45
+ if (source.label) {
46
+ const label = options.textDomain
47
+ ? `__( ${phpString(source.label)}, ${phpString(options.textDomain)} )`
48
+ : phpString(source.label);
49
+ properties.push(`'label' => ${label}`);
50
+ }
51
+ if (source.usesContext && source.usesContext.length > 0) {
52
+ properties.push(`'uses_context' => ${phpStringArray(source.usesContext)}`);
53
+ }
54
+ if (source.getValueCallback) {
55
+ properties.push(`'get_value_callback' => ${phpString(source.getValueCallback)}`);
56
+ }
57
+ return properties;
58
+ }
59
+ function createPhpBindableAttributeFilterSource(entries, functionName) {
60
+ const lines = [];
61
+ const targets = entries.flatMap((entry) => (entry.source.bindableAttributes ?? []).map((target, index) => ({
62
+ index,
63
+ metadata: entry.metadata,
64
+ sourceName: entry.source.name,
65
+ target,
66
+ })));
67
+ if (targets.length === 0) {
68
+ return lines;
69
+ }
70
+ lines.push("");
71
+ lines.push("if ( function_exists( 'get_block_bindings_supported_attributes' ) ) {");
72
+ for (const [targetIndex, target] of targets.entries()) {
73
+ if (!shouldGenerateFeature(target.metadata, "supportedAttributesFilter")) {
74
+ lines.push(`\t// block_bindings_supported_attributes requires WordPress 6.9+ for ${target.target.blockName}.`);
75
+ continue;
76
+ }
77
+ const callbackSeed = [
78
+ functionName,
79
+ target.sourceName,
80
+ target.target.blockName,
81
+ String(target.index),
82
+ String(targetIndex),
83
+ ].join("\0");
84
+ const callbackName = sanitizePhpIdentifier(`${functionName}_${target.sourceName}_${target.target.blockName}_${target.index}_${targetIndex}_${createPhpIdentifierHash(callbackSeed)}_supported_attributes`);
85
+ lines.push(`\tadd_filter( ${phpString(`block_bindings_supported_attributes_${target.target.blockName}`)}, ${phpString(callbackName)} );`);
86
+ lines.push(`\tfunction ${callbackName}( $supported_attributes ) {`);
87
+ lines.push(`\t\treturn array_values( array_unique( array_merge( $supported_attributes, ${phpStringArray(target.target.attributes)} ) ) );`);
88
+ lines.push("\t}");
89
+ }
90
+ lines.push("}");
91
+ return lines;
92
+ }
93
+ export function createPhpBindingSourceRegistrationSource(sources, options = {}) {
94
+ const functionName = sanitizePhpIdentifier(options.functionName ?? "wp_typia_register_block_binding_sources");
95
+ const hook = options.hook ?? "init";
96
+ const entries = createBindingSourceRegistrationPlan(asSourceList(sources));
97
+ const lines = [];
98
+ if (options.includeOpeningTag ?? false) {
99
+ lines.push("<?php");
100
+ lines.push("");
101
+ }
102
+ lines.push(`function ${functionName}() {`);
103
+ lines.push("\tif ( ! function_exists( 'register_block_bindings_source' ) ) {");
104
+ lines.push("\t\treturn;");
105
+ lines.push("\t}");
106
+ lines.push("");
107
+ for (const entry of entries) {
108
+ if (!shouldGenerateFeature(entry.metadata, "serverRegistration")) {
109
+ lines.push(`\t// register_block_bindings_source() requires WordPress 6.5+ for ${entry.source.name}.`);
110
+ continue;
111
+ }
112
+ const properties = createPhpSourceProperties(entry.source, options);
113
+ lines.push(`\tregister_block_bindings_source( ${phpString(entry.source.name)}, array(`);
114
+ for (const property of properties) {
115
+ lines.push(`\t\t${property},`);
116
+ }
117
+ lines.push("\t) );");
118
+ }
119
+ lines.push("}");
120
+ lines.push(`add_action( ${phpString(hook)}, ${phpString(functionName)} );`);
121
+ lines.push(...createPhpBindableAttributeFilterSource(entries, functionName));
122
+ lines.push("");
123
+ return lines.join("\n");
124
+ }
125
+ function createEditorRegistrationEntries(entries) {
126
+ const fields = {};
127
+ const skipped = [];
128
+ const skippedFields = [];
129
+ const sources = [];
130
+ for (const entry of entries) {
131
+ const editorEvaluation = getBindingEvaluation(entry.metadata, "editorRegistration");
132
+ if (editorEvaluation === undefined) {
133
+ continue;
134
+ }
135
+ if (editorEvaluation.action !== "generate") {
136
+ skipped.push(entry.source.name);
137
+ continue;
138
+ }
139
+ sources.push(normalizeStaticRegistrationValue({
140
+ label: entry.source.label,
141
+ name: entry.source.name,
142
+ usesContext: entry.source.usesContext,
143
+ }, `sources.${entry.source.name}`, { description: "binding source" }));
144
+ if ((entry.source.fields?.length ?? 0) === 0) {
145
+ continue;
146
+ }
147
+ if (!shouldGenerateFeature(entry.metadata, "editorFieldsList")) {
148
+ skippedFields.push(entry.source.name);
149
+ continue;
150
+ }
151
+ fields[entry.source.name] = normalizeStaticRegistrationValue(entry.source.fields, `fields.${entry.source.name}`, { description: "binding source" });
152
+ }
153
+ return {
154
+ fields,
155
+ skipped,
156
+ skippedFields,
157
+ sources,
158
+ };
159
+ }
160
+ export function createEditorBindingSourceRegistrationSource(sources, options = {}) {
161
+ const importSource = options.importSource ?? "@wordpress/blocks";
162
+ const functionName = options.functionName ?? "registerWpTypiaBindingSources";
163
+ const entries = createBindingSourceRegistrationPlan(asSourceList(sources));
164
+ const registration = createEditorRegistrationEntries(entries);
165
+ if (registration.sources.length === 0) {
166
+ return [
167
+ "// No editor block binding sources were generated.",
168
+ ...(registration.skipped.length > 0
169
+ ? [
170
+ `// Skipped editor registration for ${registration.skipped.join(", ")}; registerBlockBindingsSource() requires WordPress 6.7+.`,
171
+ ]
172
+ : []),
173
+ "",
174
+ ].join("\n");
175
+ }
176
+ const serializedSources = JSON.stringify(registration.sources, null, 2);
177
+ const serializedFields = JSON.stringify(registration.fields, null, 2);
178
+ const hasGeneratedFields = Object.keys(registration.fields).length > 0;
179
+ const lines = [
180
+ `import { registerBlockBindingsSource } from ${JSON.stringify(importSource)};`,
181
+ "",
182
+ `const sources = ${serializedSources};`,
183
+ ...(hasGeneratedFields ? [`const fieldsBySource = ${serializedFields};`] : []),
184
+ "",
185
+ `export function ${functionName}() {`,
186
+ " if (typeof registerBlockBindingsSource !== \"function\") {",
187
+ " return;",
188
+ " }",
189
+ " for (const source of sources) {",
190
+ ...(hasGeneratedFields
191
+ ? [
192
+ " const fields = fieldsBySource[source.name];",
193
+ " registerBlockBindingsSource({",
194
+ " ...source,",
195
+ " ...(fields ? { getFieldsList: () => fields } : {}),",
196
+ " });",
197
+ ]
198
+ : [" registerBlockBindingsSource(source);"]),
199
+ " }",
200
+ "}",
201
+ ];
202
+ if (registration.skipped.length > 0) {
203
+ lines.push("", `// Skipped editor registration for ${registration.skipped.join(", ")}; registerBlockBindingsSource() requires WordPress 6.7+.`);
204
+ }
205
+ if (registration.skippedFields.length > 0) {
206
+ lines.push("", `// Skipped getFieldsList() for ${registration.skippedFields.join(", ")}; getFieldsList() requires WordPress 6.9+.`);
207
+ }
208
+ lines.push("");
209
+ return lines.join("\n");
210
+ }