@imwz/wp-pattern-sentinel 1.1.1 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imwz/wp-pattern-sentinel",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Browser-based WordPress block pattern validator using Playwright",
5
5
  "type": "module",
6
6
  "engines": {
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,6 +191,10 @@ 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
 
@@ -200,12 +204,30 @@ async function validatePatternFile(patternPath, options, context) {
200
204
  return fail(patternName, patternPath, startTime, 'page_creation_error', 'Failed to create test page');
201
205
  }
202
206
 
203
- if (!(await insertPatternIntoEditor(page, blockContent, verbose))) {
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
+ };
222
+ }
223
+
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, blockContent, verbose);
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
@@ -93,6 +93,62 @@ const collapseInterTagWhitespace = str => str.replace(/>\s+</g, '><');
93
93
  const normalizeForComparison = str =>
94
94
  collapseInterTagWhitespace(normalizeBlockAttrJson(normalizeCssProps(stripNavRef(str))));
95
95
 
96
+ /**
97
+ * Matches a nested pattern reference: `<!-- wp:pattern {"slug":"theme/x"} /-->`.
98
+ * A reference is a link to another pattern, not content of this one: the
99
+ * referenced pattern is validated by its own run, so here we only check that
100
+ * the slug resolves and validate whatever markup the pattern adds itself.
101
+ */
102
+ const PATTERN_REF = /<!-- wp:pattern (\{[\s\S]*?\}) \/-->/g;
103
+
104
+ /**
105
+ * Split block content into the slugs it references and its own markup with
106
+ * those references removed. `ownContent` is '' for a pattern that only
107
+ * composes other patterns. A reference with unparseable JSON is left in
108
+ * place, so the editor flags it like any other invalid block.
109
+ */
110
+ export function splitPatternRefs(content) {
111
+ const slugs = [];
112
+ const ownContent = content.replace(PATTERN_REF, (match, json) => {
113
+ try {
114
+ const { slug } = JSON.parse(json);
115
+ if (typeof slug !== 'string') return match;
116
+ slugs.push(slug);
117
+ return '';
118
+ } catch {
119
+ return match;
120
+ }
121
+ }).trim();
122
+ return { slugs, ownContent };
123
+ }
124
+
125
+ /**
126
+ * Check every referenced slug is a registered pattern on the site. An
127
+ * unregistered slug (a typo, or a pattern missing from the synced theme)
128
+ * renders nothing on the page, so it is an error, not a warning.
129
+ */
130
+ export async function checkPatternRefs(page, slugs, verbose = false) {
131
+ if (slugs.length === 0) return [];
132
+ if (verbose) log(' → Checking nested pattern references...', 'gray');
133
+ try {
134
+ const registered = await page.evaluate(async () => {
135
+ const patterns = await window.wp.apiFetch({ path: '/wp/v2/block-patterns/patterns' });
136
+ return patterns.map(p => p.name);
137
+ });
138
+ return [...new Set(slugs)]
139
+ .filter(slug => !registered.includes(slug))
140
+ .map(slug => ({
141
+ type: 'pattern_ref_missing',
142
+ message: `Referenced pattern "${slug}" is not registered on the site`,
143
+ }));
144
+ } catch (error) {
145
+ return [{
146
+ type: 'pattern_ref_error',
147
+ message: `Could not check nested pattern references: ${error.message}`,
148
+ }];
149
+ }
150
+ }
151
+
96
152
  /**
97
153
  * Compare the editor's serialized output against the original source.
98
154
  * Whitespace-normalizes both sides before diffing to avoid false positives