@wp-typia/project-tools 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/runtime/scaffold-onboarding.js +3 -2
  2. package/dist/runtime/scaffold.d.ts +1 -0
  3. package/dist/runtime/scaffold.js +3 -0
  4. package/dist/runtime/schema-core.d.ts +1 -0
  5. package/dist/runtime/schema-core.js +188 -8
  6. package/package.json +1 -1
  7. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +16 -0
  8. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +10 -0
  9. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +23 -0
  10. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +45 -0
  11. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +201 -42
  12. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +37 -43
  13. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -8
  14. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +139 -0
  15. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +159 -0
  16. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +139 -0
  17. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +16 -0
  18. package/templates/_shared/persistence/core/src/api-types.ts.mustache +10 -0
  19. package/templates/_shared/persistence/core/src/api.ts.mustache +23 -0
  20. package/templates/_shared/persistence/core/src/data.ts.mustache +45 -0
  21. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +201 -44
  22. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +159 -0
  23. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +1 -1
  24. package/templates/_shared/workspace/persistence-auth/server.php.mustache +139 -0
  25. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +1 -1
  26. package/templates/_shared/workspace/persistence-public/server.php.mustache +159 -0
  27. package/templates/persistence/src/edit.tsx.mustache +1 -1
  28. package/templates/persistence/src/render.php.mustache +37 -43
  29. package/templates/persistence/src/types.ts.mustache +13 -9
@@ -158,6 +158,126 @@ function {{phpPrefix}}_build_state_response( $post_id, $resource_key, $count ) {
158
158
  );
159
159
  }
160
160
 
