@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
@@ -195,6 +195,126 @@ function {{phpPrefix}}_build_state_response( $post_id, $resource_key, $count ) {
195
195
  );
196
196
  }
197
197
 
198
+ function {{phpPrefix}}_block_tree_has_resource_key( $blocks, $block_name, $resource_key ) {
199
+ if ( ! is_array( $blocks ) ) {
200
+ return false;
201
+ }
202
+
203
+ foreach ( $blocks as $block ) {
204
+ if ( ! is_array( $block ) ) {
205
+ continue;
206
+ }
207
+
208
+ if ( (string) ( $block['blockName'] ?? '' ) === $block_name ) {
209
+ $attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
210
+ $candidate_resource_key = array_key_exists( 'resourceKey', $attributes )
211
+ ? (string) $attributes['resourceKey']
212
+ : 'primary';
213
+ if ( (string) $resource_key === $candidate_resource_key ) {
214
+ return true;
215
+ }
216
+ }
217
+
218
+ if (
219
+ isset( $block['innerBlocks'] ) &&
220
+ {{phpPrefix}}_block_tree_has_resource_key( $block['innerBlocks'], $block_name, $resource_key )
221
+ ) {
222
+ return true;
223
+ }
224
+ }
225
+
226
+ return false;
227
+ }
228
+
229
+ function {{phpPrefix}}_get_rendered_block_instance_key( $post_id, $block_name, $resource_key ) {
230
+ return 'wpt_pri_' . md5( implode( '|', array( (string) $block_name, (int) $post_id, (string) $resource_key ) ) );
231
+ }
232
+
233
+ function {{phpPrefix}}_record_rendered_block_instance( $post_id, $block_name, $resource_key ) {
234
+ if ( $post_id <= 0 || '' === (string) $resource_key || '' === (string) $block_name ) {
235
+ return;
236
+ }
237
+
238
+ set_transient(
239
+ {{phpPrefix}}_get_rendered_block_instance_key( $post_id, $block_name, $resource_key ),
240
+ 1,
241
+ 5 * MINUTE_IN_SECONDS
242
+ );
243
+ }
244
+
245
+ function {{phpPrefix}}_has_rendered_block_instance( $post_id, $resource_key ) {
246
+ if ( $post_id <= 0 || '' === (string) $resource_key ) {
247
+ return false;
248
+ }
249
+
250
+ if (
251
+ false !== get_transient(
252
+ {{phpPrefix}}_get_rendered_block_instance_key(
253
+ $post_id,
254
+ '{{namespace}}/{{slugKebabCase}}',
255
+ $resource_key
256
+ )
257
+ )
258
+ ) {
259
+ return true;
260
+ }
261
+
262
+ $post = get_post( $post_id );
263
+ if ( ! ( $post instanceof WP_Post ) ) {
264
+ return false;
265
+ }
266
+
267
+ if ( ! is_post_publicly_viewable( $post ) ) {
268
+ return false;
269
+ }
270
+
271
+ return {{phpPrefix}}_block_tree_has_resource_key(
272
+ parse_blocks( (string) $post->post_content ),
273
+ '{{namespace}}/{{slugKebabCase}}',
274
+ (string) $resource_key
275
+ );
276
+ }
277
+
278
+ function {{phpPrefix}}_build_bootstrap_response( $post_id, $resource_key ) {
279
+ $response = array(
280
+ 'canWrite' => false,
281
+ );
282
+ $post = get_post( $post_id );
283
+
284
+ if (
285
+ $post_id <= 0 ||
286
+ ! ( $post instanceof WP_Post ) ||
287
+ ! is_post_publicly_viewable( $post ) ||
288
+ ! function_exists( '{{phpPrefix}}_create_public_write_token' ) ||
289
+ ! {{phpPrefix}}_has_rendered_block_instance( (int) $post_id, (string) $resource_key )
290
+ ) {
291
+ return $response;
292
+ }
293
+
294
+ $public_write = {{phpPrefix}}_create_public_write_token( (int) $post_id, (string) $resource_key );
295
+ if ( ! is_array( $public_write ) ) {
296
+ return $response;
297
+ }
298
+
299
+ $token = isset( $public_write['token'] ) ? (string) $public_write['token'] : '';
300
+ $expires_at = isset( $public_write['expiresAt'] ) ? (int) $public_write['expiresAt'] : 0;
301
+ $is_expired = $expires_at > 0 && $expires_at <= time();
302
+
303
+ if ( '' !== $token && $expires_at > 0 && ! $is_expired ) {
304
+ $response['publicWriteToken'] = $token;
305
+ $response['canWrite'] = true;
306
+ }
307
+
308
+ if ( $expires_at > 0 ) {
309
+ $response['publicWriteExpiresAt'] = $expires_at;
310
+ if ( $is_expired ) {
311
+ $response['canWrite'] = false;
312
+ }
313
+ }
314
+
315
+ return $response;
316
+ }
317
+
198
318
  // Route handlers are the main product-level extension point for parent-block request/response shaping.
