@electriccitizen/bolt 0.1.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 (92) hide show
  1. package/README.md +361 -0
  2. package/dist/adapters/ddev.d.ts +16 -0
  3. package/dist/adapters/ddev.js +75 -0
  4. package/dist/adapters/ddev.js.map +1 -0
  5. package/dist/adapters/index.d.ts +1 -0
  6. package/dist/adapters/index.js +2 -0
  7. package/dist/adapters/index.js.map +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +167 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +4 -0
  12. package/dist/commands/doctor.js +263 -0
  13. package/dist/commands/doctor.js.map +1 -0
  14. package/dist/commands/init.d.ts +12 -0
  15. package/dist/commands/init.js +319 -0
  16. package/dist/commands/init.js.map +1 -0
  17. package/dist/commands/pr.d.ts +20 -0
  18. package/dist/commands/pr.js +282 -0
  19. package/dist/commands/pr.js.map +1 -0
  20. package/dist/commands/refresh.d.ts +22 -0
  21. package/dist/commands/refresh.js +375 -0
  22. package/dist/commands/refresh.js.map +1 -0
  23. package/dist/commands/suppress.d.ts +10 -0
  24. package/dist/commands/suppress.js +86 -0
  25. package/dist/commands/suppress.js.map +1 -0
  26. package/dist/commands/test.d.ts +5 -0
  27. package/dist/commands/test.js +106 -0
  28. package/dist/commands/test.js.map +1 -0
  29. package/dist/commands/update.d.ts +26 -0
  30. package/dist/commands/update.js +573 -0
  31. package/dist/commands/update.js.map +1 -0
  32. package/dist/config.d.ts +47 -0
  33. package/dist/config.js +187 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/formatters/index.d.ts +2 -0
  36. package/dist/formatters/index.js +3 -0
  37. package/dist/formatters/index.js.map +1 -0
  38. package/dist/formatters/json.d.ts +5 -0
  39. package/dist/formatters/json.js +7 -0
  40. package/dist/formatters/json.js.map +1 -0
  41. package/dist/formatters/markdown.d.ts +5 -0
  42. package/dist/formatters/markdown.js +144 -0
  43. package/dist/formatters/markdown.js.map +1 -0
  44. package/dist/formatters/text.d.ts +5 -0
  45. package/dist/formatters/text.js +123 -0
  46. package/dist/formatters/text.js.map +1 -0
  47. package/dist/plugins/accessibility.d.ts +5 -0
  48. package/dist/plugins/accessibility.js +116 -0
  49. package/dist/plugins/accessibility.js.map +1 -0
  50. package/dist/plugins/browser-smoke.d.ts +6 -0
  51. package/dist/plugins/browser-smoke.js +331 -0
  52. package/dist/plugins/browser-smoke.js.map +1 -0
  53. package/dist/plugins/field-interaction.d.ts +6 -0
  54. package/dist/plugins/field-interaction.js +570 -0
  55. package/dist/plugins/field-interaction.js.map +1 -0
  56. package/dist/plugins/index.d.ts +6 -0
  57. package/dist/plugins/index.js +28 -0
  58. package/dist/plugins/index.js.map +1 -0
  59. package/dist/plugins/linkit.d.ts +8 -0
  60. package/dist/plugins/linkit.js +170 -0
  61. package/dist/plugins/linkit.js.map +1 -0
  62. package/dist/plugins/media-browser.d.ts +6 -0
  63. package/dist/plugins/media-browser.js +257 -0
  64. package/dist/plugins/media-browser.js.map +1 -0
  65. package/dist/plugins/structural-smoke.d.ts +6 -0
  66. package/dist/plugins/structural-smoke.js +90 -0
  67. package/dist/plugins/structural-smoke.js.map +1 -0
  68. package/dist/plugins/visual-regression.d.ts +8 -0
  69. package/dist/plugins/visual-regression.js +214 -0
  70. package/dist/plugins/visual-regression.js.map +1 -0
  71. package/dist/plugins/wysiwyg.d.ts +8 -0
  72. package/dist/plugins/wysiwyg.js +221 -0
  73. package/dist/plugins/wysiwyg.js.map +1 -0
  74. package/dist/runner.d.ts +21 -0
  75. package/dist/runner.js +293 -0
  76. package/dist/runner.js.map +1 -0
  77. package/dist/suppression.d.ts +55 -0
  78. package/dist/suppression.js +223 -0
  79. package/dist/suppression.js.map +1 -0
  80. package/dist/types.d.ts +178 -0
  81. package/dist/types.js +5 -0
  82. package/dist/types.js.map +1 -0
  83. package/modules/bolt_inspect/bolt_inspect.info.yml +6 -0
  84. package/modules/bolt_inspect/bolt_inspect.services.yml +22 -0
  85. package/modules/bolt_inspect/composer.json +16 -0
  86. package/modules/bolt_inspect/drush.services.yml +10 -0
  87. package/modules/bolt_inspect/src/Drush/Commands/BoltInspectCommands.php +203 -0
  88. package/modules/bolt_inspect/src/Service/ContentGenerator.php +586 -0
  89. package/modules/bolt_inspect/src/Service/SiteProfiler.php +362 -0
  90. package/modules/bolt_inspect/src/Service/TestEntityTracker.php +98 -0
  91. package/package.json +46 -0
  92. package/scripts/setup.sh +34 -0
