@imwz/wp-pattern-sentinel 1.1.5 → 1.2.1

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
 
@@ -14,7 +14,7 @@ WordPress block validation is a JavaScript concern. The editor's `save()` functi
14
14
 
15
15
  Credentials are resolved in this order — the first match wins:
16
16
 
17
- 1. **`--trellis` flag** — reads directly from Roots Trellis vault + `wordpress_sites.yml`
17
+ 1. **`--trellis` flag** — reads directly from Roots Trellis vault + `wordpress_sites.yml` (`--user` / `--pass` still override the username and password)
18
18
  2. **CLI flags** — `--url`, `--user`, `--pass`
19
19
  3. **Environment variables** — `WP_URL`, `WP_USER`, `WP_PASS`
20
20
  4. **`.env` file** — placed in the directory where you run sentinel
@@ -63,6 +63,15 @@ Sentinel auto-discovers the Trellis directory by walking up from the current wor
63
63
 
64
64
  **Bedrock support:** When `--trellis` is used, sentinel auto-detects Bedrock installs by reading `WP_SITEURL` from the site's `.env` file. Bedrock puts WordPress core in `/wp/`, so admin URLs become `/wp/wp-admin/` instead of `/wp-admin/`. No extra flags needed — this is handled automatically.
65
65
 
66
+ **Different admin user:** Trellis provisions a WordPress user named `admin`, and that is the username Sentinel logs in with. If the local database was pulled from production, `admin` may not exist and login fails with "The username admin is not registered on this site". Pass the real admin with `--user`. Add `--pass` if that user's password differs from the vault's `admin_password`. URL and Bedrock detection still come from Trellis.
67
+
68
+ ```bash
69
+ sentinel --trellis --site=example.com --user=jane path/to/patterns/
70
+
71
+ # Password from your shell rather than the command line history; an empty value keeps the vault password
72
+ sentinel --trellis --site=example.com --user=jane --pass="$WP_PASS" path/to/patterns/
73
+ ```
74
+
66
75
  ---
67
76
 
68
77
  ## Quickstart with `.env`
@@ -97,6 +106,9 @@ node bin/sentinel.js \
97
106
  # Validate specific files
98
107
  node bin/sentinel.js patterns/hero.php patterns/cta.php
99
108
 
109
+ # Raw block markup (.html) — block fixtures or post drafts
110
+ node bin/sentinel.js tests/sentinel/ drafts/my-post.html
111
+
100
112
  # JSON output (one result object per line)
101
113
  node bin/sentinel.js --json --url=... path/to/patterns/
102
114
 
@@ -113,13 +125,46 @@ node bin/sentinel.js --concurrency=6 --url=... path/to/patterns/
113
125
  node bin/sentinel.js --verbose --url=... path/to/patterns/
114
126
  ```
115
127
 
128
+ ## What it validates: `.php` and `.html`
129
+
130
+ 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.
131
+
132
+ | Extension | What it holds | What Sentinel strips before inserting |
133
+ |-----------|---------------|---------------------------------------|
134
+ | `.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 |
135
+ | `.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 (`{{-- … --}}`) |
136
+
137
+ ### When to use `.html`
138
+
139
+ - **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`.
140
+ - **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.
141
+
142
+ ```bash
143
+ # A theme's block fixtures
144
+ sentinel --trellis --site=example.com tests/sentinel/
145
+
146
+ # Blog drafts before they are imported
147
+ sentinel --trellis --site=example.com drafts/blog-posts/
148
+ ```
149
+
150
+ ### Rules for `.html` files
151
+
152
+ - After the header, the file must start with a block comment (`<!-- wp:… -->`). Otherwise it fails with `extraction_error`.
153
+ - Only the header is stripped. A non-block comment *after* the first block is left in, because in post content it is real content.
154
+ - There's no PHP handling, so an `.html` file containing `<?php` is inserted as is.
155
+ - Use markup the editor actually produced, not hand-written HTML. The test's value is that a real serialization round-trips cleanly.
156
+
157
+ ### Blocks with a locked template
158
+
159
+ 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.
160
+
116
161
  ## Options
117
162
 
118
163
  | Flag | Default | Description |
119
164
  |------|---------|-------------|
120
165
  | `--url` | `http://localhost` | WordPress site URL |
