@imwz/wp-pattern-sentinel 1.1.5 → 1.2.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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dt/@imwz/wp-pattern-sentinel.svg)](https://www.npmjs.com/package/@imwz/wp-pattern-sentinel)
5
5
  [![License](https://img.shields.io/npm/l/@imwz/wp-pattern-sentinel.svg)](https://github.com/imagewize/wp-pattern-sentinel/blob/main/package.json)
6
6
 
7
- Browser-based WordPress block pattern validator. Loads each pattern into the Gutenberg editor via Playwright, saves it, and checks for block validation errors and content mismatches.
7
+ Browser-based WordPress block pattern validator. Loads each pattern into the Gutenberg editor via Playwright, saves it, and checks for block validation errors and content mismatches. Validates `.php` pattern files and `.html` files of raw block markup. See [What it validates](#what-it-validates-php-and-html).
8
8
 
9
9
  ## Why browser-based?
10
10
 
@@ -97,6 +97,9 @@ node bin/sentinel.js \
97
97
  # Validate specific files
98
98
  node bin/sentinel.js patterns/hero.php patterns/cta.php
99
99
 
100
+ # Raw block markup (.html) — block fixtures or post drafts
101
+ node bin/sentinel.js tests/sentinel/ drafts/my-post.html
102
+
100
103
  # JSON output (one result object per line)
101
104
  node bin/sentinel.js --json --url=... path/to/patterns/
102
105
 
@@ -113,6 +116,39 @@ node bin/sentinel.js --concurrency=6 --url=... path/to/patterns/
113
116
  node bin/sentinel.js --verbose --url=... path/to/patterns/
114
117
  ```
115
118
 
119
+ ## What it validates: `.php` and `.html`
120
+
121
+ Sentinel never asks WordPress for registered patterns. It reads block markup from a file, puts it into a new draft page in the editor, saves the page and compares the result. So a file doesn't have to be a registered pattern; it only has to contain serialized blocks. A folder argument is scanned recursively for both extensions, skipping `node_modules`, `vendor` and dot-folders such as `.git`. Point Sentinel at the pattern or fixture folder rather than a block theme's root: `templates/*.html` and `parts/*.html` are block markup too, but they are site templates, not post content, and don't belong in a page round-trip.
122
+
123
+ | Extension | What it holds | What Sentinel strips before inserting |
124
+ |-----------|---------------|---------------------------------------|
125
+ | `.php` | A WordPress pattern file: PHP header, then block markup | Everything up to the first `?>` (docblock, `ABSPATH` guard), then inline PHP such as `esc_html_e()` is replaced with static text |
126
+ | `.html` | Raw serialized blocks, exactly as WordPress stores them in `post_content` | Only a **leading** header made of plain HTML comments (`<!-- SUGGESTED TITLE: … -->`) and Blade comments (`{{-- … --}}`) |
127
+
128
+ ### When to use `.html`
129
+
130
+ - **Testing a block that no pattern uses.** A theme block that only ever appears in post content (a CTA, a callout) has no pattern file for Sentinel to pick up. Don't wrap it in a fake pattern header. Save the block's markup as it is serialized in a real post, for example copied from `wp post get <id> --field=post_content`, as `tests/sentinel/<block>.html`.
131
+ - **Checking post or page drafts before import.** Drafts written as `.html` block markup can be validated as they are. Header notes at the top of the file are skipped. Without that, the editor would wrap them in a Classic block and Sentinel would report a false mismatch.
132
+
133
+ ```bash
134
+ # A theme's block fixtures
135
+ sentinel --trellis --site=example.com tests/sentinel/
136
+
137
+ # Blog drafts before they are imported
138
+ sentinel --trellis --site=example.com drafts/blog-posts/
139
+ ```
140
+
141
+ ### Rules for `.html` files
142
+
143
+ - After the header, the file must start with a block comment (`<!-- wp:… -->`). Otherwise it fails with `extraction_error`.
144
+ - Only the header is stripped. A non-block comment *after* the first block is left in, because in post content it is real content.
145
+ - There's no PHP handling, so an `.html` file containing `<?php` is inserted as is.
146
+ - Use markup the editor actually produced, not hand-written HTML. The test's value is that a real serialization round-trips cleanly.
147
+
148
+ ### Blocks with a locked template
149
+
150
+ A block that renders `InnerBlocks` with a `template` and `templateLock: "all"` or `"contentOnly"` is re-synced to that template when the editor loads it. Gutenberg matches inner blocks by position, adds any the template has extra, and removes any it no longer has. A fixture holding the markup a post stored *before* a template change will therefore fail with `content_mismatch`, and that is correct. It is exactly the change an editor would see on opening such a post. Keep one fixture per current template, and run an old one on purpose when you want to see how existing posts will be affected.
151
+
116
152
  ## Options
117
153
 
118
154
  | Flag | Default | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imwz/wp-pattern-sentinel",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Browser-based WordPress block pattern validator using Playwright",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,7 +10,8 @@
10
10
  "sentinel": "bin/sentinel.js"
11
11
  },
12
12
  "scripts": {
13
- "start": "node bin/sentinel.js"
13
+ "start": "node bin/sentinel.js",
14
+ "test": "node --test test/*.test.js"
14
15
  },
15
16
  "files": [
16
17
  "bin/",
package/src/args.js CHANGED
@@ -148,7 +148,7 @@ export function resolveFiles(filePaths) {
148
148
  if (!filePaths || filePaths.length === 0) {
149
149
  throw new Error(
150
150
  'No pattern files specified.\n' +
151
- 'Usage: sentinel [--trellis [--site=example.com]] path/to/patterns/\n' +
151
+ 'Usage: sentinel [--trellis [--site=example.com]] path/to/patterns/ (.php and .html files)\n' +
152
152
  'Or set WP_URL, WP_USER, WP_PASS in .env'
153
153
  );
154
154
  }
@@ -162,7 +162,7 @@ export function resolveFiles(filePaths) {
162
162
  }
163
163
  const stat = fs.statSync(abs);
164
164
  if (stat.isDirectory()) {
165
- resolved.push(...findPhpFiles(abs));
165
+ resolved.push(...findPatternFiles(abs));
166
166
  } else {
167
167
  resolved.push(abs);
168
168
  }
@@ -170,13 +170,23 @@ export function resolveFiles(filePaths) {
170
170
  return [...new Set(resolved)];
171
171
  }
172
172
 
173
- function findPhpFiles(dir) {
173
+ // `.php` — WordPress pattern files. `.html` — raw serialized block markup, as
174
+ // stored in post_content (block fixtures, post drafts).
175
+ const PATTERN_EXTENSIONS = ['.php', '.html'];
176
+
177
+ // Dependency and VCS folders are never pattern sources, but they do hold
178
+ // stray `.php`/`.html` files (README demos, test pages) that a scan of a theme
179
+ // root would otherwise try to validate. Dot-folders (.git, .cache) likewise.
180
+ const SKIPPED_DIRS = new Set(['node_modules', 'vendor']);
181
+
182
+ function findPatternFiles(dir) {
174
183
  const results = [];
175
184
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
176
185
  const full = path.join(dir, entry.name);
177
186
  if (entry.isDirectory()) {
178
- results.push(...findPhpFiles(full));
179
- } else if (entry.isFile() && entry.name.endsWith('.php')) {
187
+ if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
188
+ results.push(...findPatternFiles(full));
189
+ } else if (entry.isFile() && PATTERN_EXTENSIONS.includes(path.extname(entry.name))) {
180
190
  results.push(full);
181
191
  }
182
192
  }
package/src/editor.js CHANGED
@@ -52,7 +52,9 @@ function stripPhpForValidation(content) {
52
52
  * regardless of what it contains (docblock only, or docblock + guard) — so a
53
53
  * single non-greedy strip handles both shapes in one pass.
54
54
  */
55
- export function extractBlockContent(fileContent) {
55
+ export function extractBlockContent(fileContent, filePath = '') {
56
+ if (filePath.endsWith('.html')) return extractHtmlBlockContent(fileContent);
57
+
56
58
  const stripped = fileContent
57
59
  .replace(/^[\s\S]*?\?>\s*/, '')
58
60
  .trim();
@@ -61,6 +63,29 @@ export function extractBlockContent(fileContent) {
61
63
  return stripPhpForValidation(stripped);
62
64
  }
63
65
 
66
+ /**
67
+ * Return the block markup of an `.html` file: serialized blocks exactly as
68
+ * WordPress stores them in post_content, with no PHP header.
69
+ *
70
+ * Only a *leading* header is removed — plain HTML comments that are not block
71
+ * delimiters (`<!-- SUGGESTED TITLE: … -->`) and Blade comments
72
+ * (`{{-- … --}}`). Draft files carry these as notes for humans; left in, the
73
+ * editor would turn them into a Classic block and report a mismatch. Comments
74
+ * after the first block are content and are left alone. Returns null if no
75
+ * block comment follows the header.
76
+ */
77
+ export function extractHtmlBlockContent(fileContent) {
78
+ const HEADER_COMMENT = /^\s*(?:<!--(?!\s*\/?wp:)[\s\S]*?-->|\{\{--[\s\S]*?--\}\})/;
79
+
80
+ let content = fileContent;
81
+ while (HEADER_COMMENT.test(content)) {
82
+ content = content.replace(HEADER_COMMENT, '');
83
+ }
84
+ content = content.trim();
85
+
86
+ return /^<!--\s*wp:/.test(content) ? content : null;
87
+ }
88
+
64
89
  const PAGE_CREATION_RETRY_DELAYS = [3_000, 8_000, 15_000]; // ms before retries 1, 2, 3 (plus jitter)
65
90
 
66
91
  /**
package/src/main.js CHANGED
@@ -192,7 +192,7 @@ async function validatePatternFile(patternPath, options, context) {
192
192
  return fail(patternName, patternPath, startTime, 'file_error', error.message);
193
193
  }
194
194
 
195
- const blockContent = extractBlockContent(fileContent);
195
+ const blockContent = extractBlockContent(fileContent, patternPath);
196
196
  if (!blockContent) {
197
197
  return fail(patternName, patternPath, startTime, 'extraction_error', 'Could not extract block content from file');
198
198
  }