@imwz/wp-pattern-sentinel 1.1.2 → 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.2",
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/editor.js CHANGED
@@ -113,21 +113,6 @@ export async function insertPatternIntoEditor(page, blockContent, verbose = fals
113
113
  { timeout: 15000 }
114
114
  );
115
115
 
116
- // Nested `core/pattern` placeholders are swapped for the referenced
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
116
  if (verbose) log(' → Pattern inserted', 'gray');
132
117
  return true;
133
118
  } catch (error) {
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
@@ -95,30 +95,59 @@ const normalizeForComparison = str =>
95
95
 
96
96
  /**
97
97
  * Matches a nested pattern reference: `<!-- wp:pattern {"slug":"theme/x"} /-->`.
98
- * The editor replaces these with the referenced pattern's blocks, so the
99
- * source must be expanded the same way before it can be diffed.
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.
100
101
  */
101
102
  const PATTERN_REF = /<!-- wp:pattern (\{[\s\S]*?\}) \/-->/g;
102
103
 
103
- /** Map of registered pattern name → raw content, read from the REST API. */
104
- const fetchRegisteredPatterns = page =>
105
- page.evaluate(async () => {
106
- const patterns = await window.wp.apiFetch({ path: '/wp/v2/block-patterns/patterns' });
107
- return Object.fromEntries(patterns.map(p => [p.name, p.content]));
108
- });
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
+ }
109
124
 
110
125
  /**
111
- * Recursively substitute each `wp:pattern` reference with the referenced
112
- * pattern's content. Unregistered slugs and self-references are left as-is,
113
- * mirroring the editor, which leaves those `core/pattern` blocks unexpanded.
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.
114
129
  */
115
- const expandPatternRefs = (str, registry, seen = []) =>
116
- str.replace(PATTERN_REF, (match, json) => {
117
- let slug;
118
- try { slug = JSON.parse(json).slug; } catch { return match; }
119
- if (typeof registry[slug] !== 'string' || seen.includes(slug)) return match;
120
- return expandPatternRefs(registry[slug], registry, [...seen, slug]);
121
- });
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
+ }
122
151
 
123
152
  /**
124
153
  * Compare the editor's serialized output against the original source.
@@ -135,17 +164,6 @@ export async function compareContent(page, originalContent, verbose = false) {
135
164
  );
136
165
  result.savedContent = savedContent;
137
166
 
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
167
  const normalize = str => str.replace(/\s+/g, ' ').trim();
150
168
  if (normalize(normalizeForComparison(savedContent)) === normalize(normalizeForComparison(originalContent))) {
151
169
  if (verbose) log(' → Content matches', 'gray');