161
+ function {{phpPrefix}}_block_tree_has_resource_key( $blocks, $block_name, $resource_key ) {
162
+ if ( ! is_array( $blocks ) ) {
163
+ return false;
164
+ }
165
+
166
+ foreach ( $blocks as $block ) {
167
+ if ( ! is_array( $block ) ) {
168
+ continue;
169
+ }
170
+
171
+ if ( (string) ( $block['blockName'] ?? '' ) === $block_name ) {
172
+ $attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
173
+ $candidate_resource_key = array_key_exists( 'resourceKey', $attributes )
174
+ ? (string) $attributes['resourceKey']
175
+ : 'primary';
176
+ if ( (string) $resource_key === $candidate_resource_key ) {
177
+ return true;
178
+ }
179
+ }
180
+
181
+ if (
182
+ isset( $block['innerBlocks'] ) &&
183
+ {{phpPrefix}}_block_tree_has_resource_key( $block['innerBlocks'], $block_name, $resource_key )
184
+ ) {
185
+ return true;
186
+ }
187
+ }
188
+
189
+ return false;
190
+ }
191
+
192
+ function {{phpPrefix}}_get_rendered_block_instance_key( $post_id, $block_name, $resource_key ) {
193
+ return 'wpt_pri_' . md5( implode( '|', array( (string) $block_name, (int) $post_id, (string) $resource_key ) ) );
194
+ }
195
+
196
+ function {{phpPrefix}}_record_rendered_block_instance( $post_id, $block_name, $resource_key ) {
197
+ if ( $post_id <= 0 || '' === (string) $resource_key || '' === (string) $block_name ) {
198
+ return;
199
+ }
200
+
201
+ set_transient(
202
+ {{phpPrefix}}_get_rendered_block_instance_key( $post_id, $block_name, $resource_key ),
203
+ 1,
204
+ 5 * MINUTE_IN_SECONDS
205
+ );
206
+ }
207
+
208
+ function {{phpPrefix}}_has_rendered_block_instance( $post_id, $resource_key ) {
209
+ if ( $post_id <= 0 || '' === (string) $resource_key ) {
210
+ return false;
211
+ }
212
+
213
+ if (
214
+ false !== get_transient(
215
+ {{phpPrefix}}_get_rendered_block_instance_key(
216
+ $post_id,
217
+ '{{namespace}}/{{slugKebabCase}}',
218
+ $resource_key
219
+ )
220
+ )
221
+ ) {
222
+ return true;
223
+ }
224
+
225
+ $post = get_post( $post_id );
226
+ if ( ! ( $post instanceof WP_Post ) ) {
227
+ return false;
228
+ }
229
+
230
+ if ( ! is_post_publicly_viewable( $post ) ) {
231
+ return false;
232
+ }
233
+
234
+ return {{phpPrefix}}_block_tree_has_resource_key(
235
+ parse_blocks( (string) $post->post_content ),
236
+ '{{namespace}}/{{slugKebabCase}}',
237
+ (string) $resource_key
238
+ );
239
+ }
240
+
241
+ function {{phpPrefix}}_build_bootstrap_response( $post_id, $resource_key ) {
242
+ $response = array(
243
+ 'canWrite' => false,
244
+ );
245
+ $post = get_post( $post_id );
246
+
247
+ if (
248
+ $post_id <= 0 ||
249
+ ! ( $post instanceof WP_Post ) ||
250
+ ! is_post_publicly_viewable( $post ) ||
251
+ ! function_exists( '{{phpPrefix}}_create_public_write_token' ) ||
252
+ ! {{phpPrefix}}_has_rendered_block_instance( (int) $post_id, (string) $resource_key )
253
+ ) {
254
+ return $response;
255
+ }
256
+
257
+ $public_write = {{phpPrefix}}_create_public_write_token( (int) $post_id, (string) $resource_key );
258
+ if ( ! is_array( $public_write ) ) {
259
+ return $response;
260
+ }
261
+
262
+ $token = isset( $public_write['token'] ) ? (string) $public_write['token'] : '';
263
+ $expires_at = isset( $public_write['expiresAt'] ) ? (int) $public_write['expiresAt'] : 0;
264
+ $is_expired = $expires_at > 0 && $expires_at <= time();
265
+
266
+ if ( '' !== $token && $expires_at > 0 && ! $is_expired ) {
267
+ $response['publicWriteToken'] = $token;
268
+ $response['canWrite'] = true;
269
+ }
270
+
271
+ if ( $expires_at > 0 ) {
272
+ $response['publicWriteExpiresAt'] = $expires_at;
273
+ if ( $is_expired ) {
274
+ $response['canWrite'] = false;
275
+ }
276
+ }
277
+
278
+ return $response;
279
+ }
280
+
161
281
  function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
162
282
  $payload = {{phpPrefix}}_validate_and_sanitize_request(
163
283
  array(
@@ -183,6 +303,33 @@ function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
183
303
  );
184
304
  }
185
305
 
306
+ function {{phpPrefix}}_handle_get_bootstrap( WP_REST_Request $request ) {
307
+ $payload = {{phpPrefix}}_validate_and_sanitize_request(
308
+ array(
309
+ 'postId' => $request->get_param( 'postId' ),
310
+ 'resourceKey' => $request->get_param( 'resourceKey' ),
311
+ ),
312
+ {{phpPrefix}}_get_rest_build_dir(),
313
+ 'bootstrap-query',
314
+ 'query'
315
+ );
316
+
317
+ if ( is_wp_error( $payload ) ) {
318
+ return $payload;
319
+ }
320
+
321
+ $response = rest_ensure_response(
322
+ {{phpPrefix}}_build_bootstrap_response(
323
+ (int) $payload['postId'],
324
+ (string) $payload['resourceKey']
325
+ )
326
+ );
327
+
328
+ $response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, s-maxage=0' );
329
+ $response->header( 'Pragma', 'no-cache' );
330
+ return $response;
331
+ }
332
+
186
333
  function {{phpPrefix}}_handle_write_state( WP_REST_Request $request ) {
187
334
  $payload = {{phpPrefix}}_validate_and_sanitize_request(
188
335
  $request->get_json_params(),
@@ -246,6 +393,18 @@ function {{phpPrefix}}_register_routes() {
246
393
  ),
247
394
  )
248
395
  );
