luckiest-co 1.0.13 → 1.0.15
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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/luckiest-ab-testing/SKILL.md +1 -1
- package/skills/luckiest-ad-creative/SKILL.md +1 -1
- package/skills/luckiest-ads/SKILL.md +1 -1
- package/skills/luckiest-advisors/SKILL.md +1 -1
- package/skills/luckiest-aeo-grader/SKILL.md +1 -1
- package/skills/luckiest-ai-seo/SKILL.md +1 -1
- package/skills/luckiest-analytics/SKILL.md +1 -1
- package/skills/luckiest-aso/SKILL.md +1 -1
- package/skills/luckiest-churn-prevention/SKILL.md +1 -1
- package/skills/luckiest-co-marketing/SKILL.md +1 -1
- package/skills/luckiest-coder/SKILL.md +1 -1
- package/skills/luckiest-coder-brainstorming/SKILL.md +1 -1
- package/skills/luckiest-coder-consistency/SKILL.md +1 -1
- package/skills/luckiest-coder-constitution/SKILL.md +1 -1
- package/skills/luckiest-coder-debugging/SKILL.md +1 -1
- package/skills/luckiest-coder-dispatching-parallel-agents/SKILL.md +1 -1
- package/skills/luckiest-coder-executing-plans/SKILL.md +1 -1
- package/skills/luckiest-coder-finishing-a-branch/SKILL.md +1 -1
- package/skills/luckiest-coder-git-worktrees/SKILL.md +1 -1
- package/skills/luckiest-coder-guard/SKILL.md +1 -1
- package/skills/luckiest-coder-qa/SKILL.md +1 -1
- package/skills/luckiest-coder-receiving-code-review/SKILL.md +1 -1
- package/skills/luckiest-coder-requesting-code-review/SKILL.md +1 -1
- package/skills/luckiest-coder-shipping/SKILL.md +1 -1
- package/skills/luckiest-coder-subagent-driven-development/SKILL.md +1 -1
- package/skills/luckiest-coder-tdd/SKILL.md +1 -1
- package/skills/luckiest-coder-verification/SKILL.md +1 -1
- package/skills/luckiest-coder-writing-plans/SKILL.md +1 -1
- package/skills/luckiest-coder-writing-skills/SKILL.md +1 -1
- package/skills/luckiest-cold-email/SKILL.md +1 -1
- package/skills/luckiest-community-marketing/SKILL.md +1 -1
- package/skills/luckiest-competitor-profiling/SKILL.md +1 -1
- package/skills/luckiest-competitors/SKILL.md +1 -1
- package/skills/luckiest-content-strategy/SKILL.md +1 -1
- package/skills/luckiest-copy-editing/SKILL.md +1 -1
- package/skills/luckiest-copywriting/SKILL.md +1 -1
- package/skills/luckiest-cro/SKILL.md +1 -1
- package/skills/luckiest-customer-research/SKILL.md +1 -1
- package/skills/luckiest-directory-submissions/SKILL.md +1 -1
- package/skills/luckiest-emails/SKILL.md +1 -1
- package/skills/luckiest-extract-design-system/SKILL.md +1 -1
- package/skills/luckiest-free-tools/SKILL.md +1 -1
- package/skills/luckiest-image/SKILL.md +1 -1
- package/skills/luckiest-launch/SKILL.md +1 -1
- package/skills/luckiest-lead-magnets/SKILL.md +1 -1
- package/skills/luckiest-marketing-ideas/SKILL.md +1 -1
- package/skills/luckiest-marketing-plan/SKILL.md +1 -1
- package/skills/luckiest-marketing-psychology/SKILL.md +1 -1
- package/skills/luckiest-model-router/SKILL.md +1 -1
- package/skills/luckiest-offers/SKILL.md +1 -1
- package/skills/luckiest-onboarding/SKILL.md +1 -1
- package/skills/luckiest-paywalls/SKILL.md +1 -1
- package/skills/luckiest-popups/SKILL.md +1 -1
- package/skills/luckiest-pricing/SKILL.md +1 -1
- package/skills/luckiest-product-marketing/SKILL.md +1 -1
- package/skills/luckiest-programmatic-seo/SKILL.md +1 -1
- package/skills/luckiest-prompt-rewrite/SKILL.md +1 -1
- package/skills/luckiest-prospecting/SKILL.md +1 -1
- package/skills/luckiest-public-relations/SKILL.md +1 -1
- package/skills/luckiest-referrals/SKILL.md +1 -1
- package/skills/luckiest-revops/SKILL.md +1 -1
- package/skills/luckiest-sales-enablement/SKILL.md +1 -1
- package/skills/luckiest-schema/SKILL.md +1 -1
- package/skills/luckiest-seo-audit/SKILL.md +1 -1
- package/skills/luckiest-session-handoff/SKILL.md +1 -1
- package/skills/luckiest-signup/SKILL.md +1 -1
- package/skills/luckiest-site-architecture/SKILL.md +1 -1
- package/skills/luckiest-sms/SKILL.md +1 -1
- package/skills/luckiest-social/SKILL.md +1 -1
- package/skills/luckiest-thinker/SKILL.md +1 -1
- package/skills/luckiest-trends/SKILL.md +1 -1
- package/skills/luckiest-video/SKILL.md +1 -1
- package/skills/luckiest-website-cloner/ATTRIBUTION.md +29 -0
- package/skills/luckiest-website-cloner/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner/LICENSE +21 -0
- package/skills/luckiest-website-cloner/SKILL.md +289 -0
- package/skills/luckiest-website-cloner/references/builtin-browser-recipes.md +74 -0
- package/skills/luckiest-website-cloner/references/compositing.md +65 -0
- package/skills/luckiest-website-cloner/references/fidelity-fast-path.md +92 -0
- package/skills/luckiest-website-cloner/references/playwright-cli-recipes.md +68 -0
- package/skills/luckiest-website-cloner/references/teardown.md +142 -0
- package/skills/luckiest-website-cloner/references/visual-qa.md +102 -0
- package/skills/luckiest-website-cloner/scripts/build-bundle.cjs +12 -0
- package/skills/luckiest-website-cloner/scripts/motion-probe.js +423 -0
- package/skills/luckiest-website-cloner/scripts/surface-map.js +392 -0
- package/skills/luckiest-website-cloner/scripts/tokens-probe.js +178 -0
- package/skills/luckiest-website-cloner-dom/ATTRIBUTION.md +4 -0
- package/skills/luckiest-website-cloner-dom/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner-dom/LICENSE +21 -0
- package/skills/luckiest-website-cloner-dom/SKILL.md +170 -0
- package/skills/luckiest-website-cloner-dom/references/asset-resolution.md +93 -0
- package/skills/luckiest-website-cloner-dom/references/interaction-fingerprints.md +85 -0
- package/skills/luckiest-website-cloner-remix/ATTRIBUTION.md +4 -0
- package/skills/luckiest-website-cloner-remix/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner-remix/LICENSE +21 -0
- package/skills/luckiest-website-cloner-remix/SKILL.md +202 -0
- package/skills/luckiest-website-cloner-remix/references/directions.md +82 -0
- package/skills/luckiest-website-cloner-remix/references/reskin.md +84 -0
- package/skills/luckiest-website-cloner-remix/references/tweak-panel.md +72 -0
- package/skills/luckiest-website-cloner-remix/scripts/apply-overrides.cjs +105 -0
- package/skills/luckiest-website-cloner-remix/scripts/gallery.cjs +71 -0
- package/skills/luckiest-website-cloner-remix/scripts/tokenize-css.cjs +127 -0
- package/skills/luckiest-website-cloner-remix/scripts/tweak-panel.js +169 -0
- package/skills/luckiest-website-cloner-shaders/ATTRIBUTION.md +11 -0
- package/skills/luckiest-website-cloner-shaders/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner-shaders/LICENSE +21 -0
- package/skills/luckiest-website-cloner-shaders/SKILL.md +129 -0
- package/skills/luckiest-website-cloner-shaders/references/capture-backends.md +89 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/SKILL.md +123 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/capture-backends.md +201 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/evidence-policy.md +93 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/operating-contract.md +82 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/qa-failure-policy.md +100 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/recon-kernel.md +206 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/replay-policy.md +145 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/shaders-com.md +212 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/source-analysis.md +112 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/surface-discovery.md +113 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/target-lock.md +205 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/three-shader-reconstruction.md +155 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/tool-capability-matrix.md +57 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/unicorn-studio.md +387 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/scripts/fetch-rendered-dom.mjs +178 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/scripts/scan-bundle.sh +80 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/extraction-report.md +41 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/known-gaps.md +14 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/qa-report.md +74 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/replay-manifest.json +147 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/run-state.json +75 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/scout-card.json +106 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Built-in browser recipes (no playwright-cli)
|
|
2
|
+
|
|
3
|
+
Use these when the only browser you can drive is the in-app browser pane
|
|
4
|
+
(`navigate`, `javascript_tool`, `computer`, `get_page_text`, `read_page`). This
|
|
5
|
+
path has **no preload**, so `instrumentGetContext()` and `instrumentMotion()`
|
|
6
|
+
cannot run before the page's own scripts. Everything here is post-hoc. That is
|
|
7
|
+
still reliable for the common case; it is weaker for canvases that request their
|
|
8
|
+
context once at startup and for motion that only fires on first paint.
|
|
9
|
+
|
|
10
|
+
## Inject the probe bundle
|
|
11
|
+
|
|
12
|
+
Build it once, then paste it in. `javascript_tool` cannot read local files, so
|
|
13
|
+
inline the bundle's text rather than pointing at a path.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node scripts/build-bundle.cjs /tmp/probes.bundle.js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then evaluate the file's contents in the page, followed by the probe call. Do it
|
|
20
|
+
in two steps so a syntax error in the bundle is easy to tell apart from a probe
|
|
21
|
+
error.
|
|
22
|
+
|
|
23
|
+
## Settle the page before probing
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
(async () => {
|
|
27
|
+
const h = document.documentElement.scrollHeight;
|
|
28
|
+
for (let y = 0; y < h; y += 700) { window.scrollTo(0, y); await new Promise(r => setTimeout(r, 120)); }
|
|
29
|
+
window.scrollTo(0, 0);
|
|
30
|
+
await new Promise(r => setTimeout(r, 800));
|
|
31
|
+
return h;
|
|
32
|
+
})()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Probe and write out
|
|
36
|
+
|
|
37
|
+
`javascript_tool` returns its last expression as JSON, and large probe output
|
|
38
|
+
will blow up the conversation. Take `motionSummary()` inline for a first look,
|
|
39
|
+
and for the full `motionProbe()` / `tokensProbe()` output return it in chunks
|
|
40
|
+
and write each chunk to the output directory from the shell, or keep only the
|
|
41
|
+
fields you need:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
JSON.stringify(surfaceMap()) // small, safe inline
|
|
45
|
+
JSON.stringify(motionSummary()) // small, safe inline
|
|
46
|
+
JSON.stringify(tokensProbe()).length // check size BEFORE returning the body
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Expect `instrumented: false`
|
|
50
|
+
|
|
51
|
+
`motionProbe().instrumented` will be false on this path. That is expected, not a
|
|
52
|
+
failure. Record it in the surface map so the teardown does not claim ground
|
|
53
|
+
truth it does not have, and treat canvas routing as a hypothesis to confirm with
|
|
54
|
+
a screenshot.
|
|
55
|
+
|
|
56
|
+
## Screenshots and viewports
|
|
57
|
+
|
|
58
|
+
Use `resize_window` for the 1440 / 768 / 390 passes and `computer` with
|
|
59
|
+
`action: "screenshot"` for section references. Reload after a viewport change so
|
|
60
|
+
load-time breakpoints re-run.
|
|
61
|
+
|
|
62
|
+
## Assets
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
JSON.stringify(performance.getEntriesByType('resource').map(e => e.name))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Viewport reads can come back zero
|
|
69
|
+
|
|
70
|
+
In a hidden or background pane, `window.innerWidth` / `innerHeight` can report
|
|
71
|
+
`0` even though the page laid out correctly and every `getBoundingClientRect()`
|
|
72
|
+
is real. Do not treat a zero viewport as a failed probe, and do not record it as
|
|
73
|
+
the breakpoint you measured at. Front the tab, or take the width you asked
|
|
74
|
+
`resize_window` for as the truth.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Compositing GPU effects into the DOM shell
|
|
2
|
+
|
|
3
|
+
The DOM track leaves a **placeholder mount** wherever a GPU surface belonged.
|
|
4
|
+
The GPU track produces a **self-contained effect module**. Compositing is the
|
|
5
|
+
contract that joins them. Most "the clone looks off" bugs are composite bugs, not
|
|
6
|
+
extraction bugs: the effect renders perfectly in its own baseline, then sits at
|
|
7
|
+
the wrong depth, swallows clicks, or renders blurry once it's in the page.
|
|
8
|
+
|
|
9
|
+
## The mount contract
|
|
10
|
+
|
|
11
|
+
Each effect module should expose a tiny, framework-agnostic lifecycle so any
|
|
12
|
+
shell (Vite, Next, plain HTML) can drive it the same way:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
// effect module (per GPU surface)
|
|
16
|
+
export function mount(canvasEl, opts) { /* start renderer + rAF loop */ return handle }
|
|
17
|
+
export function unmount(handle) { /* cancel rAF, lose context, free GPU */ }
|
|
18
|
+
export function resize(handle, w, h, dpr) { /* update drawing buffer + viewport */ }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The DOM shell mounts it into the placeholder container from the surface map:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
const el = document.querySelector(MOUNT_SELECTOR);
|
|
25
|
+
const canvas = document.createElement('canvas');
|
|
26
|
+
el.appendChild(canvas);
|
|
27
|
+
const handle = mount(canvas, { /* uniforms captured by luckiest-website-cloner-shaders */ });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Get these five things right (they are the usual failure points)
|
|
31
|
+
|
|
32
|
+
1. **Sizing + DPR.** The drawing buffer must be `cssWidth * dpr` by
|
|
33
|
+
`cssHeight * dpr`, with the canvas CSS-sized to the container. Skipping DPR is
|
|
34
|
+
the #1 "why is the clone blurry / aliased" cause on retina displays. Re-run
|
|
35
|
+
`resize()` on container resize (ResizeObserver), not just window resize.
|
|
36
|
+
2. **z-index + stacking.** Copy `zIndex` and `position` from `surface-map.json`.
|
|
37
|
+
A hero shader usually sits at a low z-index BEHIND the text, not on top. If it
|
|
38
|
+
covers the copy, the composite is wrong even though the effect is right.
|
|
39
|
+
3. **pointer-events.** Copy the original's `pointerEvents`. A full-bleed
|
|
40
|
+
background canvas almost always needs `pointer-events: none` so buttons and
|
|
41
|
+
links under/over it stay clickable. An *interactive* effect (cursor-reactive
|
|
42
|
+
blob) needs pointer events ON — the surface map records which.
|
|
43
|
+
4. **Teardown.** On route change / unmount, call `unmount()` and let the WebGL
|
|
44
|
+
context go (`loseContext()`). Leaking contexts hits the browser's ~16-context
|
|
45
|
+
limit fast on multi-effect pages and the later canvases silently go black.
|
|
46
|
+
5. **Reduced motion + offscreen pause.** Respect
|
|
47
|
+
`prefers-reduced-motion: reduce` (render a static first frame or the captured
|
|
48
|
+
poster). Pause the rAF loop when the canvas is scrolled out of view
|
|
49
|
+
(IntersectionObserver) — the original may not, but it's cheap fidelity-neutral
|
|
50
|
+
insurance against jank, and matches how good sites ship these.
|
|
51
|
+
|
|
52
|
+
## Multiple effects on one page
|
|
53
|
+
|
|
54
|
+
Mount each into its own container from the surface map. Share a single renderer
|
|
55
|
+
only if all effects came from the same driver and baseline; otherwise keep them
|
|
56
|
+
independent so one failing effect can't black out the others. Watch the context
|
|
57
|
+
budget (point 4).
|
|
58
|
+
|
|
59
|
+
## When the effect can't be reconstructed
|
|
60
|
+
|
|
61
|
+
If luckiest-website-cloner-shaders returned only an approximate or poster-only result (e.g.
|
|
62
|
+
cross-origin readback was blocked, or the surface was `CANVAS_UNKNOWN`), mount
|
|
63
|
+
its fallback (captured poster image or a low-fidelity approximation) and record
|
|
64
|
+
the gap in the completion report. Do not silently ship a dead container — a
|
|
65
|
+
poster still reads as intentional; an empty box reads as broken.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# The fidelity fast path — reuse the original's CSS instead of rebuilding it
|
|
2
|
+
|
|
3
|
+
Validated on a real clone run (Nuxt marketing site): this path reached a
|
|
4
|
+
**0.02–0.06% pixel diff** on the DOM track in one pass, where rebuild-from-spec
|
|
5
|
+
typically lands at 1–3% after several fix rounds. Prefer it whenever it applies.
|
|
6
|
+
|
|
7
|
+
## When it applies
|
|
8
|
+
|
|
9
|
+
The target ships **scope-attributed CSS** — Vue scoped styles (`data-v-*`),
|
|
10
|
+
CSS Modules (hashed class names), styled-components (hashed classes), Angular
|
|
11
|
+
(`_ngcontent-*`). The attributes/classes are baked into the rendered DOM, so:
|
|
12
|
+
|
|
13
|
+
> post-hydration HTML + the site's own stylesheets + rewritten asset paths
|
|
14
|
+
> = pixel-identical DOM layer, byte for byte.
|
|
15
|
+
|
|
16
|
+
It does NOT apply when the goal is a *maintainable* codebase to keep developing
|
|
17
|
+
(then use luckiest-website-cloner-dom's component rebuild), or when the user asked for a specific
|
|
18
|
+
stack. Say which path you chose and why in the report.
|
|
19
|
+
|
|
20
|
+
## The recipe
|
|
21
|
+
|
|
22
|
+
1. **Capture post-hydration HTML** after a full settle scroll:
|
|
23
|
+
`document.documentElement.outerHTML` via Playwright. Not view-source — the
|
|
24
|
+
served HTML lacks client-rendered state.
|
|
25
|
+
2. **Download every stylesheet** in document order (`<link rel=stylesheet>`
|
|
26
|
+
hrefs). Concatenate in that order — cascade order is load order.
|
|
27
|
+
3. **Rewrite paths** in both HTML and CSS: absolute site URLs → local, CDN image
|
|
28
|
+
URLs (strip query params, keep a deterministic filename), `/fonts/`,
|
|
29
|
+
`url(...)` in CSS. Grep the result for `https?://` afterward — remaining
|
|
30
|
+
hits should only be outbound links, or you missed an asset.
|
|
31
|
+
4. **Strip the runtime**: remove `<script>` tags and preload/prefetch links.
|
|
32
|
+
Keep everything else, including framework scope attributes and inline
|
|
33
|
+
`style=""` — they're part of the captured state.
|
|
34
|
+
5. **Capture state you can't see at rest.** SPA frameworks render only the
|
|
35
|
+
ACTIVE state into the DOM — inactive tab panels, closed dropdowns, unfired
|
|
36
|
+
dialogs simply don't exist in the capture. For each stateful widget, drive
|
|
37
|
+
the live site (click each tab/accordion/menu) and save each state's
|
|
38
|
+
innerHTML; replay those snapshots from your own JS. The interaction sweep's
|
|
39
|
+
click step is where you find these.
|
|
40
|
+
6. **Beware capture-time inline state**: elements the site's JS was mid-
|
|
41
|
+
animating carry inline `opacity`/`transform` from the moment of capture
|
|
42
|
+
(e.g. a bar captured at `opacity: 0`). Your rewritten behavior JS must own
|
|
43
|
+
those elements' initial state explicitly, not trust the captured inline value.
|
|
44
|
+
7. **Rebuild only behaviors** (one small JS entry): smooth scroll, canvas/GPU
|
|
45
|
+
mounts, state machines, toggles — from `motion.json` params. CSS-driven
|
|
46
|
+
effects (scroll-timelines, keyframes, transitions) came along with the
|
|
47
|
+
stylesheets for free — do not reimplement them.
|
|
48
|
+
|
|
49
|
+
## Trigger-position drift (bit us; will bite again)
|
|
50
|
+
|
|
51
|
+
Scroll-triggered behaviors computed at load go stale when lazy images/fonts
|
|
52
|
+
shift layout seconds later — symptom: effects fire at the wrong scroll position
|
|
53
|
+
only on some breakpoints. Standard fix, include it by default:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
let lastH = 0, t = 0;
|
|
57
|
+
new ResizeObserver(() => {
|
|
58
|
+
const h = document.documentElement.scrollHeight;
|
|
59
|
+
if (h === lastH) return; lastH = h;
|
|
60
|
+
clearTimeout(t); t = setTimeout(() => ScrollTrigger.refresh(), 120);
|
|
61
|
+
}).observe(document.body);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Matching state machines: measure thresholds, don't guess them
|
|
65
|
+
|
|
66
|
+
For each observed state flip (header theme, content swap, element fade), sample
|
|
67
|
+
the original at a FINE grain around the transition (±50px steps) and record the
|
|
68
|
+
exact trigger geometry before implementing. Two traps proven real:
|
|
69
|
+
- a flip can be **binary** where you'd assume scrubbed (and vice versa) — the
|
|
70
|
+
fine-grained samples tell you which;
|
|
71
|
+
- the trigger element is often NOT the section that changes (a header flips on
|
|
72
|
+
the NEXT section's top edge, at an offset unrelated to the header's height).
|
|
73
|
+
Also check for CSS-variable drivers before assuming classes: if a computed style
|
|
74
|
+
uses `color-mix(... var(--x-progress) ...)`, the JS writes that var — toggling
|
|
75
|
+
the class alone changes nothing visible.
|
|
76
|
+
|
|
77
|
+
## When the GPU surface is a known library, reuse it (SOURCE fidelity)
|
|
78
|
+
|
|
79
|
+
Before routing a canvas to luckiest-website-cloner-shaders, grep the bundles for library
|
|
80
|
+
fingerprints: `data-paper-shader` / `@paper-design` (Paper Shaders),
|
|
81
|
+
`unicorn.studio`, `spline-viewer`, `rive`, `lottie`, `r3f`/`@react-three`,
|
|
82
|
+
`ogl`. If the effect is an off-the-shelf component, the bundle usually carries
|
|
83
|
+
its **props verbatim** (Next.js app-router chunks are barely minified — read the
|
|
84
|
+
page chunk, it IS the source). Install the same package and pass the same props:
|
|
85
|
+
that's SOURCE fidelity for free, no capture/replay needed. Also copy the site's
|
|
86
|
+
**render gate** (reduced-motion / pointer / width / cores / GPU-renderer
|
|
87
|
+
checks) — it decides when the fallback shows, and the fallback is part of the
|
|
88
|
+
look. Add a `?shaders=1|0` override so QA can exercise both paths.
|
|
89
|
+
|
|
90
|
+
Mount-point rule: when the original renders the effect wrapper *conditionally*,
|
|
91
|
+
your mount element must be visually inert (no background, no filter) — put the
|
|
92
|
+
wrapper styling on the mounted component, or the fallback state shows a blank box.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# playwright-cli recipes for this skill
|
|
2
|
+
|
|
3
|
+
`playwright-cli` is a CLI over a persistent browser session. Each invocation is
|
|
4
|
+
a separate process, which shapes how you must use it here.
|
|
5
|
+
|
|
6
|
+
## Running the probe scripts
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# inject the probe bundle into the LIVE page, then evaluate
|
|
10
|
+
playwright-cli -s=<name> run-code "async page => { await page.addScriptTag({ path: '<abs>/probes.bundle.js' }); return page.evaluate(() => motionSummary()); }"
|
|
11
|
+
```
|
|
12
|
+
Build `probes.bundle.js` with `node scripts/build-bundle.cjs [dest]` — it concatenates
|
|
13
|
+
the three probe files AND exports them onto `window`. That export is mandatory:
|
|
14
|
+
Playwright wraps `addInitScript`/`addScriptTag` code in a function scope, so
|
|
15
|
+
bare `function` declarations are invisible to later `page.evaluate` calls
|
|
16
|
+
(symptom: `motionSummary is not defined` even though the script ran). `run-code` has **no `fs`** — return data and redirect
|
|
17
|
+
stdout, or use `eval` and parse the `### Result` block:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
playwright-cli -s=<name> eval "JSON.stringify(tokensProbe())" > raw.txt
|
|
21
|
+
# then unwrap: the JSON is the quoted string after the '### Result' line
|
|
22
|
+
```
|
|
23
|
+
`eval` treats an expression starting with `(` as a function — wrap IIFEs in
|
|
24
|
+
`run-code` + `page.evaluate` instead.
|
|
25
|
+
|
|
26
|
+
## Instrumented (preload) mode — do it in ONE run-code call
|
|
27
|
+
|
|
28
|
+
`page.addInitScript()` registered in one `run-code` invocation does NOT
|
|
29
|
+
survive into the next (the CLI's `page` binding is re-created). Do init +
|
|
30
|
+
navigate + settle + probe in a single call:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
playwright-cli -s=<name> run-code "async page => {
|
|
34
|
+
await page.addInitScript({ path: '<abs>/probes.bundle.js' });
|
|
35
|
+
await page.addInitScript(() => { instrumentGetContext(); instrumentMotion(); });
|
|
36
|
+
await page.goto('<url>', { waitUntil: 'domcontentloaded' });
|
|
37
|
+
await page.waitForTimeout(2500);
|
|
38
|
+
const h = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
39
|
+
for (let y = 0; y < h; y += 700) { await page.mouse.wheel(0, 700); await page.waitForTimeout(120); }
|
|
40
|
+
await page.evaluate(() => window.scrollTo(0, 0)); await page.waitForTimeout(800);
|
|
41
|
+
return page.evaluate(() => ({ sm: surfaceMap(), mo: motionProbe() }));
|
|
42
|
+
}" > raw-instrumented.txt
|
|
43
|
+
```
|
|
44
|
+
Verify `mo.instrumented === true`; if false the preload didn't take.
|
|
45
|
+
|
|
46
|
+
## Capturing hidden SPA state
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
playwright-cli -s=<name> run-code "async page => { const tabs = page.locator('.tabs .tab'); const out = []; for (let i = 0; i < await tabs.count(); i++) { await tabs.nth(i).click(); await page.waitForTimeout(800); out.push(await page.evaluate(() => document.querySelector('.tabs-content').innerHTML)); } return out; }"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Asset list
|
|
53
|
+
|
|
54
|
+
`playwright-cli network` only logs from the moment it's first called. For the
|
|
55
|
+
full list use the page's own record:
|
|
56
|
+
`page.evaluate(() => performance.getEntriesByType('resource').map(e => e.name))`.
|
|
57
|
+
|
|
58
|
+
## Screenshots
|
|
59
|
+
|
|
60
|
+
`page.screenshot({ path })` inside `run-code` works and is the reliable way to
|
|
61
|
+
get section references regardless of which browser tool is fronted.
|
|
62
|
+
|
|
63
|
+
## Headless GPU note
|
|
64
|
+
|
|
65
|
+
Headless Chromium on a desktop with a real GPU often exposes a real GPU renderer string, so sites
|
|
66
|
+
that gate shaders on `WEBGL_debug_renderer_info` DO render them in QA — mask
|
|
67
|
+
`[data-paper-shader]`, `canvas`, and any wrapper the library injects. Don't
|
|
68
|
+
assume headless = software GL.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# TEARDOWN.md — the human-readable blueprint
|
|
2
|
+
|
|
3
|
+
`surface-map.json`, `motion.json`, and `tokens-*.json` are machine evidence.
|
|
4
|
+
`TEARDOWN.md` is the same evidence written for a person: what the site is made
|
|
5
|
+
of, how every effect actually works, and what it would take to rebuild it. It is
|
|
6
|
+
the deliverable of `--analyze-only` runs, the briefing the builders work from on
|
|
7
|
+
full runs, and — because the reveals are usually "that impressive thing is 54
|
|
8
|
+
images swapped on mousemove" — the raw material for a video.
|
|
9
|
+
|
|
10
|
+
Write it at the end of Phase 1, from the probe outputs + your scroll/hover sweep.
|
|
11
|
+
Every claim carries an evidence tag:
|
|
12
|
+
|
|
13
|
+
- **CONFIRMED** — read from a live runtime object or computed style
|
|
14
|
+
(`ScrollTrigger.getAll()`, `getComputedStyle`, `document.getAnimations()`).
|
|
15
|
+
- **OBSERVED** — seen during the sweep (screenshots at scroll offsets, hover
|
|
16
|
+
before/after) but the mechanism wasn't exposed by a probe.
|
|
17
|
+
- **INFERRED** — guessed from class names / markup / general patterns. The
|
|
18
|
+
builder should verify before relying on it.
|
|
19
|
+
|
|
20
|
+
Never upgrade a tag. A clone built on INFERRED values needs a diff pass to prove
|
|
21
|
+
them; one built on CONFIRMED values mostly needs a diff pass to catch typos.
|
|
22
|
+
|
|
23
|
+
## Template
|
|
24
|
+
|
|
25
|
+
```markdown
|
|
26
|
+
# Teardown: {site name}
|
|
27
|
+
|
|
28
|
+
**URL:** {url} **Analyzed:** {YYYY-MM-DD} @ {1440/768/390}
|
|
29
|
+
**Platform:** {Next.js / Webflow / Framer / WordPress / custom} — {evidence}
|
|
30
|
+
**Built by:** {agency/dev, if a footer/meta credit exists; else omit}
|
|
31
|
+
**Surfaces:** {N} DOM sections, {N} GPU surfaces ({drivers}), {N} video
|
|
32
|
+
|
|
33
|
+
## Stack (from runtime)
|
|
34
|
+
|
|
35
|
+
| Layer | What | Evidence |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Framework | {…} | `__NEXT_DATA__` present / `data-framer-name` / … |
|
|
38
|
+
| Animation | GSAP {ver} + ScrollTrigger ({n} triggers, {n} pinned, scrub on {n}) | `motion.json › gsap, scrollTrigger` |
|
|
39
|
+
| Scroll | Lenis lerp {x} duration {y} | `motion.json › lenis.options` |
|
|
40
|
+
| 3D / GPU | {three.js r{ver} / Unicorn Studio / none} | `surface-map.json` |
|
|
41
|
+
| Sliders | {Swiper: loop, autoplay 4000, effect fade} | `motion.json › sliders` |
|
|
42
|
+
| Fonts | {Family A (display), Family B (text)} — self-hosted woff2 / Google | `tokens.fontFaces` |
|
|
43
|
+
|
|
44
|
+
## Design system (measured)
|
|
45
|
+
|
|
46
|
+
**Palette** — top values by usage, role-labeled:
|
|
47
|
+
| Role | Value | Uses |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Page bg | {#…} | {n} |
|
|
50
|
+
| Text | {#…} | {n} |
|
|
51
|
+
| Accent | {#…} | {n} |
|
|
52
|
+
|
|
53
|
+
**Type scale @1440** (size / line-height / weight — family):
|
|
54
|
+
| Role | Value | Example |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| h1 | {…} | `.hero__title` |
|
|
57
|
+
| h2 | {…} | |
|
|
58
|
+
| body | {…} | |
|
|
59
|
+
| label/eyebrow | {…, letter-spacing, uppercase} | |
|
|
60
|
+
{Note what changes @768 and @390 — usually 2–4 lines.}
|
|
61
|
+
|
|
62
|
+
**Spacing rhythm** — {base unit + the 5–8 values that carry the layout}.
|
|
63
|
+
**Radii** {…} · **Shadows** {…} · **Max-width** {…} · **Breakpoints** {…}
|
|
64
|
+
**Custom props** — {if the site exposes its own tokens, list the important ones;
|
|
65
|
+
reuse their names in the clone}.
|
|
66
|
+
|
|
67
|
+
## Effects (one row per distinct effect, top-to-bottom)
|
|
68
|
+
|
|
69
|
+
| # | Section | Effect | Mechanism | Tag | Complexity |
|
|
70
|
+
|---|---|---|---|---|---|
|
|
71
|
+
| 1 | Hero | Title chars rise + fade in on load | SplitText → stagger 0.03, y 100%→0, power3.out, 1.2s | CONFIRMED | Low |
|
|
72
|
+
| 2 | Hero | Background liquid gradient | WebGL2 fragment shader, three.js, 2 uniforms (time, mouse) | CONFIRMED (surface) / PARTIAL (replay) | High |
|
|
73
|
+
| 3 | Features | Sticky mock swaps as copy scrolls | `position:sticky` + IntersectionObserver threshold 0.5 | CONFIRMED (IO instrumented) | Med |
|
|
74
|
+
| … | | | | | |
|
|
75
|
+
|
|
76
|
+
## Reveals — how the impressive ones actually work
|
|
77
|
+
|
|
78
|
+
### {Effect #} — {name}
|
|
79
|
+
**What it looks like:** {one sentence}
|
|
80
|
+
**What it is:** {the mechanism in plain words — the "oh, that's all it is"}
|
|
81
|
+
**Exact params (CONFIRMED):**
|
|
82
|
+
```
|
|
83
|
+
{paste the relevant motion.json entry: trigger, start/end, scrub, vars…}
|
|
84
|
+
```
|
|
85
|
+
**Rebuild note:** {the one thing that makes it feel right — easing, the lerp
|
|
86
|
+
value, the DPR handling, the fact that it's 3 layers not 1}
|
|
87
|
+
|
|
88
|
+
{Repeat for every effect rated Med/High, and any Low that's load-bearing.}
|
|
89
|
+
|
|
90
|
+
## Assets
|
|
91
|
+
|
|
92
|
+
| Asset | Role | Tier | Notes |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| hero-bg.mp4 | decorative bg | REAL | 1920×1080, 4.2 MB |
|
|
95
|
+
| logo.svg | identity | PLACEHOLDER | don't copy brand |
|
|
96
|
+
| noise.png | texture | RECONSTRUCT | generate with canvas |
|
|
97
|
+
{Tiers from `luckiest-website-cloner-dom/references/asset-resolution.md`.}
|
|
98
|
+
|
|
99
|
+
## Build plan
|
|
100
|
+
|
|
101
|
+
**Substrate:** {Vite+TS / Next+Tailwind} — {why, per Phase 2 rules}
|
|
102
|
+
**Packages:** `npm i {gsap lenis three …}`
|
|
103
|
+
**Order:** {section list top-to-bottom, each with: interaction model, which
|
|
104
|
+
effects from the table, expected difficulty}
|
|
105
|
+
**Known gaps:** {cross-origin stylesheets that hid @font-face, un-instrumented
|
|
106
|
+
IO, surfaces flagged CANVAS_UNKNOWN, premium plugins (SplitText/ScrollSmoother
|
|
107
|
+
need Club GSAP → use free alternatives or the now-free GSAP 3.13+)}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Common reveals (look for these first)
|
|
111
|
+
|
|
112
|
+
Most "how did they do that" effects on award sites are one of these. When the
|
|
113
|
+
probe shows the signature, name it:
|
|
114
|
+
|
|
115
|
+
- **Image sequence on scroll/mouse** — N preloaded frames, one visible. Signature:
|
|
116
|
+
many sibling `<img>`/canvas draws + a scrub ScrollTrigger or mousemove listener.
|
|
117
|
+
- **SplitText reveal** — `.char/.word/.line` wrappers + staggered tween.
|
|
118
|
+
- **Parallax layers** — several tweens on siblings with different `y`/`yPercent`
|
|
119
|
+
and the same trigger, or mousemove → `transform` with different multipliers.
|
|
120
|
+
- **Scrub animation** — `ScrollTrigger.scrub: true|number`. Number = lerp delay.
|
|
121
|
+
- **Pinned section** — `pin: true` with a long `end` (`+=200%`); inside it,
|
|
122
|
+
content swaps by progress.
|
|
123
|
+
- **CSS-var driven** — JS writes `--progress`; CSS `calc()`s off it. Look for
|
|
124
|
+
custom props that change as you scroll.
|
|
125
|
+
- **Smooth scroll** — Lenis on `<html class="lenis">`. Feel comes from `lerp`.
|
|
126
|
+
- **Page transitions** — barba / Swup global + an overlay element with z-index
|
|
127
|
+
9999.
|
|
128
|
+
- **Grain** — a fixed full-viewport div, noise PNG/SVG `feTurbulence`,
|
|
129
|
+
`mix-blend-mode: overlay|soft-light`, low opacity, sometimes animated
|
|
130
|
+
`background-position`.
|
|
131
|
+
- **Custom cursor** — fixed small element + mousemove + `gsap.quickTo` /
|
|
132
|
+
`quickSetter`; often two elements (dot + ring) with different lerps.
|
|
133
|
+
- **Marquee** — `@keyframes` translateX −50% on a duplicated track, or GSAP
|
|
134
|
+
`repeat:-1` with `xPercent`.
|
|
135
|
+
- **Magnetic button** — mousemove inside a hit area → `x/y` tween toward cursor,
|
|
136
|
+
`elastic.out` on leave.
|
|
137
|
+
- **Reveal on scroll** — IO (threshold 0.1–0.3) toggles a class; CSS transition
|
|
138
|
+
does the motion. Or ScrollTrigger `toggleActions: "play none none reverse"`.
|
|
139
|
+
- **Theme flip by section** — IO or ScrollTrigger toggling a class on `<body>`;
|
|
140
|
+
CSS vars swap; 0.4–0.8s transition on background/color.
|
|
141
|
+
- **Text scramble / counter** — rAF loop writing `textContent`; not an animation
|
|
142
|
+
the probe can see — OBSERVED, note the duration.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Automated visual QA
|
|
2
|
+
|
|
3
|
+
Replace "look at a screenshot and call it done" with a real pixel diff. The catch
|
|
4
|
+
is that a cloned page is two materials with opposite QA needs: DOM must match
|
|
5
|
+
closely and deterministically; GPU effects animate and will never match pixel-
|
|
6
|
+
for-pixel. So diff DOM, and **verify** GPU — don't diff GPU.
|
|
7
|
+
|
|
8
|
+
## Harness
|
|
9
|
+
|
|
10
|
+
Use Playwright's built-in visual comparison (`toHaveScreenshot()` / `expect(page)`
|
|
11
|
+
compares via the pixelmatch library — pixel-by-pixel, local, no external
|
|
12
|
+
service). Capture original and clone at the same viewport, DPR, and scroll state.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
// qa/clone.spec.ts — run per breakpoint
|
|
16
|
+
import { test, expect } from '@playwright/test';
|
|
17
|
+
|
|
18
|
+
const BREAKPOINTS = [1440, 768, 390];
|
|
19
|
+
// GPU surface selectors come straight from surface-map.json.routingSummary.sendToShaderExtract
|
|
20
|
+
const GPU_MASKS = ['#hero-canvas', '.bg-shader'];
|
|
21
|
+
|
|
22
|
+
for (const width of BREAKPOINTS) {
|
|
23
|
+
test(`clone matches original @ ${width}`, async ({ page }) => {
|
|
24
|
+
await page.setViewportSize({ width, height: 900 });
|
|
25
|
+
await page.goto(CLONE_URL);
|
|
26
|
+
await page.waitForLoadState('networkidle');
|
|
27
|
+
await expect(page).toHaveScreenshot(`clone-${width}.png`, {
|
|
28
|
+
fullPage: true,
|
|
29
|
+
mask: GPU_MASKS.map((s) => page.locator(s)), // exclude animated GPU regions
|
|
30
|
+
maxDiffPixelRatio: 0.02, // ~2% tolerance absorbs anti-aliasing noise
|
|
31
|
+
threshold: 0.2, // per-pixel color sensitivity (default; lower = stricter)
|
|
32
|
+
animations: 'disabled', // freeze CSS animations/transitions for a stable frame
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Generate the baseline from the **original** site (first run writes the reference
|
|
39
|
+
screenshot), then run the **clone** against it. Or capture both to files and diff
|
|
40
|
+
with a standalone pixelmatch script if you want the diff image saved as an
|
|
41
|
+
artifact for the completion report.
|
|
42
|
+
|
|
43
|
+
## Why the knobs, not zero-diff
|
|
44
|
+
|
|
45
|
+
Pixel diffing is literal: it can't tell a real layout regression from a harmless
|
|
46
|
+
anti-aliasing or subpixel-font difference, which is the main source of flaky
|
|
47
|
+
"failures." So:
|
|
48
|
+
|
|
49
|
+
- **`threshold`** (0–1, default 0.2) — per-pixel color sensitivity. Leave near
|
|
50
|
+
default; drop only when chasing a genuine color mismatch.
|
|
51
|
+
- **`maxDiffPixelRatio`** — fraction of pixels allowed to differ. This is your
|
|
52
|
+
main dial. Start ~0.02 and tighten as the clone converges. A whole section
|
|
53
|
+
shifted will blow way past it; font AA won't.
|
|
54
|
+
- **`mask`** — the GPU regions. Non-negotiable: without it, a single animating
|
|
55
|
+
shader frame makes every run "fail" and hides the DOM regressions you care about.
|
|
56
|
+
- **`animations: 'disabled'`** — freezes CSS transitions so the DOM frame is
|
|
57
|
+
deterministic. (It does not stop a canvas rAF loop — that's why GPU regions are
|
|
58
|
+
masked, not just frozen.)
|
|
59
|
+
|
|
60
|
+
## Verifying the GPU regions (separately)
|
|
61
|
+
|
|
62
|
+
Masked out of the diff, so check them by behavior, not by pixel equality:
|
|
63
|
+
|
|
64
|
+
1. **Renders at all** — the canvas is non-blank after load (sample a few pixels;
|
|
65
|
+
if cross-origin blocks readback, verify visually via screenshot).
|
|
66
|
+
2. **Animates** — frames change over ~500ms (`isCanvasAnimating()` in
|
|
67
|
+
`scripts/surface-map.js`).
|
|
68
|
+
3. **Responds** — if the original reacts to pointer/scroll, the clone does too.
|
|
69
|
+
4. **Fidelity label** — carry luckiest-website-cloner-shaders's SOURCE / PARTIAL / approximate
|
|
70
|
+
verdict into the report. A masked region that "passes" the DOM diff has NOT
|
|
71
|
+
been proven faithful — only excluded. Say so honestly.
|
|
72
|
+
|
|
73
|
+
## Reporting
|
|
74
|
+
|
|
75
|
+
For each breakpoint: diff pass/fail, `maxDiffPixelRatio` achieved, and a link to
|
|
76
|
+
the saved diff image. For each GPU surface: renders / animates / responds + the
|
|
77
|
+
fidelity label. This is the evidence the clone is done — not a vibe.
|
|
78
|
+
|
|
79
|
+
## Harness gotchas (learned the hard way)
|
|
80
|
+
|
|
81
|
+
- **Don't set `reducedMotion: 'reduce'`** in the QA context. It changes the
|
|
82
|
+
ORIGINAL's JS behavior (sites branch on it), so heights and content diverge
|
|
83
|
+
and you chase phantom diffs. Freeze motion with injected CSS instead
|
|
84
|
+
(`animation-play-state: paused; transition: none`).
|
|
85
|
+
- **`waitUntil: 'networkidle'` hangs against a Vite dev server** (HMR socket).
|
|
86
|
+
Use `domcontentloaded` + a fixed settle, then a full scroll-through so lazy
|
|
87
|
+
content loads before you measure.
|
|
88
|
+
- **Measure both pages the same way.** Section heights differ between a fresh
|
|
89
|
+
load and a session that already scrolled (lazy images, fonts). Always scroll
|
|
90
|
+
top→bottom→top on both before comparing offsets, and compare fresh-load to
|
|
91
|
+
fresh-load.
|
|
92
|
+
- **First check heights, then pixels.** Dump `[selector, top, height]` for every
|
|
93
|
+
section on both pages; a 0 px delta down the whole list means the DOM track is
|
|
94
|
+
right and any remaining diff is behavior/timing. A delta localizes the bug to
|
|
95
|
+
one section instantly.
|
|
96
|
+
- **Per-viewport slices beat one fullPage shot.** Screenshot every 900 px of
|
|
97
|
+
scroll and diff slice-by-slice; the report then says *where* it's wrong
|
|
98
|
+
(`y=19800 16%`) instead of one useless global number. Fixed chrome (header)
|
|
99
|
+
shows in every slice — a header bug looks like "every slice over section X
|
|
100
|
+
fails", which is itself the diagnosis.
|
|
101
|
+
- Mask GPU canvases **and** anything time-driven (marquees, morphing counters)
|
|
102
|
+
— they're verified separately, not diffed.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Concatenate the probes into one injectable file and EXPORT them onto window.
|
|
3
|
+
// Playwright's addInitScript / addScriptTag wrap code in a function scope, so
|
|
4
|
+
// bare `function` declarations are NOT globals — the explicit export is required.
|
|
5
|
+
const fs = require('fs'), path = require('path');
|
|
6
|
+
const strip = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '').replace(/^\s*[\r\n]/gm, '');
|
|
7
|
+
const files = ['surface-map.js', 'motion-probe.js', 'tokens-probe.js'];
|
|
8
|
+
let out = files.map((f) => strip(fs.readFileSync(path.join(__dirname, f), 'utf8'))).join('\n');
|
|
9
|
+
out += '\n;(function(){ const g = typeof window !== "undefined" ? window : globalThis; Object.assign(g, { instrumentGetContext, surfaceMap, isCanvasAnimating, instrumentMotion, motionProbe, motionSummary, tokensProbe }); })();\n';
|
|
10
|
+
const dest = process.argv[2] || path.join(__dirname, 'probes.bundle.js');
|
|
11
|
+
fs.writeFileSync(dest, out);
|
|
12
|
+
console.log(dest, out.length, 'bytes');
|