@@ -0,0 +1,586 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Drupal\bolt_inspect\Service;
6
+
7
+ use Drupal\Core\Entity\EntityFieldManagerInterface;
8
+ use Drupal\Core\Entity\EntityTypeManagerInterface;
9
+ use Drupal\field\FieldConfigInterface;
10
+
11
+ /**
12
+ * Generates test content — one node per content type with required fields.
13
+ */
14
+ class ContentGenerator {
15
+
16
+ /**
17
+ * Max depth for nested paragraph creation.
18
+ */
19
+ private const MAX_PARAGRAPH_DEPTH = 3;
20
+
21
+ /**
22
+ * Fields to skip during generation (side effects or special handling).
23
+ */
24
+ private const SKIP_FIELDS = [
25
+ 'moderation_state',
26
+ 'metatag',
27
+ 'layout_builder__layout',
28
+ 'scheduler_publish_on',
29
+ 'scheduler_unpublish_on',
30
+ ];
31
+
32
+ /**
33
+ * Cache of supporting entities (media, taxonomy terms, etc.).
34
+ *
35
+ * @var array<string, mixed>
36
+ */
37
+ private array $supportingEntities = [];
38
+
39
+ /**
40
+ * Cached text format to use for formatted text fields.
41
+ */
42
+ private ?string $textFormat = NULL;
43
+
44
+ public function __construct(
45
+ private readonly EntityTypeManagerInterface $entityTypeManager,
46
+ private readonly EntityFieldManagerInterface $entityFieldManager,
47
+ private readonly TestEntityTracker $tracker,
48
+ ) {}
49
+
50
+ /**
51
+ * Generate test content for all content types.
52
+ *
53
+ * @return array<string, array{status: string, nid?: int, error?: string}>
54
+ */
55
+ public function generateAll(): array {
56
+ $results = [];
57
+ $nodeTypes = $this->entityTypeManager->getStorage('node_type')->loadMultiple();
58
+
59
+ foreach ($nodeTypes as $nodeType) {
60
+ $bundle = $nodeType->id();
61
+ try {
62
+ $node = $this->generateNode($bundle);
63
+ $results[$bundle] = [
64
+ 'status' => 'created',
65
+ 'nid' => (int) $node->id(),
66
+ 'label' => $node->label(),
67
+ ];
68
+ }
69
+ catch (\Exception $e) {
70
+ $results[$bundle] = [
71
+ 'status' => 'error',
72
+ 'error' => $e->getMessage(),
73
+ ];
74
+ }
75
+ }
76
+
77
+ return $results;
78
+ }
79
+
80
+ /**
81
+ * Generate a single test node for the given content type.
82
+ *
83
+ * @return \Drupal\node\NodeInterface
84
+ */
85
+ private function generateNode(string $bundle): \Drupal\node\NodeInterface {
86
+ $fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle);
87
+ $values = [
88
+ 'type' => $bundle,
89
+ 'title' => 'Bolt Test: ' . $bundle,
90
+ 'status' => 0, // Unpublished.
91
+ 'uid' => 1,
92
+ ];
93
+
94
+ foreach ($fields as $fieldName => $definition) {
95
+ if (!$definition instanceof FieldConfigInterface) {
96
+ continue;
97
+ }
98
+ if (in_array($fieldName, self::SKIP_FIELDS, TRUE)) {
99
+ continue;
100
+ }
101
+
102
+ $value = $this->generateFieldValue($definition, 0);
103
+ if ($value !== NULL) {
104
+ $values[$fieldName] = $value;
105
+ }
106
+ }
107
+
108
+ $storage = $this->entityTypeManager->getStorage('node');
109
+ $node = $storage->create($values);
110
+ $node->save();
111
+
112
+ $this->tracker->track('node', (int) $node->id(), $node->label());
113
+
114
+ return $node;
115
+ }
116
+
117
+ /**
118
+ * Generate a value for a field based on its type and settings.
119
+ */
120
+ private function generateFieldValue(FieldConfigInterface $definition, int $depth): mixed {
121
+ $type = $definition->getType();
122
+ $settings = $definition->getSettings();
123
+ $required = $definition->isRequired();
124
+
125
+ // Only populate required fields (and paragraph fields which are often
126
+ // not technically required but define the content structure).
127
+ if (!$required && $type !== 'entity_reference_revisions') {
128
+ return NULL;
129
+ }
130
+
131
+ return match ($type) {
132
+ 'string', 'string_long' => $this->generateString($definition),
133
+ 'text', 'text_long', 'text_with_summary' => $this->generateTextFormatted(),
134
+ 'boolean' => ['value' => 1],
135
+ 'integer' => ['value' => 1],
136
+ 'decimal', 'float' => ['value' => 1.0],
137
+ 'email' => ['value' => 'bolt-test@example.com'],
138
+ 'telephone' => ['value' => '555-555-0100'],
139
+ 'link' => ['uri' => 'https://example.com', 'title' => 'Bolt Test Link'],
140
+ 'datetime' => ['value' => date('Y-m-d\TH:i:s')],
141
+ 'daterange' => [
142
+ 'value' => date('Y-m-d\TH:i:s'),
143
+ 'end_value' => date('Y-m-d\TH:i:s', strtotime('+1 hour')),
144
+ ],
145
+ 'smartdate' => $this->generateSmartDate(),
146
+ 'list_string', 'list_integer', 'list_float' => $this->generateListValue($settings),
147
+ 'entity_reference' => $this->generateEntityReference($definition),
148
+ 'entity_reference_revisions' => $this->generateParagraphs($definition, $depth),
149
+ 'address' => $this->generateAddress(),
150
+ 'image' => $this->generateImage(),
151
+ 'file' => NULL, // Skip file uploads in MVP.
152
+ 'comment' => ['status' => 2], // 0=hidden, 1=closed, 2=open.
153
+ 'webform' => NULL, // Webform reference — skip, requires specific webform entity.
154
+ default => $required ? $this->generateFallback($type) : NULL,
155
+ };
156
+ }
157
+
158
+ private function generateString(FieldConfigInterface $definition): array {
159
+ $maxLength = $definition->getFieldStorageDefinition()->getSetting('max_length') ?? 255;
160
+ $value = 'Bolt test value';
161
+ return ['value' => substr($value, 0, $maxLength)];
162
+ }
163
+
164
+ private function generateTextFormatted(): array {
165
+ return [
166
+ 'value' => '<p>Bolt test content paragraph.</p>',
167
+ 'format' => $this->getTextFormat(),
168
+ ];
169
+ }
170
+
171
+ /**
172
+ * Detect the best available text format.
173
+ */
174
+ private function getTextFormat(): string {
175
+ if ($this->textFormat !== NULL) {
176
+ return $this->textFormat;
177
+ }
178
+
179
+ $preferred = ['full_html', 'basic_html', 'restricted_html', 'plain_text'];
180
+ $formats = $this->entityTypeManager->getStorage('filter_format')->loadMultiple();
181
+ $available = array_keys($formats);
182
+
183
+ foreach ($preferred as $format) {
184
+ if (in_array($format, $available, TRUE)) {
185
+ $this->textFormat = $format;
186
+ return $format;
187
+ }
188
+ }
189
+
190
+ // Fallback to first available.
191
+ $this->textFormat = !empty($available) ? reset($available) : 'plain_text';
192
+ return $this->textFormat;
193
+ }
194
+
195
+ private function generateSmartDate(): array {
196
+ $now = time();
197
+ return [
198
+ 'value' => $now,
199
+ 'end_value' => $now + 3600,
200
+ 'duration' => 60,
201
+ ];
202
+ }
203
+
204
+ private function generateListValue(array $settings): ?array {
205
+ $allowed = $settings['allowed_values'] ?? [];
206
+ if (empty($allowed)) {
207
+ return NULL;
208
+ }
209
+ $keys = array_keys($allowed);
210
+ return ['value' => reset($keys)];
211
+ }
212
+
213
+ private function generateEntityReference(FieldConfigInterface $definition): ?array {
214
+ $settings = $definition->getSettings();
215
+ $targetType = $settings['target_type'] ?? 'node';
216
+
217
+ return match ($targetType) {
218
+ 'taxonomy_term' => $this->getOrCreateTaxonomyTerm($definition),
219
+ 'media' => $this->getOrCreateMedia($definition),
220
+ 'node' => $this->getExistingNodeReference($definition),
221
+ 'block_content' => $this->getOrCreateBlockContent(),
222
+ default => NULL,
223
+ };
224
+ }
225
+
226
+ private function getOrCreateTaxonomyTerm(FieldConfigInterface $definition): ?array {
227
+ $settings = $definition->getSettings();
228
+ $handlerSettings = $settings['handler_settings'] ?? [];
229
+ $targetBundles = $handlerSettings['target_bundles'] ?? [];
230
+
231
+ if (empty($targetBundles)) {
232
+ return NULL;
233
+ }
234
+
235
+ $vocabulary = reset($targetBundles);
236
+ $cacheKey = 'taxonomy_term:' . $vocabulary;
237
+
238
+ if (isset($this->supportingEntities[$cacheKey])) {
239
+ return ['target_id' => $this->supportingEntities[$cacheKey]];
240
+ }
241
+
242
+ // Try to find an existing term first.
243
+ $storage = $this->entityTypeManager->getStorage('taxonomy_term');
244
+ $existing = $storage->getQuery()
245
+ ->accessCheck(FALSE)
246
+ ->condition('vid', $vocabulary)
247
+ ->range(0, 1)
248
+ ->execute();
249
+
250
+ if (!empty($existing)) {
251
+ $tid = reset($existing);
252
+ $this->supportingEntities[$cacheKey] = $tid;
253
+ return ['target_id' => $tid];
254
+ }
255
+
256
+ // Create a term.
257
+ $term = $storage->create([
258
+ 'vid' => $vocabulary,
259
+ 'name' => 'Bolt Test Term',
260
+ ]);
261
+ $term->save();
262
+ $this->tracker->track('taxonomy_term', (int) $term->id(), $term->label());
263
+ $this->supportingEntities[$cacheKey] = (int) $term->id();
264
+
265
+ return ['target_id' => (int) $term->id()];
266
+ }
267
+
268
+ private function getOrCreateMedia(FieldConfigInterface $definition): ?array {
269
+ $settings = $definition->getSettings();
270
+ $handlerSettings = $settings['handler_settings'] ?? [];
271
+ $targetBundles = $handlerSettings['target_bundles'] ?? [];
272
+
273
+ // Prefer image media if available.
274
+ $bundle = 'image';
275
+ if (!empty($targetBundles) && !isset($targetBundles['image'])) {
276
+ $bundle = reset($targetBundles);
277
+ }
278
+
279
+ $cacheKey = 'media:' . $bundle;
280
+ if (isset($this->supportingEntities[$cacheKey])) {
281
+ return ['target_id' => $this->supportingEntities[$cacheKey]];
282
+ }
283
+
284
+ // Try to find existing media.
285
+ if ($this->entityTypeManager->hasDefinition('media')) {
286
+ $storage = $this->entityTypeManager->getStorage('media');
287
+ $existing = $storage->getQuery()
288
+ ->accessCheck(FALSE)
289
+ ->condition('bundle', $bundle)
290
+ ->range(0, 1)
291
+ ->execute();
292
+
293
+ if (!empty($existing)) {
294
+ $mid = reset($existing);
295
+ $this->supportingEntities[$cacheKey] = $mid;
296
+ return ['target_id' => $mid];
297
+ }
298
+
299
+ // Create media entity with a generated image.
300
+ if ($bundle === 'image') {
301
+ return $this->createImageMedia($cacheKey);
302
+ }
303
+
304
+ // For remote_video, use a placeholder.
305
+ if ($bundle === 'remote_video') {
306
+ return $this->createRemoteVideoMedia($cacheKey);
307
+ }
308
+ }
309
+
310
+ return NULL;
311
+ }
312
+
313
+ private function createImageMedia(string $cacheKey): ?array {
314
+ // Create a simple 1x1 PNG.
315
+ $image = imagecreatetruecolor(200, 200);
316
+ if (!$image) {
317
+ return NULL;
318
+ }
319
+ $color = imagecolorallocate($image, 100, 149, 237);
320
+ imagefill($image, 0, 0, $color);
321
+
322
+ $directory = 'public://bolt-test';
323
+ \Drupal::service('file_system')->prepareDirectory($directory, \Drupal\Core\File\FileSystemInterface::CREATE_DIRECTORY);
324
+ $filepath = $directory . '/bolt-test-image.png';
325
+ imagepng($image, \Drupal::service('file_system')->realpath($filepath));
326
+ imagedestroy($image);
327
+
328
+ $file = $this->entityTypeManager->getStorage('file')->create([
329
+ 'uri' => $filepath,
330
+ 'filename' => 'bolt-test-image.png',
331
+ 'filemime' => 'image/png',
332
+ 'status' => 1,
333
+ ]);
334
+ $file->save();
335
+ $this->tracker->track('file', (int) $file->id(), 'bolt-test-image.png');
336
+
337
+ $media = $this->entityTypeManager->getStorage('media')->create([
338
+ 'bundle' => 'image',
339
+ 'name' => 'Bolt Test Image',
340
+ 'field_media_image' => [
341
+ 'target_id' => $file->id(),
342
+ 'alt' => 'Bolt test image',
343
+ ],
344
+ ]);
345
+ $media->save();
346
+ $this->tracker->track('media', (int) $media->id(), 'Bolt Test Image');
347
+
348
+ $this->supportingEntities[$cacheKey] = (int) $media->id();
349
+ return ['target_id' => (int) $media->id()];
350
+ }
351
+
352
+ private function createRemoteVideoMedia(string $cacheKey): ?array {
353
+ $media = $this->entityTypeManager->getStorage('media')->create([
354
+ 'bundle' => 'remote_video',
355
+ 'name' => 'Bolt Test Video',
356
+ 'field_media_oembed_video' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
357
+ ]);
358
+ try {
359
+ $media->save();
360
+ $this->tracker->track('media', (int) $media->id(), 'Bolt Test Video');
361
+ $this->supportingEntities[$cacheKey] = (int) $media->id();
362
+ return ['target_id' => (int) $media->id()];
363
+ }
364
+ catch (\Exception) {
365
+ return NULL;
366
+ }
367
+ }
368
+
369
+ private function getExistingNodeReference(FieldConfigInterface $definition): ?array {
370
+ $settings = $definition->getSettings();
371
+ $handlerSettings = $settings['handler_settings'] ?? [];
372
+ $targetBundles = $handlerSettings['target_bundles'] ?? [];
373
+
374
+ $query = $this->entityTypeManager->getStorage('node')->getQuery()
375
+ ->accessCheck(FALSE)
376
+ ->range(0, 1);
377
+
378
+ if (!empty($targetBundles)) {
379
+ $query->condition('type', array_keys($targetBundles), 'IN');
380
+ }
381
+
382
+ $nids = $query->execute();
383
+ if (!empty($nids)) {
384
+ return ['target_id' => reset($nids)];
385
+ }
386
+
387
+ return NULL;
388
+ }
389
+
390
+ private function getOrCreateBlockContent(): ?array {
391
+ if (!$this->entityTypeManager->hasDefinition('block_content')) {
392
+ return NULL;
393
+ }
394
+
395
+ $cacheKey = 'block_content:basic';
396
+ if (isset($this->supportingEntities[$cacheKey])) {
397
+ return ['target_id' => $this->supportingEntities[$cacheKey]];
398
+ }
399
+
400
+ $storage = $this->entityTypeManager->getStorage('block_content');
401
+
402
+ // Try existing.
403
+ $existing = $storage->getQuery()
404
+ ->accessCheck(FALSE)
405
+ ->range(0, 1)
406
+ ->execute();
407
+
408
+ if (!empty($existing)) {
409
+ $id = reset($existing);
410
+ $this->supportingEntities[$cacheKey] = $id;
411
+ return ['target_id' => $id];
412
+ }
413
+
414
+ // Check if 'basic' block type exists.
415
+ if ($this->entityTypeManager->hasDefinition('block_content_type')) {
416
+ $types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple();
417
+ if (empty($types)) {
418
+ return NULL;
419
+ }
420
+ $blockType = reset($types);
421
+
422
+ $block = $storage->create([
423
+ 'type' => $blockType->id(),
424
+ 'info' => 'Bolt Test Block',
425
+ 'body' => ['value' => '<p>Bolt test block.</p>', 'format' => 'full_html'],
426
+ ]);
427
+ $block->save();
428
+ $this->tracker->track('block_content', (int) $block->id(), 'Bolt Test Block');
429
+ $this->supportingEntities[$cacheKey] = (int) $block->id();
430
+ return ['target_id' => (int) $block->id()];
431
+ }
432
+
433
+ return NULL;
434
+ }
435
+
436
+ /**
437
+ * Generate paragraph entities for an entity_reference_revisions field.
438
+ */
439
+ private function generateParagraphs(FieldConfigInterface $definition, int $depth): ?array {
440
+ if ($depth >= self::MAX_PARAGRAPH_DEPTH) {
441
+ return NULL;
442
+ }
443
+
444
+ $settings = $definition->getSettings();
445
+ $handlerSettings = $settings['handler_settings'] ?? [];
446
+ $targetBundles = $handlerSettings['target_bundles'] ?? [];
447
+
448
+ if (empty($targetBundles)) {
449
+ return NULL;
450
+ }
451
+
452
+ $paragraphs = [];
453
+ foreach (array_keys($targetBundles) as $paragraphBundle) {
454
+ $paragraph = $this->createParagraph($paragraphBundle, $depth);
455
+ if ($paragraph) {
456
+ $paragraphs[] = $paragraph;
457
+ }
458
+ }
459
+
460
+ return !empty($paragraphs) ? $paragraphs : NULL;
461
+ }
462
+
463
+ /**
464
+ * Create a single paragraph entity.
465
+ */
466
+ private function createParagraph(string $bundle, int $depth): ?array {
467
+ $fields = $this->entityFieldManager->getFieldDefinitions('paragraph', $bundle);
468
+ $values = [
469
+ 'type' => $bundle,
470
+ ];
471
+
472
+ foreach ($fields as $fieldName => $definition) {
473
+ if (!$definition instanceof FieldConfigInterface) {
474
+ continue;
475
+ }
476
+ if (in_array($fieldName, self::SKIP_FIELDS, TRUE)) {
477
+ continue;
478
+ }
479
+
480
+ // For nested paragraphs at depth > 0, only create simple children.
481
+ if ($definition->getType() === 'entity_reference_revisions' && $depth > 0) {
482
+ // Pick only the first (simplest) bundle for nested paragraphs.
483
+ $childSettings = $definition->getSettings();
484
+ $childHandler = $childSettings['handler_settings'] ?? [];
485
+ $childBundles = $childHandler['target_bundles'] ?? [];
486
+ if (!empty($childBundles)) {
487
+ // Find a simple bundle (text, horizontal_rule) or use the first one.
488
+ $simpleBundles = array_intersect(array_keys($childBundles), ['text', 'horizontal_rule']);
489
+ $childBundle = !empty($simpleBundles) ? reset($simpleBundles) : reset(array_keys($childBundles));
490
+ $child = $this->createParagraph($childBundle, $depth + 1);
491
+ if ($child) {
492
+ $values[$fieldName] = [$child];
493
+ }
494
+ }
495
+ continue;
496
+ }
497
+
498
+ $value = $this->generateFieldValue($definition, $depth + 1);
499
+ if ($value !== NULL) {
500
+ $values[$fieldName] = $value;
501
+ }
502
+ }
503
+
504
+ try {
505
+ // Verify the paragraph type exists before creating.
506
+ $typeStorage = $this->entityTypeManager->getStorage('paragraphs_type');
507
+ if (!$typeStorage->load($bundle)) {
508
+ return NULL;
509
+ }
510
+
511
+ $storage = $this->entityTypeManager->getStorage('paragraph');
512
+ $paragraph = $storage->create($values);
513
+ $paragraph->save();
514
+ $this->tracker->track('paragraph', (int) $paragraph->id(), 'paragraph:' . $bundle);
515
+
516
+ return [
517
+ 'target_id' => $paragraph->id(),
518
+ 'target_revision_id' => $paragraph->getRevisionId(),
519
+ ];
520
+ }
521
+ catch (\Exception $e) {
522
+ // Log but don't fail — some paragraphs may have complex requirements.
523
+ \Drupal::logger('bolt_inspect')->warning('Could not create paragraph @bundle: @error', [
524
+ '@bundle' => $bundle,
525
+ '@error' => $e->getMessage(),
526
+ ]);
527
+ return NULL;
528
+ }
529
+ }
530
+
531
+ private function generateAddress(): array {
532
+ return [
533
+ 'country_code' => 'US',
534
+ 'administrative_area' => 'MT',
535
+ 'locality' => 'Missoula',
536
+ 'postal_code' => '59801',
537
+ 'address_line1' => '123 Bolt Test St',
538
+ ];
539
+ }
540
+
541
+ private function generateImage(): ?array {
542
+ // Reuse the media image creation approach but for direct image fields.
543
+ $cacheKey = 'file:image';
544
+ if (isset($this->supportingEntities[$cacheKey])) {
545
+ return [
546
+ 'target_id' => $this->supportingEntities[$cacheKey],
547
+ 'alt' => 'Bolt test image',
548
+ ];
549
+ }
550
+
551
+ $image = imagecreatetruecolor(200, 200);
552
+ if (!$image) {
553
+ return NULL;
554
+ }
555
+ $color = imagecolorallocate($image, 100, 149, 237);
556
+ imagefill($image, 0, 0, $color);
557
+
558
+ $directory = 'public://bolt-test';
559
+ \Drupal::service('file_system')->prepareDirectory($directory, \Drupal\Core\File\FileSystemInterface::CREATE_DIRECTORY);
560
+ $filepath = $directory . '/bolt-test-direct-image.png';
561
+ imagepng($image, \Drupal::service('file_system')->realpath($filepath));
562
+ imagedestroy($image);
563
+
564
+ $file = $this->entityTypeManager->getStorage('file')->create([
565
+ 'uri' => $filepath,
566
+ 'filename' => 'bolt-test-direct-image.png',
567
+ 'filemime' => 'image/png',
568
+ 'status' => 1,
569
+ ]);
570
+ $file->save();
571
+ $this->tracker->track('file', (int) $file->id(), 'bolt-test-direct-image.png');
572
+ $this->supportingEntities[$cacheKey] = (int) $file->id();
573
+
574
+ return [
575
+ 'target_id' => (int) $file->id(),
576
+ 'alt' => 'Bolt test image',
577
+ ];
578
+ }
579
+
580
+ private function generateFallback(string $type): ?array {
581
+ // For unknown required field types, try a simple string value.
582
+ // This may fail for some types but won't crash the generator.
583
+ return ['value' => 'Bolt test'];
584
+ }
585
+
586
+ }