@wp-typia/project-tools 0.16.4 → 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.
@@ -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;
@@ -27,6 +27,15 @@ export interface MaterializedBuiltInTemplateSource {
27
27
  * resolve to the shared base plus their own template directory.
28
28
  */
29
29
  export declare function getBuiltInTemplateLayerDirs(templateId: BuiltInTemplateId, { persistenceEnabled, persistencePolicy, }?: BuiltInTemplateVariantOptions): string[];
30
+ /**
31
+ * Returns whether a missing built-in overlay directory is expected because the
32
+ * template family no longer ships any Mustache assets in that layer.
33
+ *
34
+ * @param templateId Built-in template family being resolved.
35
+ * @param layerDir Candidate overlay directory for that family.
36
+ * @returns True when the missing layer can be skipped safely.
37
+ */
38
+ export declare function isOmittableBuiltInTemplateLayerDir(templateId: BuiltInTemplateId, layerDir: string): boolean;
30
39
  /**
31
40
  * Materializes a built-in template into a temporary directory by copying each
32
41
  * resolved layer in order.
@@ -1,7 +1,13 @@
1
+ import fs from "node:fs";
1
2
  import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { promises as fsp } from "node:fs";
4
5
  import { getTemplateById, SHARED_BASE_TEMPLATE_ROOT, SHARED_COMPOUND_TEMPLATE_ROOT, SHARED_PERSISTENCE_TEMPLATE_ROOT, SHARED_REST_HELPER_TEMPLATE_ROOT, } from "./template-registry.js";
6
+ const OMITTABLE_BUILT_IN_OVERLAY_TEMPLATE_IDS = new Set([
7
+ "basic",
8
+ "persistence",
9
+ "compound",
10
+ ]);
5
11
  /**
6
12
  * Returns the ordered overlay directories for a built-in template.
7
13
  *
@@ -33,6 +39,29 @@ export function getBuiltInTemplateLayerDirs(templateId, { persistenceEnabled = f
33
39
  }
34
40
  return [SHARED_BASE_TEMPLATE_ROOT, getTemplateById(templateId).templateDir];
35
41
  }
42
+ /**
43
+ * Returns whether a missing built-in overlay directory is expected because the
44
+ * template family no longer ships any Mustache assets in that layer.
45
+ *
46
+ * @param templateId Built-in template family being resolved.
47
+ * @param layerDir Candidate overlay directory for that family.
48
+ * @returns True when the missing layer can be skipped safely.
49
+ */
50
+ export function isOmittableBuiltInTemplateLayerDir(templateId, layerDir) {
51
+ return (OMITTABLE_BUILT_IN_OVERLAY_TEMPLATE_IDS.has(templateId) &&
52
+ layerDir === getTemplateById(templateId).templateDir);
53
+ }
54
+ function resolveMaterializedBuiltInTemplateLayerDirs(templateId, options) {
55
+ return getBuiltInTemplateLayerDirs(templateId, options).flatMap((layerDir) => {
56
+ if (fs.existsSync(layerDir)) {
57
+ return [layerDir];
58
+ }
59
+ if (isOmittableBuiltInTemplateLayerDir(templateId, layerDir)) {
60
+ return [];
61
+ }
62
+ throw new Error(`Built-in template layer is missing: ${layerDir}`);
63
+ });
64
+ }
36
65
  /**
37
66
  * Materializes a built-in template into a temporary directory by copying each
38
67
  * resolved layer in order.
@@ -43,11 +72,12 @@ export function getBuiltInTemplateLayerDirs(templateId, { persistenceEnabled = f
43
72
  */
44
73
  export async function resolveBuiltInTemplateSource(templateId, options = {}) {
45
74
  const template = getTemplateById(templateId);
75
+ const layerDirs = resolveMaterializedBuiltInTemplateLayerDirs(templateId, options);
46
76
  const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-template-"));
47
77
  const templateDir = path.join(tempRoot, templateId);
48
78
  try {
49
79
  await fsp.mkdir(templateDir, { recursive: true });
50
- for (const layerDir of getBuiltInTemplateLayerDirs(templateId, options)) {
80
+ for (const layerDir of layerDirs) {
51
81
  await fsp.cp(layerDir, templateDir, {
52
82
  recursive: true,
53
83
  force: true,
@@ -9,10 +9,10 @@ import { pathToFileURL } from "node:url";
9
9
  import npa from "npm-package-arg";
10
10
  import semver from "semver";
11
11
  import { x as extractTarball } from "tar";
12
- import { BUILTIN_TEMPLATE_IDS, PROJECT_TOOLS_PACKAGE_ROOT, SHARED_BASE_TEMPLATE_ROOT, TEMPLATE_ROOT, isBuiltInTemplateId, } from "./template-registry.js";
12
+ import { BUILTIN_TEMPLATE_IDS, PROJECT_TOOLS_PACKAGE_ROOT, isBuiltInTemplateId, } from "./template-registry.js";
13
13
  import { isPlainObject } from "./object-utils.js";
14
14
  import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
15
- import { resolveBuiltInTemplateSource } from "./template-builtins.js";
15
+ import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, resolveBuiltInTemplateSource, } from "./template-builtins.js";
16
16
  import { getPackageVersions } from "./package-versions.js";
17
17
  import { toSegmentPascalCase } from "./string-case.js";
18
18
  import { copyRawDirectory, copyRenderedDirectory } from "./template-render.js";
@@ -522,7 +522,13 @@ async function normalizeCreateBlockSubset(seed, context) {
522
522
  const blockJson = readRemoteBlockJson(seed.blockDir);
523
523
  const sourceRoot = getSeedSourceRoot(seed.blockDir);
524
524
  await fsp.mkdir(templateDir, { recursive: true });
525
- for (const layerDir of [SHARED_BASE_TEMPLATE_ROOT, path.join(TEMPLATE_ROOT, "basic")]) {
525
+ for (const layerDir of getBuiltInTemplateLayerDirs("basic")) {
526
+ if (!fs.existsSync(layerDir)) {
527
+ if (isOmittableBuiltInTemplateLayerDir("basic", layerDir)) {
528
+ continue;
529
+ }
530
+ throw new Error(`Built-in template layer is missing: ${layerDir}`);
531
+ }
526
532
  await fsp.cp(layerDir, templateDir, {
527
533
  recursive: true,
528
534
  force: true,