@wp-typia/project-tools 0.16.2 → 0.16.5

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 (80) hide show
  1. package/README.md +13 -0
  2. package/dist/runtime/block-generator-service.d.ts +102 -0
  3. package/dist/runtime/block-generator-service.js +268 -0
  4. package/dist/runtime/built-in-block-artifacts.d.ts +37 -0
  5. package/dist/runtime/built-in-block-artifacts.js +1203 -0
  6. package/dist/runtime/built-in-block-code-artifacts.d.ts +31 -0
  7. package/dist/runtime/built-in-block-code-artifacts.js +137 -0
  8. package/dist/runtime/built-in-block-non-ts-artifacts.d.ts +18 -0
  9. package/dist/runtime/built-in-block-non-ts-artifacts.js +563 -0
  10. package/dist/runtime/cli-doctor.js +10 -5
  11. package/dist/runtime/index.d.ts +2 -0
  12. package/dist/runtime/index.js +1 -0
  13. package/dist/runtime/scaffold-apply-utils.d.ts +47 -0
  14. package/dist/runtime/scaffold-apply-utils.js +405 -0
  15. package/dist/runtime/scaffold-identifiers.d.ts +34 -0
  16. package/dist/runtime/scaffold-identifiers.js +82 -0
  17. package/dist/runtime/scaffold.js +33 -0
  18. package/dist/runtime/starter-manifests.d.ts +3 -2
  19. package/dist/runtime/starter-manifests.js +15 -365
  20. package/dist/runtime/template-builtins.d.ts +9 -0
  21. package/dist/runtime/template-builtins.js +31 -1
  22. package/dist/runtime/template-render.d.ts +5 -0
  23. package/dist/runtime/template-render.js +13 -3
  24. package/dist/runtime/template-source.js +9 -3
  25. package/package.json +2 -2
  26. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +4 -4
  27. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +4 -4
  28. package/templates/_shared/base/src/hooks.ts.mustache +0 -19
  29. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +0 -52
  30. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +0 -123
  31. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +0 -11
  32. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +0 -305
  33. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +0 -152
  34. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +0 -3
  35. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +0 -61
  36. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +0 -43
  37. package/templates/_shared/persistence/core/src/index.tsx.mustache +0 -25
  38. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +0 -308
  39. package/templates/_shared/persistence/core/src/save.tsx.mustache +0 -5
  40. package/templates/_shared/persistence/core/src/validators.ts.mustache +0 -43
  41. package/templates/basic/src/block.json.mustache +0 -51
  42. package/templates/basic/src/edit.tsx.mustache +0 -128
  43. package/templates/basic/src/editor.scss.mustache +0 -8
  44. package/templates/basic/src/hooks.ts.mustache +0 -18
  45. package/templates/basic/src/index.tsx.mustache +0 -45
  46. package/templates/basic/src/render.php.mustache +0 -19
  47. package/templates/basic/src/save.tsx.mustache +0 -30
  48. package/templates/basic/src/style.scss.mustache +0 -40
  49. package/templates/basic/src/types.ts.mustache +0 -56
  50. package/templates/basic/src/validators.ts.mustache +0 -37
  51. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +0 -37
  52. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +0 -25
  53. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +0 -93
  54. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +0 -11
  55. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +0 -25
  56. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +0 -32
  57. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +0 -31
  58. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +0 -18
  59. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +0 -35
  60. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +0 -35
  61. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +0 -50
  62. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +0 -11
  63. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +0 -25
  64. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +0 -24
  65. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +0 -17
  66. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +0 -35
  67. package/templates/interactivity/src/block.json.mustache +0 -74
  68. package/templates/interactivity/src/edit.tsx.mustache +0 -270
  69. package/templates/interactivity/src/editor.scss.mustache +0 -8
  70. package/templates/interactivity/src/index.tsx.mustache +0 -33
  71. package/templates/interactivity/src/interactivity.ts.mustache +0 -152
  72. package/templates/interactivity/src/save.tsx.mustache +0 -101
  73. package/templates/interactivity/src/style.scss.mustache +0 -60
  74. package/templates/interactivity/src/types.ts.mustache +0 -32
  75. package/templates/interactivity/src/validators.ts.mustache +0 -47
  76. package/templates/persistence/src/block.json.mustache +0 -52
  77. package/templates/persistence/src/edit.tsx.mustache +0 -165
  78. package/templates/persistence/src/render.php.mustache +0 -120
  79. package/templates/persistence/src/style.scss.mustache +0 -46
  80. package/templates/persistence/src/types.ts.mustache +0 -59