199
319
  function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
200
320
  $payload = {{phpPrefix}}_validate_and_sanitize_request(
@@ -221,6 +341,33 @@ function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
221
341
  );
222
342
  }
223
343
 
344
+ function {{phpPrefix}}_handle_get_bootstrap( WP_REST_Request $request ) {
345
+ $payload = {{phpPrefix}}_validate_and_sanitize_request(
346
+ array(
347
+ 'postId' => $request->get_param( 'postId' ),
348
+ 'resourceKey' => $request->get_param( 'resourceKey' ),
349
+ ),
350
+ {{phpPrefix}}_get_rest_build_dir(),
351
+ 'bootstrap-query',
352
+ 'query'
353
+ );
354
+
355
+ if ( is_wp_error( $payload ) ) {
356
+ return $payload;
357
+ }
358
+
359
+ $response = rest_ensure_response(
360
+ {{phpPrefix}}_build_bootstrap_response(
361
+ (int) $payload['postId'],
362
+ (string) $payload['resourceKey']
363
+ )
364
+ );
365
+
366
+ $response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, s-maxage=0' );
367
+ $response->header( 'Pragma', 'no-cache' );
368
+ return $response;
369
+ }
370
+
224
371
  function {{phpPrefix}}_handle_write_state( WP_REST_Request $request ) {
225
372
  $payload = {{phpPrefix}}_validate_and_sanitize_request(
226
373
  $request->get_json_params(),
@@ -285,6 +432,18 @@ function {{phpPrefix}}_register_routes() {
285
432
  ),
286
433
  )
287
434
  );
435
+
436
+ register_rest_route(
437
+ '{{namespace}}/v1',
438
+ '/{{slugKebabCase}}/bootstrap',
439
+ array(
440
+ array(
441
+ 'methods' => WP_REST_Server::READABLE,
442
+ 'callback' => '{{phpPrefix}}_handle_get_bootstrap',
443
+ 'permission_callback' => '__return_true',
444
+ ),
445
+ )
446
+ );
288
447
  }
289
448
 
290
449
  function {{phpPrefix}}_register_blocks() {
@@ -197,6 +197,105 @@ function {{phpPrefix}}_build_state_response( $post_id, $resource_key, $count ) {
197
197
  );
198
198
  }
199
199
 
