@wp-typia/project-tools 0.24.10 → 0.24.12

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.
Files changed (68) hide show
  1. package/dist/runtime/add/cli-add-block-config.js +7 -15
  2. package/dist/runtime/add/cli-add-block-package-json.d.ts +5 -0
  3. package/dist/runtime/add/cli-add-block-package-json.js +32 -0
  4. package/dist/runtime/add/cli-add-block.js +17 -4
  5. package/dist/runtime/add/cli-add-workspace-admin-view-scaffold.js +1 -5
  6. package/dist/runtime/add/persistence-rest-artifacts.d.ts +32 -7
  7. package/dist/runtime/add/persistence-rest-artifacts.js +41 -15
  8. package/dist/runtime/cli/cli-diagnostics.d.ts +9 -0
  9. package/dist/runtime/cli/cli-diagnostics.js +8 -0
  10. package/dist/runtime/cli/cli-init-package-json.js +5 -1
  11. package/dist/runtime/doctor/cli-doctor-standalone-control-flow.d.ts +38 -0
  12. package/dist/runtime/doctor/cli-doctor-standalone-control-flow.js +365 -0
  13. package/dist/runtime/doctor/cli-doctor-standalone-rest.d.ts +21 -0
  14. package/dist/runtime/doctor/cli-doctor-standalone-rest.js +1373 -0
  15. package/dist/runtime/doctor/cli-doctor-standalone.d.ts +32 -0
  16. package/dist/runtime/doctor/cli-doctor-standalone.js +2613 -0
  17. package/dist/runtime/doctor/cli-doctor-wordpress-version.js +6 -3
  18. package/dist/runtime/doctor/cli-doctor-workspace.d.ts +5 -3
  19. package/dist/runtime/doctor/cli-doctor-workspace.js +27 -3
  20. package/dist/runtime/doctor/cli-doctor.d.ts +4 -2
  21. package/dist/runtime/doctor/cli-doctor.js +4 -2
  22. package/dist/runtime/shared/json-utils.d.ts +7 -0
  23. package/dist/runtime/shared/json-utils.js +10 -0
  24. package/dist/runtime/shared/package-json-types.d.ts +1 -1
  25. package/dist/runtime/shared/package-versions.d.ts +2 -0
  26. package/dist/runtime/shared/package-versions.js +2 -0
  27. package/dist/runtime/shared/php-utils.d.ts +39 -2
  28. package/dist/runtime/shared/php-utils.js +205 -6
  29. package/dist/runtime/templates/block-generator-service-core.d.ts +1 -1
  30. package/dist/runtime/templates/block-generator-service-core.js +9 -3
  31. package/dist/runtime/templates/block-generator-service-spec.d.ts +5 -0
  32. package/dist/runtime/templates/built-in-block-artifact-documents.d.ts +1 -1
  33. package/dist/runtime/templates/built-in-block-artifact-documents.js +1 -1
  34. package/dist/runtime/templates/built-in-block-artifact-types.js +2 -2
  35. package/dist/runtime/templates/built-in-block-artifacts.d.ts +1 -1
  36. package/dist/runtime/templates/built-in-block-artifacts.js +16 -12
  37. package/dist/runtime/templates/built-in-block-attribute-emitters.d.ts +5 -0
  38. package/dist/runtime/templates/built-in-block-attribute-emitters.js +113 -8
  39. package/dist/runtime/templates/built-in-block-attribute-specs.js +4 -9
  40. package/dist/runtime/templates/built-in-block-code-artifacts.js +6 -1
  41. package/dist/runtime/templates/built-in-block-code-templates/compound-persistence.d.ts +2 -2
  42. package/dist/runtime/templates/built-in-block-code-templates/compound-persistence.js +24 -1
  43. package/dist/runtime/templates/built-in-block-code-templates/persistence.d.ts +2 -2
  44. package/dist/runtime/templates/built-in-block-code-templates/persistence.js +22 -1
  45. package/dist/runtime/templates/built-in-block-non-ts-compound-templates.d.ts +2 -2
  46. package/dist/runtime/templates/built-in-block-non-ts-compound-templates.js +6 -2
  47. package/dist/runtime/templates/built-in-block-non-ts-persistence-templates.d.ts +2 -2
  48. package/dist/runtime/templates/built-in-block-non-ts-persistence-templates.js +6 -2
  49. package/dist/runtime/templates/cli-scaffold-emission.d.ts +7 -1
  50. package/dist/runtime/templates/cli-scaffold-emission.js +10 -1
  51. package/dist/runtime/templates/cli-templates.d.ts +47 -0
  52. package/dist/runtime/templates/cli-templates.js +50 -1
  53. package/dist/runtime/templates/local-dev-presets.js +1 -1
  54. package/dist/runtime/templates/scaffold-apply-utils.d.ts +4 -8
  55. package/dist/runtime/templates/scaffold-apply-utils.js +44 -28
  56. package/dist/runtime/templates/scaffold-compiler-artifacts.d.ts +17 -0
  57. package/dist/runtime/templates/scaffold-compiler-artifacts.js +81 -0
  58. package/dist/runtime/templates/scaffold-onboarding.d.ts +5 -0
  59. package/dist/runtime/templates/scaffold-onboarding.js +13 -2
  60. package/dist/runtime/templates/scaffold-template-variable-groups.d.ts +4 -0
  61. package/dist/runtime/templates/scaffold-template-variable-groups.js +7 -0
  62. package/dist/runtime/templates/scaffold.d.ts +3 -1
  63. package/dist/runtime/templates/scaffold.js +2 -1
  64. package/dist/runtime/templates/starter-manifests.js +1 -1
  65. package/package.json +3 -3
  66. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +1 -1
  67. package/templates/_shared/compound/persistence/package.json.mustache +1 -0
  68. package/templates/_shared/persistence/core/package.json.mustache +1 -0