@@ -0,0 +1,563 @@
1
+ import { renderMustacheTemplateString } from "./template-render.js";
2
+ const BASIC_STYLE_TEMPLATE = `/**
3
+ * {{title}} Block Styles
4
+ */
5
+
6
+ .{{cssClassName}} {
7
+ padding: 20px;
8
+ border: 1px solid #ddd;
9
+ border-radius: 4px;
10
+ background-color: #fff;
11
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
12
+
13
+ &.is-hidden {
14
+ display: none;
15
+ }
16
+
17
+ &__content {
18
+ font-size: 16px;
19
+ line-height: 1.6;
20
+ color: #333;
21
+
22
+ // Alignment styles
23
+ &[data-align="center"] {
24
+ text-align: center;
25
+ }
26
+
27
+ &[data-align="right"] {
28
+ text-align: right;
29
+ }
30
+
31
+ &[data-align="justify"] {
32
+ text-align: justify;
33
+ }
34
+ }
35
+
36
+ // Hover state
37
+ &:hover {
38
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
39
+ transition: box-shadow 0.2s ease;
40
+ }
41
+ }
42
+ `;
43
+ const BASIC_EDITOR_STYLE_TEMPLATE = `/**
44
+ * {{title}} Block Editor Styles
45
+ */
46
+
47
+ .{{cssClassName}} {
48
+ outline: 1px dashed #ddd;
49
+ outline-offset: -1px;
50
+ }
51
+ `;
52
+ const BASIC_RENDER_TEMPLATE = `<?php
53
+ /**
54
+ * Optional server render placeholder for {{title}}.
55
+ *
56
+ * The basic scaffold stays static by default. Keep \`src/save.tsx\` as the
57
+ * canonical frontend output unless you intentionally convert this block into a
58
+ * dynamic render path and add \`render\` to \`block.json\`.
59
+ *
60
+ * @package {{slug}}
61
+ */
62
+
63
+ if ( ! defined( 'ABSPATH' ) ) {
64
+ exit;
65
+ }
66
+
67
+ ?>
68
+ <div class="{{cssClassName}}__server-placeholder" hidden>
69
+ <?php esc_html_e( 'Server render placeholder.', '{{textDomain}}' ); ?>
70
+ </div>
71
+ `;
72
+ const INTERACTIVITY_STYLE_TEMPLATE = `.{{cssClassName}} {
73
+ position: relative;
74
+ padding: 1rem;
75
+ border: 1px solid #dcdcde;
76
+ border-radius: 0.75rem;
77
+ background: #fff;
78
+
79
+ &__content {
80
+ display: grid;
81
+ gap: 0.75rem;
82
+ }
83
+
84
+ &__counter {
85
+ display: inline-flex;
86
+ gap: 0.35rem;
87
+ align-items: center;
88
+ font-size: 0.9rem;
89
+ font-weight: 600;
90
+ }
91
+
92
+ &__progress {
93
+ width: 100%;
94
+ height: 0.5rem;
95
+ overflow: hidden;
96
+ background: #f0f0f1;
97
+ border-radius: 999px;
98
+ }
99
+
100
+ &__progress-bar {
101
+ height: 100%;
102
+ background: #3858e9;
103
+ transition: width 0.2s ease;
104
+ }
105
+
106
+ &__animation {
107
+ min-height: 1.5rem;
108
+ font-size: 0.85rem;
109
+ color: #3858e9;
110
+ opacity: 0;
111
+ transition: opacity 0.2s ease;
112
+
113
+ &.is-active {
114
+ opacity: 1;
115
+ }
116
+ }
117
+
118
+ &__completion {
119
+ font-weight: 700;
120
+ color: #06752d;
121
+ }
122
+
123
+ &__reset {
124
+ align-self: start;
125
+ padding: 0.4rem 0.7rem;
126
+ border: 1px solid #dcdcde;
127
+ border-radius: 999px;
128
+ background: transparent;
129
+ cursor: pointer;
130
+ }
131
+ }
132
+ `;
133
+ const INTERACTIVITY_EDITOR_STYLE_TEMPLATE = `/**
134
+ * {{title}} Block Editor Styles
135
+ */
136
+
137
+ .{{cssClassName}} {
138
+ outline: 1px dashed #ddd;
139
+ outline-offset: -1px;
140
+ }
141
+ `;
142
+ const PERSISTENCE_STYLE_TEMPLATE = `.{{cssClassName}} {
143
+ border: 1px solid #dcdcde;
144
+ border-radius: 12px;
145
+ padding: 16px;
146
+ background: #fff;
147
+
148
+ &__meta {
149
+ color: #50575e;
150
+ font-size: 13px;
151
+ margin: 8px 0 0;
152
+ }
153
+ }
154
+
155
+ .{{frontendCssClassName}} {
156
+ display: grid;
157
+ gap: 12px;
158
+ border: 1px solid #dcdcde;
159
+ border-radius: 12px;
160
+ padding: 16px;
161
+ background: #f6f7f7;
162
+
163
+ &__count {
164
+ display: inline-flex;
165
+ min-width: 3rem;
166
+ justify-content: center;
167
+ font-weight: 700;
168
+ }
169
+
170
+ &__notice,
171
+ &__error {
172
+ margin: 0;
173
+ font-size: 13px;
174
+ }
175
+
176
+ &__notice {
177
+ color: #50575e;
178
+ }
179
+
180
+ &__error {
181
+ color: #b32d2e;
182
+ }
183
+
184
+ button {
185
+ width: fit-content;
186
+ }
187
+ }
188
+ `;
189
+ const PERSISTENCE_RENDER_TEMPLATE = `<?php
190
+ /**
191
+ * Dynamic render entry for the {{title}} block.
192
+ *
193
+ * @package {{pascalCase}}
194
+ */
195
+
196
+ if ( ! defined( 'ABSPATH' ) ) {
197
+ exit;
198
+ }
199
+
200
+ $validator_path = __DIR__ . '/typia-validator.php';
201
+ if ( ! file_exists( $validator_path ) ) {
202
+ return '';
203
+ }
204
+
205
+ $validator = require $validator_path;
206
+ if ( ! is_object( $validator ) || ! method_exists( $validator, 'apply_defaults' ) || ! method_exists( $validator, 'validate' ) ) {
207
+ return '';
208
+ }
209
+
210
+ $normalized = $validator->apply_defaults( is_array( $attributes ) ? $attributes : array() );
211
+ $validation = $validator->validate( $normalized );
212
+ $resource_key = isset( $normalized['resourceKey'] ) ? (string) $normalized['resourceKey'] : '';
213
+
214
+ if ( empty( $validation['valid'] ) || '' === $resource_key ) {
215
+ return '';
216
+ }
217
+
218
+ $alignment = isset( $normalized['alignment'] ) ? (string) $normalized['alignment'] : 'left';
219
+ $button_label = isset( $normalized['buttonLabel'] ) ? (string) $normalized['buttonLabel'] : 'Persist Count';
220
+ $content = isset( $normalized['content'] ) ? (string) $normalized['content'] : '';
221
+ $post_id = is_object( $block ) && isset( $block->context['postId'] )
222
+ ? (int) $block->context['postId']
223
+ : (int) get_queried_object_id();
224
+ $storage_mode = '{{dataStorageMode}}';
225
+ $persistence_policy = '{{persistencePolicy}}';
226
+
227
+ {{phpPrefix}}_record_rendered_block_instance(
228
+ (int) $post_id,
229
+ '{{namespace}}/{{slugKebabCase}}',
230
+ $resource_key
231
+ );
232
+
233
+ $notice_message = 'authenticated' === $persistence_policy
234
+ ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
235
+ : __( 'Public writes are temporarily unavailable.', '{{textDomain}}' );
236
+ $context = array(
237
+ 'bootstrapReady' => false,
238
+ 'buttonLabel' => $button_label,
239
+ 'canWrite' => false,
240
+ 'client' => array(
241
+ 'writeExpiry' => 0,
242
+ 'writeNonce' => '',
243
+ 'writeToken' => '',
244
+ ),
245
+ 'count' => 0,
246
+ 'error' => '',
247
+ 'isBootstrapping' => false,
248
+ 'isLoading' => false,
249
+ 'isSaving' => false,
250
+ 'isVisible' => ! empty( $normalized['isVisible'] ),
251
+ 'persistencePolicy' => $persistence_policy,
252
+ 'postId' => (int) $post_id,
253
+ 'resourceKey' => $resource_key,
254
+ 'storage' => $storage_mode,
255
+ );
256
+
257
+ $wrapper_attributes = get_block_wrapper_attributes(
258
+ array(
259
+ 'data-wp-context' => wp_json_encode( $context ),
260
+ 'data-wp-interactive' => '{{slugKebabCase}}',
261
+ 'data-wp-init' => 'callbacks.init',
262
+ 'data-wp-run--mounted' => 'callbacks.mounted',
263
+ )
264
+ );
265
+ ?>
266
+
267
+ <div <?php echo $wrapper_attributes; ?>>
268
+ <div class="{{frontendCssClassName}}">
269
+ <p class="{{frontendCssClassName}}__content" style="<?php echo esc_attr( 'text-align:' . $alignment ); ?>">
270
+ <?php echo esc_html( $content ); ?>
271
+ </p>
272
+ <p
273
+ class="{{frontendCssClassName}}__notice"
274
+ data-wp-bind--hidden="!context.bootstrapReady || context.canWrite"
275
+ hidden
276
+ >
277
+ <?php echo esc_html( $notice_message ); ?>
278
+ </p>
279
+ <p
280
+ class="{{frontendCssClassName}}__error"
281
+ role="status"
282
+ aria-live="polite"
283
+ aria-atomic="true"
284
+ data-wp-bind--hidden="!context.error"
285
+ data-wp-text="context.error"
286
+ hidden
287
+ ></p>
288
+ <?php if ( ! empty( $normalized['showCount'] ) ) : ?>
289
+ <span
290
+ class="{{frontendCssClassName}}__count"
291
+ role="status"
292
+ aria-live="polite"
293
+ aria-atomic="true"
294
+ data-wp-text="context.count"
295
+ >
296
+ 0
297
+ </span>
298
+ <?php endif; ?>
299
+ <button
300
+ type="button"
301
+ disabled
302
+ data-wp-bind--disabled="!context.canWrite"
303
+ data-wp-on--click="actions.increment"
304
+ >
305
+ <?php echo esc_html( $button_label ); ?>
306
+ </button>
307
+ </div>
308
+ </div>
309
+ `;
310
+ const COMPOUND_STYLE_TEMPLATE = `.{{cssClassName}} {
311
+ border: 1px solid #dcdcde;
312
+ border-radius: 12px;
313
+ padding: 1.25rem;
314
+ background: #fff;
315
+ }
316
+
317
+ .{{cssClassName}}__heading {
318
+ margin: 0 0 0.5rem;
319
+ font-size: 1.2rem;
320
+ }
321
+
322
+ .{{cssClassName}}__intro {
323
+ margin: 0 0 1rem;
324
+ color: #50575e;
325
+ }
326
+
327
+ .{{cssClassName}}__items {
328
+ display: grid;
329
+ gap: 0.75rem;
330
+ }
331
+
332
+ .{{cssClassName}}[data-show-dividers='true'] .{{compoundChildCssClassName}} {
333
+ border-top: 1px solid #dcdcde;
334
+ padding-top: 0.75rem;
335
+ }
336
+
337
+ .{{cssClassName}}[data-show-dividers='true'] .{{compoundChildCssClassName}}:first-child {
338
+ border-top: 0;
339
+ padding-top: 0;
340
+ }
341
+ `;
342
+ function renderArtifact(relativePath, template, view) {
343
+ const source = renderMustacheTemplateString(template, view);
344
+ return {
345
+ relativePath,
346
+ source: source.endsWith("\n") ? source : `${source}\n`,
347
+ };
348
+ }
349
+ function toPhpSingleQuotedString(value) {
350
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
351
+ }
352
+ function buildBasicArtifacts(variables) {
353
+ return [
354
+ renderArtifact("src/editor.scss", BASIC_EDITOR_STYLE_TEMPLATE, variables),
355
+ renderArtifact("src/style.scss", BASIC_STYLE_TEMPLATE, variables),
356
+ renderArtifact("src/render.php", BASIC_RENDER_TEMPLATE, variables),
357
+ ];
358
+ }
359
+ function buildInteractivityArtifacts(variables) {
360
+ return [
361
+ renderArtifact("src/editor.scss", INTERACTIVITY_EDITOR_STYLE_TEMPLATE, variables),
362
+ renderArtifact("src/style.scss", INTERACTIVITY_STYLE_TEMPLATE, variables),
363
+ ];
364
+ }
365
+ function buildPersistenceArtifacts(variables) {
366
+ return [
367
+ renderArtifact("src/style.scss", PERSISTENCE_STYLE_TEMPLATE, variables),
368
+ renderArtifact("src/render.php", PERSISTENCE_RENDER_TEMPLATE, variables),
369
+ ];
370
+ }
371
+ function buildCompoundArtifacts(variables) {
372
+ const artifacts = [
373
+ renderArtifact(`src/blocks/${variables.slugKebabCase}/style.scss`, COMPOUND_STYLE_TEMPLATE, variables),
374
+ ];
375
+ if (variables.compoundPersistenceEnabled === "true") {
376
+ const renderView = {
377
+ ...variables,
378
+ titlePhpLiteral: toPhpSingleQuotedString(variables.title),
379
+ };
380
+ const renderSource = `<?php
381
+ /**
382
+ * Dynamic render entry for the {{title}} compound parent block.
383
+ *
384
+ * @package {{pascalCase}}
385
+ */
386
+
387
+ if ( ! defined( 'ABSPATH' ) ) {
388
+ exit;
389
+ }
390
+
391
+ $validator_path = __DIR__ . '/typia-validator.php';
392
+ if ( ! file_exists( $validator_path ) ) {
393
+ return '';
394
+ }
395
+
396
+ $validator = require $validator_path;
397
+ if ( ! is_object( $validator ) || ! method_exists( $validator, 'apply_defaults' ) || ! method_exists( $validator, 'validate' ) ) {
398
+ return '';
399
+ }
400
+
401
+ $normalized = $validator->apply_defaults( is_array( $attributes ) ? $attributes : array() );
402
+ $validation = $validator->validate( $normalized );
403
+ $resource_key = isset( $normalized['resourceKey'] ) ? (string) $normalized['resourceKey'] : '';
404
+ $heading = isset( $normalized['heading'] ) ? (string) $normalized['heading'] : {{titlePhpLiteral}};
405
+ $intro = isset( $normalized['intro'] ) ? (string) $normalized['intro'] : '';
406
+ $button_label = isset( $normalized['buttonLabel'] ) ? (string) $normalized['buttonLabel'] : 'Persist Count';
407
+ $show_count = ! empty( $normalized['showCount'] );
408
+ $show_dividers = ! empty( $normalized['showDividers'] );
409
+ $post_id = is_object( $block ) && isset( $block->context['postId'] )
410
+ ? (int) $block->context['postId']
411
+ : (int) get_queried_object_id();
412
+ $storage_mode = '{{dataStorageMode}}';
413
+ $persistence_policy = '{{persistencePolicy}}';
414
+
415
+ {{phpPrefix}}_record_rendered_block_instance(
416
+ (int) $post_id,
417
+ '{{namespace}}/{{slugKebabCase}}',
418
+ $resource_key
419
+ );
420
+
421
+ $notice_message = 'authenticated' === $persistence_policy
422
+ ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
423
+ : __( 'Public writes are temporarily unavailable.', '{{textDomain}}' );
424
+
425
+ if ( empty( $validation['valid'] ) || '' === $resource_key ) {
426
+ return '';
427
+ }
428
+
429
+ $context = array(
430
+ 'bootstrapReady' => false,
431
+ 'buttonLabel' => $button_label,
432
+ 'canWrite' => false,
433
+ 'client' => array(
434
+ 'writeExpiry' => 0,
435
+ 'writeNonce' => '',
436
+ 'writeToken' => '',
437
+ ),
438
+ 'count' => 0,
439
+ 'error' => '',
440
+ 'isBootstrapping' => false,
441
+ 'isLoading' => false,
442
+ 'isSaving' => false,
443
+ 'persistencePolicy' => $persistence_policy,
444
+ 'postId' => (int) $post_id,
445
+ 'resourceKey' => $resource_key,
446
+ 'showCount' => $show_count,
447
+ 'storage' => $storage_mode,
448
+ );
449
+
450
+ $allowed_inner_html = wp_kses_allowed_html( 'post' );
451
+
452
+ foreach ( $allowed_inner_html as &$allowed_attributes ) {
453
+ if ( ! is_array( $allowed_attributes ) ) {
454
+ continue;
455
+ }
456
+
457
+ $allowed_attributes['data-wp-bind--disabled'] = true;
458
+ $allowed_attributes['data-wp-bind--hidden'] = true;
459
+ $allowed_attributes['data-wp-bind--value'] = true;
460
+ $allowed_attributes['data-wp-class'] = true;
461
+ $allowed_attributes['data-wp-class--active'] = true;
462
+ $allowed_attributes['data-wp-context'] = true;
463
+ $allowed_attributes['data-wp-init'] = true;
464
+ $allowed_attributes['data-wp-interactive'] = true;
465
+ $allowed_attributes['data-wp-on--click'] = true;
466
+ $allowed_attributes['data-wp-on--mouseenter'] = true;
467
+ $allowed_attributes['data-wp-on--mouseleave'] = true;
468
+ $allowed_attributes['data-wp-run--mounted'] = true;
469
+ $allowed_attributes['data-wp-style--width'] = true;
470
+ $allowed_attributes['data-wp-text'] = true;
471
+ }
472
+ unset( $allowed_attributes );
473
+
474
+ $sanitized_content = wp_kses( $content, $allowed_inner_html );
475
+
476
+ $wrapper_attributes = get_block_wrapper_attributes(
477
+ array(
478
+ 'class' => '{{cssClassName}}',
479
+ 'data-show-dividers' => $show_dividers ? 'true' : 'false',
480
+ 'data-wp-context' => wp_json_encode( $context ),
481
+ 'data-wp-init' => 'callbacks.init',
482
+ 'data-wp-interactive' => '{{slugKebabCase}}',
483
+ 'data-wp-run--mounted' => 'callbacks.mounted',
484
+ )
485
+ );
486
+ ?>
487
+
488
+ <div <?php echo $wrapper_attributes; ?>>
489
+ <h3 class="{{cssClassName}}__heading"><?php echo esc_html( $heading ); ?></h3>
490
+ <?php if ( '' !== $intro ) : ?>
491
+ <p class="{{cssClassName}}__intro"><?php echo esc_html( $intro ); ?></p>
492
+ <?php endif; ?>
493
+ <p
494
+ class="{{cssClassName}}__notice"
495
+ data-wp-bind--hidden="!context.bootstrapReady || context.canWrite"
496
+ hidden
497
+ >
498
+ <?php echo esc_html( $notice_message ); ?>
499
+ </p>
500
+ <p
501
+ class="{{cssClassName}}__error"
502
+ role="status"
503
+ aria-live="polite"
504
+ aria-atomic="true"
505
+ data-wp-bind--hidden="!context.error"
506
+ data-wp-text="context.error"
507
+ hidden
508
+ ></p>
509
+ <?php if ( $show_count ) : ?>
510
+ <div class="{{cssClassName}}__counter">
511
+ <span
512
+ class="{{cssClassName}}__count"
513
+ role="status"
514
+ aria-live="polite"
515
+ aria-atomic="true"
516
+ data-wp-text="context.count"
517
+ >0</span>
518
+ <button
519
+ type="button"
520
+ disabled
521
+ data-wp-bind--disabled="!context.canWrite"
522
+ data-wp-on--click="actions.increment"
523
+ >
524
+ <?php echo esc_html( $button_label ); ?>
525
+ </button>
526
+ </div>
527
+ <?php endif; ?>
528
+ <div class="{{cssClassName}}__items">
529
+ <?php echo $sanitized_content; ?>
530
+ </div>
531
+ </div>
532
+ `;
533
+ artifacts.push(renderArtifact(`src/blocks/${variables.slugKebabCase}/render.php`, renderSource, renderView));
534
+ }
535
+ return artifacts;
536
+ }
537
+ /**
538
+ * Builds non-TypeScript scaffold artifacts for built-in block templates.
539
+ *
540
+ * @param options Build options for the selected built-in template family.
541
+ * @param options.templateId Built-in template identifier that controls which
542
+ * non-TS files should be emitted.
543
+ * @param options.variables Scaffold template variables used to render the
544
+ * generated sources.
545
+ * @returns An array of emitter-owned SCSS and PHP artifacts for the selected
546
+ * built-in template family.
547
+ */
548
+ export function buildBuiltInNonTsArtifacts({ templateId, variables, }) {
549
+ switch (templateId) {
550
+ case "basic":
551
+ return buildBasicArtifacts(variables);
552
+ case "interactivity":
553
+ return buildInteractivityArtifacts(variables);
554
+ case "persistence":
555
+ return buildPersistenceArtifacts(variables);
556
+ case "compound":
557
+ return buildCompoundArtifacts(variables);
558
+ default: {
559
+ const unhandledTemplateId = templateId;
560
+ throw new Error(`Unhandled built-in template id: ${unhandledTemplateId}`);
561
+ }
562
+ }
563
+ }
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { access, constants as fsConstants, rm, writeFile } from "node:fs/promises";
6
- import { getBuiltInTemplateLayerDirs } from "./template-builtins.js";
6
+ import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, } from "./template-builtins.js";
7
7
  import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