200
+ function {{phpPrefix}}_block_tree_has_resource_key( $blocks, $block_name, $resource_key ) {
201
+ if ( ! is_array( $blocks ) ) {
202
+ return false;
203
+ }
204
+
205
+ foreach ( $blocks as $block ) {
206
+ if ( ! is_array( $block ) ) {
207
+ continue;
208
+ }
209
+
210
+ if ( (string) ( $block['blockName'] ?? '' ) === $block_name ) {
211
+ $attributes = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
212
+ $candidate_resource_key = array_key_exists( 'resourceKey', $attributes )
213
+ ? (string) $attributes['resourceKey']
214
+ : 'primary';
215
+ if ( (string) $resource_key === $candidate_resource_key ) {
216
+ return true;
217
+ }
218
+ }
219
+
220
+ if (
221
+ isset( $block['innerBlocks'] ) &&
222
+ {{phpPrefix}}_block_tree_has_resource_key( $block['innerBlocks'], $block_name, $resource_key )
223
+ ) {
224
+ return true;
225
+ }
226
+ }
227
+
228
+ return false;
229
+ }
230
+
231
+ function {{phpPrefix}}_get_rendered_block_instance_key( $post_id, $block_name, $resource_key ) {
232
+ return 'wpt_pri_' . md5( implode( '|', array( (string) $block_name, (int) $post_id, (string) $resource_key ) ) );
233
+ }
234
+
235
+ function {{phpPrefix}}_record_rendered_block_instance( $post_id, $block_name, $resource_key ) {
236
+ if ( $post_id <= 0 || '' === (string) $resource_key || '' === (string) $block_name ) {
237
+ return;
238
+ }
239
+
240
+ set_transient(
241
+ {{phpPrefix}}_get_rendered_block_instance_key( $post_id, $block_name, $resource_key ),
242
+ 1,
243
+ 5 * MINUTE_IN_SECONDS
244
+ );
245
+ }
246
+
247
+ function {{phpPrefix}}_has_rendered_block_instance( $post_id, $resource_key ) {
248
+ if ( $post_id <= 0 || '' === (string) $resource_key ) {
249
+ return false;
250
+ }
251
+
252
+ if (
253
+ false !== get_transient(
254
+ {{phpPrefix}}_get_rendered_block_instance_key(
255
+ $post_id,
256
+ '{{namespace}}/{{slugKebabCase}}',
257
+ $resource_key
258
+ )
259
+ )
260
+ ) {
261
+ return true;
262
+ }
263
+
264
+ $post = get_post( $post_id );
265
+ if ( ! ( $post instanceof WP_Post ) ) {
266
+ return false;
267
+ }
268
+
269
+ return {{phpPrefix}}_block_tree_has_resource_key(
270
+ parse_blocks( (string) $post->post_content ),
271
+ '{{namespace}}/{{slugKebabCase}}',
272
+ (string) $resource_key
273
+ );
274
+ }
275
+
276
+ function {{phpPrefix}}_build_bootstrap_response( $post_id, $resource_key ) {
277
+ $post = get_post( $post_id );
278
+ $can_read_post =
279
+ $post instanceof WP_Post &&
280
+ (
281
+ is_post_publicly_viewable( $post ) ||
282
+ current_user_can( 'read_post', $post->ID )
283
+ );
284
+ $can_write = $post_id > 0 &&
285
+ is_user_logged_in() &&
286
+ $can_read_post &&
287
+ {{phpPrefix}}_has_rendered_block_instance( (int) $post_id, (string) $resource_key );
288
+ $response = array(
289
+ 'canWrite' => $can_write,
290
+ );
291
+
292
+ if ( $can_write ) {
293
+ $response['restNonce'] = wp_create_nonce( 'wp_rest' );
294
+ }
295
+
296
+ return $response;
297
+ }
298
+
200
299
  // Route handlers are the main product-level extension point for request/response shaping.
201
300
  function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
202
301
  $payload = {{phpPrefix}}_validate_and_sanitize_request(
@@ -223,6 +322,34 @@ function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
223
322
  );
224
323
  }
225
324
 
325
+ function {{phpPrefix}}_handle_get_bootstrap( WP_REST_Request $request ) {
326
+ $payload = {{phpPrefix}}_validate_and_sanitize_request(
327
+ array(
328
+ 'postId' => $request->get_param( 'postId' ),
329
+ 'resourceKey' => $request->get_param( 'resourceKey' ),
330
+ ),
331
+ {{phpPrefix}}_get_rest_build_dir(),
332
+ 'bootstrap-query',
333
+ 'query'
334
+ );
335
+
336
+ if ( is_wp_error( $payload ) ) {
337
+ return $payload;
338
+ }
339
+
340
+ $response = rest_ensure_response(
341
+ {{phpPrefix}}_build_bootstrap_response(
342
+ (int) $payload['postId'],
343
+ (string) $payload['resourceKey']
344
+ )
345
+ );
346
+
347
+ $response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, s-maxage=0' );
348
+ $response->header( 'Pragma', 'no-cache' );
349
+ $response->header( 'Vary', 'Cookie' );
350
+ return $response;
351
+ }
352
+
226
353
  function {{phpPrefix}}_handle_write_state( WP_REST_Request $request ) {
227
354
  $payload = {{phpPrefix}}_validate_and_sanitize_request(
228
355
  $request->get_json_params(),
@@ -272,6 +399,18 @@ function {{phpPrefix}}_register_routes() {
272
399
  ),
273
400
  )
274
401
  );
