@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.
- package/README.md +361 -0
- package/dist/adapters/ddev.d.ts +16 -0
- package/dist/adapters/ddev.js +75 -0
- package/dist/adapters/ddev.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +167 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +263 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +319 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/pr.d.ts +20 -0
- package/dist/commands/pr.js +282 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/refresh.d.ts +22 -0
- package/dist/commands/refresh.js +375 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/commands/suppress.d.ts +10 -0
- package/dist/commands/suppress.js +86 -0
- package/dist/commands/suppress.js.map +1 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +106 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/update.d.ts +26 -0
- package/dist/commands/update.js +573 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.js +187 -0
- package/dist/config.js.map +1 -0
- package/dist/formatters/index.d.ts +2 -0
- package/dist/formatters/index.js +3 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/json.d.ts +5 -0
- package/dist/formatters/json.js +7 -0
- package/dist/formatters/json.js.map +1 -0
- package/dist/formatters/markdown.d.ts +5 -0
- package/dist/formatters/markdown.js +144 -0
- package/dist/formatters/markdown.js.map +1 -0
- package/dist/formatters/text.d.ts +5 -0
- package/dist/formatters/text.js +123 -0
- package/dist/formatters/text.js.map +1 -0
- package/dist/plugins/accessibility.d.ts +5 -0
- package/dist/plugins/accessibility.js +116 -0
- package/dist/plugins/accessibility.js.map +1 -0
- package/dist/plugins/browser-smoke.d.ts +6 -0
- package/dist/plugins/browser-smoke.js +331 -0
- package/dist/plugins/browser-smoke.js.map +1 -0
- package/dist/plugins/field-interaction.d.ts +6 -0
- package/dist/plugins/field-interaction.js +570 -0
- package/dist/plugins/field-interaction.js.map +1 -0
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.js +28 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/linkit.d.ts +8 -0
- package/dist/plugins/linkit.js +170 -0
- package/dist/plugins/linkit.js.map +1 -0
- package/dist/plugins/media-browser.d.ts +6 -0
- package/dist/plugins/media-browser.js +257 -0
- package/dist/plugins/media-browser.js.map +1 -0
- package/dist/plugins/structural-smoke.d.ts +6 -0
- package/dist/plugins/structural-smoke.js +90 -0
- package/dist/plugins/structural-smoke.js.map +1 -0
- package/dist/plugins/visual-regression.d.ts +8 -0
- package/dist/plugins/visual-regression.js +214 -0
- package/dist/plugins/visual-regression.js.map +1 -0
- package/dist/plugins/wysiwyg.d.ts +8 -0
- package/dist/plugins/wysiwyg.js +221 -0
- package/dist/plugins/wysiwyg.js.map +1 -0
- package/dist/runner.d.ts +21 -0
- package/dist/runner.js +293 -0
- package/dist/runner.js.map +1 -0
- package/dist/suppression.d.ts +55 -0
- package/dist/suppression.js +223 -0
- package/dist/suppression.js.map +1 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/modules/bolt_inspect/bolt_inspect.info.yml +6 -0
- package/modules/bolt_inspect/bolt_inspect.services.yml +22 -0
- package/modules/bolt_inspect/composer.json +16 -0
- package/modules/bolt_inspect/drush.services.yml +10 -0
- package/modules/bolt_inspect/src/Drush/Commands/BoltInspectCommands.php +203 -0
- package/modules/bolt_inspect/src/Service/ContentGenerator.php +586 -0
- package/modules/bolt_inspect/src/Service/SiteProfiler.php +362 -0
- package/modules/bolt_inspect/src/Service/TestEntityTracker.php +98 -0
- package/package.json +46 -0
- package/scripts/setup.sh +34 -0
|
@@ -0,0 +1,362 @@
|
|
|
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\Core\Extension\ModuleExtensionList;
|
|
10
|
+
use Drupal\Core\Menu\MenuLinkTreeInterface;
|
|
11
|
+
use Drupal\Core\Menu\MenuTreeParameters;
|
|
12
|
+
use Drupal\Core\Routing\RouteProviderInterface;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds a complete site profile for the Bolt test runner.
|
|
16
|
+
*/
|
|
17
|
+
class SiteProfiler {
|
|
18
|
+
|
|
19
|
+
public function __construct(
|
|
20
|
+
private readonly EntityTypeManagerInterface $entityTypeManager,
|
|
21
|
+
private readonly EntityFieldManagerInterface $entityFieldManager,
|
|
22
|
+
private readonly ModuleExtensionList $moduleExtensionList,
|
|
23
|
+
private readonly RouteProviderInterface $routeProvider,
|
|
24
|
+
private readonly MenuLinkTreeInterface $menuLinkTree,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the full site profile.
|
|
29
|
+
*
|
|
30
|
+
* @return array<string, mixed>
|
|
31
|
+
*/
|
|
32
|
+
public function profile(): array {
|
|
33
|
+
return [
|
|
34
|
+
'boltInspectVersion' => $this->getModuleVersion(),
|
|
35
|
+
'contentTypes' => $this->getContentTypes(),
|
|
36
|
+
'paragraphBundles' => $this->getParagraphBundles(),
|
|
37
|
+
'enabledModules' => $this->getEnabledModules(),
|
|
38
|
+
'routes' => $this->getRoutes(),
|
|
39
|
+
'mediaTypes' => $this->getMediaTypes(),
|
|
40
|
+
'menus' => $this->getMenus(),
|
|
41
|
+
'representativeUrls' => $this->getRepresentativeUrls(),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the bolt_inspect module version from info.yml.
|
|
47
|
+
*/
|
|
48
|
+
private function getModuleVersion(): string {
|
|
49
|
+
$info = $this->moduleExtensionList->getExtensionInfo('bolt_inspect');
|
|
50
|
+
return $info['version'] ?? 'unknown';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all content types with their field definitions.
|
|
55
|
+
*
|
|
56
|
+
* @return array<int, array<string, mixed>>
|
|
57
|
+
*/
|
|
58
|
+
private function getContentTypes(): array {
|
|
59
|
+
$types = [];
|
|
60
|
+
$nodeTypes = $this->entityTypeManager->getStorage('node_type')->loadMultiple();
|
|
61
|
+
|
|
62
|
+
foreach ($nodeTypes as $nodeType) {
|
|
63
|
+
$bundle = $nodeType->id();
|
|
64
|
+
$fields = $this->getFieldDefinitions('node', $bundle);
|
|
65
|
+
$types[] = [
|
|
66
|
+
'id' => $bundle,
|
|
67
|
+
'label' => $nodeType->label(),
|
|
68
|
+
'fields' => $fields,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return $types;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get all paragraph bundles with their field definitions.
|
|
77
|
+
*
|
|
78
|
+
* @return array<int, array<string, mixed>>
|
|
79
|
+
*/
|
|
80
|
+
private function getParagraphBundles(): array {
|
|
81
|
+
$bundles = [];
|
|
82
|
+
|
|
83
|
+
if (!$this->entityTypeManager->hasDefinition('paragraphs_type')) {
|
|
84
|
+
return $bundles;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$paragraphTypes = $this->entityTypeManager->getStorage('paragraphs_type')->loadMultiple();
|
|
88
|
+
|
|
89
|
+
foreach ($paragraphTypes as $paragraphType) {
|
|
90
|
+
$bundle = $paragraphType->id();
|
|
91
|
+
$fields = $this->getFieldDefinitions('paragraph', $bundle);
|
|
92
|
+
$bundles[] = [
|
|
93
|
+
'id' => $bundle,
|
|
94
|
+
'label' => $paragraphType->label(),
|
|
95
|
+
'fields' => $fields,
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return $bundles;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get field definitions for an entity type + bundle.
|
|
104
|
+
*
|
|
105
|
+
* @return array<int, array<string, mixed>>
|
|
106
|
+
*/
|
|
107
|
+
private function getFieldDefinitions(string $entityType, string $bundle): array {
|
|
108
|
+
$definitions = $this->entityFieldManager->getFieldDefinitions($entityType, $bundle);
|
|
109
|
+
$fields = [];
|
|
110
|
+
|
|
111
|
+
foreach ($definitions as $fieldName => $definition) {
|
|
112
|
+
// Skip base fields (nid, uuid, etc.) — only configurable fields.
|
|
113
|
+
if (!$definition instanceof \Drupal\field\FieldConfigInterface) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$storage = $definition->getFieldStorageDefinition();
|
|
118
|
+
$fields[] = [
|
|
119
|
+
'name' => $fieldName,
|
|
120
|
+
'label' => $definition->getLabel(),
|
|
121
|
+
'type' => $definition->getType(),
|
|
122
|
+
'required' => $definition->isRequired(),
|
|
123
|
+
'cardinality' => $storage->getCardinality(),
|
|
124
|
+
'settings' => $definition->getSettings(),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return $fields;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get accessible frontend routes.
|
|
133
|
+
*
|
|
134
|
+
* @return string[]
|
|
135
|
+
*/
|
|
136
|
+
private function getRoutes(): array {
|
|
137
|
+
$routes = [];
|
|
138
|
+
|
|
139
|
+
foreach ($this->routeProvider->getAllRoutes() as $name => $route) {
|
|
140
|
+
$path = $route->getPath();
|
|
141
|
+
|
|
142
|
+
// Skip admin, system, internal, and utility routes.
|
|
143
|
+
if (str_starts_with($path, '/admin')
|
|
144
|
+
|| str_starts_with($path, '/batch')
|
|
145
|
+
|| str_starts_with($path, '/devel')
|
|
146
|
+
|| str_starts_with($path, '/editor')
|
|
147
|
+
|| str_starts_with($path, '/entity_reference_autocomplete')
|
|
148
|
+
|| str_starts_with($path, '/machine_name')
|
|
149
|
+
|| str_starts_with($path, '/antibot')
|
|
150
|
+
|| str_starts_with($path, '/ckeditor')
|
|
151
|
+
|| str_starts_with($path, '/contextual')
|
|
152
|
+
|| str_starts_with($path, '/history')
|
|
153
|
+
|| str_starts_with($path, '/media')
|
|
154
|
+
|| str_starts_with($path, '/node/add')
|
|
155
|
+
|| str_starts_with($path, '/user')
|
|
156
|
+
|| $path === '/<current>'
|
|
157
|
+
|| $path === '/<front>'
|
|
158
|
+
|| $path === '/<nolink>'
|
|
159
|
+
|| $path === '/<none>'
|
|
160
|
+
|| str_contains($name, 'system.')
|
|
161
|
+
|| str_contains($name, 'entity.node.edit_form')
|
|
162
|
+
|| str_contains($name, 'entity.node.delete_form')
|
|
163
|
+
|| $route->getOption('_admin_route')
|
|
164
|
+
|| str_contains($path, '{')
|
|
165
|
+
) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Only include GET-able routes.
|
|
170
|
+
$methods = $route->getMethods();
|
|
171
|
+
if (!empty($methods) && !in_array('GET', $methods, TRUE)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
$routes[] = $path;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return array_values(array_unique($routes));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get list of enabled modules.
|
|
183
|
+
*
|
|
184
|
+
* @return string[]
|
|
185
|
+
*/
|
|
186
|
+
private function getEnabledModules(): array {
|
|
187
|
+
$installed = $this->moduleExtensionList->getAllInstalledInfo();
|
|
188
|
+
return array_keys($installed);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get media types.
|
|
193
|
+
*
|
|
194
|
+
* @return string[]
|
|
195
|
+
*/
|
|
196
|
+
private function getMediaTypes(): array {
|
|
197
|
+
if (!$this->entityTypeManager->hasDefinition('media_type')) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
$mediaTypes = $this->entityTypeManager->getStorage('media_type')->loadMultiple();
|
|
202
|
+
return array_map(fn($t) => $t->id(), array_values($mediaTypes));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get main menu structure (first level).
|
|
207
|
+
*
|
|
208
|
+
* @return array<int, array<string, mixed>>
|
|
209
|
+
*/
|
|
210
|
+
private function getMenus(): array {
|
|
211
|
+
$parameters = new MenuTreeParameters();
|
|
212
|
+
$parameters->setMaxDepth(1);
|
|
213
|
+
$tree = $this->menuLinkTree->load('main', $parameters);
|
|
214
|
+
$manipulators = [
|
|
215
|
+
['callable' => 'menu.default_tree_manipulators:checkAccess'],
|
|
216
|
+
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
|
|
217
|
+
];
|
|
218
|
+
$tree = $this->menuLinkTree->transform($tree, $manipulators);
|
|
219
|
+
|
|
220
|
+
$items = [];
|
|
221
|
+
foreach ($tree as $element) {
|
|
222
|
+
$link = $element->link;
|
|
223
|
+
$url = $link->getUrlObject();
|
|
224
|
+
try {
|
|
225
|
+
$path = $url->toString();
|
|
226
|
+
}
|
|
227
|
+
catch (\Exception) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
$items[] = [
|
|
231
|
+
'title' => $link->getTitle(),
|
|
232
|
+
'url' => $path,
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return [
|
|
237
|
+
[
|
|
238
|
+
'name' => 'main',
|
|
239
|
+
'items' => $items,
|
|
240
|
+
],
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build representative URLs for browser-smoke and visual-regression.
|
|
246
|
+
*
|
|
247
|
+
* Includes: homepage, main menu items, one node per content type.
|
|
248
|
+
*
|
|
249
|
+
* @return array<int, array<string, mixed>>
|
|
250
|
+
*/
|
|
251
|
+
private function getRepresentativeUrls(): array {
|
|
252
|
+
$urls = [];
|
|
253
|
+
|
|
254
|
+
// Homepage.
|
|
255
|
+
$urls[] = [
|
|
256
|
+
'url' => '/',
|
|
257
|
+
'source' => 'homepage',
|
|
258
|
+
'label' => 'Homepage',
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
// Main menu items.
|
|
262
|
+
$menus = $this->getMenus();
|
|
263
|
+
foreach ($menus as $menu) {
|
|
264
|
+
foreach ($menu['items'] as $item) {
|
|
265
|
+
$urls[] = [
|
|
266
|
+
'url' => $item['url'],
|
|
267
|
+
'source' => 'menu',
|
|
268
|
+
'label' => $item['title'],
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Views with page displays.
|
|
274
|
+
if (\Drupal::moduleHandler()->moduleExists('views')) {
|
|
275
|
+
$viewsUrls = $this->getViewsPageUrls();
|
|
276
|
+
// Only add views URLs that aren't already covered by menu items.
|
|
277
|
+
$existingPaths = array_map(fn($u) => $u['url'], $urls);
|
|
278
|
+
foreach ($viewsUrls as $viewUrl) {
|
|
279
|
+
if (!in_array($viewUrl['url'], $existingPaths, TRUE)) {
|
|
280
|
+
$urls[] = $viewUrl;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// One node per content type (most recent published).
|
|
286
|
+
$nodeTypes = $this->entityTypeManager->getStorage('node_type')->loadMultiple();
|
|
287
|
+
$nodeStorage = $this->entityTypeManager->getStorage('node');
|
|
288
|
+
|
|
289
|
+
foreach ($nodeTypes as $nodeType) {
|
|
290
|
+
$bundle = $nodeType->id();
|
|
291
|
+
$query = $nodeStorage->getQuery()
|
|
292
|
+
->accessCheck(TRUE)
|
|
293
|
+
->condition('type', $bundle)
|
|
294
|
+
->condition('status', 1)
|
|
295
|
+
->sort('created', 'DESC')
|
|
296
|
+
->range(0, 1);
|
|
297
|
+
$nids = $query->execute();
|
|
298
|
+
|
|
299
|
+
if (!empty($nids)) {
|
|
300
|
+
$nid = reset($nids);
|
|
301
|
+
$node = $nodeStorage->load($nid);
|
|
302
|
+
if ($node && $node->hasLinkTemplate('canonical')) {
|
|
303
|
+
try {
|
|
304
|
+
$url = $node->toUrl()->toString();
|
|
305
|
+
$urls[] = [
|
|
306
|
+
'url' => $url,
|
|
307
|
+
'source' => 'content_type',
|
|
308
|
+
'label' => $nodeType->label() . ': ' . $node->label(),
|
|
309
|
+
'contentType' => $bundle,
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
catch (\Exception) {
|
|
313
|
+
// Node may not have a routable path.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return $urls;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get URLs for all enabled views with page displays.
|
|
324
|
+
*
|
|
325
|
+
* @return array<int, array<string, mixed>>
|
|
326
|
+
*/
|
|
327
|
+
private function getViewsPageUrls(): array {
|
|
328
|
+
$urls = [];
|
|
329
|
+
$views = \Drupal\views\Views::getEnabledViews();
|
|
330
|
+
|
|
331
|
+
foreach ($views as $view) {
|
|
332
|
+
foreach ($view->get('display') as $displayId => $display) {
|
|
333
|
+
if (($display['display_plugin'] ?? '') !== 'page') {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
$path = $display['display_options']['path'] ?? NULL;
|
|
337
|
+
if (!$path || str_contains($path, '%') || str_contains($path, '{')) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// Skip admin paths and Drupal defaults nobody visits directly.
|
|
341
|
+
if (str_starts_with($path, 'admin')
|
|
342
|
+
|| $path === 'node'
|
|
343
|
+
|| $path === 'rss.xml'
|
|
344
|
+
|| $path === 'search'
|
|
345
|
+
) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
$urls[] = [
|
|
350
|
+
'url' => '/' . ltrim($path, '/'),
|
|
351
|
+
'source' => 'view',
|
|
352
|
+
'label' => 'View: ' . ($view->label() ?? $view->id()) . ' (' . ($display['display_title'] ?? $displayId) . ')',
|
|
353
|
+
'viewId' => $view->id(),
|
|
354
|
+
'displayId' => $displayId,
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return $urls;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Drupal\bolt_inspect\Service;
|
|
6
|
+
|
|
7
|
+
use Drupal\Core\Database\Connection;
|
|
8
|
+
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
|
9
|
+
use Drupal\Core\State\StateInterface;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tracks test entities created by bolt-inspect:generate for cleanup.
|
|
13
|
+
*/
|
|
14
|
+
class TestEntityTracker {
|
|
15
|
+
|
|
16
|
+
private const STATE_KEY = 'bolt_inspect.tracked_entities';
|
|
17
|
+
|
|
18
|
+
public function __construct(
|
|
19
|
+
private readonly StateInterface $state,
|
|
20
|
+
private readonly EntityTypeManagerInterface $entityTypeManager,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Track a created entity for later cleanup.
|
|
25
|
+
*/
|
|
26
|
+
public function track(string $entityType, int $id, string $label = ''): void {
|
|
27
|
+
$tracked = $this->getTracked();
|
|
28
|
+
$tracked[] = [
|
|
29
|
+
'entity_type' => $entityType,
|
|
30
|
+
'id' => $id,
|
|
31
|
+
'label' => $label,
|
|
32
|
+
'created' => time(),
|
|
33
|
+
];
|
|
34
|
+
$this->state->set(self::STATE_KEY, $tracked);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get all tracked entities.
|
|
39
|
+
*
|
|
40
|
+
* @return array<int, array{entity_type: string, id: int, label: string, created: int}>
|
|
41
|
+
*/
|
|
42
|
+
public function getTracked(): array {
|
|
43
|
+
return $this->state->get(self::STATE_KEY, []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if any tracked entities exist.
|
|
48
|
+
*/
|
|
49
|
+
public function hasTracked(): bool {
|
|
50
|
+
return !empty($this->getTracked());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Delete all tracked entities in reverse order and clear tracking state.
|
|
55
|
+
*
|
|
56
|
+
* @return array<string, int>
|
|
57
|
+
* Count of deleted entities per type.
|
|
58
|
+
*/
|
|
59
|
+
public function cleanupAll(): array {
|
|
60
|
+
$tracked = $this->getTracked();
|
|
61
|
+
$counts = [];
|
|
62
|
+
|
|
63
|
+
// Delete in reverse order (children before parents).
|
|
64
|
+
foreach (array_reverse($tracked) as $entry) {
|
|
65
|
+
$entityType = $entry['entity_type'];
|
|
66
|
+
$id = $entry['id'];
|
|
67
|
+
try {
|
|
68
|
+
$storage = $this->entityTypeManager->getStorage($entityType);
|
|
69
|
+
$entity = $storage->load($id);
|
|
70
|
+
if ($entity) {
|
|
71
|
+
$entity->delete();
|
|
72
|
+
$counts[$entityType] = ($counts[$entityType] ?? 0) + 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (\Exception $e) {
|
|
76
|
+
// Entity API delete may fail if hooks (e.g. auto_entitylabel) interfere.
|
|
77
|
+
// Ensure the entity has a valid title and retry.
|
|
78
|
+
try {
|
|
79
|
+
$entity = $storage->load($id);
|
|
80
|
+
if ($entity && method_exists($entity, 'setTitle')) {
|
|
81
|
+
$entity->setTitle('Bolt Cleanup');
|
|
82
|
+
}
|
|
83
|
+
if ($entity) {
|
|
84
|
+
$entity->delete();
|
|
85
|
+
$counts[$entityType] = ($counts[$entityType] ?? 0) + 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (\Exception) {
|
|
89
|
+
// Still failed — continue with others.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
$this->state->delete(self::STATE_KEY);
|
|
95
|
+
return $counts;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@electriccitizen/bolt",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for Drupal 11+ site testing and operations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/electriccitizen/bolt.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"bolt": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"modules/bolt_inspect/",
|
|
17
|
+
"scripts/setup.sh",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"test:coverage": "vitest run --coverage",
|
|
26
|
+
"prepare": "tsc",
|
|
27
|
+
"prepublishOnly": "npm run build && npm test"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@axe-core/playwright": "^4.11.1",
|
|
34
|
+
"commander": "^13.1.0",
|
|
35
|
+
"pixelmatch": "^7.1.0",
|
|
36
|
+
"playwright": "^1.52.0",
|
|
37
|
+
"pngjs": "^7.0.0",
|
|
38
|
+
"yaml": "^2.7.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.15.0",
|
|
42
|
+
"@types/pngjs": "^6.0.5",
|
|
43
|
+
"typescript": "^5.8.0",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/scripts/setup.sh
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Set up bolt for development (git clone) or verify an npm global install.
|
|
4
|
+
#
|
|
5
|
+
# Dev setup (from git clone):
|
|
6
|
+
# cd ~/projects/bolt && ./scripts/setup.sh
|
|
7
|
+
#
|
|
8
|
+
# Post npm install:
|
|
9
|
+
# bolt doctor && npx playwright install chromium
|
|
10
|
+
#
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
15
|
+
|
|
16
|
+
cd "$PROJECT_DIR"
|
|
17
|
+
|
|
18
|
+
echo "Installing dependencies..."
|
|
19
|
+
npm install
|
|
20
|
+
|
|
21
|
+
echo "Building TypeScript..."
|
|
22
|
+
npm run build
|
|
23
|
+
|
|
24
|
+
echo "Linking CLI globally..."
|
|
25
|
+
npm link
|
|
26
|
+
|
|
27
|
+
echo "Installing Playwright browsers..."
|
|
28
|
+
npx playwright install chromium
|
|
29
|
+
|
|
30
|
+
echo ""
|
|
31
|
+
echo "Setup complete."
|
|
32
|
+
echo " bolt --version Verify installation"
|
|
33
|
+
echo " bolt doctor Check environment"
|
|
34
|
+
echo ""
|