@imwz/wp-pattern-sentinel 1.1.2 → 1.1.4
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 +3 -1
- package/package.json +1 -1
- package/src/editor.js +83 -48
- package/src/format.js +12 -0
- package/src/main.js +27 -5
- package/src/validation.js +52 -32
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ node bin/sentinel.js --verbose --url=... path/to/patterns/
|
|
|
125
125
|
| `--concurrency` | `4` | Parallel workers |
|
|
126
126
|
| `--json` | `false` | Output JSON (one result per line) |
|
|
127
127
|
| `--keep-page` | `false` | Don't delete draft pages after validation |
|
|
128
|
-
| `--verbose` | `false` | Show detailed step-by-step progress for each pattern |
|
|
128
|
+
| `--verbose` | `false` | Show detailed step-by-step progress for each pattern, with the time each step took |
|
|
129
129
|
| `--width` | `1280` | Viewport width |
|
|
130
130
|
| `--height` | `800` | Viewport height |
|
|
131
131
|
| `--cache` | `false` | Skip patterns that previously passed with the same file content (see [Pass cache](#pass-cache)) |
|
|
@@ -231,3 +231,5 @@ npx wp-pattern-sentinel --url=http://imagewize.test --user=admin --pass=secret p
|
|
|
231
231
|
|
|
232
232
|
- `0` — all patterns passed
|
|
233
233
|
- `1` — one or more patterns failed
|
|
234
|
+
|
|
235
|
+
A `page_creation_error` means the block editor didn't load, so the pattern was never tested. This happens when several workers load the editor at once on a small PHP-FPM pool (e.g. Laravel Valet) and some editor scripts return 502. Sentinel detects this as soon as the page loads and retries up to three times, with a randomized delay, before giving up. The summary counts these failures as infrastructure errors, separate from validation failures. They still exit with `1`. If they keep happening, lower `--concurrency`.
|
package/package.json
CHANGED
package/src/editor.js
CHANGED
|
@@ -61,46 +61,94 @@ export function extractBlockContent(fileContent) {
|
|
|
61
61
|
return stripPhpForValidation(stripped);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const PAGE_CREATION_RETRY_DELAYS = [3_000, 8_000, 15_000]; // ms before retries 1, 2, 3 (plus jitter)
|
|
65
|
+
|
|
64
66
|
/**
|
|
65
|
-
* Navigate to a new draft page and return its post ID.
|
|
67
|
+
* Navigate to a new draft page and return its post ID, or null on failure.
|
|
66
68
|
* WordPress redirects post-new.php → post.php?post=ID&action=edit,
|
|
67
69
|
* so we can read the ID directly from the final URL.
|
|
70
|
+
*
|
|
71
|
+
* When several workers open post-new.php at once, each cold editor requests
|
|
72
|
+
* ~150 scripts. On a small local PHP-FPM pool (Laravel Valet routes even
|
|
73
|
+
* static files through PHP) the socket backlog overflows and some scripts
|
|
74
|
+
* come back 502, so the editor never boots (#21). That's an infrastructure
|
|
75
|
+
* error, not a pattern failure: detect it as soon as the document has loaded
|
|
76
|
+
* and retry after a jittered backoff, so workers don't collide again.
|
|
68
77
|
*/
|
|
69
78
|
export async function createDraftPage(page, baseUrl, verbose = false) {
|
|
79
|
+
for (let attempt = 0; attempt <= PAGE_CREATION_RETRY_DELAYS.length; attempt++) {
|
|
80
|
+
if (attempt > 0) {
|
|
81
|
+
const base = PAGE_CREATION_RETRY_DELAYS[attempt - 1];
|
|
82
|
+
const delay = base + Math.round(Math.random() * base);
|
|
83
|
+
log(` Editor load failed — waiting ${(delay / 1000).toFixed(1)}s before retry ${attempt}/${PAGE_CREATION_RETRY_DELAYS.length}...`, 'yellow');
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return await attemptCreateDraftPage(page, baseUrl, verbose);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const shortMsg = error.message.split('\n')[0];
|
|
91
|
+
if (attempt < PAGE_CREATION_RETRY_DELAYS.length) {
|
|
92
|
+
log(` Draft page attempt ${attempt + 1} failed: ${shortMsg}`, 'yellow');
|
|
93
|
+
} else {
|
|
94
|
+
log(`Failed to create draft page after ${attempt + 1} attempts: ${error.message}`, 'red');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function attemptCreateDraftPage(page, baseUrl, verbose) {
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
if (verbose) log(' → Creating draft page...', 'gray');
|
|
104
|
+
|
|
105
|
+
// Editor scripts are parser-blocking, so every one has responded by
|
|
106
|
+
// domcontentloaded. A server error on any of them means the editor can't
|
|
107
|
+
// boot — fail now instead of waiting out the selector timeout.
|
|
108
|
+
const brokenScripts = [];
|
|
109
|
+
const onResponse = response => {
|
|
110
|
+
if (response.status() >= 500 && response.request().resourceType() === 'script') {
|
|
111
|
+
brokenScripts.push(response.status());
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
page.on('response', onResponse);
|
|
70
115
|
try {
|
|
71
|
-
if (verbose) log(' → Creating draft page...', 'gray');
|
|
72
116
|
await page.goto(`${baseUrl}/wp-admin/post-new.php?post_type=page`, {
|
|
73
117
|
waitUntil: 'domcontentloaded',
|
|
74
|
-
timeout: 30000,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
await page.waitForSelector('.edit-post-layout, .editor-styles-wrapper', {
|
|
78
|
-
timeout: 30000,
|
|
79
118
|
});
|
|
119
|
+
} finally {
|
|
120
|
+
page.off('response', onResponse);
|
|
121
|
+
}
|
|
122
|
+
if (brokenScripts.length > 0) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`${brokenScripts.length} editor script(s) failed to load (HTTP ${[...new Set(brokenScripts)].join(', ')}) — server overloaded`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (verbose) log(` → post-new.php loaded (${Date.now() - start}ms)`, 'gray');
|
|
80
128
|
|
|
81
|
-
|
|
82
|
-
const match = url.match(/[?&]post=(\d+)/);
|
|
83
|
-
if (match) {
|
|
84
|
-
if (verbose) log(' → Draft page created', 'gray');
|
|
85
|
-
return parseInt(match[1], 10);
|
|
86
|
-
}
|
|
129
|
+
await page.waitForSelector('.edit-post-layout, .editor-styles-wrapper');
|
|
87
130
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
return pageId;
|
|
94
|
-
} catch (error) {
|
|
95
|
-
log(`Failed to create draft page: ${error.message}`, 'red');
|
|
96
|
-
return null;
|
|
131
|
+
const url = page.url();
|
|
132
|
+
const match = url.match(/[?&]post=(\d+)/);
|
|
133
|
+
if (match) {
|
|
134
|
+
if (verbose) log(` → Draft page created (${Date.now() - start}ms)`, 'gray');
|
|
135
|
+
return parseInt(match[1], 10);
|
|
97
136
|
}
|
|
137
|
+
|
|
138
|
+
// Fallback: read from wp.data (editor may not have redirected yet)
|
|
139
|
+
const pageId = await page.evaluate(() =>
|
|
140
|
+
window.wp?.data?.select('core/editor')?.getCurrentPostId?.() ?? null
|
|
141
|
+
);
|
|
142
|
+
if (pageId === null) throw new Error('Editor loaded but no post ID was found');
|
|
143
|
+
if (verbose) log(` → Draft page created (${Date.now() - start}ms)`, 'gray');
|
|
144
|
+
return pageId;
|
|
98
145
|
}
|
|
99
146
|
|
|
100
147
|
/**
|
|
101
148
|
* Set the editor content via wp.data and wait for blocks to parse.
|
|
102
149
|
*/
|
|
103
150
|
export async function insertPatternIntoEditor(page, blockContent, verbose = false) {
|
|
151
|
+
const start = Date.now();
|
|
104
152
|
try {
|
|
105
153
|
if (verbose) log(' → Inserting pattern into editor...', 'gray');
|
|
106
154
|
await page.evaluate(content => {
|
|
@@ -110,25 +158,11 @@ export async function insertPatternIntoEditor(page, blockContent, verbose = fals
|
|
|
110
158
|
// Wait until at least one block is present
|
|
111
159
|
await page.waitForFunction(
|
|
112
160
|
() => window.wp.data.select('core/block-editor').getBlocks().length > 0,
|
|
161
|
+
null,
|
|
113
162
|
{ timeout: 15000 }
|
|
114
163
|
);
|
|
115
164
|
|
|
116
|
-
|
|
117
|
-
// pattern's blocks asynchronously, once the pattern registry has loaded.
|
|
118
|
-
// Wait for that so the save and the content comparison see the expanded
|
|
119
|
-
// tree. Unregistered slugs never expand, so a timeout here is non-fatal.
|
|
120
|
-
if (/<!-- wp:pattern\s/.test(blockContent)) {
|
|
121
|
-
await page
|
|
122
|
-
.waitForFunction(() => {
|
|
123
|
-
const hasPattern = blocks => blocks.some(
|
|
124
|
-
b => b.name === 'core/pattern' || hasPattern(b.innerBlocks ?? [])
|
|
125
|
-
);
|
|
126
|
-
return !hasPattern(window.wp.data.select('core/block-editor').getBlocks());
|
|
127
|
-
}, { timeout: 10000 })
|
|
128
|
-
.catch(() => {});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (verbose) log(' → Pattern inserted', 'gray');
|
|
165
|
+
if (verbose) log(` → Pattern inserted (${Date.now() - start}ms)`, 'gray');
|
|
132
166
|
return true;
|
|
133
167
|
} catch (error) {
|
|
134
168
|
log(`Failed to insert pattern: ${error.message}`, 'red');
|
|
@@ -141,24 +175,24 @@ export async function insertPatternIntoEditor(page, blockContent, verbose = fals
|
|
|
141
175
|
*/
|
|
142
176
|
export async function savePage(page, verbose = false) {
|
|
143
177
|
const result = { success: false, errors: [], warnings: [] };
|
|
178
|
+
const start = Date.now();
|
|
144
179
|
try {
|
|
145
180
|
if (verbose) log(' → Saving page...', 'gray');
|
|
181
|
+
// savePost() resolves once the save request and its hooks have finished,
|
|
182
|
+
// and evaluate() awaits it — so isSavingPost() is already false here.
|
|
183
|
+
// Don't wait for the save to *start*: that condition never becomes true
|
|
184
|
+
// again and the wait runs to its full timeout (the 60s stall in #21).
|
|
146
185
|
await page.evaluate(() => window.wp.data.dispatch('core/editor').savePost());
|
|
147
186
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
.waitForFunction(
|
|
151
|
-
() => window.wp.data.select('core/editor').isSavingPost(),
|
|
152
|
-
{ timeout: 5000 }
|
|
153
|
-
)
|
|
154
|
-
.catch(() => {}); // save might be near-instant
|
|
155
|
-
|
|
187
|
+
// waitForFunction's second parameter is the page-function arg, so options
|
|
188
|
+
// must go third — passed second, the timeout is silently ignored.
|
|
156
189
|
await page.waitForFunction(
|
|
157
190
|
() => !window.wp.data.select('core/editor').isSavingPost(),
|
|
191
|
+
null,
|
|
158
192
|
{ timeout: 30000 }
|
|
159
193
|
);
|
|
160
194
|
|
|
161
|
-
if (verbose) log(
|
|
195
|
+
if (verbose) log(` → Page saved (${Date.now() - start}ms)`, 'gray');
|
|
162
196
|
result.success = true;
|
|
163
197
|
} catch (error) {
|
|
164
198
|
result.errors.push({ type: 'save_error', message: error.message });
|
|
@@ -171,6 +205,7 @@ export async function savePage(page, verbose = false) {
|
|
|
171
205
|
* Non-fatal — a failure here does not affect validation results.
|
|
172
206
|
*/
|
|
173
207
|
export async function deletePage(page, baseUrl, pageId, verbose = false) {
|
|
208
|
+
const start = Date.now();
|
|
174
209
|
try {
|
|
175
210
|
if (verbose) log(' → Deleting draft page...', 'gray');
|
|
176
211
|
await page.evaluate(async id => {
|
|
@@ -180,7 +215,7 @@ export async function deletePage(page, baseUrl, pageId, verbose = false) {
|
|
|
180
215
|
headers: { 'X-WP-Nonce': nonce },
|
|
181
216
|
});
|
|
182
217
|
}, pageId);
|
|
183
|
-
if (verbose) log(
|
|
218
|
+
if (verbose) log(` → Draft page deleted (${Date.now() - start}ms)`, 'gray');
|
|
184
219
|
} catch {
|
|
185
220
|
// Non-fatal
|
|
186
221
|
}
|
package/src/format.js
CHANGED
|
@@ -7,6 +7,14 @@ const C = {
|
|
|
7
7
|
gray: '\x1b[90m',
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Error types that mean the pattern could not be tested at all (the editor
|
|
12
|
+
* never loaded), as opposed to the pattern failing validation.
|
|
13
|
+
*/
|
|
14
|
+
const INFRA_ERRORS = new Set(['page_creation_error']);
|
|
15
|
+
|
|
16
|
+
const isInfraFailure = r => !r.passed && r.errors.some(e => INFRA_ERRORS.has(e.type));
|
|
17
|
+
|
|
10
18
|
export function log(message, color = 'reset') {
|
|
11
19
|
console.log(`${C[color] ?? C.reset}${message}${C.reset}`);
|
|
12
20
|
}
|
|
@@ -31,6 +39,7 @@ export function formatResult(result) {
|
|
|
31
39
|
export function printSummary(results, skipped = 0) {
|
|
32
40
|
const passed = results.filter(r => r.passed).length;
|
|
33
41
|
const failed = results.filter(r => !r.passed).length;
|
|
42
|
+
const infra = results.filter(isInfraFailure).length;
|
|
34
43
|
const totalErrors = results.reduce((n, r) => n + r.errors.length, 0);
|
|
35
44
|
const totalWarns = results.reduce((n, r) => n + r.warnings.length, 0);
|
|
36
45
|
const totalMs = results.reduce((n, r) => n + r.duration, 0);
|
|
@@ -43,6 +52,9 @@ export function printSummary(results, skipped = 0) {
|
|
|
43
52
|
if (skipped > 0) log(`Skipped : ${skipped} (cached)`, 'gray');
|
|
44
53
|
log(`Passed : ${passed}`, passed > 0 ? 'green' : 'gray');
|
|
45
54
|
log(`Failed : ${failed}`, failed > 0 ? 'red' : 'gray');
|
|
55
|
+
if (infra > 0) {
|
|
56
|
+
log(` of which : ${infra} infrastructure (editor did not load — re-run, or lower --concurrency)`, 'yellow');
|
|
57
|
+
}
|
|
46
58
|
log(`Errors : ${totalErrors}`, totalErrors > 0 ? 'red' : 'gray');
|
|
47
59
|
log(`Warnings : ${totalWarns}`, totalWarns > 0 ? 'yellow' : 'gray');
|
|
48
60
|
log(`Time : ${totalMs}ms (avg ${avgMs}ms/pattern)`);
|
package/src/main.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
savePage,
|
|
13
13
|
deletePage,
|
|
14
14
|
} from './editor.js';
|
|
15
|
-
import { checkBlockValidation, compareContent } from './validation.js';
|
|
15
|
+
import { checkBlockValidation, compareContent, splitPatternRefs, checkPatternRefs } from './validation.js';
|
|
16
16
|
import { log, formatResult, printSummary } from './format.js';
|
|
17
17
|
|
|
18
18
|
const CACHE_FILE = '.sentinel-cache.json';
|
|
@@ -191,21 +191,43 @@ async function validatePatternFile(patternPath, options, context) {
|
|
|
191
191
|
return fail(patternName, patternPath, startTime, 'extraction_error', 'Could not extract block content from file');
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// Nested `wp:pattern` references are validated by their own pattern's run;
|
|
195
|
+
// here they only need to resolve. The editor round-trip covers the rest.
|
|
196
|
+
const { slugs, ownContent } = splitPatternRefs(blockContent);
|
|
197
|
+
|
|
194
198
|
const page = await context.newPage();
|
|
195
199
|
page.setDefaultTimeout(60000);
|
|
196
200
|
|
|
197
201
|
try {
|
|
198
202
|
const pageId = await createDraftPage(page, options.adminUrl, verbose);
|
|
199
203
|
if (pageId === null) {
|
|
200
|
-
return fail(patternName, patternPath, startTime, 'page_creation_error', '
|
|
204
|
+
return fail(patternName, patternPath, startTime, 'page_creation_error', 'Editor did not load after retries — not a pattern problem; re-run or lower --concurrency');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const refErrors = await checkPatternRefs(page, slugs, verbose);
|
|
208
|
+
|
|
209
|
+
// A pattern that only composes other patterns has nothing of its own to
|
|
210
|
+
// round-trip through the editor.
|
|
211
|
+
if (!ownContent) {
|
|
212
|
+
await deletePage(page, options.adminUrl, pageId, verbose);
|
|
213
|
+
return {
|
|
214
|
+
pattern: patternName,
|
|
215
|
+
patternPath,
|
|
216
|
+
hash: hashContent(fileContent),
|
|
217
|
+
passed: refErrors.length === 0,
|
|
218
|
+
errors: refErrors,
|
|
219
|
+
warnings: [],
|
|
220
|
+
duration: Date.now() - startTime,
|
|
221
|
+
};
|
|
201
222
|
}
|
|
202
223
|
|
|
203
|
-
if (!(await insertPatternIntoEditor(page,
|
|
224
|
+
if (!(await insertPatternIntoEditor(page, ownContent, verbose))) {
|
|
204
225
|
await deletePage(page, options.adminUrl, pageId, verbose);
|
|
205
226
|
return fail(patternName, patternPath, startTime, 'insertion_error', 'Failed to insert pattern into editor');
|
|
206
227
|
}
|
|
207
228
|
|
|
208
229
|
const saveResult = await savePage(page, verbose);
|
|
230
|
+
saveResult.errors.push(...refErrors);
|
|
209
231
|
const blockErrors = await checkBlockValidation(page, verbose);
|
|
210
232
|
|
|
211
233
|
if (blockErrors.length > 0) {
|
|
@@ -219,7 +241,7 @@ async function validatePatternFile(patternPath, options, context) {
|
|
|
219
241
|
})));
|
|
220
242
|
}
|
|
221
243
|
|
|
222
|
-
const comparison = await compareContent(page,
|
|
244
|
+
const comparison = await compareContent(page, ownContent, verbose);
|
|
223
245
|
saveResult.errors.push(...comparison.errors);
|
|
224
246
|
saveResult.warnings.push(...comparison.warnings);
|
|
225
247
|
|
|
@@ -231,7 +253,7 @@ async function validatePatternFile(patternPath, options, context) {
|
|
|
231
253
|
pattern: patternName,
|
|
232
254
|
patternPath,
|
|
233
255
|
hash: hashContent(fileContent),
|
|
234
|
-
passed: saveResult.success && comparison.matches && blockErrors.length === 0,
|
|
256
|
+
passed: saveResult.success && comparison.matches && blockErrors.length === 0 && refErrors.length === 0,
|
|
235
257
|
errors: saveResult.errors,
|
|
236
258
|
warnings: saveResult.warnings,
|
|
237
259
|
duration: Date.now() - startTime,
|
package/src/validation.js
CHANGED
|
@@ -4,6 +4,7 @@ import { log } from './format.js';
|
|
|
4
4
|
* Walk the block tree and collect any blocks where isValid === false.
|
|
5
5
|
*/
|
|
6
6
|
export async function checkBlockValidation(page, verbose = false) {
|
|
7
|
+
const start = Date.now();
|
|
7
8
|
if (verbose) log(' → Checking block validation...', 'gray');
|
|
8
9
|
try {
|
|
9
10
|
const errors = await page.evaluate(() => {
|
|
@@ -26,7 +27,7 @@ export async function checkBlockValidation(page, verbose = false) {
|
|
|
26
27
|
]);
|
|
27
28
|
return walk(window.wp.data.select('core/block-editor').getBlocks());
|
|
28
29
|
});
|
|
29
|
-
if (verbose) log(
|
|
30
|
+
if (verbose) log(` → Block validation complete (${Date.now() - start}ms)`, 'gray');
|
|
30
31
|
return errors;
|
|
31
32
|
} catch (error) {
|
|
32
33
|
log(`Block validation check error: ${error.message}`, 'yellow');
|
|
@@ -95,30 +96,59 @@ const normalizeForComparison = str =>
|
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
98
|
* Matches a nested pattern reference: `<!-- wp:pattern {"slug":"theme/x"} /-->`.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
99
|
+
* A reference is a link to another pattern, not content of this one: the
|
|
100
|
+
* referenced pattern is validated by its own run, so here we only check that
|
|
101
|
+
* the slug resolves and validate whatever markup the pattern adds itself.
|
|
100
102
|
*/
|
|
101
103
|
const PATTERN_REF = /<!-- wp:pattern (\{[\s\S]*?\}) \/-->/g;
|
|
102
104
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Split block content into the slugs it references and its own markup with
|
|
107
|
+
* those references removed. `ownContent` is '' for a pattern that only
|
|
108
|
+
* composes other patterns. A reference with unparseable JSON is left in
|
|
109
|
+
* place, so the editor flags it like any other invalid block.
|
|
110
|
+
*/
|
|
111
|
+
export function splitPatternRefs(content) {
|
|
112
|
+
const slugs = [];
|
|
113
|
+
const ownContent = content.replace(PATTERN_REF, (match, json) => {
|
|
114
|
+
try {
|
|
115
|
+
const { slug } = JSON.parse(json);
|
|
116
|
+
if (typeof slug !== 'string') return match;
|
|
117
|
+
slugs.push(slug);
|
|
118
|
+
return '';
|
|
119
|
+
} catch {
|
|
120
|
+
return match;
|
|
121
|
+
}
|
|
122
|
+
}).trim();
|
|
123
|
+
return { slugs, ownContent };
|
|
124
|
+
}
|
|
109
125
|
|
|
110
126
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
127
|
+
* Check every referenced slug is a registered pattern on the site. An
|
|
128
|
+
* unregistered slug (a typo, or a pattern missing from the synced theme)
|
|
129
|
+
* renders nothing on the page, so it is an error, not a warning.
|
|
114
130
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
export async function checkPatternRefs(page, slugs, verbose = false) {
|
|
132
|
+
if (slugs.length === 0) return [];
|
|
133
|
+
if (verbose) log(' → Checking nested pattern references...', 'gray');
|
|
134
|
+
try {
|
|
135
|
+
const registered = await page.evaluate(async () => {
|
|
136
|
+
const patterns = await window.wp.apiFetch({ path: '/wp/v2/block-patterns/patterns' });
|
|
137
|
+
return patterns.map(p => p.name);
|
|
138
|
+
});
|
|
139
|
+
return [...new Set(slugs)]
|
|
140
|
+
.filter(slug => !registered.includes(slug))
|
|
141
|
+
.map(slug => ({
|
|
142
|
+
type: 'pattern_ref_missing',
|
|
143
|
+
message: `Referenced pattern "${slug}" is not registered on the site`,
|
|
144
|
+
}));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return [{
|
|
147
|
+
type: 'pattern_ref_error',
|
|
148
|
+
message: `Could not check nested pattern references: ${error.message}`,
|
|
149
|
+
}];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
122
152
|
|
|
123
153
|
/**
|
|
124
154
|
* Compare the editor's serialized output against the original source.
|
|
@@ -127,6 +157,7 @@ const expandPatternRefs = (str, registry, seen = []) =>
|
|
|
127
157
|
*/
|
|
128
158
|
export async function compareContent(page, originalContent, verbose = false) {
|
|
129
159
|
const result = { matches: true, errors: [], warnings: [], savedContent: null };
|
|
160
|
+
const start = Date.now();
|
|
130
161
|
|
|
131
162
|
try {
|
|
132
163
|
if (verbose) log(' → Comparing content...', 'gray');
|
|
@@ -135,20 +166,9 @@ export async function compareContent(page, originalContent, verbose = false) {
|
|
|
135
166
|
);
|
|
136
167
|
result.savedContent = savedContent;
|
|
137
168
|
|
|
138
|
-
if (originalContent.includes('<!-- wp:pattern ')) {
|
|
139
|
-
try {
|
|
140
|
-
originalContent = expandPatternRefs(originalContent, await fetchRegisteredPatterns(page));
|
|
141
|
-
} catch (error) {
|
|
142
|
-
result.warnings.push({
|
|
143
|
-
type: 'pattern_ref_unresolved',
|
|
144
|
-
message: `Could not resolve nested wp:pattern references: ${error.message}`,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
169
|
const normalize = str => str.replace(/\s+/g, ' ').trim();
|
|
150
170
|
if (normalize(normalizeForComparison(savedContent)) === normalize(normalizeForComparison(originalContent))) {
|
|
151
|
-
if (verbose) log(
|
|
171
|
+
if (verbose) log(` → Content matches (${Date.now() - start}ms)`, 'gray');
|
|
152
172
|
return result;
|
|
153
173
|
}
|
|
154
174
|
|
|
@@ -172,7 +192,7 @@ export async function compareContent(page, originalContent, verbose = false) {
|
|
|
172
192
|
message: `Content injected by editor:\n ${added.join('\n ')}`,
|
|
173
193
|
});
|
|
174
194
|
}
|
|
175
|
-
if (verbose) log(
|
|
195
|
+
if (verbose) log(` → Content comparison complete (${Date.now() - start}ms)`, 'gray');
|
|
176
196
|
} catch (error) {
|
|
177
197
|
result.errors.push({ type: 'comparison_error', message: error.message });
|
|
178
198
|
}
|