@@ -32,6 +32,6 @@ export declare function buildBuiltInBlockArtifacts({ templateId, variables, }: {
32
32
  * Serializes a generated `block.json` document using scaffold formatting.
33
33
  *
34
34
  * @param document Structured `block.json` document.
35
- * @returns Pretty-printed JSON with a trailing newline.
35
+ * @returns Pretty-printed JSON using tab indentation.
36
36
  */
37
37
  export declare function stringifyBuiltInBlockJsonDocument(document: Record<string, unknown>): string;
@@ -1,8 +1,8 @@
1
- import { buildBasicAttributes, buildBlockJsonAttributes, buildCompoundChildAttributes, buildCompoundParentAttributes, buildInteractivityAttributes, buildManifestDocument, buildPersistenceAttributes, DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER, } from "./built-in-block-artifact-documents.js";
1
+ import { buildBasicAttributes, buildBlockJsonAttributes, buildBlockJsonExampleAttributes, buildCompoundChildAttributes, buildCompoundParentAttributes, buildInteractivityAttributes, buildManifestDocument, buildPersistenceAttributes, DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER, } from "./built-in-block-artifact-documents.js";
2
2
  import { buildBasicTypesSource, buildCompoundChildTypesSource, buildCompoundTypesSource, buildInteractivityTypesSource, buildPersistenceTypesSource, } from "./built-in-block-artifact-types.js";
3
3
  import { getScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
4
4
  function stringifyBlockJsonDocument(document) {
5
- return `${JSON.stringify(document, null, "\t")}\n`;
5
+ return JSON.stringify(document, null, "\t");
6
6
  }
7
7
  function buildBasicArtifact(variables) {
8
8
  const attributes = buildBasicAttributes();
@@ -18,11 +18,7 @@ function buildBasicArtifact(variables) {
18
18
  description: variables.description,
19
19
  keywords: [variables.keyword, "typia", "block"],
20
20
  example: {
21
- attributes: {
22
- content: "Example content",
23
- alignment: "center",
24
- isVisible: true,
25
- },
21
+ attributes: buildBlockJsonExampleAttributes(attributes),
26
22
  },
27
23
  supports: {
28
24
  html: false,
@@ -50,7 +46,9 @@ function buildInteractivityArtifact(variables) {
50
46
  category: variables.category,
51
47
  icon: variables.icon,
52
48
  description: variables.description,
53
- example: {},
49
+ example: {
50
+ attributes: buildBlockJsonExampleAttributes(attributes),
51
+ },
54
52
  supports: {
55
53
  html: false,
56
54
  align: true,
@@ -82,7 +80,9 @@ function buildPersistenceArtifact(variables) {
82
80
  category: variables.category,
83
81
  icon: variables.icon,
84
82
  description: variables.description,
85
- example: {},
83
+ example: {
84
+ attributes: buildBlockJsonExampleAttributes(attributes),
85
+ },
86
86
  supports: {
87
87
  html: false,
88
88
  align: true,
@@ -116,7 +116,9 @@ function buildCompoundParentArtifact(variables) {
116
116
  category: variables.category,
117
117
  icon: variables.icon,
118
118
  description: variables.description,
119
- example: {},
119
+ example: {
120
+ attributes: buildBlockJsonExampleAttributes(attributes),
121
+ },
120
122
  allowedBlocks: [`${variables.namespace}/${variables.slugKebabCase}-item`],
121
123
  supports: persistenceEnabled
122
124
  ? {
@@ -159,7 +161,9 @@ function buildCompoundChildArtifact(variables, bodyPlaceholder = DEFAULT_COMPOUN
159
161
  icon: variables.compoundChildIcon,
160
162
  description: `Internal item block used by ${variables.title}.`,
161
163
  parent: [`${variables.namespace}/${variables.slugKebabCase}`],
162
- example: {},
164
+ example: {
165
+ attributes: buildBlockJsonExampleAttributes(attributes),
166
+ },
163
167
  supports: {
164
168
  html: false,
165
169
  inserter: false,
@@ -216,7 +220,7 @@ export function buildBuiltInBlockArtifacts({ templateId, variables, }) {
216
220
  * Serializes a generated `block.json` document using scaffold formatting.
217
221
  *
218
222
  * @param document Structured `block.json` document.
219
- * @returns Pretty-printed JSON with a trailing newline.
223
+ * @returns Pretty-printed JSON using tab indentation.
220
224
  */
221
225
  export function stringifyBuiltInBlockJsonDocument(document) {
222
226
  return stringifyBlockJsonDocument(document);
@@ -68,4 +68,9 @@ export declare function buildManifestDocument(sourceType: string, attributes: re
68
68
  * @returns A block.json-compatible attributes record.
69
69
  */
70
70
  export declare function buildBlockJsonAttributes(attributes: readonly EmittedAttributeDefinition[]): Record<string, Record<string, unknown>>;
71
+ /**
72
+ * Builds the canonical block preview examples derived from emitted defaults,
73
+ * enums, and supported scalar constraints.
74
+ */
75
+ export declare function buildBlockJsonExampleAttributes(attributes: readonly EmittedAttributeDefinition[]): Record<string, JsonValue>;
71
76
  export {};
@@ -2,6 +2,91 @@
2
2
  * Default placeholder copy used for generated compound child body fields.
3
3
  */
4
4
  export const DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER = "Add supporting details for this internal item.";
5
+ const EXAMPLE_UUID = "00000000-0000-4000-8000-000000000000";
6
+ const STRING_EXAMPLES_BY_FORMAT = {
7
+ date: "2026-01-01",
8
+ "date-time": "2026-01-01T00:00:00.000Z",
9
+ email: "example@example.com",
10
+ hostname: "example.com",
11
+ ipv4: "192.0.2.1",
12
+ ipv6: "2001:db8::1",
13
+ time: "00:00:00Z",
14
+ uri: "https://example.com/",
15
+ uuid: EXAMPLE_UUID,
16
+ };
17
+ function createNumberExampleValue(attributeName, constraints) {
18
+ const minimum = constraints?.minimum ?? Number.NEGATIVE_INFINITY;
19
+ const exclusiveMinimum = constraints?.exclusiveMinimum ?? Number.NEGATIVE_INFINITY;
20
+ const maximum = constraints?.maximum ?? Number.POSITIVE_INFINITY;
21
+ const exclusiveMaximum = constraints?.exclusiveMaximum ?? Number.POSITIVE_INFINITY;
22
+ const lowerBound = Math.max(minimum, exclusiveMinimum);
23
+ const upperBound = Math.min(maximum, exclusiveMaximum);
24
+ const lowerIsExclusive = exclusiveMinimum >= minimum;
25
+ const upperIsExclusive = exclusiveMaximum <= maximum;
26
+ if (lowerBound > upperBound ||
27
+ (lowerBound === upperBound && (lowerIsExclusive || upperIsExclusive))) {
28
+ throw new Error(`Built-in block attribute "${attributeName}" has incompatible numeric bounds.`);
29
+ }
30
+ let candidate = 0;
31
+ if (candidate < lowerBound ||
32
+ (candidate === lowerBound && lowerIsExclusive) ||
33
+ candidate > upperBound ||
34
+ (candidate === upperBound && upperIsExclusive)) {
35
+ if (Number.isFinite(lowerBound) && Number.isFinite(upperBound)) {
36
+ candidate =
37
+ lowerBound === upperBound
38
+ ? lowerBound
39
+ : lowerBound / 2 + upperBound / 2;
40
+ }
41
+ else if (Number.isFinite(lowerBound)) {
42
+ candidate = lowerIsExclusive ? lowerBound + 1 : lowerBound;
43
+ }
44
+ else if (Number.isFinite(upperBound)) {
45
+ candidate = upperIsExclusive ? upperBound - 1 : upperBound;
46
+ }
47
+ }
48
+ return candidate;
49
+ }
50
+ /**
51
+ * Resolves a deterministic preview value from defaults, enums, scalar kinds,
52
+ * and the supported built-in constraint formats, in that priority order.
53
+ */
54
+ function createBlockJsonExampleValue(attribute) {
55
+ const { constraints, defaultValue, enumValues } = attribute.manifest;
56
+ if (defaultValue !== undefined) {
57
+ return defaultValue;
58
+ }
59
+ if (enumValues && enumValues.length > 0) {
60
+ return enumValues[0];
61
+ }
62
+ if (attribute.manifest.kind === "boolean") {
63
+ return true;
64
+ }
65
+ if (attribute.manifest.kind === "number") {
66
+ return createNumberExampleValue(attribute.name, constraints);
67
+ }
68
+ if (attribute.manifest.kind === "array") {
69
+ return [];
70
+ }
71
+ if (attribute.manifest.kind === "object") {
72
+ return {};
73
+ }
74
+ if (attribute.manifest.kind === "union") {
75
+ return null;
76
+ }
77
+ const format = constraints?.format;
78
+ const formattedExample = format
79
+ ? STRING_EXAMPLES_BY_FORMAT[format]
80
+ : undefined;
81
+ if (formattedExample) {
82
+ return formattedExample;
83
+ }
84
+ const example = `Example ${attribute.name}`;
85
+ const minLength = constraints?.minLength ?? 0;
86
+ return example.length >= minLength
87
+ ? example
88
+ : example.padEnd(minLength, "_");
89
+ }
5
90
  function createConstraints(overrides = {}) {
6
91
  return {
7
92
  exclusiveMaximum: null,
@@ -22,6 +107,11 @@ function createConstraints(overrides = {}) {
22
107
  function createManifestAttribute({ constraints, defaultValue, enumValues = null, kind, required, selector = null, source = null, sourceType, }) {
23
108
  const hasDefault = defaultValue !== undefined;
24
109
  return {
110
+ typia: {
111
+ constraints: createConstraints(constraints),
112
+ defaultValue: hasDefault ? defaultValue : null,
113
+ hasDefault,
114
+ },
25
115
  ts: {
26
116
  items: null,
27
117
  kind,
@@ -29,11 +119,6 @@ function createManifestAttribute({ constraints, defaultValue, enumValues = null,
29
119
  required,
30
120
  union: null,
31
121
  },
32
- typia: {
33
- constraints: createConstraints(constraints),
34
- defaultValue: hasDefault ? defaultValue : null,
35
- hasDefault,
36
- },
37
122
  wp: {
38
123
  defaultValue: hasDefault ? defaultValue : null,
39
124
  enum: enumValues,
@@ -121,8 +206,18 @@ function resolveBuiltInAttributeValue(value, context) {
121
206
  }
122
207
  return value;
123
208
  }
209
+ function appendWordPressExtractionTags(typeExpression, source, selector) {
210
+ return [
211
+ typeExpression,
212
+ ...(source ? [`tags.Source<${JSON.stringify(source)}>`] : []),
213
+ ...(selector ? [`tags.Selector<${JSON.stringify(selector)}>`] : []),
214
+ ].join(" & ");
215
+ }
124
216
  export function buildAttributesFromSpecs(specs, context) {
125
217
  return specs.map((spec) => {
218
+ const selector = resolveBuiltInAttributeValue(spec.selector, context);
219
+ const source = resolveBuiltInAttributeValue(spec.source, context);
220
+ const typeExpression = resolveBuiltInAttributeValue(spec.typeExpression, context) ?? "unknown";
126
221
  const resolvedSpec = {
127
222
  blockJsonDefaultValue: resolveBuiltInAttributeValue(spec.blockJsonDefaultValue, context),
128
223
  constraints: resolveBuiltInAttributeValue(spec.constraints, context),
@@ -132,9 +227,9 @@ export function buildAttributesFromSpecs(specs, context) {
132
227
  manifestDefaultValue: resolveBuiltInAttributeValue(spec.manifestDefaultValue, context),
133
228
  name: spec.name,
134
229
  optional: spec.optional,
135
- selector: resolveBuiltInAttributeValue(spec.selector, context),
136
- source: resolveBuiltInAttributeValue(spec.source, context),
137
- typeExpression: resolveBuiltInAttributeValue(spec.typeExpression, context) ?? "unknown",
230
+ selector,
231
+ source,
232
+ typeExpression: appendWordPressExtractionTags(typeExpression, source, selector),
138
233
  };
139
234
  if (spec.attributeType === "boolean") {
140
235
  return defineBooleanAttribute(resolvedSpec);
@@ -174,3 +269,13 @@ export function buildBlockJsonAttributes(attributes) {
174
269
  createBlockJsonAttribute(attribute.blockJson),
175
270
  ]));
176
271
  }
272
+ /**
273
+ * Builds the canonical block preview examples derived from emitted defaults,
274
+ * enums, and supported scalar constraints.
275
+ */
276
+ export function buildBlockJsonExampleAttributes(attributes) {
277
+ return Object.fromEntries(attributes.map((attribute) => [
278
+ attribute.name,
279
+ createBlockJsonExampleValue(attribute),
280
+ ]));
281
+ }
@@ -1,7 +1,6 @@
1
1
  import { isCompoundPersistenceEnabled } from "./scaffold-template-variable-groups.js";
2
2
  import { DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER, buildAttributesFromSpecs, describe, } from "./built-in-block-attribute-emitters.js";
3
- const ALIGNMENT_VALUES = ["left", "center", "right"];
4
- const BASIC_ALIGNMENT_VALUES = ["left", "center", "right", "justify"];
3
+ const ALIGNMENT_VALUES = ["left", "center", "right", "justify"];
5
4
  const INTERACTIVE_MODE_VALUES = ["click", "hover"];
6
5
  const ANIMATION_VALUES = ["none", "bounce", "pulse", "shake", "flip"];
7
6
  const BASIC_ATTRIBUTE_SPECS = [
@@ -20,7 +19,7 @@ const BASIC_ATTRIBUTE_SPECS = [
20
19
  attributeType: "string",
21
20
  defaultValue: "left",
22
21
  description: describe("Alignment"),
23
- enumValues: [...BASIC_ALIGNMENT_VALUES],
22
+ enumValues: [...ALIGNMENT_VALUES],
24
23
  name: "alignment",
25
24
  optional: true,
26
25
  typeExpression: 'TextAlignment & tags.Default<"left">',
@@ -196,15 +195,13 @@ const PERSISTENCE_ATTRIBUTE_SPECS = [
196
195
  },
197
196
  {
198
197
  attributeType: "string",
199
- blockJsonDefaultValue: "",
200
198
  constraints: {
201
199
  maxLength: 100,
202
200
  minLength: 1,
203
201
  },
204
- manifestDefaultValue: "primary",
205
202
  name: "resourceKey",
206
203
  optional: true,
207
- typeExpression: 'string & tags.MinLength<1> & tags.MaxLength<100> & tags.Default<"primary">',
204
+ typeExpression: "string & tags.MinLength<1> & tags.MaxLength<100>",
208
205
  },
209
206
  ];
210
207
  const COMPOUND_PARENT_BASE_ATTRIBUTE_SPECS = [
@@ -263,15 +260,13 @@ const COMPOUND_PARENT_PERSISTENCE_ATTRIBUTE_SPECS = [
263
260
  },
264
261
  {
265
262
  attributeType: "string",
266
- blockJsonDefaultValue: "",
267
263
  constraints: {
268
264
  maxLength: 100,
269
265
  minLength: 1,
270
266
  },
271
- manifestDefaultValue: "primary",
272
267
  name: "resourceKey",
273
268
  optional: true,
274
- typeExpression: 'string & tags.MinLength<1> & tags.MaxLength<100> & tags.Default<"primary">',
269
+ typeExpression: "string & tags.MinLength<1> & tags.MaxLength<100>",
275
270
  },
276
271
  ];
277
272
  const COMPOUND_CHILD_ATTRIBUTE_SPECS = [
@@ -3,9 +3,14 @@ import { BASIC_EDIT_TEMPLATE, BASIC_INDEX_TEMPLATE, BASIC_SAVE_TEMPLATE, BASIC_V
3
3
  import { getScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
4
4
  import { assertScaffoldTemplateCodeIdentifiers } from "./scaffold-template-assertions.js";
5
5
  import { renderMustacheTemplateString } from "./template-render.js";
6
+ // resourceKey is MaxLength<100>; generated scoped ids add "-" plus 9 characters.
7
+ const RESOURCE_KEY_PREFIX_MAX_LENGTH = 90;
6
8
  function renderCodeTemplate(template, variables) {
7
9
  assertScaffoldTemplateCodeIdentifiers(variables);
8
- const rendered = renderMustacheTemplateString(template, variables);
10
+ const rendered = renderMustacheTemplateString(template, {
11
+ ...variables,
12
+ resourceKeyPrefix: variables.slugKebabCase.slice(0, RESOURCE_KEY_PREFIX_MAX_LENGTH),
13
+ });
9
14
  return rendered.endsWith("\n") ? rendered : `${rendered}\n`;
10
15
  }
11
16
  function createCodeArtifact(relativePath, template, variables) {
@@ -1,4 +1,4 @@
1
- export declare const COMPOUND_PERSISTENCE_PARENT_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { __ } from '@wordpress/i18n';\nimport {\n\tInspectorControls,\n\tInnerBlocks,\n\tRichText,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport {\n\tNotice,\n\tPanelBody,\n\tTextControl,\n\tToggleControl,\n} from '@wordpress/components';\n\nimport {\n\tgetRootInnerBlocksPropsOptions,\n} from './children';\nimport { useTypiaValidation } from './hooks';\nimport type { {{pascalCase}}Attributes } from './types';\nimport {\n\tcreateAttributeUpdater,\n\tvalidate{{pascalCase}}Attributes,\n} from './validators';\n\ntype EditProps = BlockEditProps< {{pascalCase}}Attributes >;\ntype CompoundInnerBlocksProps = Parameters< typeof InnerBlocks >[ 0 ] & {\n\tdefaultBlock?: [ string, Record< string, unknown > ];\n\tdirectInsert?: boolean;\n};\n\nconst TypedInnerBlocks = InnerBlocks as unknown as (\n\tprops: CompoundInnerBlocksProps\n) => ReturnType< typeof InnerBlocks >;\n\nexport default function Edit( {\n\tattributes,\n\tsetAttributes,\n}: EditProps ) {\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}Attributes\n\t);\n\tconst updateAttribute = createAttributeUpdater( attributes, setAttributes );\n\tconst blockProps = useBlockProps( {\n\t\tclassName: '{{cssClassName}}',\n\t} );\n\tconst rootInnerBlocksPropsOptions = getRootInnerBlocksPropsOptions();\n\n\treturn (\n\t\t<>\n\t\t\t<InspectorControls>\n\t\t\t\t<PanelBody title={ __( 'Compound Settings', '{{textDomain}}' ) }>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Show dividers between items', '{{textDomain}}' ) }\n\t\t\t\t\t\tchecked={ attributes.showDividers ?? true }\n\t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showDividers', value ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Show persisted count', '{{textDomain}}' ) }\n\t\t\t\t\t\tchecked={ attributes.showCount ?? true }\n\t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showCount', value ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Button label', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.buttonLabel ?? 'Persist Count' }\n\t\t\t\t\t\tonChange={ ( buttonLabel ) => updateAttribute( 'buttonLabel', buttonLabel ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Resource key', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.resourceKey ?? '' }\n\t\t\t\t\t\tonChange={ ( resourceKey ) => updateAttribute( 'resourceKey', resourceKey ) }\n\t\t\t\t\t\thelp={ __( 'Stable key used by the persisted counter endpoint.', '{{textDomain}}' ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Storage mode: {{dataStorageMode}}', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Persistence policy: {{persistencePolicy}}', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t</PanelBody>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<PanelBody title={ __( 'Validation Errors', '{{textDomain}}' ) } initialOpen>\n\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => (\n\t\t\t\t\t\t\t<Notice key={ index } status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t\t\t{ error }\n\t\t\t\t\t\t\t</Notice>\n\t\t\t\t\t\t) ) }\n\t\t\t\t\t</PanelBody>\n\t\t\t\t) }\n\t\t\t</InspectorControls>\n\t\t\t<div { ...blockProps }>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"h3\"\n\t\t\t\t\tclassName=\"{{cssClassName}}__heading\"\n\t\t\t\t\tvalue={ attributes.heading }\n\t\t\t\t\tonChange={ ( heading ) => updateAttribute( 'heading', heading ) }\n\t\t\t\t\tplaceholder={ __( {{titleJson}}, '{{textDomain}}' ) }\n\t\t\t\t/>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"p\"\n\t\t\t\t\tclassName=\"{{cssClassName}}__intro\"\n\t\t\t\t\tvalue={ attributes.intro ?? '' }\n\t\t\t\t\tonChange={ ( intro ) => updateAttribute( 'intro', intro ) }\n\t\t\t\t\tplaceholder={ __(\n\t\t\t\t\t\t'Add and reorder internal items inside this compound block.',\n\t\t\t\t\t\t'{{textDomain}}'\n\t\t\t\t\t) }\n\t\t\t\t/>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<Notice status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</Notice>\n\t\t\t\t) }\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Resource key:', '{{textDomain}}' ) } { attributes.resourceKey || '\u2014' }\n\t\t\t\t</p>\n\t\t\t\t<div className=\"{{cssClassName}}__items\">\n\t\t\t\t\t<TypedInnerBlocks { ...rootInnerBlocksPropsOptions } />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n";
1
+ export declare const COMPOUND_PERSISTENCE_PARENT_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { __ } from '@wordpress/i18n';\nimport {\n\tInspectorControls,\n\tInnerBlocks,\n\tRichText,\n\tstore as blockEditorStore,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport {\n\tNotice,\n\tPanelBody,\n\tTextControl,\n\tToggleControl,\n} from '@wordpress/components';\nimport { useSelect } from '@wordpress/data';\n\nimport {\n\ttype PersistentBlockIdentityNode,\n\tusePersistentBlockIdentity,\n} from '@wp-typia/block-runtime/inspector';\nimport {\n\tgetRootInnerBlocksPropsOptions,\n} from './children';\nimport { useTypiaValidation } from './hooks';\nimport type { {{pascalCase}}Attributes } from './types';\nimport {\n\tcreateAttributeUpdater,\n\tvalidate{{pascalCase}}Attributes,\n} from './validators';\n\ntype EditProps = BlockEditProps< {{pascalCase}}Attributes >;\ntype CompoundInnerBlocksProps = Parameters< typeof InnerBlocks >[ 0 ] & {\n\tdefaultBlock?: [ string, Record< string, unknown > ];\n\tdirectInsert?: boolean;\n};\n\nconst TypedInnerBlocks = InnerBlocks as unknown as (\n\tprops: CompoundInnerBlocksProps\n) => ReturnType< typeof InnerBlocks >;\n\nexport default function Edit( {\n\tattributes,\n\tclientId,\n\tsetAttributes,\n}: EditProps ) {\n\tconst blocks = useSelect(\n\t\t( select ) =>\n\t\t\t( select( blockEditorStore ) as {\n\t\t\t\tgetBlocks: () => readonly PersistentBlockIdentityNode[];\n\t\t\t} ).getBlocks(),\n\t\t[]\n\t);\n\tusePersistentBlockIdentity( {\n\t\tattributeName: 'resourceKey',\n\t\tattributes,\n\t\tblockName: '{{namespace}}/{{slugKebabCase}}',\n\t\tblocks,\n\t\tclientId,\n\t\tprefix: '{{resourceKeyPrefix}}',\n\t\tsetAttributes,\n\t} );\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}Attributes\n\t);\n\tconst updateAttribute = createAttributeUpdater( attributes, setAttributes );\n\tconst blockProps = useBlockProps( {\n\t\tclassName: '{{cssClassName}}',\n\t} );\n\tconst rootInnerBlocksPropsOptions = getRootInnerBlocksPropsOptions();\n\n\treturn (\n\t\t<>\n\t\t\t<InspectorControls>\n\t\t\t\t<PanelBody title={ __( 'Compound Settings', '{{textDomain}}' ) }>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Show dividers between items', '{{textDomain}}' ) }\n\t\t\t\t\t\tchecked={ attributes.showDividers ?? true }\n\t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showDividers', value ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Show persisted count', '{{textDomain}}' ) }\n\t\t\t\t\t\tchecked={ attributes.showCount ?? true }\n\t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showCount', value ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Button label', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.buttonLabel ?? 'Persist Count' }\n\t\t\t\t\t\tonChange={ ( buttonLabel ) => updateAttribute( 'buttonLabel', buttonLabel ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Resource key', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.resourceKey ?? '' }\n\t\t\t\t\t\tonChange={ ( resourceKey ) => updateAttribute( 'resourceKey', resourceKey ) }\n\t\t\t\t\t\thelp={ __( 'Stable key used by the persisted counter endpoint.', '{{textDomain}}' ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Storage mode: {{dataStorageMode}}', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Persistence policy: {{persistencePolicy}}', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t</PanelBody>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<PanelBody title={ __( 'Validation Errors', '{{textDomain}}' ) } initialOpen>\n\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => (\n\t\t\t\t\t\t\t<Notice key={ index } status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t\t\t{ error }\n\t\t\t\t\t\t\t</Notice>\n\t\t\t\t\t\t) ) }\n\t\t\t\t\t</PanelBody>\n\t\t\t\t) }\n\t\t\t</InspectorControls>\n\t\t\t<div { ...blockProps }>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"h3\"\n\t\t\t\t\tclassName=\"{{cssClassName}}__heading\"\n\t\t\t\t\tvalue={ attributes.heading }\n\t\t\t\t\tonChange={ ( heading ) => updateAttribute( 'heading', heading ) }\n\t\t\t\t\tplaceholder={ __( {{titleJson}}, '{{textDomain}}' ) }\n\t\t\t\t/>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"p\"\n\t\t\t\t\tclassName=\"{{cssClassName}}__intro\"\n\t\t\t\t\tvalue={ attributes.intro ?? '' }\n\t\t\t\t\tonChange={ ( intro ) => updateAttribute( 'intro', intro ) }\n\t\t\t\t\tplaceholder={ __(\n\t\t\t\t\t\t'Add and reorder internal items inside this compound block.',\n\t\t\t\t\t\t'{{textDomain}}'\n\t\t\t\t\t) }\n\t\t\t\t/>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<Notice status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</Notice>\n\t\t\t\t) }\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Resource key:', '{{textDomain}}' ) } { attributes.resourceKey || '\u2014' }\n\t\t\t\t</p>\n\t\t\t\t<div className=\"{{cssClassName}}__items\">\n\t\t\t\t\t<TypedInnerBlocks { ...rootInnerBlocksPropsOptions } />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n";
2
2
  export declare const COMPOUND_PERSISTENCE_PARENT_SAVE_TEMPLATE = "export default function Save() {\n\treturn null;\n}\n";
3
- export declare const COMPOUND_PERSISTENCE_PARENT_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from './manifest-defaults-document';\nimport type {\n\t{{pascalCase}}Attributes,\n\t{{pascalCase}}ValidationResult,\n} from './types';\nimport { generateResourceKey } from '@wp-typia/block-runtime/identifiers';\nimport { createTemplateValidatorToolkit } from '../../validator-toolkit';\n\nconst scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attributes >( {\n\tassert: typia.createAssert< {{pascalCase}}Attributes >(),\n\tclone: typia.misc.createClone< {{pascalCase}}Attributes >() as (\n\t\tvalue: {{pascalCase}}Attributes,\n\t) => {{pascalCase}}Attributes,\n\tis: typia.createIs< {{pascalCase}}Attributes >(),\n\tmanifest: currentManifest,\n\tprune: typia.misc.createPrune< {{pascalCase}}Attributes >(),\n\trandom: typia.createRandom< {{pascalCase}}Attributes >() as (\n\t\t...args: unknown[]\n\t) => {{pascalCase}}Attributes,\n\tfinalize: ( normalized ) => ( {\n\t\t...normalized,\n\t\tresourceKey:\n\t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0\n\t\t\t\t? normalized.resourceKey\n\t\t\t\t: generateResourceKey( '{{slugKebabCase}}' ),\n\t} ),\n\tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),\n} );\n\nexport const validators = scaffoldValidators.validators;\n\nexport const validate{{pascalCase}}Attributes =\n\tscaffoldValidators.validateAttributes as (\n\t\tattributes: unknown\n\t) => {{pascalCase}}ValidationResult;\n\nexport const sanitize{{pascalCase}}Attributes =\n\tscaffoldValidators.sanitizeAttributes as (\n\t\tattributes: Partial< {{pascalCase}}Attributes >\n\t) => {{pascalCase}}Attributes;\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";
3
+ export declare const COMPOUND_PERSISTENCE_PARENT_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from './manifest-defaults-document';\nimport type {\n\t{{pascalCase}}Attributes,\n\t{{pascalCase}}ValidationResult,\n} from './types';\nimport { generateResourceKey } from '@wp-typia/block-runtime/identifiers';\nimport { createTemplateValidatorToolkit } from '../../validator-toolkit';\n\nconst scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attributes >( {\n\tassert: typia.createAssert< {{pascalCase}}Attributes >(),\n\tclone: typia.misc.createClone< {{pascalCase}}Attributes >() as (\n\t\tvalue: {{pascalCase}}Attributes,\n\t) => {{pascalCase}}Attributes,\n\tis: typia.createIs< {{pascalCase}}Attributes >(),\n\tmanifest: currentManifest,\n\tprune: typia.misc.createPrune< {{pascalCase}}Attributes >(),\n\trandom: typia.createRandom< {{pascalCase}}Attributes >() as (\n\t\t...args: unknown[]\n\t) => {{pascalCase}}Attributes,\n\tfinalize: ( normalized ) => ( {\n\t\t...normalized,\n\t\tresourceKey:\n\t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0\n\t\t\t\t? normalized.resourceKey\n\t\t\t\t: generateResourceKey( '{{resourceKeyPrefix}}' ),\n\t} ),\n\tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),\n} );\n\nexport const validators = scaffoldValidators.validators;\n\nexport const validate{{pascalCase}}Attributes =\n\tscaffoldValidators.validateAttributes as (\n\t\tattributes: unknown\n\t) => {{pascalCase}}ValidationResult;\n\nexport const sanitize{{pascalCase}}Attributes =\n\tscaffoldValidators.sanitizeAttributes as (\n\t\tattributes: Partial< {{pascalCase}}Attributes >\n\t) => {{pascalCase}}Attributes;\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";
4
4
  export declare const COMPOUND_PERSISTENCE_PARENT_INTERACTIVITY_TEMPLATE = "import { getContext, store } from '@wordpress/interactivity';\nimport { generatePublicWriteRequestId } from '@wp-typia/block-runtime/identifiers';\n\nimport { fetchBootstrap, fetchState, writeState } from './api';\nimport type {\n\t{{pascalCase}}ClientState,\n\t{{pascalCase}}Context,\n\t{{pascalCase}}State,\n} from './types';\n\nfunction hasExpiredPublicWriteToken(\n\texpiresAt?: number\n): boolean {\n\treturn (\n\t\ttypeof expiresAt === 'number' &&\n\t\texpiresAt > 0 &&\n\t\tDate.now() >= expiresAt * 1000\n\t);\n}\n\nfunction getWriteBlockedMessage(\n\tcontext: {{pascalCase}}Context\n): string {\n\treturn context.persistencePolicy === 'authenticated'\n\t\t? 'Sign in to persist this counter.'\n\t\t: 'Public writes are temporarily unavailable.';\n}\n\nconst BOOTSTRAP_MAX_ATTEMPTS = 3;\nconst BOOTSTRAP_RETRY_DELAYS_MS = [ 250, 500 ];\n\nasync function waitForBootstrapRetry( delayMs: number ): Promise< void > {\n\tawait new Promise( ( resolve ) => {\n\t\tsetTimeout( resolve, delayMs );\n\t} );\n}\n\nfunction getClientState(\n\tcontext: {{pascalCase}}Context\n): {{pascalCase}}ClientState {\n\tif ( context.client ) {\n\t\treturn context.client;\n\t}\n\n\tcontext.client = {\n\t\tbootstrapError: '',\n\t\twriteExpiry: 0,\n\t\twriteNonce: '',\n\t\twriteToken: '',\n\t};\n\n\treturn context.client;\n}\n\nfunction clearBootstrapError(\n\tcontext: {{pascalCase}}Context,\n\tclientState: {{pascalCase}}ClientState\n): void {\n\tif ( context.error === clientState.bootstrapError ) {\n\t\tcontext.error = '';\n\t}\n\tclientState.bootstrapError = '';\n}\n\nfunction setBootstrapError(\n\tcontext: {{pascalCase}}Context,\n\tclientState: {{pascalCase}}ClientState,\n\tmessage: string\n): void {\n\tclientState.bootstrapError = message;\n\tcontext.error = message;\n}\n\nconst { actions, state } = store( '{{slugKebabCase}}', {\n\tstate: {\n\t\tisHydrated: false,\n\t} as {{pascalCase}}State,\n\n\tactions: {\n\t\tasync loadState() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isLoading = true;\n\t\t\tcontext.error = '';\n\n\t\t\ttry {\n\t\t\t\tconst result = await fetchState( {\n\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t}, {\n\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t} );\n\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontext.count = result.data.count;\n\t\t\t} catch ( error ) {\n\t\t\t\tcontext.error =\n\t\t\t\t\terror instanceof Error ? error.message : 'Unknown loading error';\n\t\t\t} finally {\n\t\t\t\tcontext.isLoading = false;\n\t\t\t}\n\t\t},\n\t\tasync loadBootstrap() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tconst clientState = getClientState( context );\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\tcontext.bootstrapReady = true;\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tclientState.bootstrapError = '';\n\t\t\t\tclientState.writeExpiry = 0;\n\t\t\t\tclientState.writeNonce = '';\n\t\t\t\tclientState.writeToken = '';\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isBootstrapping = true;\n\n\t\t\tlet bootstrapSucceeded = false;\n\t\t\tlet lastBootstrapError =\n\t\t\t\t'Unable to initialize write access';\n\t\t\tconst includePublicWriteCredentials = {{isPublicPersistencePolicy}};\n\t\t\tconst includeRestNonce = {{isAuthenticatedPersistencePolicy}};\n\n\t\t\tfor ( let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1 ) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await fetchBootstrap( {\n\t\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t\t}, {\n\t\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t\t} );\n\t\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\t\tlastBootstrapError =\n\t\t\t\t\t\t\tresult.errors[ 0 ]?.expected ??\n\t\t\t\t\t\t\t'Unable to initialize write access';\n\t\t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {\n\t\t\t\t\t\t\tawait waitForBootstrapRetry(\n\t\t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tclientState.writeExpiry =\n\t\t\t\t\t\tincludePublicWriteCredentials &&\n\t\t\t\t\t\t'publicWriteExpiresAt' in result.data &&\n\t\t\t\t\t\ttypeof result.data.publicWriteExpiresAt === 'number' &&\n\t\t\t\t\t\tresult.data.publicWriteExpiresAt > 0\n\t\t\t\t\t\t\t? result.data.publicWriteExpiresAt\n\t\t\t\t\t\t\t: 0;\n\t\t\t\t\tclientState.writeToken =\n\t\t\t\t\t\tincludePublicWriteCredentials &&\n\t\t\t\t\t\t'publicWriteToken' in result.data &&\n\t\t\t\t\t\ttypeof result.data.publicWriteToken === 'string' &&\n\t\t\t\t\t\tresult.data.publicWriteToken.length > 0\n\t\t\t\t\t\t\t? result.data.publicWriteToken\n\t\t\t\t\t\t\t: '';\n\t\t\t\t\tclientState.writeNonce =\n\t\t\t\t\t\tincludeRestNonce &&\n\t\t\t\t\t\t'restNonce' in result.data &&\n\t\t\t\t\t\ttypeof result.data.restNonce === 'string' &&\n\t\t\t\t\t\tresult.data.restNonce.length > 0\n\t\t\t\t\t\t\t? result.data.restNonce\n\t\t\t\t\t\t\t: '';\n\t\t\t\t\tcontext.bootstrapReady = true;\n\t\t\t\t\tcontext.canWrite =\n\t\t\t\t\t\tresult.data.canWrite === true &&\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tcontext.persistencePolicy === 'authenticated'\n\t\t\t\t\t\t\t\t? clientState.writeNonce.length > 0\n\t\t\t\t\t\t\t\t: clientState.writeToken.length > 0 &&\n\t\t\t\t\t\t\t\t\t! hasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t\t\t\t);\n\t\t\t\t\tclearBootstrapError( context, clientState );\n\t\t\t\t\tbootstrapSucceeded = true;\n\t\t\t\t\tbreak;\n\t\t\t\t} catch ( error ) {\n\t\t\t\t\tlastBootstrapError =\n\t\t\t\t\t\terror instanceof Error ? error.message : 'Unknown bootstrap error';\n\t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {\n\t\t\t\t\t\tawait waitForBootstrapRetry(\n\t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( ! bootstrapSucceeded ) {\n\t\t\t\tcontext.bootstrapReady = false;\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tclientState.writeExpiry = 0;\n\t\t\t\tclientState.writeNonce = '';\n\t\t\t\tclientState.writeToken = '';\n\t\t\t\tsetBootstrapError( context, clientState, lastBootstrapError );\n\t\t\t}\n\t\t\tcontext.isBootstrapping = false;\n\t\t},\n\t\tasync increment() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tconst clientState = getClientState( context );\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! context.bootstrapReady ) {\n\t\t\t\tawait actions.loadBootstrap();\n\t\t\t}\n\t\t\tif ( ! context.bootstrapReady ) {\n\t\t\t\tcontext.error = 'Write access is still initializing.';\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t) {\n\t\t\t\tawait actions.loadBootstrap();\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t) {\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tcontext.error = getWriteBlockedMessage( context );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! context.canWrite ) {\n\t\t\t\tcontext.error = getWriteBlockedMessage( context );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isSaving = true;\n\t\t\tcontext.error = '';\n\n\t\t\ttry {\n\t\t\t\tconst result = await writeState( {\n\t\t\t\t\tdelta: 1,\n\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\tpublicWriteRequestId:\n\t\t\t\t\t\tcontext.persistencePolicy === 'public'\n\t\t\t\t\t\t\t? generatePublicWriteRequestId()\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tpublicWriteToken:\n\t\t\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\t\t\tclientState.writeToken.length > 0\n\t\t\t\t\t\t\t? clientState.writeToken\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t}, {\n\t\t\t\t\trestNonce:\n\t\t\t\t\t\tclientState.writeNonce.length > 0\n\t\t\t\t\t\t\t? clientState.writeNonce\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t} );\n\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontext.count = result.data.count;\n\t\t\t\tcontext.storage = result.data.storage;\n\t\t\t} catch ( error ) {\n\t\t\t\tcontext.error =\n\t\t\t\t\terror instanceof Error ? error.message : 'Unknown update error';\n\t\t\t} finally {\n\t\t\t\tcontext.isSaving = false;\n\t\t\t}\n\t\t},\n\t},\n\n\tcallbacks: {\n\t\tinit() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tcontext.client = {\n\t\t\t\tbootstrapError: '',\n\t\t\t\twriteExpiry: 0,\n\t\t\t\twriteNonce: '',\n\t\t\t\twriteToken: '',\n\t\t\t};\n\t\t\tcontext.bootstrapReady = false;\n\t\t\tcontext.canWrite = false;\n\t\t\tcontext.count = 0;\n\t\t\tcontext.error = '';\n\t\t\tcontext.isBootstrapping = false;\n\t\t\tcontext.isLoading = false;\n\t\t\tcontext.isSaving = false;\n\t\t},\n\t\tmounted() {\n\t\t\tstate.isHydrated = true;\n\t\t\tif ( typeof document !== 'undefined' ) {\n\t\t\t\tdocument.documentElement.dataset[ '{{slugCamelCase}}Hydrated' ] = 'true';\n\t\t\t}\n\t\t\tvoid Promise.allSettled( [\n\t\t\t\tactions.loadState(),\n\t\t\t\tactions.loadBootstrap(),\n\t\t\t] );\n\t\t},\n\t},\n} );\n";
@@ -4,6 +4,7 @@ import {
4
4
  \tInspectorControls,
5
5
  \tInnerBlocks,
6
6
  \tRichText,
7
+ \tstore as blockEditorStore,
7
8
  \tuseBlockProps,
8
9
  } from '@wordpress/block-editor';
9
10
  import {
@@ -12,7 +13,12 @@ import {
12
13
  \tTextControl,
13
14
  \tToggleControl,
14
15
  } from '@wordpress/components';
16
+ import { useSelect } from '@wordpress/data';
15
17
 
18
+ import {
19
+ \ttype PersistentBlockIdentityNode,
20
+ \tusePersistentBlockIdentity,
21
+ } from '@wp-typia/block-runtime/inspector';
16
22
  import {
17
23
  \tgetRootInnerBlocksPropsOptions,
18
24
  } from './children';
@@ -35,8 +41,25 @@ const TypedInnerBlocks = InnerBlocks as unknown as (
35
41
 
36
42
  export default function Edit( {
37
43
  \tattributes,
44
+ \tclientId,
38
45
  \tsetAttributes,
39
46
  }: EditProps ) {
47
+ \tconst blocks = useSelect(
48
+ \t\t( select ) =>
49
+ \t\t\t( select( blockEditorStore ) as {
50
+ \t\t\t\tgetBlocks: () => readonly PersistentBlockIdentityNode[];
51
+ \t\t\t} ).getBlocks(),
52
+ \t\t[]
53
+ \t);
54
+ \tusePersistentBlockIdentity( {
55
+ \t\tattributeName: 'resourceKey',
56
+ \t\tattributes,
57
+ \t\tblockName: '{{namespace}}/{{slugKebabCase}}',
58
+ \t\tblocks,
59
+ \t\tclientId,
60
+ \t\tprefix: '{{resourceKeyPrefix}}',
61
+ \t\tsetAttributes,
62
+ \t} );
40
63
  \tconst { errorMessages, isValid } = useTypiaValidation(
41
64
  \t\tattributes,
42
65
  \t\tvalidate{{pascalCase}}Attributes
@@ -154,7 +177,7 @@ const scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attribu
154
177
  \t\tresourceKey:
155
178
  \t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0
156
179
  \t\t\t\t? normalized.resourceKey
157
- \t\t\t\t: generateResourceKey( '{{slugKebabCase}}' ),
180
+ \t\t\t\t: generateResourceKey( '{{resourceKeyPrefix}}' ),
158
181
  \t} ),
159
182
  \tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),
160
183
  } );
@@ -1,5 +1,5 @@
1
- export declare const PERSISTENCE_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { __ } from '@wordpress/i18n';\nimport {\n\tAlignmentToolbar,\n\tBlockControls,\n\tInspectorControls,\n\tRichText,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport {\n\tNotice,\n\tPanelBody,\n\tTextControl,\n} from '@wordpress/components';\nimport currentManifest from './manifest-document';\nimport {\n\tInspectorFromManifest,\n\tuseEditorFields,\n\tuseTypedAttributeUpdater,\n} from '@wp-typia/block-runtime/inspector';\nimport type { {{pascalCase}}Attributes } from './types';\nimport {\n\tsanitize{{pascalCase}}Attributes,\n\tvalidate{{pascalCase}}Attributes,\n} from './validators';\nimport { useTypiaValidation } from './hooks';\n\ntype EditProps = BlockEditProps< {{pascalCase}}Attributes >;\n\nexport default function Edit( {\n\tattributes,\n\tsetAttributes,\n}: EditProps ) {\n\tconst editorFields = useEditorFields(\n\t\tcurrentManifest,\n\t\t{\n\t\t\tmanual: [ 'content', 'resourceKey' ],\n\t\t\tlabels: {\n\t\t\t\tbuttonLabel: __( 'Button Label', '{{textDomain}}' ),\n\t\t\t\tresourceKey: __( 'Resource Key', '{{textDomain}}' ),\n\t\t\t\tshowCount: __( 'Show Count', '{{textDomain}}' ),\n\t\t\t},\n\t\t}\n\t);\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}Attributes\n\t);\n\tconst validateEditorUpdate = (\n\t\tnextAttributes: {{pascalCase}}Attributes\n\t) => {\n\t\ttry {\n\t\t\treturn {\n\t\t\t\tdata: sanitize{{pascalCase}}Attributes( nextAttributes ),\n\t\t\t\terrors: [],\n\t\t\t\tisValid: true as const,\n\t\t\t};\n\t\t} catch {\n\t\t\treturn validate{{pascalCase}}Attributes( nextAttributes );\n\t\t}\n\t};\n\tconst { updateField } = useTypedAttributeUpdater(\n\t\tattributes,\n\t\tsetAttributes,\n\t\tvalidateEditorUpdate\n\t);\n\tconst alignmentValue = editorFields.getStringValue(\n\t\tattributes,\n\t\t'alignment',\n\t\t'left'\n\t);\n\tconst persistencePolicy = '{{persistencePolicy}}';\n\tconst persistencePolicyDescription = __(\n\t\t{{persistencePolicyDescriptionJson}},\n\t\t'{{textDomain}}'\n\t);\n\n\treturn (\n\t\t<>\n\t\t\t<BlockControls>\n\t\t\t\t<AlignmentToolbar\n\t\t\t\t\tvalue={ alignmentValue }\n\t\t\t\t\tonChange={ ( value ) =>\n\t\t\t\t\t\tupdateField(\n\t\t\t\t\t\t\t'alignment',\n\t\t\t\t\t\t\t( value || alignmentValue ) as NonNullable< {{pascalCase}}Attributes[ 'alignment' ] >\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t</BlockControls>\n\t\t\t<InspectorControls>\n\t\t\t\t<InspectorFromManifest\n\t\t\t\t\tattributes={ attributes }\n\t\t\t\t\tfieldLookup={ editorFields }\n\t\t\t\t\tonChange={ updateField }\n\t\t\t\t\tpaths={ [ 'alignment', 'isVisible', 'showCount', 'buttonLabel' ] }\n\t\t\t\t\ttitle={ __( 'Persistence Settings', '{{textDomain}}' ) }\n\t\t\t\t>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Resource Key', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.resourceKey ?? '' }\n\t\t\t\t\t\tonChange={ ( value ) => updateField( 'resourceKey', value ) }\n\t\t\t\t\t\thelp={ __( 'Stable persisted identifier used by the storage-backed counter endpoint.', '{{textDomain}}' ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Storage mode: {{dataStorageMode}}', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Persistence policy: {{persistencePolicy}}', '{{textDomain}}' ) }\n\t\t\t\t\t\t<br />\n\t\t\t\t\t\t{ persistencePolicyDescription }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Render mode: dynamic. `render.php` bootstraps durable post context, while fresh session-only write data is loaded from the dedicated `/bootstrap` endpoint after hydration.', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t</InspectorFromManifest>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<PanelBody\n\t\t\t\t\t\ttitle={ __( 'Validation Errors', '{{textDomain}}' ) }\n\t\t\t\t\t\tinitialOpen\n\t\t\t\t\t>\n\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => (\n\t\t\t\t\t\t\t<Notice key={ index } status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t\t\t{ error }\n\t\t\t\t\t\t\t</Notice>\n\t\t\t\t\t\t) ) }\n\t\t\t\t\t</PanelBody>\n\t\t\t\t) }\n\t\t\t</InspectorControls>\n\t\t\t<div\n\t\t\t\t{ ...useBlockProps( {\n\t\t\t\t\tclassName: '{{cssClassName}}',\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\ttextAlign:\n\t\t\t\t\t\t\talignmentValue as NonNullable< {{pascalCase}}Attributes[ 'alignment' ] >,\n\t\t\t\t\t},\n\t\t\t\t} ) }\n\t\t\t>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"p\"\n\t\t\t\t\tvalue={ attributes.content }\n\t\t\t\t\tonChange={ ( value ) => updateField( 'content', value ) }\n\t\t\t\t\tplaceholder={ __( {{titleJson}} + ' persistence block', '{{textDomain}}' ) }\n\t\t\t\t/>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Resource key:', '{{textDomain}}' ) } { attributes.resourceKey || '\u2014' }\n\t\t\t\t</p>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Storage mode:', '{{textDomain}}' ) } {{dataStorageMode}}\n\t\t\t\t</p>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Persistence policy:', '{{textDomain}}' ) } {{persistencePolicy}}\n\t\t\t\t</p>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<Notice status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</Notice>\n\t\t\t\t) }\n\t\t\t</div>\n\t\t</>\n\t);\n}\n";
1
+ export declare const PERSISTENCE_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { __ } from '@wordpress/i18n';\nimport {\n\tAlignmentToolbar,\n\tBlockControls,\n\tInspectorControls,\n\tRichText,\n\tstore as blockEditorStore,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport {\n\tNotice,\n\tPanelBody,\n\tTextControl,\n} from '@wordpress/components';\nimport { useSelect } from '@wordpress/data';\nimport currentManifest from './manifest-document';\nimport {\n\tInspectorFromManifest,\n\ttype PersistentBlockIdentityNode,\n\tuseEditorFields,\n\tusePersistentBlockIdentity,\n\tuseTypedAttributeUpdater,\n} from '@wp-typia/block-runtime/inspector';\nimport type { {{pascalCase}}Attributes } from './types';\nimport {\n\tsanitize{{pascalCase}}Attributes,\n\tvalidate{{pascalCase}}Attributes,\n} from './validators';\nimport { useTypiaValidation } from './hooks';\n\ntype EditProps = BlockEditProps< {{pascalCase}}Attributes >;\n\nexport default function Edit( {\n\tattributes,\n\tclientId,\n\tsetAttributes,\n}: EditProps ) {\n\tconst blocks = useSelect(\n\t\t( select ) =>\n\t\t\t( select( blockEditorStore ) as {\n\t\t\t\tgetBlocks: () => readonly PersistentBlockIdentityNode[];\n\t\t\t} ).getBlocks(),\n\t\t[]\n\t);\n\tusePersistentBlockIdentity( {\n\t\tattributeName: 'resourceKey',\n\t\tattributes,\n\t\tblockName: '{{namespace}}/{{slugKebabCase}}',\n\t\tblocks,\n\t\tclientId,\n\t\tprefix: '{{resourceKeyPrefix}}',\n\t\tsetAttributes,\n\t} );\n\tconst editorFields = useEditorFields(\n\t\tcurrentManifest,\n\t\t{\n\t\t\tmanual: [ 'content', 'resourceKey' ],\n\t\t\tlabels: {\n\t\t\t\tbuttonLabel: __( 'Button Label', '{{textDomain}}' ),\n\t\t\t\tresourceKey: __( 'Resource Key', '{{textDomain}}' ),\n\t\t\t\tshowCount: __( 'Show Count', '{{textDomain}}' ),\n\t\t\t},\n\t\t}\n\t);\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}Attributes\n\t);\n\tconst validateEditorUpdate = (\n\t\tnextAttributes: {{pascalCase}}Attributes\n\t) => {\n\t\ttry {\n\t\t\treturn {\n\t\t\t\tdata: sanitize{{pascalCase}}Attributes( nextAttributes ),\n\t\t\t\terrors: [],\n\t\t\t\tisValid: true as const,\n\t\t\t};\n\t\t} catch {\n\t\t\treturn validate{{pascalCase}}Attributes( nextAttributes );\n\t\t}\n\t};\n\tconst { updateField } = useTypedAttributeUpdater(\n\t\tattributes,\n\t\tsetAttributes,\n\t\tvalidateEditorUpdate\n\t);\n\tconst alignmentValue = editorFields.getStringValue(\n\t\tattributes,\n\t\t'alignment',\n\t\t'left'\n\t);\n\tconst persistencePolicy = '{{persistencePolicy}}';\n\tconst persistencePolicyDescription = __(\n\t\t{{persistencePolicyDescriptionJson}},\n\t\t'{{textDomain}}'\n\t);\n\n\treturn (\n\t\t<>\n\t\t\t<BlockControls>\n\t\t\t\t<AlignmentToolbar\n\t\t\t\t\tvalue={ alignmentValue }\n\t\t\t\t\tonChange={ ( value ) =>\n\t\t\t\t\t\tupdateField(\n\t\t\t\t\t\t\t'alignment',\n\t\t\t\t\t\t\t( value || alignmentValue ) as NonNullable< {{pascalCase}}Attributes[ 'alignment' ] >\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t</BlockControls>\n\t\t\t<InspectorControls>\n\t\t\t\t<InspectorFromManifest\n\t\t\t\t\tattributes={ attributes }\n\t\t\t\t\tfieldLookup={ editorFields }\n\t\t\t\t\tonChange={ updateField }\n\t\t\t\t\tpaths={ [ 'alignment', 'isVisible', 'showCount', 'buttonLabel' ] }\n\t\t\t\t\ttitle={ __( 'Persistence Settings', '{{textDomain}}' ) }\n\t\t\t\t>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Resource Key', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.resourceKey ?? '' }\n\t\t\t\t\t\tonChange={ ( value ) => updateField( 'resourceKey', value ) }\n\t\t\t\t\t\thelp={ __( 'Stable persisted identifier used by the storage-backed counter endpoint.', '{{textDomain}}' ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Storage mode: {{dataStorageMode}}', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Persistence policy: {{persistencePolicy}}', '{{textDomain}}' ) }\n\t\t\t\t\t\t<br />\n\t\t\t\t\t\t{ persistencePolicyDescription }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Render mode: dynamic. `render.php` bootstraps durable post context, while fresh session-only write data is loaded from the dedicated `/bootstrap` endpoint after hydration.', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t</InspectorFromManifest>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<PanelBody\n\t\t\t\t\t\ttitle={ __( 'Validation Errors', '{{textDomain}}' ) }\n\t\t\t\t\t\tinitialOpen\n\t\t\t\t\t>\n\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => (\n\t\t\t\t\t\t\t<Notice key={ index } status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t\t\t{ error }\n\t\t\t\t\t\t\t</Notice>\n\t\t\t\t\t\t) ) }\n\t\t\t\t\t</PanelBody>\n\t\t\t\t) }\n\t\t\t</InspectorControls>\n\t\t\t<div\n\t\t\t\t{ ...useBlockProps( {\n\t\t\t\t\tclassName: '{{cssClassName}}',\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\ttextAlign:\n\t\t\t\t\t\t\talignmentValue as NonNullable< {{pascalCase}}Attributes[ 'alignment' ] >,\n\t\t\t\t\t},\n\t\t\t\t} ) }\n\t\t\t>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"p\"\n\t\t\t\t\tvalue={ attributes.content }\n\t\t\t\t\tonChange={ ( value ) => updateField( 'content', value ) }\n\t\t\t\t\tplaceholder={ __( {{titleJson}} + ' persistence block', '{{textDomain}}' ) }\n\t\t\t\t/>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Resource key:', '{{textDomain}}' ) } { attributes.resourceKey || '\u2014' }\n\t\t\t\t</p>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Storage mode:', '{{textDomain}}' ) } {{dataStorageMode}}\n\t\t\t\t</p>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Persistence policy:', '{{textDomain}}' ) } {{persistencePolicy}}\n\t\t\t\t</p>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<Notice status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</Notice>\n\t\t\t\t) }\n\t\t\t</div>\n\t\t</>\n\t);\n}\n";
2
2
  export declare const PERSISTENCE_INDEX_TEMPLATE = "import {\n\tregisterScaffoldBlockType,\n\ttype BlockConfiguration,\n} from '@wp-typia/block-types/blocks/registration';\nimport {\n\tbuildScaffoldBlockRegistration,\n\tparseScaffoldBlockMetadata,\n} from '@wp-typia/block-runtime/blocks';\n\nimport Edit from './edit';\nimport Save from './save';\nimport metadata from './block-metadata';\nimport './style.scss';\n\nimport type { {{pascalCase}}Attributes } from './types';\n\nconst registration = buildScaffoldBlockRegistration(\n\tparseScaffoldBlockMetadata<BlockConfiguration< {{pascalCase}}Attributes >>( metadata ),\n\t{\n\t\tedit: Edit,\n\t\tsave: Save,\n\t}\n);\n\nregisterScaffoldBlockType(registration.name, registration.settings);\n";
3
3
  export declare const PERSISTENCE_SAVE_TEMPLATE = "export default function Save() {\n\t// This block is intentionally server-rendered. PHP bootstraps post context,\n\t// storage-backed state, and write-policy data before the frontend hydrates.\n\treturn null;\n}\n";
4
- export declare const PERSISTENCE_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from './manifest-defaults-document';\nimport type {\n\t{{pascalCase}}Attributes,\n\t{{pascalCase}}ValidationResult,\n} from './types';\nimport { generateResourceKey } from '@wp-typia/block-runtime/identifiers';\nimport { createTemplateValidatorToolkit } from './validator-toolkit';\n\nconst scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attributes >( {\n\tassert: typia.createAssert< {{pascalCase}}Attributes >(),\n\tclone: typia.misc.createClone< {{pascalCase}}Attributes >() as (\n\t\tvalue: {{pascalCase}}Attributes,\n\t) => {{pascalCase}}Attributes,\n\tis: typia.createIs< {{pascalCase}}Attributes >(),\n\tmanifest: currentManifest,\n\tprune: typia.misc.createPrune< {{pascalCase}}Attributes >(),\n\trandom: typia.createRandom< {{pascalCase}}Attributes >() as (\n\t\t...args: unknown[]\n\t) => {{pascalCase}}Attributes,\n\tfinalize: ( normalized ) => ( {\n\t\t...normalized,\n\t\tresourceKey:\n\t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0\n\t\t\t\t? normalized.resourceKey\n\t\t\t\t: generateResourceKey( '{{slugKebabCase}}' ),\n\t} ),\n\tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),\n} );\n\nexport const validators = scaffoldValidators.validators;\n\nexport const validate{{pascalCase}}Attributes =\n\tscaffoldValidators.validateAttributes as (\n\t\tattributes: unknown\n\t) => {{pascalCase}}ValidationResult;\n\nexport const sanitize{{pascalCase}}Attributes =\n\tscaffoldValidators.sanitizeAttributes as (\n\t\tattributes: Partial< {{pascalCase}}Attributes >\n\t) => {{pascalCase}}Attributes;\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";
4
+ export declare const PERSISTENCE_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from './manifest-defaults-document';\nimport type {\n\t{{pascalCase}}Attributes,\n\t{{pascalCase}}ValidationResult,\n} from './types';\nimport { generateResourceKey } from '@wp-typia/block-runtime/identifiers';\nimport { createTemplateValidatorToolkit } from './validator-toolkit';\n\nconst scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attributes >( {\n\tassert: typia.createAssert< {{pascalCase}}Attributes >(),\n\tclone: typia.misc.createClone< {{pascalCase}}Attributes >() as (\n\t\tvalue: {{pascalCase}}Attributes,\n\t) => {{pascalCase}}Attributes,\n\tis: typia.createIs< {{pascalCase}}Attributes >(),\n\tmanifest: currentManifest,\n\tprune: typia.misc.createPrune< {{pascalCase}}Attributes >(),\n\trandom: typia.createRandom< {{pascalCase}}Attributes >() as (\n\t\t...args: unknown[]\n\t) => {{pascalCase}}Attributes,\n\tfinalize: ( normalized ) => ( {\n\t\t...normalized,\n\t\tresourceKey:\n\t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0\n\t\t\t\t? normalized.resourceKey\n\t\t\t\t: generateResourceKey( '{{resourceKeyPrefix}}' ),\n\t} ),\n\tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),\n} );\n\nexport const validators = scaffoldValidators.validators;\n\nexport const validate{{pascalCase}}Attributes =\n\tscaffoldValidators.validateAttributes as (\n\t\tattributes: unknown\n\t) => {{pascalCase}}ValidationResult;\n\nexport const sanitize{{pascalCase}}Attributes =\n\tscaffoldValidators.sanitizeAttributes as (\n\t\tattributes: Partial< {{pascalCase}}Attributes >\n\t) => {{pascalCase}}Attributes;\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";
5
5
  export declare const PERSISTENCE_INTERACTIVITY_TEMPLATE = "import { getContext, store } from '@wordpress/interactivity';\nimport { generatePublicWriteRequestId } from '@wp-typia/block-runtime/identifiers';\n\nimport { fetchBootstrap, fetchState, writeState } from './api';\nimport type {\n\t{{pascalCase}}ClientState,\n\t{{pascalCase}}Context,\n\t{{pascalCase}}State,\n} from './types';\nimport type {\n\t{{pascalCase}}WriteStateRequest,\n} from './api-types';\n\nfunction hasExpiredPublicWriteToken(\n\texpiresAt?: number\n): boolean {\n\treturn (\n\t\ttypeof expiresAt === 'number' &&\n\t\texpiresAt > 0 &&\n\t\tDate.now() >= expiresAt * 1000\n\t);\n}\n\nfunction getWriteBlockedMessage(\n\tcontext: {{pascalCase}}Context\n): string {\n\treturn context.persistencePolicy === 'authenticated'\n\t\t? 'Sign in to persist this counter.'\n\t\t: 'Public writes are temporarily unavailable.';\n}\n\nconst BOOTSTRAP_MAX_ATTEMPTS = 3;\nconst BOOTSTRAP_RETRY_DELAYS_MS = [ 250, 500 ];\n\nasync function waitForBootstrapRetry( delayMs: number ): Promise< void > {\n\tawait new Promise( ( resolve ) => {\n\t\tsetTimeout( resolve, delayMs );\n\t} );\n}\n\nfunction getClientState(\n\tcontext: {{pascalCase}}Context\n): {{pascalCase}}ClientState {\n\tif ( context.client ) {\n\t\treturn context.client;\n\t}\n\n\tcontext.client = {\n\t\tbootstrapError: '',\n\t\twriteExpiry: 0,\n\t\twriteNonce: '',\n\t\twriteToken: '',\n\t};\n\n\treturn context.client;\n}\n\nfunction clearBootstrapError(\n\tcontext: {{pascalCase}}Context,\n\tclientState: {{pascalCase}}ClientState\n): void {\n\tif ( context.error === clientState.bootstrapError ) {\n\t\tcontext.error = '';\n\t}\n\tclientState.bootstrapError = '';\n}\n\nfunction setBootstrapError(\n\tcontext: {{pascalCase}}Context,\n\tclientState: {{pascalCase}}ClientState,\n\tmessage: string\n): void {\n\tclientState.bootstrapError = message;\n\tcontext.error = message;\n}\n\nconst { actions, state } = store( '{{slugKebabCase}}', {\n\tstate: {\n\t\tisHydrated: false,\n\t} as {{pascalCase}}State,\n\n\tactions: {\n\t\tasync loadState() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isLoading = true;\n\t\t\tcontext.error = '';\n\n\t\t\ttry {\n\t\t\t\tconst result = await fetchState( {\n\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t}, {\n\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t} );\n\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontext.count = result.data.count;\n\t\t\t} catch ( error ) {\n\t\t\t\tcontext.error =\n\t\t\t\t\terror instanceof Error ? error.message : 'Unknown loading error';\n\t\t\t} finally {\n\t\t\t\tcontext.isLoading = false;\n\t\t\t}\n\t\t},\n\t\tasync loadBootstrap() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tconst clientState = getClientState( context );\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\tcontext.bootstrapReady = true;\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tclientState.bootstrapError = '';\n\t\t\t\tclientState.writeExpiry = 0;\n\t\t\t\tclientState.writeNonce = '';\n\t\t\t\tclientState.writeToken = '';\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isBootstrapping = true;\n\n\t\t\tlet bootstrapSucceeded = false;\n\t\t\tlet lastBootstrapError =\n\t\t\t\t'Unable to initialize write access';\n\t\t\tconst includePublicWriteCredentials = {{isPublicPersistencePolicy}};\n\t\t\tconst includeRestNonce = {{isAuthenticatedPersistencePolicy}};\n\n\t\t\tfor ( let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1 ) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await fetchBootstrap( {\n\t\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t\t}, {\n\t\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t\t} );\n\t\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\t\tlastBootstrapError =\n\t\t\t\t\t\t\tresult.errors[ 0 ]?.expected ??\n\t\t\t\t\t\t\t'Unable to initialize write access';\n\t\t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {\n\t\t\t\t\t\t\tawait waitForBootstrapRetry(\n\t\t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tclientState.writeExpiry =\n\t\t\t\t\t\tincludePublicWriteCredentials &&\n\t\t\t\t\t\t'publicWriteExpiresAt' in result.data &&\n\t\t\t\t\t\ttypeof result.data.publicWriteExpiresAt === 'number' &&\n\t\t\t\t\t\tresult.data.publicWriteExpiresAt > 0\n\t\t\t\t\t\t\t? result.data.publicWriteExpiresAt\n\t\t\t\t\t\t\t: 0;\n\t\t\t\t\tclientState.writeToken =\n\t\t\t\t\t\tincludePublicWriteCredentials &&\n\t\t\t\t\t\t'publicWriteToken' in result.data &&\n\t\t\t\t\t\ttypeof result.data.publicWriteToken === 'string' &&\n\t\t\t\t\t\tresult.data.publicWriteToken.length > 0\n\t\t\t\t\t\t\t? result.data.publicWriteToken\n\t\t\t\t\t\t\t: '';\n\t\t\t\t\tclientState.writeNonce =\n\t\t\t\t\t\tincludeRestNonce &&\n\t\t\t\t\t\t'restNonce' in result.data &&\n\t\t\t\t\t\ttypeof result.data.restNonce === 'string' &&\n\t\t\t\t\t\tresult.data.restNonce.length > 0\n\t\t\t\t\t\t\t? result.data.restNonce\n\t\t\t\t\t\t\t: '';\n\t\t\t\t\tcontext.bootstrapReady = true;\n\t\t\t\t\tcontext.canWrite =\n\t\t\t\t\t\tresult.data.canWrite === true &&\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tcontext.persistencePolicy === 'authenticated'\n\t\t\t\t\t\t\t\t? clientState.writeNonce.length > 0\n\t\t\t\t\t\t\t\t: clientState.writeToken.length > 0 &&\n\t\t\t\t\t\t\t\t\t! hasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t\t\t\t);\n\t\t\t\t\tclearBootstrapError( context, clientState );\n\t\t\t\t\tbootstrapSucceeded = true;\n\t\t\t\t\tbreak;\n\t\t\t\t} catch ( error ) {\n\t\t\t\t\tlastBootstrapError =\n\t\t\t\t\t\terror instanceof Error ? error.message : 'Unknown bootstrap error';\n\t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {\n\t\t\t\t\t\tawait waitForBootstrapRetry(\n\t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( ! bootstrapSucceeded ) {\n\t\t\t\tcontext.bootstrapReady = false;\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tclientState.writeExpiry = 0;\n\t\t\t\tclientState.writeNonce = '';\n\t\t\t\tclientState.writeToken = '';\n\t\t\t\tsetBootstrapError( context, clientState, lastBootstrapError );\n\t\t\t}\n\t\t\tcontext.isBootstrapping = false;\n\t\t},\n\t\tasync increment() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tconst clientState = getClientState( context );\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! context.bootstrapReady ) {\n\t\t\t\tawait actions.loadBootstrap();\n\t\t\t}\n\t\t\tif ( ! context.bootstrapReady ) {\n\t\t\t\tcontext.error = 'Write access is still initializing.';\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t) {\n\t\t\t\tawait actions.loadBootstrap();\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t) {\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tcontext.error = getWriteBlockedMessage( context );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! context.canWrite ) {\n\t\t\t\tcontext.error = getWriteBlockedMessage( context );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isSaving = true;\n\t\t\tcontext.error = '';\n\n\t\t\ttry {\n\t\t\t\tconst request = {\n\t\t\t\t\tdelta: 1,\n\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t} as {{pascalCase}}WriteStateRequest;\n\t\t\t\tif ( {{isPublicPersistencePolicy}} ) {\n\t\t\t\t\trequest.publicWriteRequestId =\n\t\t\t\t\t\tgeneratePublicWriteRequestId() as {{pascalCase}}WriteStateRequest[ 'publicWriteRequestId' ];\n\t\t\t\t\tif ( clientState.writeToken.length > 0 ) {\n\t\t\t\t\t\trequest.publicWriteToken =\n\t\t\t\t\t\t\tclientState.writeToken as {{pascalCase}}WriteStateRequest[ 'publicWriteToken' ];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst result = await writeState( request, {\n\t\t\t\t\trestNonce:\n\t\t\t\t\t\tclientState.writeNonce.length > 0\n\t\t\t\t\t\t\t? clientState.writeNonce\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t} );\n\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontext.count = result.data.count;\n\t\t\t\tcontext.storage = result.data.storage;\n\t\t\t} catch ( error ) {\n\t\t\t\tcontext.error =\n\t\t\t\t\terror instanceof Error ? error.message : 'Unknown update error';\n\t\t\t} finally {\n\t\t\t\tcontext.isSaving = false;\n\t\t\t}\n\t\t},\n\t},\n\n\tcallbacks: {\n\t\tinit() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tcontext.client = {\n\t\t\t\tbootstrapError: '',\n\t\t\t\twriteExpiry: 0,\n\t\t\t\twriteNonce: '',\n\t\t\t\twriteToken: '',\n\t\t\t};\n\t\t\tcontext.bootstrapReady = false;\n\t\t\tcontext.canWrite = false;\n\t\t\tcontext.count = 0;\n\t\t\tcontext.error = '';\n\t\t\tcontext.isBootstrapping = false;\n\t\t\tcontext.isLoading = false;\n\t\t\tcontext.isSaving = false;\n\t\t},\n\t\tmounted() {\n\t\t\tstate.isHydrated = true;\n\t\t\tif ( typeof document !== 'undefined' ) {\n\t\t\t\tdocument.documentElement.dataset[ '{{slugCamelCase}}Hydrated' ] = 'true';\n\t\t\t}\n\t\t\tvoid Promise.allSettled( [\n\t\t\t\tactions.loadState(),\n\t\t\t\tactions.loadBootstrap(),\n\t\t\t] );\n\t\t},\n\t},\n} );\n";
@@ -5,6 +5,7 @@ import {
5
5
  BlockControls,
6
6
  InspectorControls,
7
7
  RichText,
8
+ store as blockEditorStore,
8
9
  useBlockProps,
9
10
  } from '@wordpress/block-editor';
10
11
  import {
@@ -12,10 +13,13 @@ import {
12
13
  PanelBody,
13
14
  TextControl,
14
15
  } from '@wordpress/components';
16
+ import { useSelect } from '@wordpress/data';
15
17
  import currentManifest from './manifest-document';
16
18
  import {
17
19
  InspectorFromManifest,
20
+ type PersistentBlockIdentityNode,
18
21
  useEditorFields,
22
+ usePersistentBlockIdentity,
19
23
  useTypedAttributeUpdater,
20
24
  } from '@wp-typia/block-runtime/inspector';
21
25
  import type { {{pascalCase}}Attributes } from './types';
@@ -29,8 +33,25 @@ type EditProps = BlockEditProps< {{pascalCase}}Attributes >;
29
33
 
30
34
  export default function Edit( {
31
35
  attributes,
36
+ clientId,
32
37
  setAttributes,
33
38
  }: EditProps ) {
39
+ const blocks = useSelect(
40
+ ( select ) =>
41
+ ( select( blockEditorStore ) as {
42
+ getBlocks: () => readonly PersistentBlockIdentityNode[];
43
+ } ).getBlocks(),
44
+ []
45
+ );
46
+ usePersistentBlockIdentity( {
47
+ attributeName: 'resourceKey',
48
+ attributes,
49
+ blockName: '{{namespace}}/{{slugKebabCase}}',
50
+ blocks,
51
+ clientId,
52
+ prefix: '{{resourceKeyPrefix}}',
53
+ setAttributes,
54
+ } );
34
55
  const editorFields = useEditorFields(
35
56
  currentManifest,
36
57
  {
@@ -220,7 +241,7 @@ const scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attribu
220
241
  \t\tresourceKey:
221
242
  \t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0
222
243
  \t\t\t\t? normalized.resourceKey
223
- \t\t\t\t: generateResourceKey( '{{slugKebabCase}}' ),
244
+ \t\t\t\t: generateResourceKey( '{{resourceKeyPrefix}}' ),
224
245
  \t} ),
225
246
  \tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),
226
247
  } );