hyperframes 0.6.97 → 0.6.98
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/dist/beat-analyzer.global.js +326 -0
- package/dist/cli.js +2479 -1961
- package/dist/commands/layout-audit.browser.js +86 -0
- package/dist/hyperframe-runtime.js +22 -22
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +22 -22
- package/dist/skills/hyperframes-cli/SKILL.md +67 -103
- package/dist/skills/hyperframes-cli/references/doctor-browser.md +45 -0
- package/dist/skills/hyperframes-cli/references/init-and-scaffold.md +51 -0
- package/dist/skills/hyperframes-cli/references/lambda.md +132 -0
- package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +93 -0
- package/dist/skills/hyperframes-cli/references/preview-render.md +107 -0
- package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
- package/dist/studio/assets/hyperframes-player-DgsMQSvV.js +418 -0
- package/dist/studio/assets/index-B62bDCQv.css +1 -0
- package/dist/studio/assets/index-Ce3pBm_I.js +252 -0
- package/dist/studio/assets/{index-HveJ0MuV.js → index-D-ET9M0b.js} +1 -1
- package/dist/studio/assets/index-D-bS9Dxx.js +1 -0
- package/dist/studio/index.html +2 -2
- package/dist/templates/_shared/AGENTS.md +46 -21
- package/dist/templates/_shared/CLAUDE.md +16 -14
- package/package.json +3 -2
- package/dist/pngDecodeBlitWorker.js +0 -239
- package/dist/skills/gsap/SKILL.md +0 -240
- package/dist/skills/gsap/references/effects.md +0 -297
- package/dist/skills/gsap/scripts/extract-audio-data.py +0 -188
- package/dist/skills/hyperframes/SKILL.md +0 -491
- package/dist/skills/hyperframes/data-in-motion.md +0 -19
- package/dist/skills/hyperframes/house-style.md +0 -73
- package/dist/skills/hyperframes/palettes/bold-energetic.md +0 -14
- package/dist/skills/hyperframes/palettes/clean-corporate.md +0 -14
- package/dist/skills/hyperframes/palettes/dark-premium.md +0 -14
- package/dist/skills/hyperframes/palettes/jewel-rich.md +0 -14
- package/dist/skills/hyperframes/palettes/monochrome.md +0 -14
- package/dist/skills/hyperframes/palettes/nature-earth.md +0 -14
- package/dist/skills/hyperframes/palettes/neon-electric.md +0 -14
- package/dist/skills/hyperframes/palettes/pastel-soft.md +0 -14
- package/dist/skills/hyperframes/palettes/warm-editorial.md +0 -14
- package/dist/skills/hyperframes/patterns.md +0 -191
- package/dist/skills/hyperframes/references/audio-reactive.md +0 -76
- package/dist/skills/hyperframes/references/beat-direction.md +0 -171
- package/dist/skills/hyperframes/references/captions.md +0 -163
- package/dist/skills/hyperframes/references/css-patterns.md +0 -373
- package/dist/skills/hyperframes/references/design-picker.md +0 -117
- package/dist/skills/hyperframes/references/dynamic-techniques.md +0 -102
- package/dist/skills/hyperframes/references/html-in-canvas-patterns.md +0 -507
- package/dist/skills/hyperframes/references/motion-principles.md +0 -150
- package/dist/skills/hyperframes/references/narration.md +0 -92
- package/dist/skills/hyperframes/references/prompt-expansion.md +0 -68
- package/dist/skills/hyperframes/references/techniques.md +0 -525
- package/dist/skills/hyperframes/references/text-effects.md +0 -64
- package/dist/skills/hyperframes/references/transcript-guide.md +0 -107
- package/dist/skills/hyperframes/references/transitions/catalog.md +0 -117
- package/dist/skills/hyperframes/references/transitions/css-3d.md +0 -12
- package/dist/skills/hyperframes/references/transitions/css-blur.md +0 -51
- package/dist/skills/hyperframes/references/transitions/css-cover.md +0 -43
- package/dist/skills/hyperframes/references/transitions/css-destruction.md +0 -95
- package/dist/skills/hyperframes/references/transitions/css-dissolve.md +0 -66
- package/dist/skills/hyperframes/references/transitions/css-distortion.md +0 -45
- package/dist/skills/hyperframes/references/transitions/css-grid.md +0 -10
- package/dist/skills/hyperframes/references/transitions/css-light.md +0 -49
- package/dist/skills/hyperframes/references/transitions/css-mechanical.md +0 -30
- package/dist/skills/hyperframes/references/transitions/css-other.md +0 -25
- package/dist/skills/hyperframes/references/transitions/css-push.md +0 -41
- package/dist/skills/hyperframes/references/transitions/css-radial.md +0 -37
- package/dist/skills/hyperframes/references/transitions/css-scale.md +0 -24
- package/dist/skills/hyperframes/references/transitions.md +0 -138
- package/dist/skills/hyperframes/references/typography.md +0 -175
- package/dist/skills/hyperframes/references/video-composition.md +0 -62
- package/dist/skills/hyperframes/scripts/animation-map.mjs +0 -601
- package/dist/skills/hyperframes/scripts/contrast-report.mjs +0 -348
- package/dist/skills/hyperframes/scripts/package-loader.mjs +0 -269
- package/dist/skills/hyperframes/templates/design-picker.html +0 -1432
- package/dist/skills/hyperframes/visual-styles.md +0 -443
- package/dist/studio/assets/hyperframes-player-Daj5djxa.js +0 -418
- package/dist/studio/assets/index-B0twsRu0.css +0 -1
- package/dist/studio/assets/index-Cfye9xzo.js +0 -251
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// contrast-report.mjs — HyperFrames contrast audit
|
|
3
|
-
//
|
|
4
|
-
// Reads a composition, seeks to N sample timestamps, walks the DOM for text
|
|
5
|
-
// elements, measures the WCAG 2.1 contrast ratio between each element's
|
|
6
|
-
// declared foreground color and the pixels behind it, and emits:
|
|
7
|
-
//
|
|
8
|
-
// - contrast-report.json (machine-readable, one entry per text element × sample)
|
|
9
|
-
// - contrast-overlay.png (sprite grid; magenta=fail AA, yellow=pass AA only, green=AAA)
|
|
10
|
-
//
|
|
11
|
-
// Usage:
|
|
12
|
-
// node skills/hyperframes/scripts/contrast-report.mjs <composition-dir> \
|
|
13
|
-
// [--samples N] [--out <dir>] [--width W] [--height H] [--fps N]
|
|
14
|
-
//
|
|
15
|
-
// The composition directory must contain an index.html. Raw authoring HTML
|
|
16
|
-
// works — the producer's file server auto-injects the runtime at serve time.
|
|
17
|
-
// Exits 1 if any text element fails WCAG AA.
|
|
18
|
-
|
|
19
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
20
|
-
import { resolve } from "node:path";
|
|
21
|
-
import { hyperframesPackageSpec, importPackagesOrBootstrap } from "./package-loader.mjs";
|
|
22
|
-
|
|
23
|
-
// Use the producer's file server — it auto-injects the HyperFrames runtime
|
|
24
|
-
// and render-seek bridge, so raw authoring HTML works without a build step.
|
|
25
|
-
const packages = await importPackagesOrBootstrap(["@hyperframes/producer", "sharp"], {
|
|
26
|
-
npmPackages: [hyperframesPackageSpec("@hyperframes/producer"), "sharp@0.34.5"],
|
|
27
|
-
});
|
|
28
|
-
const sharp = packages.sharp.default;
|
|
29
|
-
const {
|
|
30
|
-
createFileServer,
|
|
31
|
-
createCaptureSession,
|
|
32
|
-
initializeSession,
|
|
33
|
-
closeCaptureSession,
|
|
34
|
-
captureFrameToBuffer,
|
|
35
|
-
getCompositionDuration,
|
|
36
|
-
} = packages["@hyperframes/producer"];
|
|
37
|
-
|
|
38
|
-
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
const args = parseArgs(process.argv.slice(2));
|
|
41
|
-
if (!args.composition) die("missing <composition-dir>");
|
|
42
|
-
|
|
43
|
-
const SAMPLES = Number(args.samples ?? 10);
|
|
44
|
-
const OUT_DIR = resolve(args.out ?? ".hyperframes/contrast");
|
|
45
|
-
const WIDTH = Number(args.width ?? 1920);
|
|
46
|
-
const HEIGHT = Number(args.height ?? 1080);
|
|
47
|
-
const FPS = Number(args.fps ?? 30);
|
|
48
|
-
const COMP_DIR = resolve(args.composition);
|
|
49
|
-
|
|
50
|
-
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
await mkdir(OUT_DIR, { recursive: true });
|
|
53
|
-
|
|
54
|
-
const server = await createFileServer({ projectDir: COMP_DIR, port: 0 });
|
|
55
|
-
const session = await createCaptureSession(
|
|
56
|
-
server.url,
|
|
57
|
-
OUT_DIR,
|
|
58
|
-
{ width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
|
|
59
|
-
null,
|
|
60
|
-
);
|
|
61
|
-
await initializeSession(session);
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const duration = await getCompositionDuration(session);
|
|
65
|
-
const times = Array.from(
|
|
66
|
-
{ length: SAMPLES },
|
|
67
|
-
(_, i) => +(((i + 0.5) / SAMPLES) * duration).toFixed(3),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const allEntries = [];
|
|
71
|
-
const overlayFrames = [];
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < times.length; i++) {
|
|
74
|
-
const t = times[i];
|
|
75
|
-
const { buffer: pngBuf } = await captureFrameToBuffer(session, i, t);
|
|
76
|
-
const elements = await probeTextElements(session, t);
|
|
77
|
-
const annotated = await annotateFrame(pngBuf, elements);
|
|
78
|
-
overlayFrames.push({ t, png: annotated });
|
|
79
|
-
for (const el of elements) allEntries.push({ time: t, ...el });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const report = {
|
|
83
|
-
composition: COMP_DIR,
|
|
84
|
-
width: WIDTH,
|
|
85
|
-
height: HEIGHT,
|
|
86
|
-
duration,
|
|
87
|
-
samples: times,
|
|
88
|
-
entries: allEntries,
|
|
89
|
-
summary: summarize(allEntries),
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
await writeFile(resolve(OUT_DIR, "contrast-report.json"), JSON.stringify(report, null, 2));
|
|
93
|
-
await writeOverlaySprite(overlayFrames, resolve(OUT_DIR, "contrast-overlay.png"));
|
|
94
|
-
|
|
95
|
-
printSummary(report);
|
|
96
|
-
process.exitCode = report.summary.failAA > 0 ? 1 : 0;
|
|
97
|
-
} finally {
|
|
98
|
-
await closeCaptureSession(session).catch(() => {});
|
|
99
|
-
server.close();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── DOM probe (runs in the page) ────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
async function probeTextElements(session, _t) {
|
|
105
|
-
// `session.page` is the Puppeteer Page owned by the capture session.
|
|
106
|
-
// We pass a pure function to `evaluate`: it walks the DOM and returns
|
|
107
|
-
// enough info for us to compute a ratio in Node using the frame buffer.
|
|
108
|
-
return await session.page.evaluate(() => {
|
|
109
|
-
/** @type {Array<{selector: string, text: string, fg: [number,number,number,number], fontSize: number, fontWeight: number, bbox: {x:number,y:number,w:number,h:number}}>} */
|
|
110
|
-
const out = [];
|
|
111
|
-
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
112
|
-
const parseColor = (c) => {
|
|
113
|
-
const m = c.match(/rgba?\(([^)]+)\)/);
|
|
114
|
-
if (!m) return [0, 0, 0, 1];
|
|
115
|
-
const parts = m[1].split(",").map((s) => parseFloat(s.trim()));
|
|
116
|
-
return [parts[0], parts[1], parts[2], parts[3] ?? 1];
|
|
117
|
-
};
|
|
118
|
-
const selectorOf = (el) => {
|
|
119
|
-
if (el.id) return `#${el.id}`;
|
|
120
|
-
const cls = [...el.classList].slice(0, 2).join(".");
|
|
121
|
-
return cls ? `${el.tagName.toLowerCase()}.${cls}` : el.tagName.toLowerCase();
|
|
122
|
-
};
|
|
123
|
-
let el;
|
|
124
|
-
while ((el = walker.nextNode())) {
|
|
125
|
-
// must have direct text
|
|
126
|
-
const direct = [...el.childNodes].some(
|
|
127
|
-
(n) => n.nodeType === 3 && n.textContent.trim().length,
|
|
128
|
-
);
|
|
129
|
-
if (!direct) continue;
|
|
130
|
-
const cs = getComputedStyle(el);
|
|
131
|
-
if (cs.visibility === "hidden" || cs.display === "none") continue;
|
|
132
|
-
if (parseFloat(cs.opacity) <= 0.01) continue;
|
|
133
|
-
const rect = el.getBoundingClientRect();
|
|
134
|
-
if (rect.width < 8 || rect.height < 8) continue;
|
|
135
|
-
out.push({
|
|
136
|
-
selector: selectorOf(el),
|
|
137
|
-
text: el.textContent.trim().slice(0, 60),
|
|
138
|
-
fg: parseColor(cs.color),
|
|
139
|
-
fontSize: parseFloat(cs.fontSize),
|
|
140
|
-
fontWeight: Number(cs.fontWeight) || 400,
|
|
141
|
-
bbox: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─── Pixel sampling + WCAG math ──────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
async function annotateFrame(pngBuf, elements) {
|
|
151
|
-
const img = sharp(pngBuf);
|
|
152
|
-
const meta = await img.metadata();
|
|
153
|
-
const { width, height } = meta;
|
|
154
|
-
const raw = await img.ensureAlpha().raw().toBuffer();
|
|
155
|
-
const channels = 4;
|
|
156
|
-
|
|
157
|
-
const measured = [];
|
|
158
|
-
for (const el of elements) {
|
|
159
|
-
if (isBBoxOutsideFrame(el.bbox, width, height)) continue;
|
|
160
|
-
const bg = sampleRingMedian(raw, width, height, channels, el.bbox);
|
|
161
|
-
if (!bg) continue;
|
|
162
|
-
const fg = compositeOver(el.fg, bg); // flatten any alpha against measured bg
|
|
163
|
-
const ratio = wcagRatio(fg, bg);
|
|
164
|
-
const large = isLargeText(el.fontSize, el.fontWeight);
|
|
165
|
-
el.bg = bg;
|
|
166
|
-
el.ratio = +ratio.toFixed(2);
|
|
167
|
-
el.wcagAA = large ? ratio >= 3 : ratio >= 4.5;
|
|
168
|
-
el.wcagAALarge = ratio >= 3;
|
|
169
|
-
el.wcagAAA = large ? ratio >= 4.5 : ratio >= 7;
|
|
170
|
-
measured.push(el);
|
|
171
|
-
}
|
|
172
|
-
elements.length = 0;
|
|
173
|
-
elements.push(...measured);
|
|
174
|
-
|
|
175
|
-
// Draw boxes + ratio labels as an SVG overlay (sharp composite).
|
|
176
|
-
const svg = buildOverlaySVG(measured, width, height);
|
|
177
|
-
return await sharp(pngBuf)
|
|
178
|
-
.composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
|
|
179
|
-
.png()
|
|
180
|
-
.toBuffer();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function sampleRingMedian(raw, width, height, channels, bbox) {
|
|
184
|
-
// 4-px ring immediately outside the element bbox. Median of each channel.
|
|
185
|
-
const r = [],
|
|
186
|
-
g = [],
|
|
187
|
-
b = [];
|
|
188
|
-
const x0 = Math.max(0, Math.floor(bbox.x) - 4);
|
|
189
|
-
const x1 = Math.min(width - 1, Math.ceil(bbox.x + bbox.w) + 4);
|
|
190
|
-
const y0 = Math.max(0, Math.floor(bbox.y) - 4);
|
|
191
|
-
const y1 = Math.min(height - 1, Math.ceil(bbox.y + bbox.h) + 4);
|
|
192
|
-
const pushPixel = (x, y) => {
|
|
193
|
-
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
|
194
|
-
const i = (y * width + x) * channels;
|
|
195
|
-
r.push(raw[i]);
|
|
196
|
-
g.push(raw[i + 1]);
|
|
197
|
-
b.push(raw[i + 2]);
|
|
198
|
-
};
|
|
199
|
-
for (let x = x0; x <= x1; x++) {
|
|
200
|
-
pushPixel(x, y0);
|
|
201
|
-
pushPixel(x, y1);
|
|
202
|
-
}
|
|
203
|
-
for (let y = y0; y <= y1; y++) {
|
|
204
|
-
pushPixel(x0, y);
|
|
205
|
-
pushPixel(x1, y);
|
|
206
|
-
}
|
|
207
|
-
if (r.length === 0) return null;
|
|
208
|
-
return [median(r), median(g), median(b), 1];
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function isBBoxOutsideFrame(bbox, width, height) {
|
|
212
|
-
return bbox.x + bbox.w <= 0 || bbox.y + bbox.h <= 0 || bbox.x >= width || bbox.y >= height;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function median(arr) {
|
|
216
|
-
const s = [...arr].sort((a, b) => a - b);
|
|
217
|
-
return s[Math.floor(s.length / 2)];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function compositeOver([fr, fg, fb, fa], [br, bg, bb]) {
|
|
221
|
-
return [
|
|
222
|
-
Math.round(fr * fa + br * (1 - fa)),
|
|
223
|
-
Math.round(fg * fa + bg * (1 - fa)),
|
|
224
|
-
Math.round(fb * fa + bb * (1 - fa)),
|
|
225
|
-
1,
|
|
226
|
-
];
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function relLum([r, g, b]) {
|
|
230
|
-
const ch = (v) => {
|
|
231
|
-
const s = v / 255;
|
|
232
|
-
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
233
|
-
};
|
|
234
|
-
return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function wcagRatio(a, b) {
|
|
238
|
-
const la = relLum(a);
|
|
239
|
-
const lb = relLum(b);
|
|
240
|
-
const [L1, L2] = la > lb ? [la, lb] : [lb, la];
|
|
241
|
-
return (L1 + 0.05) / (L2 + 0.05);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function isLargeText(fontSize, fontWeight) {
|
|
245
|
-
return fontSize >= 24 || (fontSize >= 19 && fontWeight >= 700);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ─── Overlay rendering ───────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
function buildOverlaySVG(elements, w, h) {
|
|
251
|
-
const rects = elements
|
|
252
|
-
.map((el) => {
|
|
253
|
-
const color = !el.wcagAA ? "#ff00aa" : !el.wcagAAA ? "#ffcc00" : "#00e08a";
|
|
254
|
-
const { x, y, w: bw, h: bh } = el.bbox;
|
|
255
|
-
return `
|
|
256
|
-
<rect x="${x}" y="${y}" width="${bw}" height="${bh}"
|
|
257
|
-
fill="none" stroke="${color}" stroke-width="3"/>
|
|
258
|
-
<rect x="${x}" y="${y - 18}" width="${48}" height="16" fill="${color}"/>
|
|
259
|
-
<text x="${x + 4}" y="${y - 5}" font-family="monospace" font-size="12" fill="#000">
|
|
260
|
-
${el.ratio.toFixed(1)}:1
|
|
261
|
-
</text>`;
|
|
262
|
-
})
|
|
263
|
-
.join("");
|
|
264
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">${rects}</svg>`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function writeOverlaySprite(frames, outPath) {
|
|
268
|
-
if (!frames.length) return;
|
|
269
|
-
const cols = Math.min(frames.length, 5);
|
|
270
|
-
const rows = Math.ceil(frames.length / cols);
|
|
271
|
-
const { width, height } = await sharp(frames[0].png).metadata();
|
|
272
|
-
const scale = 0.25;
|
|
273
|
-
const cellW = Math.round(width * scale);
|
|
274
|
-
const cellH = Math.round(height * scale);
|
|
275
|
-
|
|
276
|
-
const cells = await Promise.all(
|
|
277
|
-
frames.map(async (f) => ({
|
|
278
|
-
input: await sharp(f.png).resize(cellW, cellH).png().toBuffer(),
|
|
279
|
-
time: f.t,
|
|
280
|
-
})),
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const composites = cells.map((c, i) => ({
|
|
284
|
-
input: c.input,
|
|
285
|
-
top: Math.floor(i / cols) * cellH,
|
|
286
|
-
left: (i % cols) * cellW,
|
|
287
|
-
}));
|
|
288
|
-
|
|
289
|
-
await sharp({
|
|
290
|
-
create: {
|
|
291
|
-
width: cols * cellW,
|
|
292
|
-
height: rows * cellH,
|
|
293
|
-
channels: 3,
|
|
294
|
-
background: { r: 16, g: 16, b: 20 },
|
|
295
|
-
},
|
|
296
|
-
})
|
|
297
|
-
.composite(composites)
|
|
298
|
-
.png()
|
|
299
|
-
.toFile(outPath);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ─── Summary ────────────────────────────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
function summarize(entries) {
|
|
305
|
-
const total = entries.length;
|
|
306
|
-
const failAA = entries.filter((e) => !e.wcagAA).length;
|
|
307
|
-
const passAAonly = entries.filter((e) => e.wcagAA && !e.wcagAAA).length;
|
|
308
|
-
const passAAA = entries.filter((e) => e.wcagAAA).length;
|
|
309
|
-
return { total, failAA, passAAonly, passAAA };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function printSummary({ summary, entries }) {
|
|
313
|
-
const { total, failAA, passAAonly, passAAA } = summary;
|
|
314
|
-
console.log(`\nContrast report: ${total} text-element samples`);
|
|
315
|
-
console.log(` fail WCAG AA: ${failAA}`);
|
|
316
|
-
console.log(` pass AA, not AAA: ${passAAonly}`);
|
|
317
|
-
console.log(` pass AAA: ${passAAA}`);
|
|
318
|
-
if (failAA) {
|
|
319
|
-
console.log("\nFailures:");
|
|
320
|
-
for (const e of entries.filter((x) => !x.wcagAA)) {
|
|
321
|
-
console.log(` t=${e.time}s ${e.selector.padEnd(24)} ${e.ratio.toFixed(2)}:1 "${e.text}"`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
327
|
-
|
|
328
|
-
function parseArgs(argv) {
|
|
329
|
-
const out = {};
|
|
330
|
-
let positional = 0;
|
|
331
|
-
for (let i = 0; i < argv.length; i++) {
|
|
332
|
-
const a = argv[i];
|
|
333
|
-
if (a.startsWith("--")) {
|
|
334
|
-
const k = a.slice(2);
|
|
335
|
-
const v = argv[i + 1]?.startsWith("--") ? true : argv[++i];
|
|
336
|
-
out[k] = v;
|
|
337
|
-
} else if (positional === 0) {
|
|
338
|
-
out.composition = a;
|
|
339
|
-
positional++;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return out;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function die(msg) {
|
|
346
|
-
console.error(`contrast-report: ${msg}`);
|
|
347
|
-
process.exit(2);
|
|
348
|
-
}
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { basename, delimiter, dirname, join, parse, resolve } from "node:path";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
|
|
9
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const BOOTSTRAP_ENV = "HYPERFRAMES_SKILL_DEPS_BOOTSTRAPPED";
|
|
11
|
-
const BOOTSTRAP_CONFIRM_ENV = "HYPERFRAMES_SKILL_BOOTSTRAP_DEPS";
|
|
12
|
-
const NODE_MODULES_ENV = "HYPERFRAMES_SKILL_NODE_MODULES";
|
|
13
|
-
|
|
14
|
-
export async function importPackagesOrBootstrap(packageNames, options = {}) {
|
|
15
|
-
const entries = new Map();
|
|
16
|
-
const missing = [];
|
|
17
|
-
|
|
18
|
-
for (const packageName of packageNames) {
|
|
19
|
-
const entry = resolvePackageEntry(packageName);
|
|
20
|
-
if (entry) entries.set(packageName, entry);
|
|
21
|
-
else missing.push(packageName);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (missing.length > 0 && !process.env[BOOTSTRAP_ENV]) {
|
|
25
|
-
const npmPackages = options.npmPackages ?? missing;
|
|
26
|
-
assertPinnedPackageSpecs(npmPackages);
|
|
27
|
-
await confirmBootstrap(npmPackages);
|
|
28
|
-
bootstrapWithNpmInstall(npmPackages);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (missing.length > 0) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
[
|
|
34
|
-
`Could not resolve required package(s): ${missing.join(", ")}`,
|
|
35
|
-
"Install them in this project, for example:",
|
|
36
|
-
` npm install --save-dev ${packageNames.map(shellQuote).join(" ")}`,
|
|
37
|
-
].join("\n"),
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const modules = {};
|
|
42
|
-
for (const [packageName, entry] of entries) {
|
|
43
|
-
modules[packageName] = await import(pathToFileURL(entry).href);
|
|
44
|
-
}
|
|
45
|
-
return modules;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function hyperframesPackageSpec(packageName) {
|
|
49
|
-
const version = readBundledHyperframesVersion();
|
|
50
|
-
if (!version) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
[
|
|
53
|
-
`Could not determine the bundled HyperFrames version for ${packageName}.`,
|
|
54
|
-
"Install the package yourself or pass a pinned options.npmPackages entry.",
|
|
55
|
-
].join("\n"),
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return `${packageName}@${version}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolvePackageEntry(packageName) {
|
|
62
|
-
const bases = [process.cwd(), HERE, ...envNodeModulesDirs(), ...nodeModulesDirsFromPath()];
|
|
63
|
-
|
|
64
|
-
const seen = new Set();
|
|
65
|
-
for (const base of bases) {
|
|
66
|
-
const normalized = resolve(base);
|
|
67
|
-
if (seen.has(normalized)) continue;
|
|
68
|
-
seen.add(normalized);
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
return createRequire(join(normalized, "__hyperframes_skill_loader__.cjs")).resolve(
|
|
72
|
-
packageName,
|
|
73
|
-
);
|
|
74
|
-
} catch {
|
|
75
|
-
const packageDir = findPackageDir(normalized, packageName);
|
|
76
|
-
const packageEntry = packageDir ? readPackageEntry(packageDir) : null;
|
|
77
|
-
if (packageEntry) return packageEntry;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function readBundledHyperframesVersion() {
|
|
85
|
-
for (const ancestor of ancestors(HERE)) {
|
|
86
|
-
const directVersion = readPackageVersion(join(ancestor, "package.json"));
|
|
87
|
-
if (directVersion) return directVersion;
|
|
88
|
-
|
|
89
|
-
const monorepoCliVersion = readPackageVersion(
|
|
90
|
-
join(ancestor, "packages", "cli", "package.json"),
|
|
91
|
-
);
|
|
92
|
-
if (monorepoCliVersion) return monorepoCliVersion;
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function readPackageVersion(packageJsonPath) {
|
|
98
|
-
try {
|
|
99
|
-
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
100
|
-
if (manifest.name === "hyperframes" || manifest.name === "@hyperframes/cli") {
|
|
101
|
-
return typeof manifest.version === "string" ? manifest.version : null;
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
104
|
-
// Keep searching ancestor package manifests.
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function envNodeModulesDirs() {
|
|
110
|
-
return (process.env[NODE_MODULES_ENV] ?? "").split(delimiter).filter(Boolean);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function nodeModulesDirsFromPath() {
|
|
114
|
-
const dirs = [];
|
|
115
|
-
for (const entry of (process.env.PATH ?? "").split(delimiter)) {
|
|
116
|
-
if (!entry.endsWith(`${join("node_modules", ".bin")}`)) continue;
|
|
117
|
-
dirs.push(dirname(entry));
|
|
118
|
-
}
|
|
119
|
-
return dirs;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function findPackageDir(base, packageName) {
|
|
123
|
-
const packageSegments = packageName.split("/");
|
|
124
|
-
const roots =
|
|
125
|
-
basename(base) === "node_modules"
|
|
126
|
-
? [base]
|
|
127
|
-
: ancestors(base).map((ancestor) => join(ancestor, "node_modules"));
|
|
128
|
-
|
|
129
|
-
for (const root of roots) {
|
|
130
|
-
const packageDir = join(root, ...packageSegments);
|
|
131
|
-
if (existsSync(join(packageDir, "package.json"))) return packageDir;
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function readPackageEntry(packageDir) {
|
|
137
|
-
try {
|
|
138
|
-
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
139
|
-
const entry = exportEntry(manifest.exports) ?? manifest.module ?? manifest.main ?? "index.js";
|
|
140
|
-
const entryPath = join(packageDir, entry);
|
|
141
|
-
return existsSync(entryPath) ? entryPath : null;
|
|
142
|
-
} catch {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function exportEntry(exports) {
|
|
148
|
-
const root =
|
|
149
|
-
typeof exports === "object" && exports !== null ? (exports["."] ?? exports) : exports;
|
|
150
|
-
if (typeof root === "string") return root;
|
|
151
|
-
if (typeof root !== "object" || root === null) return null;
|
|
152
|
-
if (typeof root.import === "string") return root.import;
|
|
153
|
-
if (typeof root.default === "string") return root.default;
|
|
154
|
-
if (typeof root.node === "string") return root.node;
|
|
155
|
-
if (typeof root.node === "object" && root.node !== null) {
|
|
156
|
-
return root.node.import ?? root.node.default ?? null;
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function assertPinnedPackageSpecs(packageSpecs) {
|
|
162
|
-
const unpinned = packageSpecs.filter((spec) => !hasVersionSpec(spec));
|
|
163
|
-
if (unpinned.length === 0) return;
|
|
164
|
-
throw new Error(
|
|
165
|
-
[
|
|
166
|
-
`Refusing to bootstrap unpinned package spec(s): ${unpinned.join(", ")}`,
|
|
167
|
-
"Pass pinned npm package specs, for example:",
|
|
168
|
-
` ${packageSpecs.map((spec) => (hasVersionSpec(spec) ? spec : `${spec}@<version>`)).join(" ")}`,
|
|
169
|
-
].join("\n"),
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function hasVersionSpec(packageSpec) {
|
|
174
|
-
if (packageSpec.startsWith("@")) {
|
|
175
|
-
const slash = packageSpec.indexOf("/");
|
|
176
|
-
return slash !== -1 && packageSpec.indexOf("@", slash + 1) !== -1;
|
|
177
|
-
}
|
|
178
|
-
return packageSpec.includes("@");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function confirmBootstrap(packageSpecs) {
|
|
182
|
-
if (process.env[BOOTSTRAP_CONFIRM_ENV] === "1") return;
|
|
183
|
-
|
|
184
|
-
const installLine = `npm install --ignore-scripts --no-save ${packageSpecs.map(shellQuote).join(" ")}`;
|
|
185
|
-
if (!process.stdin.isTTY) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
[
|
|
188
|
-
"Required helper package(s) are missing.",
|
|
189
|
-
"To allow a one-time temporary dependency bootstrap for this run, set:",
|
|
190
|
-
` ${BOOTSTRAP_CONFIRM_ENV}=1`,
|
|
191
|
-
"The bootstrap command will be:",
|
|
192
|
-
` ${installLine}`,
|
|
193
|
-
].join("\n"),
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
198
|
-
try {
|
|
199
|
-
const answer = await rl.question(
|
|
200
|
-
[
|
|
201
|
-
"HyperFrames helper package(s) are missing.",
|
|
202
|
-
`Run a temporary install with lifecycle scripts disabled?`,
|
|
203
|
-
` ${installLine}`,
|
|
204
|
-
"Proceed? [y/N] ",
|
|
205
|
-
].join("\n"),
|
|
206
|
-
);
|
|
207
|
-
if (!/^(y|yes)$/i.test(answer.trim())) {
|
|
208
|
-
throw new Error("Dependency bootstrap cancelled.");
|
|
209
|
-
}
|
|
210
|
-
} finally {
|
|
211
|
-
rl.close();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function ancestors(start) {
|
|
216
|
-
const dirs = [];
|
|
217
|
-
let current = resolve(start);
|
|
218
|
-
const root = parse(current).root;
|
|
219
|
-
while (current && current !== root) {
|
|
220
|
-
dirs.push(current);
|
|
221
|
-
current = dirname(current);
|
|
222
|
-
}
|
|
223
|
-
dirs.push(root);
|
|
224
|
-
return dirs;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function bootstrapWithNpmInstall(packageNames) {
|
|
228
|
-
const installRoot = mkdtempSync(join(tmpdir(), "hyperframes-skill-deps-"));
|
|
229
|
-
const installResult = spawnSync(
|
|
230
|
-
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
231
|
-
[
|
|
232
|
-
"install",
|
|
233
|
-
"--silent",
|
|
234
|
-
"--no-audit",
|
|
235
|
-
"--no-fund",
|
|
236
|
-
"--ignore-scripts",
|
|
237
|
-
"--no-save",
|
|
238
|
-
"--prefix",
|
|
239
|
-
installRoot,
|
|
240
|
-
...packageNames,
|
|
241
|
-
],
|
|
242
|
-
{ stdio: "inherit" },
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
if (installResult.error) throw installResult.error;
|
|
246
|
-
if (installResult.status !== 0) {
|
|
247
|
-
rmSync(installRoot, { recursive: true, force: true });
|
|
248
|
-
process.exit(installResult.status ?? 1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const args = [...process.argv.slice(1)];
|
|
252
|
-
const result = spawnSync(process.execPath, args, {
|
|
253
|
-
stdio: "inherit",
|
|
254
|
-
env: {
|
|
255
|
-
...process.env,
|
|
256
|
-
[BOOTSTRAP_ENV]: "1",
|
|
257
|
-
[NODE_MODULES_ENV]: join(installRoot, "node_modules"),
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
rmSync(installRoot, { recursive: true, force: true });
|
|
262
|
-
if (result.error) throw result.error;
|
|
263
|
-
process.exit(result.status ?? 1);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function shellQuote(value) {
|
|
267
|
-
if (/^[A-Za-z0-9_./:@=-]+$/.test(value)) return value;
|
|
268
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
269
|
-
}
|