8
8
  import { isBuiltInTemplateId, listTemplates } from "./template-registry.js";
9
9
  import { readWorkspaceInventory } from "./workspace-inventory.js";
@@ -319,13 +319,18 @@ export async function getDoctorChecks(cwd) {
319
319
  }),
320
320
  ]))
321
321
  : getBuiltInTemplateLayerDirs(builtInTemplateId);
322
- const hasAssets = layerDirs.every((layerDir) => fs.existsSync(layerDir)) &&
323
- layerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "package.json.mustache"))) &&
324
- layerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "src")));
322
+ const missingRequiredLayer = layerDirs.some((layerDir) => !fs.existsSync(layerDir) &&
323
+ !isOmittableBuiltInTemplateLayerDir(builtInTemplateId, layerDir));
324
+ const existingLayerDirs = layerDirs.filter((layerDir) => fs.existsSync(layerDir));
325
+ const hasAssets = !missingRequiredLayer &&
326
+ existingLayerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "package.json.mustache"))) &&
327
+ existingLayerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "src")));
325
328
  checks.push({
326
329
  status: hasAssets ? "pass" : "fail",
327
330
  label: `Template ${template.id}`,
328
- detail: hasAssets ? layerDirs.join(" + ") : "Missing core template assets",
331
+ detail: hasAssets
332
+ ? existingLayerDirs.join(" + ")
333
+ : "Missing core template assets",
329
334
  });