396
+
397
+ register_rest_route(
398
+ '{{namespace}}/v1',
399
+ '/{{slugKebabCase}}/bootstrap',
400
+ array(
401
+ array(
402
+ 'methods' => WP_REST_Server::READABLE,
403
+ 'callback' => '{{phpPrefix}}_handle_get_bootstrap',
404
+ 'permission_callback' => '__return_true',
405
+ ),
406
+ )
407
+ );
249
408
  }
250
409
 
251
410
  add_action( 'init', '{{phpPrefix}}_ensure_storage_installed' );
@@ -112,7 +112,7 @@ export default function Edit( {
112
112
  { persistencePolicyDescription }
113
113
  </Notice>
114
114
  <Notice status="info" isDismissible={ false }>
115
- { __( 'Render mode: dynamic. `render.php` bootstraps post context, storage-backed state, and write-policy data before hydration.', '{{textDomain}}' ) }
115
+ { __( '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}}' ) }
116
116
  </Notice>
117
117
  </InspectorFromManifest>
118
118
  { ! isValid && (
@@ -35,45 +35,37 @@ $post_id = is_object( $block ) && isset( $block->context['postId'] )
35
35
  : (int) get_queried_object_id();
36
36
  $storage_mode = '{{dataStorageMode}}';
37
37
  $persistence_policy = '{{persistencePolicy}}';
38
- $can_write = false;
38
+
39
+ {{phpPrefix}}_record_rendered_block_instance(
40
+ (int) $post_id,
41
+ '{{namespace}}/{{slugKebabCase}}',
42
+ $resource_key
43
+ );
44
+
39
45
  $notice_message = 'authenticated' === $persistence_policy
40
46
  ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
41
- : __( 'Reload the page to refresh this write token.', '{{textDomain}}' );
47
+ : __( 'Public writes are temporarily unavailable.', '{{textDomain}}' );
42
48
  $context = array(
43
- 'buttonLabel' => $button_label,
44
- 'canWrite' => false,
45
- 'count' => 0,
46
- 'isVisible' => ! empty( $normalized['isVisible'] ),
47
- 'persistencePolicy' => $persistence_policy,
48
- 'postId' => (int) $post_id,
49
- 'resourceKey' => $resource_key,
50
- 'storage' => $storage_mode,
49
+ 'bootstrapReady' => false,
50
+ 'buttonLabel' => $button_label,
51
+ 'canWrite' => false,
52
+ 'client' => array(
53
+ 'writeExpiry' => 0,
54
+ 'writeNonce' => '',
55
+ 'writeToken' => '',
56
+ ),
57
+ 'count' => 0,
58
+ 'error' => '',
59
+ 'isBootstrapping' => false,
60
+ 'isLoading' => false,
61
+ 'isSaving' => false,
62
+ 'isVisible' => ! empty( $normalized['isVisible'] ),
63
+ 'persistencePolicy' => $persistence_policy,
64
+ 'postId' => (int) $post_id,
65
+ 'resourceKey' => $resource_key,
66
+ 'storage' => $storage_mode,
51
67
  );
52
68
 
53
- if ( 'authenticated' === $persistence_policy ) {
54
- $can_write = $post_id > 0 && is_user_logged_in();
55
- if ( $can_write ) {
56
- $context['restNonce'] = wp_create_nonce( 'wp_rest' );
57
- }
58
- } elseif ( $post_id > 0 && function_exists( '{{phpPrefix}}_create_public_write_token' ) ) {
59
- $public_write = {{phpPrefix}}_create_public_write_token( (int) $post_id, $resource_key );
60
- if ( is_array( $public_write ) ) {
61
- $token = isset( $public_write['token'] ) ? (string) $public_write['token'] : '';
62
- $expires_at = isset( $public_write['expiresAt'] ) ? (int) $public_write['expiresAt'] : 0;
63
-
64
- if ( '' !== $token ) {
65
- $context['publicWriteToken'] = $token;
66
- $can_write = true;
67
- }
68
-
69
- if ( $expires_at > 0 ) {
70
- $context['publicWriteExpiresAt'] = $expires_at;
71
- }
72
- }
73
- }
74
-
75
- $context['canWrite'] = $can_write;
76
-
77
69
  $wrapper_attributes = get_block_wrapper_attributes(
78
70
  array(
79
71
  'data-wp-context' => wp_json_encode( $context ),
@@ -89,18 +81,20 @@ $wrapper_attributes = get_block_wrapper_attributes(
89
81
  <p class="{{frontendCssClassName}}__content" style="<?php echo esc_attr( 'text-align:' . $alignment ); ?>">
90
82
  <?php echo esc_html( $content ); ?>
91
83
  </p>
92
- <?php if ( ! $can_write ) : ?>
93
- <p class="{{frontendCssClassName}}__notice">
94
- <?php echo esc_html( $notice_message ); ?>
95
- </p>
96
- <?php endif; ?>
84
+ <p
85
+ class="{{frontendCssClassName}}__notice"
86
+ data-wp-bind--hidden="!context.bootstrapReady || context.canWrite"
87
+ hidden
88
+ >
89
+ <?php echo esc_html( $notice_message ); ?>
90
+ </p>
97
91
  <p
98
92
  class="{{frontendCssClassName}}__error"
99
93
  role="status"
100
94
  aria-live="polite"
101
95
  aria-atomic="true"
102
- data-wp-bind--hidden="!state.error"
103
- data-wp-text="state.error"
96
+ data-wp-bind--hidden="!context.error"
97
+ data-wp-text="context.error"
104
98
  hidden
105
99
  ></p>
106
100
  <?php if ( ! empty( $normalized['showCount'] ) ) : ?>
@@ -109,14 +103,14 @@ $wrapper_attributes = get_block_wrapper_attributes(
109
103
  role="status"
110
104
  aria-live="polite"
111
105
  aria-atomic="true"
112
- data-wp-text="state.count"
106
+ data-wp-text="context.count"
113
107
  >
114
108
  0
115
109
  </span>
116
110
  <?php endif; ?>
117
111
  <button
118
112
  type="button"
119
- <?php echo $can_write ? '' : 'disabled'; ?>
113
+ disabled
120
114
  data-wp-bind--disabled="!context.canWrite"
121
115
  data-wp-on--click="actions.increment"
122
116
  >
@@ -30,26 +30,30 @@ export interface {{pascalCase}}Attributes {
30
30
 
31
31
  export interface {{pascalCase}}Context {
32
32
  buttonLabel: string;
33
+ bootstrapReady: boolean;
33
34
  canWrite: boolean;
34
35
  count: number;
36
+ error: string;
37
+ isBootstrapping: boolean;
38
+ isLoading: boolean;
39
+ isSaving: boolean;
35
40
  persistencePolicy: 'authenticated' | 'public';
36
41
  postId: number;
37
- publicWriteExpiresAt?: number;
38
- publicWriteToken?: string;
39
42
  resourceKey: string;
40
- restNonce?: string;
41
43
  storage: 'post-meta' | 'custom-table';
42
44
  isVisible: boolean;
45
+ client?: {{pascalCase}}ClientState;
43
46
  }
44
47
 
45
48
  export interface {{pascalCase}}State {
46
- canWrite: boolean;
47
- count: number;
48
- error?: string;
49
49
  isHydrated: boolean;
50
- isLoading: boolean;
51
- isSaving: boolean;
52
- isVisible: boolean;
50
+ }
51
+
52
+ export interface {{pascalCase}}ClientState {
53
+ bootstrapError: string;
54
+ writeExpiry: number;
55
+ writeNonce: string;
56
+ writeToken: string;
53
57
  }
54
58
 
55
59
  export type {{pascalCase}}ValidationResult = ValidationResult< {{pascalCase}}Attributes >;