121
- | `--user` | `admin` | Admin username |
122
- | `--pass` | `password` | Admin password |
166
+ | `--user` | `admin` | Admin username. Overrides the Trellis username when used with `--trellis` |
167
+ | `--pass` | `password` | Admin password. Overrides the vault password when used with `--trellis` |
123
168
  | `--wp-subdir` | — | WP core subdir when not using `--trellis` (e.g. `wp` for Bedrock). Sets admin URL to `{url}/{subdir}`. Auto-detected from `WP_SITEURL` when `--trellis` is used. |
124
169
  | `--headless` | `true` | Run browser headless |
125
170
  | `--concurrency` | `4` | Parallel workers |
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.1",
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
@@ -7,6 +7,7 @@ import { findTrellisDir, loadTrellisCredentials } from './trellis.js';
7
7
  /**
8
8
  * Credential resolution priority:
9
9
  * 1. --trellis flag → reads Roots Trellis vault + wordpress_sites.yml
10
+ * (--user / --pass still override the vault values)
10
11
  * 2. CLI flags → --url, --user, --pass
11
12
  * 3. Env vars → WP_URL, WP_USER, WP_PASS
12
13
  * 4. .env file → loaded from cwd automatically
@@ -64,6 +65,19 @@ function loadDotEnv() {
64
65
  }
65
66
  }
66
67
 
68
+ /**
69
+ * Let --user / --pass override credentials read from Trellis. Trellis
70
+ * provisions the WordPress user "admin", but a site whose database was pulled
71
+ * from production only has that site's real admin, so explicit flags win. An
72
+ * empty flag (`--pass="$UNSET_VAR"` in an npm script) keeps the vault value.
73
+ */
74
+ export function applyCredentialFlags({ user, pass }, values) {
75
+ return {
76
+ user: values.user || user,
77
+ pass: values.pass || pass,
78
+ };
79
+ }
80
+
67
81
  export async function parseArgs(args) {
68
82
  loadDotEnv();
69
83
 
@@ -88,6 +102,8 @@ export async function parseArgs(args) {
88
102
  subsite: values.subsite ?? null,
89
103
  }));
90
104
 
105
+ ({ user, pass } = applyCredentialFlags({ user, pass }, values));
106
+
91
107
  } else {
92
108
  // --- Source 2: CLI flags ---
93
109
  url = values.url;
@@ -148,7 +164,7 @@ export function resolveFiles(filePaths) {
148
164
  if (!filePaths || filePaths.length === 0) {
149
165
  throw new Error(
150
166
  'No pattern files specified.\n' +
151
- 'Usage: sentinel [--trellis [--site=example.com]] path/to/patterns/\n' +
167
+ 'Usage: sentinel [--trellis [--site=example.com]] path/to/patterns/ (.php and .html files)\n' +
152
168
  'Or set WP_URL, WP_USER, WP_PASS in .env'
153
169
  );
154
170
  }
@@ -162,7 +178,7 @@ export function resolveFiles(filePaths) {
162
178
  }
163
179
  const stat = fs.statSync(abs);
164
180
  if (stat.isDirectory()) {
165
- resolved.push(...findPhpFiles(abs));
181
+ resolved.push(...findPatternFiles(abs));
166
182
  } else {
167
183
  resolved.push(abs);
168
184
  }
@@ -170,13 +186,23 @@ export function resolveFiles(filePaths) {
170
186
  return [...new Set(resolved)];
171
187
  }
172
188
 
173
- function findPhpFiles(dir) {
189
+ // `.php` — WordPress pattern files. `.html` — raw serialized block markup, as
190
+ // stored in post_content (block fixtures, post drafts).
191
+ const PATTERN_EXTENSIONS = ['.php', '.html'];
192
+
193
+ // Dependency and VCS folders are never pattern sources, but they do hold
194
+ // stray `.php`/`.html` files (README demos, test pages) that a scan of a theme
195
+ // root would otherwise try to validate. Dot-folders (.git, .cache) likewise.
196
+ const SKIPPED_DIRS = new Set(['node_modules', 'vendor']);
197
+
198
+ function findPatternFiles(dir) {
174
199
  const results = [];
175
200
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
176
201
  const full = path.join(dir, entry.name);
177
202
  if (entry.isDirectory()) {
178
- results.push(...findPhpFiles(full));
179
- } else if (entry.isFile() && entry.name.endsWith('.php')) {
203
+ if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
204
+ results.push(...findPatternFiles(full));
205
+ } else if (entry.isFile() && PATTERN_EXTENSIONS.includes(path.extname(entry.name))) {
180
206
  results.push(full);
181
207
  }
182
208
  }
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
  }