similarbuild 0.3.4 → 0.4.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/package.json +1 -1
- package/templates/commands/build-page.md +62 -493
- package/templates/commands/build-site.md +98 -705
- package/templates/commands/clip-section.md +102 -501
- package/templates/skills/sb-build-wp/SKILL.md +37 -164
- package/templates/skills/sb-build-wp/references/wp-build-rules.md +11 -1
- package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +4 -367
- package/templates/skills/sb-review-checks/SKILL.md +0 -113
- package/templates/skills/sb-review-checks/references/review-rules.md +0 -195
- package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +0 -385
- package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +0 -115
- package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +0 -541
- package/templates/skills/sb-review-checks/scripts/review-checks.mjs +0 -250
- package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +0 -366
- package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +0 -170
- package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +0 -493
- package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +0 -267
- package/templates/skills/sb-test-interactivity/SKILL.md +0 -133
- package/templates/skills/sb-test-interactivity/scripts/test-interactivity.mjs +0 -970
- package/templates/skills/sb-test-interactivity/scripts/tests/fixtures/aria-controls-broken.html +0 -32
- package/templates/skills/sb-test-interactivity/scripts/tests/fixtures/aria-controls-good.html +0 -47
- package/templates/skills/sb-test-interactivity/scripts/tests/fixtures/deferred-listeners.html +0 -67
- package/templates/skills/sb-test-interactivity/scripts/tests/fixtures/details-good.html +0 -25
- package/templates/skills/sb-test-interactivity/scripts/tests/fixtures/dialog-good.html +0 -38
- package/templates/skills/sb-test-interactivity/scripts/tests/fixtures/no-interactive.html +0 -15
- package/templates/skills/sb-test-interactivity/scripts/tests/test-test-interactivity.mjs +0 -223
|
@@ -198,104 +198,11 @@ async function main() {
|
|
|
198
198
|
}
|
|
199
199
|
})
|
|
200
200
|
|
|
201
|
-
// §V03-0a — lazy-hydration wait WHILE SCROLLED TO BOTTOM. Many
|
|
202
|
-
// Shopify themes populate footer menus (Contact, Privacy Policy,
|
|
203
|
-
// Collections links) via <store-footer-menu>-style custom elements
|
|
204
|
-
// only AFTER the host intersects the viewport. Wait here, with the
|
|
205
|
-
// footer in view, for any <footer> <ul> to gain children or for
|
|
206
|
-
// <footer> <a href> count to reach >= 3.
|
|
207
|
-
log('waiting for lazy-hydrated footer content (max 5s)')
|
|
208
|
-
try {
|
|
209
|
-
await page.waitForFunction(
|
|
210
|
-
() => {
|
|
211
|
-
const lists = document.querySelectorAll('footer ul, [class*="footer"] ul')
|
|
212
|
-
for (const ul of lists) {
|
|
213
|
-
if (ul.children.length > 0) return true
|
|
214
|
-
}
|
|
215
|
-
const links = document.querySelectorAll('footer a[href], [class*="footer"] a[href]')
|
|
216
|
-
return links.length >= 3
|
|
217
|
-
},
|
|
218
|
-
{ timeout: 5000 },
|
|
219
|
-
)
|
|
220
|
-
log('footer content hydrated')
|
|
221
|
-
} catch (_) {
|
|
222
|
-
log('footer hydration timeout — proceeding')
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// §V03-0a — capture hydrated chrome HTML WHILE STILL IN VIEW.
|
|
226
|
-
// Shopify themes that populate footer menus on viewport intersection
|
|
227
|
-
// sometimes tear the content down when the host leaves the viewport.
|
|
228
|
-
// The walker can't see torn-down menus. Snapshot the relevant
|
|
229
|
-
// subtrees here, before we scroll back to the top for layout reads,
|
|
230
|
-
// and stash them on `window` for extractInPage to read during walk.
|
|
231
|
-
log('snapshotting hydrated header/footer HTML for downstream use')
|
|
232
|
-
await page.evaluate(() => {
|
|
233
|
-
function pickFooter() {
|
|
234
|
-
const candidates = document.querySelectorAll(
|
|
235
|
-
'footer, [class*="footer"][class*="section"], [role="contentinfo"]',
|
|
236
|
-
)
|
|
237
|
-
let best = null
|
|
238
|
-
for (const el of candidates) {
|
|
239
|
-
const r = el.getBoundingClientRect()
|
|
240
|
-
if (r.height < 50) continue
|
|
241
|
-
const links = el.querySelectorAll('a[href]').length
|
|
242
|
-
const inputs = el.querySelectorAll('input,form').length
|
|
243
|
-
const score = links + inputs * 2 + r.height / 100
|
|
244
|
-
if (!best || score > best.score) best = { el, score }
|
|
245
|
-
}
|
|
246
|
-
return best?.el || null
|
|
247
|
-
}
|
|
248
|
-
function pickHeader() {
|
|
249
|
-
const candidates = document.querySelectorAll(
|
|
250
|
-
'header, [class*="header"][class*="section"], [role="banner"]',
|
|
251
|
-
)
|
|
252
|
-
let best = null
|
|
253
|
-
for (const el of candidates) {
|
|
254
|
-
const r = el.getBoundingClientRect()
|
|
255
|
-
if (r.height < 30) continue
|
|
256
|
-
const links = el.querySelectorAll('a[href]').length
|
|
257
|
-
const score = links + r.height / 100
|
|
258
|
-
if (!best || score > best.score) best = { el, score }
|
|
259
|
-
}
|
|
260
|
-
return best?.el || null
|
|
261
|
-
}
|
|
262
|
-
const footer = pickFooter()
|
|
263
|
-
const header = pickHeader()
|
|
264
|
-
window.__sbHydratedFooter = footer
|
|
265
|
-
? {
|
|
266
|
-
html: footer.outerHTML,
|
|
267
|
-
bbox: (() => {
|
|
268
|
-
const r = footer.getBoundingClientRect()
|
|
269
|
-
return {
|
|
270
|
-
x: Math.round(r.x + window.scrollX),
|
|
271
|
-
y: Math.round(r.y + window.scrollY),
|
|
272
|
-
w: Math.round(r.width),
|
|
273
|
-
h: Math.round(r.height),
|
|
274
|
-
}
|
|
275
|
-
})(),
|
|
276
|
-
}
|
|
277
|
-
: null
|
|
278
|
-
window.__sbHydratedHeader = header
|
|
279
|
-
? {
|
|
280
|
-
html: header.outerHTML,
|
|
281
|
-
bbox: (() => {
|
|
282
|
-
const r = header.getBoundingClientRect()
|
|
283
|
-
return {
|
|
284
|
-
x: Math.round(r.x + window.scrollX),
|
|
285
|
-
y: Math.round(r.y + window.scrollY),
|
|
286
|
-
w: Math.round(r.width),
|
|
287
|
-
h: Math.round(r.height),
|
|
288
|
-
}
|
|
289
|
-
})(),
|
|
290
|
-
}
|
|
291
|
-
: null
|
|
292
|
-
})
|
|
293
|
-
|
|
294
201
|
// Return to the top before layout reads. content-visibility:auto on
|
|
295
|
-
// viewport-distant nodes returns {w:0,h:0} bboxes — walker
|
|
296
|
-
//
|
|
202
|
+
// viewport-distant nodes returns {w:0,h:0} bboxes — walker reads from
|
|
203
|
+
// the at-top state. 800ms settle for any re-hydration.
|
|
297
204
|
await page.evaluate(() => window.scrollTo(0, 0))
|
|
298
|
-
await page.waitForTimeout(
|
|
205
|
+
await page.waitForTimeout(800)
|
|
299
206
|
|
|
300
207
|
// Force-eager: any <img loading="lazy"> that didn't intersect during scroll
|
|
301
208
|
// gets rewritten to eager and re-fetched. <picture> sources too.
|
|
@@ -454,88 +361,7 @@ async function main() {
|
|
|
454
361
|
return slug || 'section'
|
|
455
362
|
}
|
|
456
363
|
|
|
457
|
-
|
|
458
|
-
// its visible content is dominated by a single <img>: either by
|
|
459
|
-
// bbox coverage (img takes ≥80% of section area) OR by a known
|
|
460
|
-
// wrapper-class pattern (Shopify Dawn / OS 2.0 themes use specific
|
|
461
|
-
// wrapper classes like `product-info__image` to delimit a region
|
|
462
|
-
// whose entire visual content is one CDN-hosted SVG/PNG/JPG asset).
|
|
463
|
-
// The composer (sb-build-wp §V03-4 rule A) treats image-block
|
|
464
|
-
// sections as "download asset + emit <img src>" — NEVER tries to
|
|
465
|
-
// reproduce the visual contents in HTML/CSS.
|
|
466
|
-
const IMAGE_BLOCK_WRAPPER_PATTERNS = [
|
|
467
|
-
/product-info__image/i,
|
|
468
|
-
/image-with-text/i,
|
|
469
|
-
/\bhero-image\b/i,
|
|
470
|
-
/\btrust-badges\b/i,
|
|
471
|
-
/\bfeatures-points\b/i,
|
|
472
|
-
/\bmothers?-day\b/i,
|
|
473
|
-
/\bsingle-image\b/i,
|
|
474
|
-
/\bbanner-image\b/i,
|
|
475
|
-
/\bguarantee-card\b/i,
|
|
476
|
-
/\bbest-fit-size-chart\b/i,
|
|
477
|
-
]
|
|
478
|
-
function classifyAsImageBlock(node) {
|
|
479
|
-
if (!node || typeof node !== 'object') return null
|
|
480
|
-
const sectionBbox = node.bbox
|
|
481
|
-
if (!sectionBbox || !sectionBbox.h || !sectionBbox.w) return null
|
|
482
|
-
const sectionArea = sectionBbox.h * sectionBbox.w
|
|
483
|
-
if (sectionArea <= 0) return null
|
|
484
|
-
// Match by wrapper-class pattern on the node itself OR any
|
|
485
|
-
// descendant (the wrapper might be a child of the section root).
|
|
486
|
-
function findClassMatch(n) {
|
|
487
|
-
if (!n || typeof n !== 'object') return null
|
|
488
|
-
if (Array.isArray(n.classes)) {
|
|
489
|
-
for (const cls of n.classes) {
|
|
490
|
-
for (const pat of IMAGE_BLOCK_WRAPPER_PATTERNS) {
|
|
491
|
-
if (pat.test(cls)) return cls
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (Array.isArray(n.children)) {
|
|
496
|
-
for (const c of n.children) {
|
|
497
|
-
const m = findClassMatch(c)
|
|
498
|
-
if (m) return m
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return null
|
|
502
|
-
}
|
|
503
|
-
const classMatch = findClassMatch(node)
|
|
504
|
-
// Find the largest <img> descendant by bbox area.
|
|
505
|
-
let bestImg = null
|
|
506
|
-
function findBiggestImg(n) {
|
|
507
|
-
if (!n || typeof n !== 'object') return
|
|
508
|
-
if (n.tag === 'img' && n.bbox && n.bbox.h > 0 && n.bbox.w > 0) {
|
|
509
|
-
const a = n.bbox.h * n.bbox.w
|
|
510
|
-
if (!bestImg || a > bestImg.area) {
|
|
511
|
-
const src = (n.attrs && (n.attrs.src || n.attrs.srcset)) || ''
|
|
512
|
-
bestImg = { area: a, bbox: n.bbox, src }
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (Array.isArray(n.children)) {
|
|
516
|
-
for (const c of n.children) findBiggestImg(c)
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
findBiggestImg(node)
|
|
520
|
-
const imgArea = bestImg ? bestImg.area : 0
|
|
521
|
-
const coverage = sectionArea > 0 ? imgArea / sectionArea : 0
|
|
522
|
-
if (classMatch || coverage >= 0.8) {
|
|
523
|
-
return {
|
|
524
|
-
reason: classMatch
|
|
525
|
-
? `wrapper-class:${classMatch}`
|
|
526
|
-
: `img-coverage:${(coverage * 100).toFixed(0)}%`,
|
|
527
|
-
imgSrc: bestImg ? bestImg.src : null,
|
|
528
|
-
imgBbox: bestImg ? bestImg.bbox : null,
|
|
529
|
-
coverage,
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
return null
|
|
533
|
-
}
|
|
534
|
-
const sourceDom = result.domLive
|
|
535
|
-
? Array.isArray(result.domLive)
|
|
536
|
-
? result.domLive
|
|
537
|
-
: [result.domLive]
|
|
538
|
-
: result.dom
|
|
364
|
+
const sourceDom = result.dom
|
|
539
365
|
|
|
540
366
|
// Find the host container (body or main) — walk in one level and
|
|
541
367
|
// pick the node with the most children + largest bbox.
|
|
@@ -574,7 +400,6 @@ async function main() {
|
|
|
574
400
|
sectionList.push({
|
|
575
401
|
sectionType: labelFromNode(grand),
|
|
576
402
|
bbox: grand.bbox,
|
|
577
|
-
imageBlock: classifyAsImageBlock(grand),
|
|
578
403
|
})
|
|
579
404
|
}
|
|
580
405
|
}
|
|
@@ -586,7 +411,6 @@ async function main() {
|
|
|
586
411
|
sectionList.push({
|
|
587
412
|
sectionType: labelFromNode(child),
|
|
588
413
|
bbox,
|
|
589
|
-
imageBlock: classifyAsImageBlock(child),
|
|
590
414
|
})
|
|
591
415
|
}
|
|
592
416
|
|
|
@@ -647,7 +471,6 @@ async function main() {
|
|
|
647
471
|
sectionType: sec.sectionType,
|
|
648
472
|
bbox: sec.bbox,
|
|
649
473
|
path: cropPath,
|
|
650
|
-
imageBlock: sec.imageBlock || null,
|
|
651
474
|
})
|
|
652
475
|
} catch (err) {
|
|
653
476
|
log(`section crop ${idx} ${sec.sectionType} failed: ${err?.message || err}`)
|
|
@@ -1297,11 +1120,6 @@ function extractInPage({ selector, maxDepth, maxChildren, maxText }) {
|
|
|
1297
1120
|
// iframes (Klaviyo embeds, recaptcha) are recorded as opaque rectangles.
|
|
1298
1121
|
let shadowDOMTraversed = false
|
|
1299
1122
|
let shadowRootCount = 0
|
|
1300
|
-
// §V03-C — counts hosts whose shadow tree was successfully re-serialized
|
|
1301
|
-
// via getHTML+parseHTMLUnsafe and re-walked into a flattened light-DOM
|
|
1302
|
-
// representation. 0 means the post-render shadow-flatten phase was either
|
|
1303
|
-
// unavailable, skipped, or hit zero open roots.
|
|
1304
|
-
let shadowSerializedHostCount = 0
|
|
1305
1123
|
const warnings = []
|
|
1306
1124
|
const externalIframes = []
|
|
1307
1125
|
function classifyIframePurpose(src) {
|
|
@@ -1752,198 +1570,17 @@ function extractInPage({ selector, maxDepth, maxChildren, maxText }) {
|
|
|
1752
1570
|
const dom = [walk(root, 0)].filter(Boolean)
|
|
1753
1571
|
const { sectionType, sectionBoundingBox } = findSectionAndBox(root, !!selector)
|
|
1754
1572
|
|
|
1755
|
-
// §V03-C — Shadow DOM serialization via getHTML + parseHTMLUnsafe.
|
|
1756
|
-
// walk() above only sees light DOM and `.shadowRoot.children` direct.
|
|
1757
|
-
// Custom elements whose shadow tree is populated by JS (Shopify
|
|
1758
|
-
// <x-product-form>, <price-list>, <variant-radios>, <store-footer-menu>)
|
|
1759
|
-
// expose content that the children-only walker can technically read,
|
|
1760
|
-
// but `<slot>`-projected content and content composed from declarative
|
|
1761
|
-
// shadow DOM gets fragmented. getHTML({ serializableShadowRoots: true })
|
|
1762
|
-
// emits a single HTML string with `<template shadowrootmode="open">`
|
|
1763
|
-
// declarations inline; parseHTMLUnsafe re-attaches those as live shadow
|
|
1764
|
-
// roots in a parsed document, which the same walk() can then flatten
|
|
1765
|
-
// uniformly. Layout (bbox/computedStyle) on the parsed doc is detached
|
|
1766
|
-
// and returns UA defaults — that's an acknowledged trade-off; the
|
|
1767
|
-
// structural content (tags, classes, attrs, text, src) is what makes
|
|
1768
|
-
// PDP gallery/price/variants visible to the composer downstream.
|
|
1769
|
-
const originalShadowRootCount = shadowRootCount
|
|
1770
|
-
const originalShadowDOMTraversed = shadowDOMTraversed
|
|
1771
|
-
// §V03-C — preserve a snapshot of the live-walker dom[] before any
|
|
1772
|
-
// potential substitution. If the re-walk replaces dom[] with the
|
|
1773
|
-
// shadow-flattened tree (which carries detached parsedDoc bboxes
|
|
1774
|
-
// === {0,0,0,0}), downstream consumers that need real layout (e.g.
|
|
1775
|
-
// /build-site Step 3.5e --crop-live-bbox for header/footer compare)
|
|
1776
|
-
// can fall back to domLive. When substitution doesn't fire, domLive
|
|
1777
|
-
// is left null and consumers use dom[] as-is.
|
|
1778
|
-
let domLive = null
|
|
1779
|
-
try {
|
|
1780
|
-
if (typeof document.documentElement.getHTML === 'function' &&
|
|
1781
|
-
typeof Document.parseHTMLUnsafe === 'function') {
|
|
1782
|
-
const hostsSeen = new Set()
|
|
1783
|
-
const hostsCollected = []
|
|
1784
|
-
function collectHostsFrom(el) {
|
|
1785
|
-
if (!el) return
|
|
1786
|
-
if (el.shadowRoot && !hostsSeen.has(el)) {
|
|
1787
|
-
hostsSeen.add(el)
|
|
1788
|
-
hostsCollected.push(el)
|
|
1789
|
-
for (const child of el.shadowRoot.children) collectHostsFrom(child)
|
|
1790
|
-
}
|
|
1791
|
-
for (const child of el.children) collectHostsFrom(child)
|
|
1792
|
-
}
|
|
1793
|
-
collectHostsFrom(document.documentElement)
|
|
1794
|
-
|
|
1795
|
-
if (hostsCollected.length > 0) {
|
|
1796
|
-
const html = document.documentElement.getHTML({
|
|
1797
|
-
serializableShadowRoots: true,
|
|
1798
|
-
shadowRoots: hostsCollected.map((h) => h.shadowRoot),
|
|
1799
|
-
})
|
|
1800
|
-
const parsedDoc = Document.parseHTMLUnsafe(html)
|
|
1801
|
-
const parsedRoot = selector
|
|
1802
|
-
? parsedDoc.querySelector(selector)
|
|
1803
|
-
: parsedDoc.body
|
|
1804
|
-
if (parsedRoot) {
|
|
1805
|
-
const flattened = walk(parsedRoot, 0)
|
|
1806
|
-
// §V03-C safety guard: only substitute the live dom[] if the
|
|
1807
|
-
// re-walk did not lose value on EITHER axis (nodes or aggregate
|
|
1808
|
-
// text chars). parseHTMLUnsafe returns a detached doc;
|
|
1809
|
-
// getComputedStyle/getBoundingClientRect degrade to UA defaults
|
|
1810
|
-
// (zero bbox, empty computed). On pages where shadow flattening
|
|
1811
|
-
// adds value (PDPs Shopify with populated custom elements) the
|
|
1812
|
-
// gain is large; on pages without that workload (policies,
|
|
1813
|
-
// plain HTML), the re-walk can lose content because hydrated
|
|
1814
|
-
// shadow content visible to the live walker doesn't reproduce
|
|
1815
|
-
// on the detached tree. When that happens, keep the live
|
|
1816
|
-
// walker result and surface a warning.
|
|
1817
|
-
function measureTree(arr) {
|
|
1818
|
-
let nodes = 0
|
|
1819
|
-
let textChars = 0
|
|
1820
|
-
const stack = Array.isArray(arr) ? [...arr] : [arr]
|
|
1821
|
-
while (stack.length) {
|
|
1822
|
-
const node = stack.pop()
|
|
1823
|
-
if (!node || typeof node !== 'object') continue
|
|
1824
|
-
nodes++
|
|
1825
|
-
if (typeof node.text === 'string') textChars += node.text.length
|
|
1826
|
-
if (Array.isArray(node.children)) {
|
|
1827
|
-
for (const c of node.children) stack.push(c)
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
return { nodes, textChars }
|
|
1831
|
-
}
|
|
1832
|
-
if (flattened) {
|
|
1833
|
-
const orig = measureTree(dom)
|
|
1834
|
-
const flat = measureTree([flattened])
|
|
1835
|
-
if (flat.nodes >= orig.nodes && flat.textChars >= orig.textChars) {
|
|
1836
|
-
// Snapshot the live walker result BEFORE substitution so
|
|
1837
|
-
// downstream consumers that depend on real layout (bbox
|
|
1838
|
-
// values are zero on the parsed detached doc) can fall
|
|
1839
|
-
// back when needed — see §V03-C domLive comment above.
|
|
1840
|
-
domLive = dom.length === 1 ? dom[0] : [...dom]
|
|
1841
|
-
dom.length = 0
|
|
1842
|
-
dom.push(flattened)
|
|
1843
|
-
shadowSerializedHostCount = hostsCollected.length
|
|
1844
|
-
} else {
|
|
1845
|
-
warnings.push({
|
|
1846
|
-
code: 'shadow-flatten-skipped-lossy',
|
|
1847
|
-
message: `re-walk lossy: nodes ${flat.nodes} vs ${orig.nodes}, textChars ${flat.textChars} vs ${orig.textChars}; keeping live walker result`,
|
|
1848
|
-
})
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
// The re-walk of the parsed (detached) doc re-discovers shadow
|
|
1852
|
-
// roots that parseHTMLUnsafe re-attached, so it would
|
|
1853
|
-
// double-count if we let those increments leak. Restore the
|
|
1854
|
-
// canonical live counts here.
|
|
1855
|
-
shadowRootCount = originalShadowRootCount
|
|
1856
|
-
shadowDOMTraversed = originalShadowDOMTraversed
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
} else {
|
|
1860
|
-
warnings.push({
|
|
1861
|
-
code: 'shadow-serialize-unavailable',
|
|
1862
|
-
message:
|
|
1863
|
-
'getHTML or Document.parseHTMLUnsafe not available; shadow DOM falls back to .shadowRoot.children walk (v0.2.x behavior)',
|
|
1864
|
-
})
|
|
1865
|
-
}
|
|
1866
|
-
} catch (err) {
|
|
1867
|
-
shadowRootCount = originalShadowRootCount
|
|
1868
|
-
shadowDOMTraversed = originalShadowDOMTraversed
|
|
1869
|
-
warnings.push({
|
|
1870
|
-
code: 'shadow-serialize-failed',
|
|
1871
|
-
message: String(err && err.message ? err.message : err),
|
|
1872
|
-
})
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
// §V03-0a — extract structured content from the hydrated-chrome
|
|
1876
|
-
// HTML snapshots taken before scroll-back. Shopify themes that
|
|
1877
|
-
// populate menus on viewport intersection may have torn those down
|
|
1878
|
-
// by the time the walker runs. The snapshots preserve the truth.
|
|
1879
|
-
function extractChrome(snapshot) {
|
|
1880
|
-
if (!snapshot || !snapshot.html) return null
|
|
1881
|
-
let parsed
|
|
1882
|
-
try {
|
|
1883
|
-
parsed = new DOMParser().parseFromString(snapshot.html, 'text/html')
|
|
1884
|
-
} catch (_) {
|
|
1885
|
-
return { ...snapshot, links: [], headings: [], inputs: [], forms: [], images: [] }
|
|
1886
|
-
}
|
|
1887
|
-
const root = parsed.body.firstElementChild || parsed.body
|
|
1888
|
-
const norm = (s) => (s || '').replace(/\s+/g, ' ').trim()
|
|
1889
|
-
const links = Array.from(root.querySelectorAll('a[href]'))
|
|
1890
|
-
.map((a) => ({
|
|
1891
|
-
href: a.getAttribute('href') || '',
|
|
1892
|
-
text: norm(a.textContent),
|
|
1893
|
-
label: a.getAttribute('aria-label') || null,
|
|
1894
|
-
}))
|
|
1895
|
-
.filter((l) => l.href && (l.text || l.label))
|
|
1896
|
-
const headings = Array.from(root.querySelectorAll('h1, h2, h3, h4, h5, h6, p.bold, .footer__block-title, [class*="heading"]'))
|
|
1897
|
-
.map((h) => ({ tag: h.tagName.toLowerCase(), text: norm(h.textContent) }))
|
|
1898
|
-
.filter((h) => h.text)
|
|
1899
|
-
const inputs = Array.from(root.querySelectorAll('input')).map((i) => ({
|
|
1900
|
-
type: i.getAttribute('type') || 'text',
|
|
1901
|
-
name: i.getAttribute('name') || null,
|
|
1902
|
-
placeholder: i.getAttribute('placeholder') || null,
|
|
1903
|
-
required: i.hasAttribute('required'),
|
|
1904
|
-
ariaLabel: i.getAttribute('aria-label') || null,
|
|
1905
|
-
}))
|
|
1906
|
-
const forms = Array.from(root.querySelectorAll('form')).map((f) => ({
|
|
1907
|
-
action: f.getAttribute('action') || null,
|
|
1908
|
-
method: f.getAttribute('method') || 'get',
|
|
1909
|
-
id: f.getAttribute('id') || null,
|
|
1910
|
-
}))
|
|
1911
|
-
const images = Array.from(root.querySelectorAll('img'))
|
|
1912
|
-
.map((img) => ({
|
|
1913
|
-
src: img.getAttribute('src') || '',
|
|
1914
|
-
alt: img.getAttribute('alt') || '',
|
|
1915
|
-
width: img.getAttribute('width') || null,
|
|
1916
|
-
height: img.getAttribute('height') || null,
|
|
1917
|
-
}))
|
|
1918
|
-
.filter((img) => img.src)
|
|
1919
|
-
return {
|
|
1920
|
-
html: snapshot.html,
|
|
1921
|
-
bbox: snapshot.bbox,
|
|
1922
|
-
links,
|
|
1923
|
-
headings,
|
|
1924
|
-
inputs,
|
|
1925
|
-
forms,
|
|
1926
|
-
images,
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
const hydratedHeader = extractChrome(window.__sbHydratedHeader)
|
|
1930
|
-
const hydratedFooter = extractChrome(window.__sbHydratedFooter)
|
|
1931
|
-
|
|
1932
1573
|
return {
|
|
1933
1574
|
sectionType,
|
|
1934
1575
|
sectionBoundingBox,
|
|
1935
1576
|
tokens,
|
|
1936
1577
|
dom,
|
|
1937
|
-
domLive,
|
|
1938
1578
|
pseudoElements,
|
|
1939
1579
|
imgUrls,
|
|
1940
1580
|
shadowDOMTraversed,
|
|
1941
1581
|
shadowRootCount,
|
|
1942
|
-
shadowSerializedHostCount,
|
|
1943
1582
|
warnings,
|
|
1944
1583
|
externalIframes,
|
|
1945
|
-
hydratedHeader,
|
|
1946
|
-
hydratedFooter,
|
|
1947
1584
|
}
|
|
1948
1585
|
}
|
|
1949
1586
|
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sb-review-checks
|
|
3
|
-
description: Audits a built HTML/Liquid fragment against canonical visual anti-patterns, accessibility/performance/web-standards baselines, and (optionally) cross-references the violations with `sb-compare-visual` diffs to produce a prioritized list of specific candidate fixes for the auto-correct loop. Use when the SimilarBuild orchestrator (`/build-page`, `/build-site`, `/clip-section`) needs to audit a build before shipping or to generate `fixHints` for `sb-build-wp` / `sb-build-shopify`, or when the user asks to 'review checks' on a built fragment.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# sb-review-checks
|
|
7
|
-
|
|
8
|
-
## Overview
|
|
9
|
-
|
|
10
|
-
Closes the **build → validate → compare → review** cycle. Takes whatever `sb-build-wp` or `sb-build-shopify` produced, parses it (cheerio for HTML/Liquid, regex for CSS), and runs three independent groups of deterministic checks:
|
|
11
|
-
|
|
12
|
-
1. **Anti-patterns** — the canon `#1, #2, #3, #4, #5, #5b, #6, #8` from the migration bootstrap (`#7` and `#9` are process-level, prevented by other skills).
|
|
13
|
-
2. **Design quality** — the 12 a11y / performance / web-standards baselines that every production fragment must clear.
|
|
14
|
-
3. **Cross-reference** — when `--compare-diffs` is supplied, violations whose location overlaps a high-severity visual diff are escalated and tagged.
|
|
15
|
-
|
|
16
|
-
Pure determinism — no chromium, no network. The script does the work; this SKILL.md only describes inputs, outputs, and how to react to a non-zero exit.
|
|
17
|
-
|
|
18
|
-
This skill is the **engine of the auto-correct loop**. Its `candidateFix` strings are the contract with `sb-build-{wp,shopify}`: specific enough to apply programmatically as `fixHints`, never generic ("improve specificity" is useless — "rewrite `.hero__title` as `.hero .hero__title`" is actionable). When you generate or hand-edit fix text, treat it as patch instructions, not prose.
|
|
19
|
-
|
|
20
|
-
The split between **mechanical** (script) and **semantic** (LLM) is explicit: regex/AST decides whether `alt` is *present*; you decide whether it's *meaningful*. The script never tries to judge wording or pattern correctness.
|
|
21
|
-
|
|
22
|
-
## Inputs
|
|
23
|
-
|
|
24
|
-
| Argument | Required | Default | Notes |
|
|
25
|
-
| ----------------- | -------- | ------- | -------------------------------------------------------------------------------------- |
|
|
26
|
-
| `file` | yes | — | Path to the built fragment. HTML for `wp-elementor`, Liquid for `shopify-section`. |
|
|
27
|
-
| `preset` | yes | — | `wp-elementor` or `shopify-section`. Determines which checks apply. |
|
|
28
|
-
| `output-dir` | yes | — | Directory where `report.json` is written. |
|
|
29
|
-
| `compare-diffs` | no | — | JSON from `sb-compare-visual` (full report or just the `structuredDiffs` array). |
|
|
30
|
-
| `memory-dir` | no | (auto) | Override `<plugin>/memory` location. Auto-detects `~/.claude/similarbuild-memory` and the bundled `references/` fallback otherwise. |
|
|
31
|
-
|
|
32
|
-
## Output
|
|
33
|
-
|
|
34
|
-
A single JSON object printed to stdout AND saved to `{output-dir}/report.json`:
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{
|
|
38
|
-
"passed": false,
|
|
39
|
-
"file": "/abs/path/build.html",
|
|
40
|
-
"preset": "wp-elementor",
|
|
41
|
-
"violationCount": { "high": 2, "medium": 3, "low": 1 },
|
|
42
|
-
"violations": [
|
|
43
|
-
{
|
|
44
|
-
"group": "anti-pattern",
|
|
45
|
-
"id": "#5b",
|
|
46
|
-
"location": "selector `.hero__title`",
|
|
47
|
-
"issue": "`!important` without an ancestor in the selector — theme has both ancestor + !important so this loses anyway",
|
|
48
|
-
"candidateFix": "prefix `.hero__title` with the scope as ancestor: rewrite as `.{scope} .hero__title`",
|
|
49
|
-
"severity": "high",
|
|
50
|
-
"correlatedDiff": { "area": "h1", "issue": "font-size 24px vs 27px expected", "deltaPx": -3 }
|
|
51
|
-
}
|
|
52
|
-
],
|
|
53
|
-
"report": "{output-dir}/report.json",
|
|
54
|
-
"inputs": { "file": "...", "preset": "...", "compareDiffs": "...", "memoryDir": "...", "memorySource": "bundled|home|flag" }
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
`violations` is sorted **high → medium → low**, with cross-referenced (visual-diff-correlated) entries surfacing first within each severity. The orchestrator's first slice is always the most urgent fix.
|
|
59
|
-
|
|
60
|
-
## Dependencies
|
|
61
|
-
|
|
62
|
-
The host project must have `cheerio` installed (the SimilarBuild installer handles it). No browser, no native bindings. Node ≥ 20.
|
|
63
|
-
|
|
64
|
-
## On Activation
|
|
65
|
-
|
|
66
|
-
1. **Resolve inputs.** Collect `file`, `preset`, `output-dir`. Optional: `compare-diffs`, `memory-dir`.
|
|
67
|
-
|
|
68
|
-
2. **Ensure `output-dir` exists.** `mkdir -p` it.
|
|
69
|
-
|
|
70
|
-
3. **Run the script.** From the project root:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
node {skill-root}/scripts/review-checks.mjs \
|
|
74
|
-
--file "{built-file}" \
|
|
75
|
-
--preset "wp-elementor" \
|
|
76
|
-
--output-dir "{output-dir}" \
|
|
77
|
-
[--compare-diffs "{compare-report.json}"] \
|
|
78
|
-
[--memory-dir "{path-to-memory}"]
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The script handles: file read, cheerio parse, CSS extraction (also from `{% style %}` Liquid blocks), 8 anti-pattern checks, 12 design-quality checks, optional cross-reference with compare diffs, prioritization, and persistence. See `scripts/review-checks.mjs --help` for the full flag list.
|
|
82
|
-
|
|
83
|
-
4. **Branch on exit code.**
|
|
84
|
-
- `0` — `passed: true`. Either zero violations or only `low`-severity ones. Ship it.
|
|
85
|
-
- `3` — `passed: false`, medium/high violations exist. **Not a script error.** Parse the JSON; the `violations` list is exactly what `sb-build-{wp,shopify}` expects as `fixHints` for the next re-roll.
|
|
86
|
-
- `1` — actual script error (missing dependency, malformed file). Surface stderr to the user and stop.
|
|
87
|
-
- `2` — invalid args. Fix the call.
|
|
88
|
-
|
|
89
|
-
5. **Forward the JSON unchanged** to the caller. Do not summarize, mutate, or strip fields. Downstream consumers (the orchestrator's auto-correct loop, the HTML report) read every field directly.
|
|
90
|
-
|
|
91
|
-
## Memory and presets
|
|
92
|
-
|
|
93
|
-
The skill consults a memory directory in this order: `--memory-dir` flag → `~/.claude/similarbuild-memory/` → bundled `references/`. When present, files there should override the bundled defaults — they reflect the user's most recent curated knowledge. The bundled `references/review-rules.md` ships the canonical anti-patterns and design-quality rules so the skill is fully functional out of the box.
|
|
94
|
-
|
|
95
|
-
The memory files are **data**, not code: adding a new anti-pattern is a `.md` edit, not a `.mjs` change. The orchestrator can hand-curate them and `sb-review-checks` picks them up next run.
|
|
96
|
-
|
|
97
|
-
## Failure modes
|
|
98
|
-
|
|
99
|
-
| Symptom | Likely cause | What to surface |
|
|
100
|
-
| ---------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
101
|
-
| Exit 1, stderr mentions `cheerio` | Dependency not installed | Pass stderr verbatim. Suggest `npm i cheerio`. |
|
|
102
|
-
| Exit 1, "could not read" | Input file missing or not readable | Pass stderr verbatim. The build path may be stale — re-check what `sb-build-{wp,shopify}` returned. |
|
|
103
|
-
| Exit 3 with only `low` severity in `violationCount` | Should not happen — exit 3 implies med/high | If you see this, file a bug — exit-code policy is wrong. |
|
|
104
|
-
| `inputs.memorySource: "bundled"` but you expected curated rules | The user-curated memory dir is not where the skill looks | Pass `--memory-dir` explicitly, or move the curated files to `~/.claude/similarbuild-memory/`. |
|
|
105
|
-
| `--compare-diffs` provided but no `correlatedDiff` annotations | The diffs file has no `severity: 'high'` entries | Cross-reference only escalates on `high` diffs; med/low are advisory. This is by design. |
|
|
106
|
-
|
|
107
|
-
## Conventions
|
|
108
|
-
|
|
109
|
-
- Bare paths (e.g. `scripts/review-checks.mjs`) resolve from the skill root.
|
|
110
|
-
- `{skill-root}` resolves to this skill's installed directory.
|
|
111
|
-
- `{project-root}` resolves to the project working directory.
|
|
112
|
-
- `<plugin>/memory/...` paths are inside the SimilarBuild plugin install — read them when present, fall back to bundled `references/` when absent.
|
|
113
|
-
- `candidateFix` is a contract: every value names the selector, attribute, or property to change. Never emit prose-style suggestions ("improve X"). The auto-correct loop depends on this.
|