402
+
403
+ register_rest_route(
404
+ '{{namespace}}/v1',
405
+ '/{{slugKebabCase}}/bootstrap',
406
+ array(
407
+ array(
408
+ 'methods' => WP_REST_Server::READABLE,
409
+ 'callback' => '{{phpPrefix}}_handle_get_bootstrap',
410
+ 'permission_callback' => '__return_true',
411
+ ),
412
+ )
413
+ );
275
414
  }
276
415
 
277
416
  function {{phpPrefix}}_register_block() {
@@ -28,9 +28,15 @@ const REST_ENDPOINT_MANIFEST = defineEndpointManifest( {
28
28
  'state-query': {
29
29
  sourceTypeName: '{{pascalCase}}StateQuery',
30
30
  },
31
+ 'bootstrap-query': {
32
+ sourceTypeName: '{{pascalCase}}BootstrapQuery',
33
+ },
31
34
  'write-state-request': {
32
35
  sourceTypeName: '{{pascalCase}}WriteStateRequest',
33
36
  },
37
+ 'bootstrap-response': {
38
+ sourceTypeName: '{{pascalCase}}BootstrapResponse',
39
+ },
34
40
  'state-response': {
35
41
  sourceTypeName: '{{pascalCase}}StateResponse',
36
42
  },
@@ -59,6 +65,16 @@ const REST_ENDPOINT_MANIFEST = defineEndpointManifest( {
59
65
  mechanism: '{{restWriteAuthMechanism}}',
60
66
  },
61
67
  },
68
+ {
69
+ auth: 'public',
70
+ method: 'GET',
71
+ operationId: 'get{{pascalCase}}Bootstrap',
72
+ path: '/{{namespace}}/v1/{{slugKebabCase}}/bootstrap',
73
+ queryContract: 'bootstrap-query',
74
+ responseContract: 'bootstrap-response',
75
+ summary: 'Read fresh session bootstrap state for the current viewer.',
76
+ tags: [ '{{title}}' ],
77
+ },
62
78
  ],
63
79
  info: {
64
80
  title: '{{title}} REST API',
@@ -5,6 +5,11 @@ export interface {{pascalCase}}StateQuery {
5
5
  resourceKey: string & tags.MinLength< 1 > & tags.MaxLength< 100 >;
6
6
  }
7
7
 
8
+ export interface {{pascalCase}}BootstrapQuery {
9
+ postId: number & tags.Type< 'uint32' >;
10
+ resourceKey: string & tags.MinLength< 1 > & tags.MaxLength< 100 >;
11
+ }
12
+
8
13
  export interface {{pascalCase}}WriteStateRequest {
9
14
  postId: number & tags.Type< 'uint32' >;
10
15
  {{publicWriteRequestIdDeclaration}}
@@ -13,6 +18,11 @@ export interface {{pascalCase}}WriteStateRequest {
13
18
  delta?: number & tags.Minimum< 1 > & tags.Type< 'uint32' > & tags.Default< 1 >;
14
19
  }
15
20
 
21
+ export interface {{pascalCase}}BootstrapResponse {
22
+ canWrite: boolean;
23
+ {{bootstrapCredentialDeclarations}}
24
+ }
25
+
16
26
  export interface {{pascalCase}}StateResponse {
17
27
  postId: number & tags.Type< 'uint32' >;
18
28
  resourceKey: string & tags.MinLength< 1 > & tags.MaxLength< 100 >;
@@ -3,10 +3,12 @@ import {
3
3
  } from '@wp-typia/rest';
4
4
 
5
5
  import {
6
+ type {{pascalCase}}BootstrapQuery,
6
7
  type {{pascalCase}}StateQuery,
7
8
  type {{pascalCase}}WriteStateRequest,
8
9
  } from './api-types';
9
10
  import {
11
+ get{{pascalCase}}BootstrapEndpoint,
10
12
  get{{pascalCase}}StateEndpoint,
11
13
  write{{pascalCase}}StateEndpoint,
12
14
  } from './api-client';
@@ -15,6 +17,10 @@ import {
15
17
  type PersistenceTransportOptions,
16
18
  } from './transport';
17
19
 
20
+ export const bootstrapEndpoint = {
21
+ ...get{{pascalCase}}BootstrapEndpoint,
22
+ };
23
+
18
24
  export const stateEndpoint = {
19
25
  ...get{{pascalCase}}StateEndpoint,
20
26
  };
@@ -40,6 +46,23 @@ export function fetchState(
40
46
  );
41
47
  }
42
48
 
49
+ export function fetchBootstrap(
50
+ request: {{pascalCase}}BootstrapQuery,
51
+ options: PersistenceTransportOptions = {}
52
+ ) {
53
+ return callEndpoint(
54
+ bootstrapEndpoint,
55
+ request,
56
+ resolveTransportCallOptions(
57
+ options.transportTarget ?? 'frontend',
58
+ 'read',
59
+ bootstrapEndpoint,
60
+ request,
61
+ options
62
+ )
63
+ );
64
+ }
65
+
43
66
  export function writeState(
44
67
  request: {{pascalCase}}WriteStateRequest,
45
68
  options: PersistenceTransportOptions = {}
@@ -6,11 +6,14 @@ import {
6
6
  } from '@wp-typia/rest/react';
7
7
 
8
8
  import type {
9
+ {{pascalCase}}BootstrapQuery,
10
+ {{pascalCase}}BootstrapResponse,
9
11
  {{pascalCase}}StateQuery,
10
12
  {{pascalCase}}StateResponse,
11
13
  {{pascalCase}}WriteStateRequest,
12
14
  } from './api-types';
13
15
  import {
16
+ bootstrapEndpoint,
14
17
  stateEndpoint,
15
18
  writeStateEndpoint,
16
19
  } from './api';
@@ -40,6 +43,20 @@ export interface Use{{pascalCase}}StateQueryOptions<
40
43
  transportTarget?: PersistenceTransportTarget;
41
44
  }
42
45
 
46
+ export interface Use{{pascalCase}}BootstrapQueryOptions<
47
+ Selected = {{pascalCase}}BootstrapResponse,
48
+ > extends Omit<
49
+ UseEndpointQueryOptions<
50
+ {{pascalCase}}BootstrapQuery,
51
+ {{pascalCase}}BootstrapResponse,
52
+ Selected
53
+ >,
54
+ 'resolveCallOptions'
55
+ > {
56
+ restNonce?: string;
57
+ transportTarget?: PersistenceTransportTarget;
58
+ }
59
+
43
60
  export interface UseWrite{{pascalCase}}StateMutationOptions<
44
61
  Context = unknown,
45
62
  > extends Omit<
@@ -92,6 +109,34 @@ export function use{{pascalCase}}StateQuery<
92
109
  } );
93
110
  }
94
111
 
112
+ export function use{{pascalCase}}BootstrapQuery<
113
+ Selected = {{pascalCase}}BootstrapResponse,
114
+ >(
115
+ request: {{pascalCase}}BootstrapQuery,
116
+ options: Use{{pascalCase}}BootstrapQueryOptions< Selected > = {}
117
+ ) {
118
+ const {
119
+ restNonce,
120
+ transportTarget = 'editor',
121
+ ...queryOptions
122
+ } = options;
123
+
124
+ return useEndpointQuery( bootstrapEndpoint, request, {
125
+ ...queryOptions,
126
+ resolveCallOptions: () =>
127
+ resolveTransportCallOptions(
128
+ transportTarget,
129
+ 'read',
130
+ bootstrapEndpoint,
131
+ request,
132
+ {
133
+ restNonce,
134
+ transportTarget,
135
+ }
136
+ ),
137
+ } );
138
+ }
139
+
95
140
  export function useWrite{{pascalCase}}StateMutation<
96
141
  Context = unknown,
97
142
  >(