@imwz/wp-pattern-sentinel 0.3.0 → 1.0.1

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 CHANGED
@@ -192,6 +192,18 @@ Commit `.sentinel-cache.json` to track validated state across sessions. Add it t
192
192
 
193
193
  When any pattern fails, sentinel automatically writes a `sentinel-<timestamp>.log.json` file in the current working directory and prints the path after the summary. This preserves error details for later inspection without needing to re-run. Use `--log` to write the file even on a fully-passing run.
194
194
 
195
+ Failing results include a `savedContent` field — the editor's serialized output — so you can diff it directly against the source file:
196
+
197
+ ```bash
198
+ node -e "
199
+ const log = JSON.parse(require('fs').readFileSync('sentinel-*.log.json'));
200
+ const r = log.results.find(r => !r.passed);
201
+ console.log(r.savedContent);
202
+ " | diff - patterns/my-pattern.php
203
+ ```
204
+
205
+ `block_validation` errors also surface Gutenberg's human-readable issue messages (e.g. `"Expected attribute 'class' of value '…' but got '…'"`), so you no longer need to open the browser console to identify what failed.
206
+
195
207
  ## npm publish
196
208
 
197
209
  When ready to publish:
package/bin/sentinel.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imwz/wp-pattern-sentinel",
3
- "version": "0.3.0",
3
+ "version": "1.0.1",
4
4
  "description": "Browser-based WordPress block pattern validator using Playwright",
5
5
  "type": "module",
6
6
  "engines": {
package/src/main.js CHANGED
@@ -210,7 +210,11 @@ async function validatePatternFile(patternPath, options, context) {
210
210
  if (blockErrors.length > 0) {
211
211
  saveResult.errors.push(...blockErrors.map(e => ({
212
212
  type: 'block_validation',
213
- message: `${e.blockName} (${e.blockId}): ${e.error}`,
213
+ message: `${e.blockName} (${e.blockId}): ${e.error}${
214
+ e.validationIssues?.length
215
+ ? '\n Issues: ' + e.validationIssues.join('\n ')
216
+ : ''
217
+ }`,
214
218
  })));
215
219
  }
216
220
 
@@ -266,6 +270,7 @@ async function writeLogFile(results) {
266
270
  duration: r.duration,
267
271
  errors: r.errors,
268
272
  warnings: r.warnings,
273
+ ...(r.passed ? {} : { savedContent: r.savedContent ?? null }),
269
274
  })),
270
275
  };
271
276
  await fs.promises.writeFile(logPath, JSON.stringify(payload, null, 2));
package/src/validation.js CHANGED
@@ -8,7 +8,18 @@ export async function checkBlockValidation(page) {
8
8
  return await page.evaluate(() => {
9
9
  const walk = blocks => blocks.flatMap(block => [
10
10
  ...(block.isValid === false
11
- ? [{ blockId: block.clientId, blockName: block.name, error: 'Block validation failed' }]
11
+ ? [{
12
+ blockId: block.clientId,
13
+ blockName: block.name,
14
+ error: 'Block validation failed',
15
+ validationIssues: (block.validationIssues ?? []).map(issue => {
16
+ try {
17
+ return (issue.args ?? [])
18
+ .map(a => (typeof a === 'string' ? a : JSON.stringify(a)))
19
+ .join(' ');
20
+ } catch { return 'unknown issue'; }
21
+ }),
22
+ }]
12
23
  : []),
13
24
  ...walk(block.innerBlocks ?? []),
14
25
  ]);
@@ -20,6 +31,15 @@ export async function checkBlockValidation(page) {
20
31
  }
21
32
  }
22
33
 
34
+ /**
35
+ * Strip the `ref` key WordPress injects into wp:navigation block comments when
36
+ * a pattern is first loaded in the editor. All FSE themes ship navigation
37
+ * patterns without a ref; WordPress assigns one automatically and it must not
38
+ * be treated as a content mismatch.
39
+ */
40
+ const stripNavRef = str =>
41
+ str.replace(/(<!-- wp:navigation \{)"ref":\d+,\s*/g, '$1');
42
+
23
43
  /**
24
44
  * Compare the editor's serialized output against the original source.
25
45
  * Whitespace-normalizes both sides before diffing to avoid false positives
@@ -35,12 +55,12 @@ export async function compareContent(page, originalContent) {
35
55
  result.savedContent = savedContent;
36
56
 
37
57
  const normalize = str => str.replace(/\s+/g, ' ').trim();
38
- if (normalize(savedContent) === normalize(originalContent)) return result;
58
+ if (normalize(stripNavRef(savedContent)) === normalize(stripNavRef(originalContent))) return result;
39
59
 
40
60
  result.matches = false;
41
61
 
42
- const origLines = originalContent.split('\n').map(l => l.trim()).filter(Boolean);
43
- const savedLines = savedContent.split('\n').map(l => l.trim()).filter(Boolean);
62
+ const origLines = stripNavRef(originalContent).split('\n').map(l => l.trim()).filter(Boolean);
63
+ const savedLines = stripNavRef(savedContent).split('\n').map(l => l.trim()).filter(Boolean);
44
64
 
45
65
  const removed = origLines.filter(l => !savedLines.includes(l)).slice(0, 5);
46
66
  const added = savedLines.filter(l => !origLines.includes(l)).slice(0, 5);