330
335
  }
331
336
  let workspace = null;
@@ -10,6 +10,8 @@
10
10
  * `HOOKED_BLOCK_POSITION_IDS`, and `runDoctor`.
11
11
  */
12
12
  export { scaffoldProject, collectScaffoldAnswers, getDefaultAnswers, getTemplateVariables, resolvePackageManagerId, resolveTemplateId, } from "./scaffold.js";
13
+ export { BlockGeneratorService } from "./block-generator-service.js";
14
+ export type { ApplyBlockInput, BlockGenerationTarget, BlockSpec, PlanBlockInput, PlanBlockResult, RenderBlockInput, RenderBlockResult, ValidateBlockInput, ValidateBlockResult, } from "./block-generator-service.js";
13
15
  export { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand, } from "./migrations.js";
14
16
  export { parseWorkspacePackageManagerId, resolveWorkspaceProject, tryResolveWorkspaceProject, } from "./workspace-project.js";
15
17
  export { manifestAttributeToJsonSchema, projectJsonSchemaDocument, manifestToJsonSchema, manifestToOpenApi, normalizeEndpointAuthDefinition, } from "./schema-core.js";
@@ -10,6 +10,7 @@
10
10
  * `HOOKED_BLOCK_POSITION_IDS`, and `runDoctor`.
