@imwz/wp-pattern-sentinel 0.2.6 → 0.3.0
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 +36 -1
- package/package.json +1 -1
- package/src/args.js +7 -0
- package/src/format.js +3 -2
- package/src/main.js +92 -10
package/README.md
CHANGED
|
@@ -120,6 +120,9 @@ node bin/sentinel.js --concurrency=6 --url=... path/to/patterns/
|
|
|
120
120
|
| `--keep-page` | `false` | Don't delete draft pages after validation |
|
|
121
121
|
| `--width` | `1280` | Viewport width |
|
|
122
122
|
| `--height` | `800` | Viewport height |
|
|
123
|
+
| `--cache` | `false` | Skip patterns that previously passed with the same file content (see [Pass cache](#pass-cache)) |
|
|
124
|
+
| `--clear-cache` | `false` | Delete `.sentinel-cache.json` and exit (or combine with a path to clear then validate) |
|
|
125
|
+
| `--log` | `false` | Always write `sentinel-<timestamp>.log.json`, even when all patterns pass |
|
|
123
126
|
|
|
124
127
|
## Architecture
|
|
125
128
|
|
|
@@ -153,9 +156,41 @@ Credential rejections (wrong password) are not retried — only timeout errors t
|
|
|
153
156
|
|
|
154
157
|
Each pattern result is printed to the terminal as soon as that worker finishes, rather than buffering everything until the full batch completes. During a long concurrent run you see progress immediately.
|
|
155
158
|
|
|
159
|
+
### Pass cache
|
|
160
|
+
|
|
161
|
+
`--cache` stores a `.sentinel-cache.json` file in the working directory. Each entry records the file's content hash and the last pass result:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"patterns/main-hero.php": {
|
|
166
|
+
"hash": "a1b2c3d4e5f6",
|
|
167
|
+
"passed": true,
|
|
168
|
+
"checkedAt": "2026-05-16T10:00:00.000Z"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
On subsequent runs, if a pattern file's content hash matches the cached entry **and** it previously passed, the pattern is skipped. If the file has changed (even by one byte), it is re-validated and the cache entry is updated. Failed patterns are always removed from the cache so they are never skipped.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# First run — validates all, populates cache
|
|
177
|
+
sentinel --cache --log patterns/
|
|
178
|
+
|
|
179
|
+
# Later runs — only validates new or changed patterns
|
|
180
|
+
sentinel --cache --log patterns/
|
|
181
|
+
|
|
182
|
+
# Reset the cache (e.g. after a theme.json change that affects all patterns)
|
|
183
|
+
sentinel --clear-cache
|
|
184
|
+
|
|
185
|
+
# Clear and immediately re-validate
|
|
186
|
+
sentinel --clear-cache --cache --log patterns/
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Commit `.sentinel-cache.json` to track validated state across sessions. Add it to `.gitignore` if you prefer each developer to maintain their own local cache.
|
|
190
|
+
|
|
156
191
|
### Failure log
|
|
157
192
|
|
|
158
|
-
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.
|
|
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.
|
|
159
194
|
|
|
160
195
|
## npm publish
|
|
161
196
|
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -34,6 +34,10 @@ const ARG_OPTIONS = {
|
|
|
34
34
|
concurrency: { type: 'string', default: '4' },
|
|
35
35
|
width: { type: 'string', default: '1280' },
|
|
36
36
|
height: { type: 'string', default: '800' },
|
|
37
|
+
// Persistence
|
|
38
|
+
cache: { type: 'boolean', default: false },
|
|
39
|
+
'clear-cache': { type: 'boolean', default: false },
|
|
40
|
+
log: { type: 'boolean', default: false },
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -126,6 +130,9 @@ export async function parseArgs(args) {
|
|
|
126
130
|
headless: values.headless,
|
|
127
131
|
json: values.json,
|
|
128
132
|
keepPage: values['keep-page'],
|
|
133
|
+
cache: values.cache,
|
|
134
|
+
clearCache: values['clear-cache'],
|
|
135
|
+
log: values.log,
|
|
129
136
|
concurrency: Math.max(1, parseInt(values.concurrency, 10)),
|
|
130
137
|
viewport: {
|
|
131
138
|
width: parseInt(values.width, 10),
|
package/src/format.js
CHANGED
|
@@ -28,18 +28,19 @@ export function formatResult(result) {
|
|
|
28
28
|
return lines.join('\n');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function printSummary(results) {
|
|
31
|
+
export function printSummary(results, skipped = 0) {
|
|
32
32
|
const passed = results.filter(r => r.passed).length;
|
|
33
33
|
const failed = results.filter(r => !r.passed).length;
|
|
34
34
|
const totalErrors = results.reduce((n, r) => n + r.errors.length, 0);
|
|
35
35
|
const totalWarns = results.reduce((n, r) => n + r.warnings.length, 0);
|
|
36
36
|
const totalMs = results.reduce((n, r) => n + r.duration, 0);
|
|
37
|
-
const avgMs = Math.round(totalMs / results.length);
|
|
37
|
+
const avgMs = results.length > 0 ? Math.round(totalMs / results.length) : 0;
|
|
38
38
|
|
|
39
39
|
log('\n' + '='.repeat(60));
|
|
40
40
|
log('Validation Summary', 'cyan');
|
|
41
41
|
log('='.repeat(60));
|
|
42
42
|
log(`Patterns : ${results.length}`);
|
|
43
|
+
if (skipped > 0) log(`Skipped : ${skipped} (cached)`, 'gray');
|
|
43
44
|
log(`Passed : ${passed}`, passed > 0 ? 'green' : 'gray');
|
|
44
45
|
log(`Failed : ${failed}`, failed > 0 ? 'red' : 'gray');
|
|
45
46
|
log(`Errors : ${totalErrors}`, totalErrors > 0 ? 'red' : 'gray');
|
package/src/main.js
CHANGED
|
@@ -2,6 +2,7 @@ import { chromium } from 'playwright';
|
|
|
2
2
|
import PQueue from 'p-queue';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs';
|
|
5
|
+
import crypto from 'crypto';
|
|
5
6
|
import { parseArgs, resolveFiles } from './args.js';
|
|
6
7
|
import { loginToWordPress } from './login.js';
|
|
7
8
|
import {
|
|
@@ -14,15 +15,78 @@ import {
|
|
|
14
15
|
import { checkBlockValidation, compareContent } from './validation.js';
|
|
15
16
|
import { log, formatResult, printSummary } from './format.js';
|
|
16
17
|
|
|
18
|
+
const CACHE_FILE = '.sentinel-cache.json';
|
|
19
|
+
|
|
20
|
+
function loadCache() {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(path.join(process.cwd(), CACHE_FILE), 'utf8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function saveCache(cache) {
|
|
29
|
+
try {
|
|
30
|
+
await fs.promises.writeFile(
|
|
31
|
+
path.join(process.cwd(), CACHE_FILE),
|
|
32
|
+
JSON.stringify(cache, null, 2)
|
|
33
|
+
);
|
|
34
|
+
} catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hashContent(content) {
|
|
38
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
export async function main() {
|
|
18
42
|
const options = await parseArgs(process.argv.slice(2));
|
|
19
|
-
|
|
43
|
+
|
|
44
|
+
if (options.clearCache) {
|
|
45
|
+
try {
|
|
46
|
+
fs.unlinkSync(path.join(process.cwd(), CACHE_FILE));
|
|
47
|
+
log('Cache cleared.', 'green');
|
|
48
|
+
} catch {
|
|
49
|
+
log('No cache file found.', 'gray');
|
|
50
|
+
}
|
|
51
|
+
if (options.files.length === 0) process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let files = resolveFiles(options.files);
|
|
20
55
|
|
|
21
56
|
if (files.length === 0) {
|
|
22
57
|
log('No pattern files found.', 'red');
|
|
23
58
|
process.exit(1);
|
|
24
59
|
}
|
|
25
60
|
|
|
61
|
+
// Skip patterns that previously passed with the same file content.
|
|
62
|
+
const cache = options.cache ? loadCache() : {};
|
|
63
|
+
const skipped = [];
|
|
64
|
+
if (options.cache) {
|
|
65
|
+
const pending = [];
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
69
|
+
const key = path.relative(process.cwd(), file);
|
|
70
|
+
if (cache[key]?.passed && cache[key]?.hash === hashContent(content)) {
|
|
71
|
+
skipped.push(file);
|
|
72
|
+
} else {
|
|
73
|
+
pending.push(file);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
pending.push(file);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
files = pending;
|
|
80
|
+
if (skipped.length > 0) {
|
|
81
|
+
log(`Skipping ${skipped.length} previously-passed pattern(s) (cached)`, 'gray');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
log('All patterns already validated — nothing to do.', 'green');
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
26
90
|
log(`\nFound ${files.length} pattern(s) — concurrency: ${options.concurrency}`, 'cyan');
|
|
27
91
|
|
|
28
92
|
const browser = await chromium.launch({
|
|
@@ -82,11 +146,25 @@ export async function main() {
|
|
|
82
146
|
for (const r of results) console.log(JSON.stringify(r));
|
|
83
147
|
}
|
|
84
148
|
|
|
85
|
-
printSummary(results);
|
|
149
|
+
printSummary(results, skipped.length);
|
|
150
|
+
|
|
151
|
+
// Update cache with results from this run.
|
|
152
|
+
if (options.cache) {
|
|
153
|
+
for (const result of results) {
|
|
154
|
+
const key = path.relative(process.cwd(), result.patternPath);
|
|
155
|
+
if (result.passed) {
|
|
156
|
+
cache[key] = { hash: result.hash, passed: true, checkedAt: new Date().toISOString() };
|
|
157
|
+
} else {
|
|
158
|
+
delete cache[key];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await saveCache(cache);
|
|
162
|
+
log(` Cache updated → ${CACHE_FILE}`, 'gray');
|
|
163
|
+
}
|
|
86
164
|
|
|
87
|
-
// Write a log file
|
|
165
|
+
// Write a log file on failure, or always when --log is set.
|
|
88
166
|
const hasFailed = results.some(r => !r.passed);
|
|
89
|
-
if (hasFailed) {
|
|
167
|
+
if (hasFailed || options.log) {
|
|
90
168
|
const logPath = await writeLogFile(results);
|
|
91
169
|
if (logPath) log(` Log saved → ${logPath}`, 'gray');
|
|
92
170
|
}
|
|
@@ -104,12 +182,12 @@ async function validatePatternFile(patternPath, options, context) {
|
|
|
104
182
|
try {
|
|
105
183
|
fileContent = await fs.promises.readFile(patternPath, 'utf8');
|
|
106
184
|
} catch (error) {
|
|
107
|
-
return fail(patternName, startTime, 'file_error', error.message);
|
|
185
|
+
return fail(patternName, patternPath, startTime, 'file_error', error.message);
|
|
108
186
|
}
|
|
109
187
|
|
|
110
188
|
const blockContent = extractBlockContent(fileContent);
|
|
111
189
|
if (!blockContent) {
|
|
112
|
-
return fail(patternName, startTime, 'extraction_error', 'Could not extract block content from file');
|
|
190
|
+
return fail(patternName, patternPath, startTime, 'extraction_error', 'Could not extract block content from file');
|
|
113
191
|
}
|
|
114
192
|
|
|
115
193
|
const page = await context.newPage();
|
|
@@ -118,12 +196,12 @@ async function validatePatternFile(patternPath, options, context) {
|
|
|
118
196
|
try {
|
|
119
197
|
const pageId = await createDraftPage(page, options.adminUrl);
|
|
120
198
|
if (pageId === null) {
|
|
121
|
-
return fail(patternName, startTime, 'page_creation_error', 'Failed to create test page');
|
|
199
|
+
return fail(patternName, patternPath, startTime, 'page_creation_error', 'Failed to create test page');
|
|
122
200
|
}
|
|
123
201
|
|
|
124
202
|
if (!(await insertPatternIntoEditor(page, blockContent))) {
|
|
125
203
|
await deletePage(page, options.adminUrl, pageId);
|
|
126
|
-
return fail(patternName, startTime, 'insertion_error', 'Failed to insert pattern into editor');
|
|
204
|
+
return fail(patternName, patternPath, startTime, 'insertion_error', 'Failed to insert pattern into editor');
|
|
127
205
|
}
|
|
128
206
|
|
|
129
207
|
const saveResult = await savePage(page);
|
|
@@ -146,6 +224,8 @@ async function validatePatternFile(patternPath, options, context) {
|
|
|
146
224
|
|
|
147
225
|
return {
|
|
148
226
|
pattern: patternName,
|
|
227
|
+
patternPath,
|
|
228
|
+
hash: hashContent(fileContent),
|
|
149
229
|
passed: saveResult.success && comparison.matches && blockErrors.length === 0,
|
|
150
230
|
errors: saveResult.errors,
|
|
151
231
|
warnings: saveResult.warnings,
|
|
@@ -155,15 +235,17 @@ async function validatePatternFile(patternPath, options, context) {
|
|
|
155
235
|
|
|
156
236
|
} catch (error) {
|
|
157
237
|
log(`Unexpected error — ${patternName}: ${error.message}`, 'red');
|
|
158
|
-
return fail(patternName, startTime, 'validation_error', error.message);
|
|
238
|
+
return fail(patternName, patternPath, startTime, 'validation_error', error.message);
|
|
159
239
|
} finally {
|
|
160
240
|
await page.close();
|
|
161
241
|
}
|
|
162
242
|
}
|
|
163
243
|
|
|
164
|
-
function fail(pattern, startTime, type, message) {
|
|
244
|
+
function fail(pattern, patternPath, startTime, type, message) {
|
|
165
245
|
return {
|
|
166
246
|
pattern,
|
|
247
|
+
patternPath,
|
|
248
|
+
hash: null,
|
|
167
249
|
passed: false,
|
|
168
250
|
errors: [{ type, message }],
|
|
169
251
|
warnings: [],
|