@wp-typia/project-tools 0.11.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.
Files changed (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,192 @@
1
+ import {
2
+ useEndpointMutation,
3
+ useEndpointQuery,
4
+ type UseEndpointMutationOptions,
5
+ type UseEndpointQueryOptions,
6
+ } from '@wp-typia/rest/react';
7
+
8
+ import type {
9
+ {{pascalCase}}StateQuery,
10
+ {{pascalCase}}StateResponse,
11
+ {{pascalCase}}WriteStateRequest,
12
+ } from './api-types';
13
+ import {
14
+ resolveRestNonce,
15
+ stateEndpoint,
16
+ writeStateEndpoint,
17
+ } from './api';
18
+
19
+ {{#isAuthenticatedPersistencePolicy}}
20
+ function buildWriteCallOptions( restNonce?: string ) {
21
+ const nonce = resolveRestNonce( restNonce );
22
+
23
+ return nonce
24
+ ? {
25
+ requestOptions: {
26
+ headers: {
27
+ 'X-WP-Nonce': nonce,
28
+ },
29
+ },
30
+ }
31
+ : undefined;
32
+ }
33
+ {{/isAuthenticatedPersistencePolicy}}
34
+
35
+ interface WriteStateMutationContext< Context > {
36
+ previous:
37
+ | {{pascalCase}}StateResponse
38
+ | undefined;
39
+ userContext: Context | undefined;
40
+ }
41
+
42
+ export interface Use{{pascalCase}}StateQueryOptions<
43
+ Selected = {{pascalCase}}StateResponse,
44
+ > extends Omit<
45
+ UseEndpointQueryOptions<
46
+ {{pascalCase}}StateQuery,
47
+ {{pascalCase}}StateResponse,
48
+ Selected
49
+ >,
50
+ 'resolveCallOptions'
51
+ > {
52
+ {{#isAuthenticatedPersistencePolicy}}
53
+ restNonce?: string;
54
+ {{/isAuthenticatedPersistencePolicy}}
55
+ }
56
+
57
+ export interface UseWrite{{pascalCase}}StateMutationOptions<
58
+ Context = unknown,
59
+ > extends Omit<
60
+ UseEndpointMutationOptions<
61
+ {{pascalCase}}WriteStateRequest,
62
+ {{pascalCase}}StateResponse,
63
+ WriteStateMutationContext< Context >
64
+ >,
65
+ 'invalidate' | 'onError' | 'onMutate'{{#isAuthenticatedPersistencePolicy}} | 'resolveCallOptions'{{/isAuthenticatedPersistencePolicy}}
66
+ > {
67
+ onError?: (
68
+ error: unknown,
69
+ request: {{pascalCase}}WriteStateRequest,
70
+ client: import('@wp-typia/rest/react').EndpointDataClient,
71
+ context: Context | undefined
72
+ ) => void | Promise<void>;
73
+ onMutate?: (
74
+ request: {{pascalCase}}WriteStateRequest,
75
+ client: import('@wp-typia/rest/react').EndpointDataClient
76
+ ) => Context | Promise<Context>;
77
+ {{#isAuthenticatedPersistencePolicy}}
78
+ restNonce?: string;
79
+ {{/isAuthenticatedPersistencePolicy}}
80
+ }
81
+
82
+ export function use{{pascalCase}}StateQuery<
83
+ Selected = {{pascalCase}}StateResponse,
84
+ >(
85
+ request: {{pascalCase}}StateQuery,
86
+ options: Use{{pascalCase}}StateQueryOptions< Selected > = {}
87
+ ) {
88
+ {{#isAuthenticatedPersistencePolicy}}
89
+ const {
90
+ restNonce,
91
+ ...queryOptions
92
+ } = options;
93
+
94
+ return useEndpointQuery( stateEndpoint, request, {
95
+ ...queryOptions,
96
+ resolveCallOptions: () => buildWriteCallOptions( restNonce ),
97
+ } );
98
+ {{/isAuthenticatedPersistencePolicy}}
99
+ {{^isAuthenticatedPersistencePolicy}}
100
+ return useEndpointQuery( stateEndpoint, request, options );
101
+ {{/isAuthenticatedPersistencePolicy}}
102
+ }
103
+
104
+ export function useWrite{{pascalCase}}StateMutation<
105
+ Context = unknown,
106
+ >(
107
+ options: UseWrite{{pascalCase}}StateMutationOptions< Context > = {}
108
+ ) {
109
+ const {
110
+ onError,
111
+ onMutate,
112
+ {{#isAuthenticatedPersistencePolicy}}
113
+ restNonce,
114
+ {{/isAuthenticatedPersistencePolicy}}
115
+ ...mutationOptions
116
+ } = options;
117
+
118
+ return useEndpointMutation( writeStateEndpoint, {
119
+ ...mutationOptions,
120
+ invalidate: ( _data, request ) => ( {
121
+ endpoint: stateEndpoint,
122
+ request: {
123
+ postId: request.postId,
124
+ resourceKey: request.resourceKey,
125
+ },
126
+ } ),
127
+ onError: async ( error, request, client, context ) => {
128
+ if ( context?.previous ) {
129
+ client.setData(
130
+ stateEndpoint,
131
+ {
132
+ postId: request.postId,
133
+ resourceKey: request.resourceKey,
134
+ },
135
+ context.previous
136
+ );
137
+ }
138
+
139
+ await onError?.( error, request, client, context?.userContext );
140
+ },
141
+ onMutate: async ( request, client ) => {
142
+ const queryRequest = {
143
+ postId: request.postId,
144
+ resourceKey: request.resourceKey,
145
+ } satisfies {{pascalCase}}StateQuery;
146
+ const previous = client.getData(
147
+ stateEndpoint,
148
+ queryRequest
149
+ );
150
+
151
+ if ( previous !== undefined ) {
152
+ client.setData(
153
+ stateEndpoint,
154
+ queryRequest,
155
+ ( current ) => {
156
+ if ( ! current ) {
157
+ return current;
158
+ }
159
+
160
+ return {
161
+ ...current,
162
+ count: Math.max(
163
+ 0,
164
+ current.count + ( request.delta ?? 1 )
165
+ ),
166
+ };
167
+ }
168
+ );
169
+ }
170
+
171
+ let userContext: Context | undefined;
172
+ try {
173
+ userContext = onMutate
174
+ ? await onMutate( request, client )
175
+ : undefined;
176
+ } catch ( error ) {
177
+ if ( previous !== undefined ) {
178
+ client.setData( stateEndpoint, queryRequest, previous );
179
+ }
180
+ throw error;
181
+ }
182
+
183
+ return {
184
+ previous,
185
+ userContext,
186
+ };
187
+ },
188
+ {{#isAuthenticatedPersistencePolicy}}
189
+ resolveCallOptions: () => buildWriteCallOptions( restNonce ),
190
+ {{/isAuthenticatedPersistencePolicy}}
191
+ } );
192
+ }
@@ -0,0 +1,123 @@
1
+ import { __ } from '@wordpress/i18n';
2
+ import {
3
+ InspectorControls,
4
+ InnerBlocks,
5
+ RichText,
6
+ useBlockProps,
7
+ } from '@wordpress/block-editor';
8
+ import {
9
+ Notice,
10
+ PanelBody,
11
+ TextControl,
12
+ ToggleControl,
13
+ } from '@wordpress/components';
14
+
15
+ import {
16
+ ALLOWED_CHILD_BLOCKS,
17
+ DEFAULT_CHILD_TEMPLATE,
18
+ } from './children';
19
+ import { useTypiaValidation } from './hooks';
20
+ import type { {{pascalCase}}Attributes } from './types';
21
+ import {
22
+ createAttributeUpdater,
23
+ validate{{pascalCase}}Attributes,
24
+ } from './validators';
25
+
26
+ export default function Edit( {
27
+ attributes,
28
+ setAttributes,
29
+ }: {
30
+ attributes: {{pascalCase}}Attributes;
31
+ setAttributes: ( attrs: Partial< {{pascalCase}}Attributes > ) => void;
32
+ } ) {
33
+ const { errorMessages, isValid } = useTypiaValidation(
34
+ attributes,
35
+ validate{{pascalCase}}Attributes
36
+ );
37
+ const updateAttribute = createAttributeUpdater( attributes, setAttributes );
38
+ const blockProps = useBlockProps( {
39
+ className: '{{cssClassName}}',
40
+ } );
41
+
42
+ return (
43
+ <>
44
+ <InspectorControls>
45
+ <PanelBody title={ __( 'Compound Settings', '{{textDomain}}' ) }>
46
+ <ToggleControl
47
+ label={ __( 'Show dividers between items', '{{textDomain}}' ) }
48
+ checked={ attributes.showDividers ?? true }
49
+ onChange={ ( value ) => updateAttribute( 'showDividers', value ) }
50
+ />
51
+ <ToggleControl
52
+ label={ __( 'Show persisted count', '{{textDomain}}' ) }
53
+ checked={ attributes.showCount ?? true }
54
+ onChange={ ( value ) => updateAttribute( 'showCount', value ) }
55
+ />
56
+ <TextControl
57
+ label={ __( 'Button label', '{{textDomain}}' ) }
58
+ value={ attributes.buttonLabel ?? 'Persist Count' }
59
+ onChange={ ( buttonLabel ) => updateAttribute( 'buttonLabel', buttonLabel ) }
60
+ />
61
+ <TextControl
62
+ label={ __( 'Resource key', '{{textDomain}}' ) }
63
+ value={ attributes.resourceKey ?? '' }
64
+ onChange={ ( resourceKey ) => updateAttribute( 'resourceKey', resourceKey ) }
65
+ help={ __( 'Stable key used by the persisted counter endpoint.', '{{textDomain}}' ) }
66
+ />
67
+ <Notice status="info" isDismissible={ false }>
68
+ { __( 'Storage mode: {{dataStorageMode}}', '{{textDomain}}' ) }
69
+ </Notice>
70
+ <Notice status="info" isDismissible={ false }>
71
+ { __( 'Persistence policy: {{persistencePolicy}}', '{{textDomain}}' ) }
72
+ </Notice>
73
+ </PanelBody>
74
+ { ! isValid && (
75
+ <PanelBody title={ __( 'Validation Errors', '{{textDomain}}' ) } initialOpen>
76
+ { errorMessages.map( ( error, index ) => (
77
+ <Notice key={ index } status="error" isDismissible={ false }>
78
+ { error }
79
+ </Notice>
80
+ ) ) }
81
+ </PanelBody>
82
+ ) }
83
+ </InspectorControls>
84
+ <div { ...blockProps }>
85
+ <RichText
86
+ tagName="h3"
87
+ className="{{cssClassName}}__heading"
88
+ value={ attributes.heading }
89
+ onChange={ ( heading ) => updateAttribute( 'heading', heading ) }
90
+ placeholder={ __( {{titleJson}}, '{{textDomain}}' ) }
91
+ />
92
+ <RichText
93
+ tagName="p"
94
+ className="{{cssClassName}}__intro"
95
+ value={ attributes.intro ?? '' }
96
+ onChange={ ( intro ) => updateAttribute( 'intro', intro ) }
97
+ placeholder={ __(
98
+ 'Add and reorder internal items inside this compound block.',
99
+ '{{textDomain}}'
100
+ ) }
101
+ />
102
+ { ! isValid && (
103
+ <Notice status="error" isDismissible={ false }>
104
+ <ul>
105
+ { errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }
106
+ </ul>
107
+ </Notice>
108
+ ) }
109
+ <p className="{{cssClassName}}__meta">
110
+ { __( 'Resource key:', '{{textDomain}}' ) } { attributes.resourceKey || '—' }
111
+ </p>
112
+ <div className="{{cssClassName}}__items">
113
+ <InnerBlocks
114
+ allowedBlocks={ ALLOWED_CHILD_BLOCKS }
115
+ renderAppender={ InnerBlocks.ButtonBlockAppender }
116
+ template={ DEFAULT_CHILD_TEMPLATE }
117
+ templateLock={ false }
118
+ />
119
+ </div>
120
+ </div>
121
+ </>
122
+ );
123
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ formatValidationError,
3
+ formatValidationErrors,
4
+ useTypiaValidation,
5
+ } from '../../hooks';
6
+
7
+ export type {
8
+ TypiaValidationError,
9
+ ValidationResult,
10
+ ValidationState,
11
+ } from '../../hooks';
@@ -0,0 +1,132 @@
1
+ import { getContext, store } from '@wordpress/interactivity';
2
+ import { generatePublicWriteRequestId } from '@wp-typia/block-runtime/identifiers';
3
+
4
+ import { fetchState, writeState } from './api';
5
+ import type { {{pascalCase}}Context, {{pascalCase}}State } from './types';
6
+
7
+ function hasExpiredPublicWriteToken(
8
+ context: {{pascalCase}}Context
9
+ ): boolean {
10
+ return (
11
+ context.persistencePolicy === 'public' &&
12
+ typeof context.publicWriteExpiresAt === 'number' &&
13
+ context.publicWriteExpiresAt > 0 &&
14
+ Date.now() >= context.publicWriteExpiresAt * 1000
15
+ );
16
+ }
17
+
18
+ function getWriteBlockedMessage(
19
+ context: {{pascalCase}}Context
20
+ ): string {
21
+ return context.persistencePolicy === 'authenticated'
22
+ ? 'Sign in to persist this counter.'
23
+ : 'Reload the page to refresh this write token.';
24
+ }
25
+
26
+ const { actions, state } = store( '{{slugKebabCase}}', {
27
+ state: {
28
+ canWrite: false,
29
+ count: 0,
30
+ error: undefined,
31
+ isHydrated: false,
32
+ isLoading: false,
33
+ isSaving: false,
34
+ } as {{pascalCase}}State,
35
+
36
+ actions: {
37
+ async loadCounter() {
38
+ const context = getContext< {{pascalCase}}Context >();
39
+ if ( context.postId <= 0 || ! context.resourceKey ) {
40
+ return;
41
+ }
42
+
43
+ state.isLoading = true;
44
+ state.error = undefined;
45
+
46
+ try {
47
+ const result = await fetchState( {
48
+ postId: context.postId,
49
+ resourceKey: context.resourceKey,
50
+ } );
51
+ if ( ! result.isValid || ! result.data ) {
52
+ state.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';
53
+ return;
54
+ }
55
+ context.count = result.data.count;
56
+ context.storage = result.data.storage;
57
+ state.count = result.data.count;
58
+ } catch ( error ) {
59
+ state.error =
60
+ error instanceof Error ? error.message : 'Unknown loading error';
61
+ } finally {
62
+ state.isLoading = false;
63
+ }
64
+ },
65
+ async increment() {
66
+ const context = getContext< {{pascalCase}}Context >();
67
+ if ( context.postId <= 0 || ! context.resourceKey ) {
68
+ return;
69
+ }
70
+ if ( hasExpiredPublicWriteToken( context ) ) {
71
+ context.canWrite = false;
72
+ state.canWrite = false;
73
+ state.error = getWriteBlockedMessage( context );
74
+ return;
75
+ }
76
+ if ( ! context.canWrite || ! state.canWrite ) {
77
+ state.error = getWriteBlockedMessage( context );
78
+ return;
79
+ }
80
+
81
+ state.isSaving = true;
82
+ state.error = undefined;
83
+
84
+ try {
85
+ const result = await writeState( {
86
+ delta: 1,
87
+ postId: context.postId,
88
+ publicWriteRequestId:
89
+ context.persistencePolicy === 'public'
90
+ ? generatePublicWriteRequestId()
91
+ : undefined,
92
+ publicWriteToken:
93
+ context.persistencePolicy === 'public' &&
94
+ typeof context.publicWriteToken === 'string' &&
95
+ context.publicWriteToken.length > 0
96
+ ? context.publicWriteToken
97
+ : undefined,
98
+ resourceKey: context.resourceKey,
99
+ }, context.restNonce );
100
+ if ( ! result.isValid || ! result.data ) {
101
+ state.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';
102
+ return;
103
+ }
104
+ context.count = result.data.count;
105
+ context.storage = result.data.storage;
106
+ state.count = result.data.count;
107
+ } catch ( error ) {
108
+ state.error =
109
+ error instanceof Error ? error.message : 'Unknown update error';
110
+ } finally {
111
+ state.isSaving = false;
112
+ }
113
+ },
114
+ },
115
+
116
+ callbacks: {
117
+ init() {
118
+ const context = getContext< {{pascalCase}}Context >();
119
+ context.canWrite =
120
+ context.canWrite && ! hasExpiredPublicWriteToken( context );
121
+ state.canWrite = context.canWrite;
122
+ state.count = context.count;
123
+ },
124
+ mounted() {
125
+ state.isHydrated = true;
126
+ if ( typeof document !== 'undefined' ) {
127
+ document.documentElement.dataset[ '{{slugCamelCase}}Hydrated' ] = 'true';
128
+ }
129
+ void actions.loadCounter();
130
+ },
131
+ },
132
+ } );
@@ -0,0 +1,158 @@
1
+ <?php
2
+ /**
3
+ * Dynamic render entry for the {{title}} compound parent block.
4
+ *
5
+ * @package {{pascalCase}}
6
+ */
7
+
8
+ if ( ! defined( 'ABSPATH' ) ) {
9
+ exit;
10
+ }
11
+
12
+ $validator_path = __DIR__ . '/typia-validator.php';
13
+ if ( ! file_exists( $validator_path ) ) {
14
+ return '';
15
+ }
16
+
17
+ $validator = require $validator_path;
18
+ if ( ! is_object( $validator ) || ! method_exists( $validator, 'apply_defaults' ) || ! method_exists( $validator, 'validate' ) ) {
19
+ return '';
20
+ }
21
+
22
+ $normalized = $validator->apply_defaults( is_array( $attributes ) ? $attributes : array() );
23
+ $validation = $validator->validate( $normalized );
24
+ $resource_key = isset( $normalized['resourceKey'] ) ? (string) $normalized['resourceKey'] : '';
25
+ $heading = isset( $normalized['heading'] ) ? (string) $normalized['heading'] : '{{title}}';
26
+ $intro = isset( $normalized['intro'] ) ? (string) $normalized['intro'] : '';
27
+ $button_label = isset( $normalized['buttonLabel'] ) ? (string) $normalized['buttonLabel'] : 'Persist Count';
28
+ $show_count = ! empty( $normalized['showCount'] );
29
+ $show_dividers = ! empty( $normalized['showDividers'] );
30
+ $post_id = is_object( $block ) && isset( $block->context['postId'] )
31
+ ? (int) $block->context['postId']
32
+ : (int) get_queried_object_id();
33
+ $storage_mode = '{{dataStorageMode}}';
34
+ $persistence_policy = '{{persistencePolicy}}';
35
+ $can_write = false;
36
+ $notice_message = 'authenticated' === $persistence_policy
37
+ ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
38
+ : __( 'Reload the page to refresh this write token.', '{{textDomain}}' );
39
+
40
+ if ( empty( $validation['valid'] ) || '' === $resource_key ) {
41
+ return '';
42
+ }
43
+
44
+ $context = array(
45
+ 'buttonLabel' => $button_label,
46
+ 'canWrite' => false,
47
+ 'count' => 0,
48
+ 'persistencePolicy' => $persistence_policy,
49
+ 'postId' => (int) $post_id,
50
+ 'resourceKey' => $resource_key,
51
+ 'showCount' => $show_count,
52
+ 'storage' => $storage_mode,
53
+ );
54
+
55
+ if ( 'authenticated' === $persistence_policy ) {
56
+ $can_write = $post_id > 0 && is_user_logged_in();
57
+ if ( $can_write ) {
58
+ $context['restNonce'] = wp_create_nonce( 'wp_rest' );
59
+ }
60
+ } elseif ( $post_id > 0 && function_exists( '{{phpPrefix}}_create_public_write_token' ) ) {
61
+ $public_write = {{phpPrefix}}_create_public_write_token( (int) $post_id, $resource_key );
62
+ if ( is_array( $public_write ) ) {
63
+ $token = isset( $public_write['token'] ) ? (string) $public_write['token'] : '';
64
+ $expires_at = isset( $public_write['expiresAt'] ) ? (int) $public_write['expiresAt'] : 0;
65
+
66
+ if ( '' !== $token ) {
67
+ $context['publicWriteToken'] = $token;
68
+ $can_write = true;
69
+ }
70
+
71
+ if ( $expires_at > 0 ) {
72
+ $context['publicWriteExpiresAt'] = $expires_at;
73
+ }
74
+ }
75
+ }
76
+
77
+ $context['canWrite'] = $can_write;
78
+
79
+ $allowed_inner_html = wp_kses_allowed_html( 'post' );
80
+
81
+ foreach ( $allowed_inner_html as &$allowed_attributes ) {
82
+ if ( ! is_array( $allowed_attributes ) ) {
83
+ continue;
84
+ }
85
+
86
+ $allowed_attributes['data-wp-bind--disabled'] = true;
87
+ $allowed_attributes['data-wp-bind--hidden'] = true;
88
+ $allowed_attributes['data-wp-bind--value'] = true;
89
+ $allowed_attributes['data-wp-class'] = true;
90
+ $allowed_attributes['data-wp-class--active'] = true;
91
+ $allowed_attributes['data-wp-context'] = true;
92
+ $allowed_attributes['data-wp-init'] = true;
93
+ $allowed_attributes['data-wp-interactive'] = true;
94
+ $allowed_attributes['data-wp-on--click'] = true;
95
+ $allowed_attributes['data-wp-on--mouseenter'] = true;
96
+ $allowed_attributes['data-wp-on--mouseleave'] = true;
97
+ $allowed_attributes['data-wp-run--mounted'] = true;
98
+ $allowed_attributes['data-wp-style--width'] = true;
99
+ $allowed_attributes['data-wp-text'] = true;
100
+ }
101
+ unset( $allowed_attributes );
102
+
103
+ $sanitized_content = wp_kses( $content, $allowed_inner_html );
104
+
105
+ $wrapper_attributes = get_block_wrapper_attributes(
106
+ array(
107
+ 'class' => '{{cssClassName}}',
108
+ 'data-show-dividers' => $show_dividers ? 'true' : 'false',
109
+ 'data-wp-context' => wp_json_encode( $context ),
110
+ 'data-wp-init' => 'callbacks.init',
111
+ 'data-wp-interactive' => '{{slugKebabCase}}',
112
+ 'data-wp-run--mounted' => 'callbacks.mounted',
113
+ )
114
+ );
115
+ ?>
116
+
117
+ <div <?php echo $wrapper_attributes; ?>>
118
+ <h3 class="{{cssClassName}}__heading"><?php echo esc_html( $heading ); ?></h3>
119
+ <?php if ( '' !== $intro ) : ?>
120
+ <p class="{{cssClassName}}__intro"><?php echo esc_html( $intro ); ?></p>
121
+ <?php endif; ?>
122
+ <?php if ( ! $can_write ) : ?>
123
+ <p class="{{cssClassName}}__notice">
124
+ <?php echo esc_html( $notice_message ); ?>
125
+ </p>
126
+ <?php endif; ?>
127
+ <p
128
+ class="{{cssClassName}}__error"
129
+ role="status"
130
+ aria-live="polite"
131
+ aria-atomic="true"
132
+ data-wp-bind--hidden="!state.error"
133
+ data-wp-text="state.error"
134
+ hidden
135
+ ></p>
136
+ <?php if ( $show_count ) : ?>
137
+ <div class="{{cssClassName}}__counter">
138
+ <span
139
+ class="{{cssClassName}}__count"
140
+ role="status"
141
+ aria-live="polite"
142
+ aria-atomic="true"
143
+ data-wp-text="state.count"
144
+ >0</span>
145
+ <button
146
+ type="button"
147
+ <?php echo $can_write ? '' : 'disabled'; ?>
148
+ data-wp-bind--disabled="!context.canWrite"
149
+ data-wp-on--click="actions.increment"
150
+ >
151
+ <?php echo esc_html( $button_label ); ?>
152
+ </button>
153
+ </div>
154
+ <?php endif; ?>
155
+ <div class="{{cssClassName}}__items">
156
+ <?php echo $sanitized_content; ?>
157
+ </div>
158
+ </div>
@@ -0,0 +1,3 @@
1
+ export default function Save() {
2
+ return null;
3
+ }
@@ -0,0 +1,56 @@
1
+ import type {
2
+ TypiaValidationError,
3
+ ValidationResult,
4
+ } from '@wp-typia/block-runtime/validation';
5
+ import { tags } from 'typia';
6
+
7
+ export type {
8
+ TypiaValidationError,
9
+ ValidationResult,
10
+ } from '@wp-typia/block-runtime/validation';
11
+
12
+ export interface {{pascalCase}}Attributes {
13
+ heading: string &
14
+ tags.MinLength< 1 > &
15
+ tags.MaxLength< 80 > &
16
+ tags.Default< {{titleJson}} >;
17
+ intro?: string &
18
+ tags.MinLength< 1 > &
19
+ tags.MaxLength< 180 > &
20
+ tags.Default< 'Add and reorder internal items inside this compound block.' >;
21
+ showDividers?: boolean & tags.Default< true >;
22
+ showCount?: boolean & tags.Default< true >;
23
+ buttonLabel?: string &
24
+ tags.MinLength< 1 > &
25
+ tags.MaxLength< 40 > &
26
+ tags.Default< 'Persist Count' >;
27
+ resourceKey?: string &
28
+ tags.MinLength< 1 > &
29
+ tags.MaxLength< 100 > &
30
+ tags.Default< 'primary' >;
31
+ }
32
+
33
+ export interface {{pascalCase}}Context {
34
+ buttonLabel: string;
35
+ canWrite: boolean;
36
+ count: number;
37
+ persistencePolicy: 'authenticated' | 'public';
38
+ postId: number;
39
+ publicWriteExpiresAt?: number;
40
+ publicWriteToken?: string;
41
+ resourceKey: string;
42
+ restNonce?: string;
43
+ showCount: boolean;
44
+ storage: 'post-meta' | 'custom-table';
45
+ }
46
+
47
+ export interface {{pascalCase}}State {
48
+ canWrite: boolean;
49
+ count: number;
50
+ error?: string;
51
+ isHydrated: boolean;
52
+ isLoading: boolean;
53
+ isSaving: boolean;
54
+ }
55
+
56
+ export type {{pascalCase}}ValidationResult = ValidationResult< {{pascalCase}}Attributes >;