11
11
  */
12
12
  export { scaffoldProject, collectScaffoldAnswers, getDefaultAnswers, getTemplateVariables, resolvePackageManagerId, resolveTemplateId, } from "./scaffold.js";
13
+ export { BlockGeneratorService } from "./block-generator-service.js";
13
14
  export { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand, } from "./migrations.js";
14
15
  export { parseWorkspacePackageManagerId, resolveWorkspaceProject, tryResolveWorkspaceProject, } from "./workspace-project.js";
15
16
  export { manifestAttributeToJsonSchema, projectJsonSchemaDocument, manifestToJsonSchema, manifestToOpenApi, normalizeEndpointAuthDefinition, } from "./schema-core.js";
@@ -0,0 +1,47 @@
1
+ import { type BuiltInBlockArtifact } from "./built-in-block-artifacts.js";
2
+ import type { BuiltInCodeArtifact } from "./built-in-block-code-artifacts.js";
3
+ import { type BuiltInTemplateId } from "./template-registry.js";
4
+ import type { PackageManagerId } from "./package-managers.js";
5
+ import type { ScaffoldTemplateVariables } from "./scaffold.js";
6
+ export interface InstallDependenciesOptions {
7
+ packageManager: PackageManagerId;
8
+ projectDir: string;
9
+ }
10
+ export declare function ensureDirectory(targetDir: string, allowExisting?: boolean): Promise<void>;
11
+ export declare function buildReadme(templateId: string, variables: ScaffoldTemplateVariables, packageManager: PackageManagerId, { withMigrationUi, withTestPreset, withWpEnv, }?: {
12
+ withMigrationUi?: boolean;
13
+ withTestPreset?: boolean;
14
+ withWpEnv?: boolean;
15
+ }): string;
16
+ export declare function buildGitignore(): string;
17
+ export declare function mergeTextLines(primaryContent: string, existingContent: string): string;
18
+ export declare function writeStarterManifestFiles(targetDir: string, templateId: string, variables: ScaffoldTemplateVariables, artifacts?: readonly BuiltInBlockArtifact[]): Promise<void>;
19
+ /**
20
+ * Seed REST-derived persistence artifacts into a newly scaffolded built-in
21
+ * project before the first manual `sync-rest` run.
22
+ */
23
+ export declare function seedBuiltInPersistenceArtifacts(targetDir: string, templateId: BuiltInTemplateId, variables: ScaffoldTemplateVariables): Promise<void>;
24
+ export declare function normalizePackageManagerFiles(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
25
+ export declare function normalizePackageJson(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
26
+ export declare function removeUnexpectedLockfiles(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
27
+ export declare function replaceTextRecursively(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
28
+ export declare function defaultInstallDependencies({ projectDir, packageManager, }: InstallDependenciesOptions): Promise<void>;
29
+ export declare function isOfficialWorkspaceProject(projectDir: string): boolean;
30
+ export declare function applyWorkspaceMigrationCapability(projectDir: string, packageManager: PackageManagerId): Promise<void>;
31
+ export declare function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir, packageManager, withMigrationUi, withTestPreset, withWpEnv, noInstall, installDependencies, }: {
32
+ projectDir: string;
33
+ templateDir: string;
34
+ templateId: BuiltInTemplateId;
35
+ variables: ScaffoldTemplateVariables;
36
+ artifacts?: readonly BuiltInBlockArtifact[];
37
+ codeArtifacts?: readonly BuiltInCodeArtifact[];
38
+ readmeContent?: string;
39
+ gitignoreContent?: string;
40
+ allowExistingDir?: boolean;
41
+ packageManager: PackageManagerId;
42
+ withMigrationUi?: boolean;
43
+ withTestPreset?: boolean;
44
+ withWpEnv?: boolean;
45
+ noInstall?: boolean;
46
+ installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
47
+ }): Promise<void>;