@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 +1 -1
- package/src/editor.js +0 -15
- package/src/main.js +26 -4
- package/src/validation.js +47 -29
package/package.json
CHANGED
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
|
-
|
|
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,
|
|
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
|
-
*
|
|
99
|
-
*
|
|
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
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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');
|