launchframe 0.1.13 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +143 -175
  2. package/bin/launchframe.mjs +234 -30
  3. package/package.json +52 -65
  4. package/template/.aider.conf.yml +3 -0
  5. package/template/.amazonq/cli-agents/clone-website.json +9 -0
  6. package/template/.amazonq/rules/project.md +156 -0
  7. package/template/.augment/commands/clone-website.md +516 -0
  8. package/template/.claude/skills/clone-website/SKILL.md +515 -0
  9. package/template/.clinerules +156 -0
  10. package/template/.codex/skills/clone-website/SKILL.md +515 -0
  11. package/template/.continue/commands/clone-website.md +517 -0
  12. package/template/.continue/rules/project.md +160 -0
  13. package/template/.cursor/commands/clone-website.md +512 -0
  14. package/template/.cursor/rules/project.mdc +7 -0
  15. package/template/.dockerignore +60 -0
  16. package/template/.gemini/commands/clone-website.toml +518 -0
  17. package/template/.gitattributes +9 -0
  18. package/template/.github/ISSUE_TEMPLATE/bug_report.yml +86 -0
  19. package/template/.github/ISSUE_TEMPLATE/config.yml +5 -0
  20. package/template/.github/ISSUE_TEMPLATE/feature_request.yml +50 -0
  21. package/template/.github/PULL_REQUEST_TEMPLATE.md +19 -0
  22. package/template/.github/copilot-instructions.md +156 -0
  23. package/template/.github/copilot-setup-steps.yml +3 -0
  24. package/template/.github/skills/clone-website/SKILL.md +515 -0
  25. package/template/.github/workflows/ci.yml +36 -0
  26. package/template/.nvmrc +1 -0
  27. package/template/.opencode/commands/clone-website.md +515 -0
  28. package/template/.windsurf/workflows/clone-website.md +512 -0
  29. package/template/.windsurfrules +2 -0
  30. package/template/AGENTS.md +74 -0
  31. package/template/CHANGELOG.md +80 -0
  32. package/template/CLAUDE.md +1 -0
  33. package/template/Dockerfile +114 -0
  34. package/template/Dockerfile.dev +15 -0
  35. package/template/GEMINI.md +1 -0
  36. package/template/README.md +129 -0
  37. package/template/components.json +25 -0
  38. package/template/docker-compose.yml +53 -0
  39. package/template/docs/design-references/.gitkeep +0 -0
  40. package/template/docs/design-references/comparison.png +0 -0
  41. package/template/docs/research/INSPECTION_GUIDE.md +80 -0
  42. package/template/eslint.config.mjs +18 -0
  43. package/template/next.config.ts +8 -0
  44. package/template/package.json +59 -0
  45. package/template/postcss.config.mjs +7 -0
  46. package/template/public/images/.gitkeep +0 -0
  47. package/template/public/seo/.gitkeep +0 -0
  48. package/template/public/videos/.gitkeep +0 -0
  49. package/template/scripts/.gitkeep +0 -0
  50. package/template/scripts/sync-agent-rules.sh +88 -0
  51. package/template/scripts/sync-skills.mjs +111 -0
  52. package/template/src/app/favicon.ico +0 -0
  53. package/template/src/app/globals.css +130 -0
  54. package/template/src/app/layout.tsx +33 -0
  55. package/template/src/app/page.tsx +9 -0
  56. package/template/src/components/ui/button.tsx +60 -0
  57. package/template/src/hooks/.gitkeep +0 -0
  58. package/template/src/lib/utils.ts +6 -0
  59. package/template/src/types/.gitkeep +0 -0
  60. package/template/tsconfig.json +34 -0
  61. package/packages/extract/automated-clone-pass.ts +0 -353
  62. package/packages/extract/browser-extract.ts +0 -237
  63. package/packages/extract/cloner-research-emit.ts +0 -270
  64. package/packages/extract/dom-crawler.ts +0 -521
  65. package/packages/extract/emit.ts +0 -553
  66. package/packages/extract/extract.ts +0 -547
  67. package/packages/extract/host-slug.ts +0 -5
  68. package/packages/extract/mirror-emit.ts +0 -620
  69. package/packages/extract/package.json +0 -13
  70. package/packages/extract/reference-dump.ts +0 -431
  71. package/packages/extract/synthesize.ts +0 -551
  72. package/packages/extract/types.ts +0 -316
@@ -1,431 +0,0 @@
1
- /**
2
- * Verbatim reference dump for AI / human review.
3
- *
4
- * Writes everything under `output/<runId>/reference/<host>/`:
5
- * - page.html — full document HTML after JS render (`page.content()`)
6
- * - dom-structure.json — exact `body` subtree: tag order, attributes, text nodes (JSON)
7
- * - structure-outline.txt — indented tag skeleton (ids/classes/roles) for quick scanning
8
- * - visible-text.json — structured visible copy (headings, buttons, key blocks)
9
- * - media.json — img / video / source URLs and attributes
10
- * - meta.json — title, description, canonical, lang
11
- * - FOR_AI_REFERENCE.md — how to use these files with an AI
12
- */
13
-
14
- import { mkdirSync, writeFileSync } from "node:fs";
15
- import { join } from "node:path";
16
-
17
- import type { Page } from "playwright";
18
-
19
- /** Element node in the serialized `body` tree. */
20
- export interface DomStructureElement {
21
- tag: string;
22
- attrs?: Record<string, string>;
23
- children?: DomStructureChild[];
24
- }
25
-
26
- export interface DomStructureTextNode {
27
- type: "text";
28
- value: string;
29
- }
30
-
31
- export type DomStructureChild = DomStructureElement | DomStructureTextNode;
32
-
33
- export interface DomStructurePayload {
34
- title: string | null;
35
- lang: string | null;
36
- stats: {
37
- elementNodes: number;
38
- omitted: { script: number; style: number; noscript: number; template: number };
39
- truncated: boolean;
40
- maxNodes: number;
41
- maxDepth: number;
42
- };
43
- root: DomStructureElement | null;
44
- }
45
-
46
- export interface ReferenceSnapshot {
47
- url: string;
48
- capturedAt: string;
49
- title: string | null;
50
- description: string | null;
51
- canonical: string | null;
52
- lang: string | null;
53
- /** Flattened visible strings in DOM order (useful for grep / LLM context). */
54
- visibleTextBlocks: Array<{
55
- tag: string;
56
- role: string | null;
57
- text: string;
58
- }>;
59
- links: Array<{ href: string; text: string }>;
60
- media: Array<
61
- | { type: "img"; src: string; alt: string; width: number | null; height: number | null }
62
- | { type: "video"; src: string | null; poster: string | null }
63
- | { type: "source"; src: string; kind: string | null }
64
- >;
65
- }
66
-
67
- export async function emitPageReference(
68
- page: Page,
69
- url: string,
70
- refDir: string,
71
- viewport?: { width: number; height: number },
72
- ): Promise<string[]> {
73
- mkdirSync(refDir, { recursive: true });
74
- const written: string[] = [];
75
- const capturedAt = new Date().toISOString();
76
-
77
- await page.evaluate(() => {
78
- const g = globalThis as unknown as { __name?: (fn: unknown) => unknown };
79
- if (typeof g.__name === "undefined") g.__name = (fn: unknown) => fn;
80
- });
81
-
82
- const html = await page.content();
83
- const htmlPath = join(refDir, "page.html");
84
- writeFileSync(htmlPath, html, "utf8");
85
- written.push(htmlPath);
86
-
87
- const domEval = (await page.evaluate(collectDomStructureInPage)) as DomStructurePayload;
88
- const domMerged = {
89
- url,
90
- capturedAt,
91
- viewport: viewport ?? null,
92
- ...domEval,
93
- };
94
- const domPath = join(refDir, "dom-structure.json");
95
- writeFileSync(domPath, JSON.stringify(domMerged, null, 2) + "\n", "utf8");
96
- written.push(domPath);
97
-
98
- const outlinePath = join(refDir, "structure-outline.txt");
99
- writeFileSync(outlinePath, buildStructureOutline(domMerged.root, domMerged.stats) + "\n", "utf8");
100
- written.push(outlinePath);
101
-
102
- const snapshot = (await page.evaluate(collectSnapshot)) as Omit<ReferenceSnapshot, "url" | "capturedAt">;
103
- const full: ReferenceSnapshot = {
104
- url,
105
- capturedAt,
106
- ...snapshot,
107
- };
108
-
109
- writeFileSync(join(refDir, "visible-text.json"), JSON.stringify(full, null, 2) + "\n", "utf8");
110
- written.push(join(refDir, "visible-text.json"));
111
-
112
- const txtLines = [
113
- `# ${full.title ?? "Untitled"}`,
114
- "",
115
- ...full.visibleTextBlocks.map((b) => b.text),
116
- "",
117
- "--- links ---",
118
- ...full.links.map((l) => `${l.text}\t${l.href}`),
119
- ];
120
- writeFileSync(join(refDir, "visible-text.txt"), txtLines.join("\n"), "utf8");
121
- written.push(join(refDir, "visible-text.txt"));
122
-
123
- const mediaOnly = { url, capturedAt, media: full.media };
124
- writeFileSync(join(refDir, "media.json"), JSON.stringify(mediaOnly, null, 2) + "\n", "utf8");
125
- written.push(join(refDir, "media.json"));
126
-
127
- const meta = {
128
- url,
129
- capturedAt,
130
- title: full.title,
131
- description: full.description,
132
- canonical: full.canonical,
133
- lang: full.lang,
134
- };
135
- writeFileSync(join(refDir, "meta.json"), JSON.stringify(meta, null, 2) + "\n", "utf8");
136
- written.push(join(refDir, "meta.json"));
137
-
138
- writeFileSync(join(refDir, "FOR_AI_REFERENCE.md"), emitAiReadme(url, refDir), "utf8");
139
- written.push(join(refDir, "FOR_AI_REFERENCE.md"));
140
-
141
- return written;
142
- }
143
-
144
- function emitAiReadme(url: string, refDir: string): string {
145
- const base = refDir.replace(/\\/g, "/");
146
- return [
147
- `# Reference capture — ${url}`,
148
- "",
149
- "Use these files when rebuilding the page in React / Next.js:",
150
- "",
151
- "| File | Purpose |",
152
- "| ---- | ------- |",
153
- "| `page.html` | Full serialized DOM after JavaScript ran in Chromium. Layout, copy, and structure match what crawled (not necessarily valid static HTML elsewhere). |",
154
- "| `dom-structure.json` | **Exact body subtree (JSON):** same child order as the live DOM; every attribute on each element (long `src` / `href` / `style` values truncated); inline text as separate typed entries. Use this as the canonical structure map for React. |",
155
- "| `structure-outline.txt` | Indented tag skeleton (ids/classes/roles only; no text) for quick navigation. |",
156
- "| `visible-text.json` | Exact visible strings: headings, buttons, links, and block text — good for **verbatim copy** when rewriting `page.tsx`. |",
157
- "| `media.json` | Every image / video / source URL from the DOM. Host your own assets or swap for placeholders; do not hotlink without permission. |",
158
- "| `meta.json` | Title, description, lang. |",
159
- "",
160
- "**Workflow:** (1) Recon — use **`dom-structure.json`** (or `structure-outline.txt` + `page.html`) for **exact tag order and nesting**; (2) Wire — map `visible-text.*` + `media.json` into that tree; (3) Build — implement `page.tsx` (start from `../mirror/<host>/page.tsx` if present) so component boundaries follow this structure. See the run folder’s **FOR_AI.md** for full authority order and compliance notes.",
161
- "",
162
- `Sibling folder \`../mirror/<host>/\` has a typed \`page.tsx\` with Framer Motion, Phosphor icons, and slots — wire copy from \`visible-text.json\` and media from \`media.json\` into that file.`,
163
- "",
164
- `Captured path: \`${base}\``,
165
- "",
166
- ].join("\n");
167
- }
168
-
169
- function buildStructureOutline(root: DomStructureElement | null, stats: DomStructurePayload["stats"]): string {
170
- const MAX_LINES = 15_000;
171
- const lines: string[] = [
172
- "# structure-outline.txt — tag skeleton under <body>",
173
- "# Text nodes omitted here; see dom-structure.json for interleaved copy.",
174
- `# elementNodes=${stats.elementNodes} truncated=${stats.truncated} omitted=${JSON.stringify(stats.omitted)}`,
175
- "",
176
- ];
177
- let n = 0;
178
- function walkEl(node: DomStructureElement, depth: number) {
179
- if (n >= MAX_LINES) return;
180
- const a = node.attrs ?? {};
181
- let suffix = "";
182
- if (a.id) suffix += `#${String(a.id).replace(/\s+/g, "")}`;
183
- if (a.class) {
184
- const parts = String(a.class)
185
- .split(/\s+/)
186
- .filter(Boolean)
187
- .slice(0, 6)
188
- .map((c) => `.${c}`);
189
- suffix += parts.join("");
190
- }
191
- if (a.role) suffix += `[role=${a.role}]`;
192
- lines.push(`${" ".repeat(depth)}${node.tag}${suffix}`);
193
- n++;
194
- for (const c of node.children ?? []) {
195
- if (n >= MAX_LINES) return;
196
- if ("type" in c && c.type === "text") continue;
197
- walkEl(c as DomStructureElement, depth + 1);
198
- }
199
- }
200
- if (root) walkEl(root, 0);
201
- if (n >= MAX_LINES) lines.push("\n… outline truncated …");
202
- return lines.join("\n");
203
- }
204
-
205
- /**
206
- * Runs in browser context. Serializes `document.body` with exact child order; skips
207
- * script/style/noscript/template (counts only).
208
- */
209
- function collectDomStructureInPage(): DomStructurePayload {
210
- const MAX_NODES = 40_000;
211
- const MAX_DEPTH = 128;
212
- const MAX_TEXT = 12_000;
213
-
214
- let nodeCount = 0;
215
- let truncated = false;
216
- const omitted = { script: 0, style: 0, noscript: 0, template: 0 };
217
-
218
- function trimAttr(name: string, v: string): string {
219
- const big =
220
- name === "src" ||
221
- name === "href" ||
222
- name === "srcset" ||
223
- name === "style" ||
224
- name === "content" ||
225
- name.startsWith("data-");
226
- const max = big ? 3500 : 2000;
227
- if (v.length <= max) return v;
228
- return v.slice(0, max) + "…";
229
- }
230
-
231
- function walk(el: Element, depth: number): DomStructureElement | null {
232
- if (nodeCount >= MAX_NODES) {
233
- truncated = true;
234
- return null;
235
- }
236
- const tag = el.tagName.toLowerCase();
237
- if (tag === "script") {
238
- omitted.script++;
239
- return null;
240
- }
241
- if (tag === "style") {
242
- omitted.style++;
243
- return null;
244
- }
245
- if (tag === "noscript") {
246
- omitted.noscript++;
247
- return null;
248
- }
249
- if (tag === "template") {
250
- omitted.template++;
251
- return null;
252
- }
253
- if (depth > MAX_DEPTH) return null;
254
-
255
- nodeCount++;
256
- const attrs: Record<string, string> = {};
257
- for (let i = 0; i < el.attributes.length; i++) {
258
- const a = el.attributes.item(i);
259
- if (!a) continue;
260
- attrs[a.name] = trimAttr(a.name, a.value);
261
- }
262
-
263
- const children: DomStructureChild[] = [];
264
- for (const child of el.childNodes) {
265
- if (nodeCount >= MAX_NODES) {
266
- truncated = true;
267
- break;
268
- }
269
- if (child.nodeType === 3) {
270
- let t = (child.textContent ?? "").replace(/\s+/g, " ").trim();
271
- if (!t) continue;
272
- if (t.length > MAX_TEXT) t = t.slice(0, MAX_TEXT) + "…";
273
- children.push({ type: "text", value: t });
274
- } else if (child.nodeType === 1) {
275
- const sub = walk(child as Element, depth + 1);
276
- if (sub) children.push(sub);
277
- }
278
- }
279
-
280
- const out: DomStructureElement = { tag };
281
- if (Object.keys(attrs).length > 0) out.attrs = attrs;
282
- if (children.length > 0) out.children = children;
283
- return out;
284
- }
285
-
286
- const body = document.body;
287
- if (!body) {
288
- return {
289
- title: document.title || null,
290
- lang: document.documentElement.getAttribute("lang"),
291
- stats: {
292
- elementNodes: 0,
293
- omitted,
294
- truncated: false,
295
- maxNodes: MAX_NODES,
296
- maxDepth: MAX_DEPTH,
297
- },
298
- root: null,
299
- };
300
- }
301
-
302
- const root = walk(body, 0);
303
- return {
304
- title: document.title || null,
305
- lang: document.documentElement.getAttribute("lang"),
306
- stats: {
307
- elementNodes: nodeCount,
308
- omitted,
309
- truncated,
310
- maxNodes: MAX_NODES,
311
- maxDepth: MAX_DEPTH,
312
- },
313
- root,
314
- };
315
- }
316
-
317
- /**
318
- * Runs in browser context.
319
- */
320
- function collectSnapshot(): Omit<ReferenceSnapshot, "url" | "capturedAt"> {
321
- const title = document.title || null;
322
- const descEl = document.querySelector('meta[name="description"]');
323
- const description = descEl?.getAttribute("content")?.trim() || null;
324
- const canonicalEl = document.querySelector('link[rel="canonical"]');
325
- const canonical = canonicalEl?.getAttribute("href") || null;
326
- const lang = document.documentElement.getAttribute("lang");
327
-
328
- const visibleTextBlocks: Array<{ tag: string; role: string | null; text: string }> = [];
329
- const pushBlock = (tag: string, el: HTMLElement, role: string | null) => {
330
- const text = el.innerText?.trim().replace(/\s+/g, " ") ?? "";
331
- if (text.length < 2 || text.length > 4000) return;
332
- visibleTextBlocks.push({ tag, role, text });
333
- };
334
-
335
- for (const tag of ["H1", "H2", "H3", "H4", "H5", "H6"] as const) {
336
- for (const el of Array.from(document.querySelectorAll(tag))) {
337
- if (!(el instanceof HTMLElement)) continue;
338
- const style = getComputedStyle(el);
339
- if (style.visibility === "hidden" || style.display === "none") continue;
340
- pushBlock(tag, el, el.getAttribute("role"));
341
- }
342
- }
343
-
344
- for (const el of Array.from(document.querySelectorAll("p, li, blockquote, figcaption, label"))) {
345
- if (!(el instanceof HTMLElement)) continue;
346
- const style = getComputedStyle(el);
347
- if (style.visibility === "hidden" || style.display === "none") continue;
348
- pushBlock(el.tagName, el, el.getAttribute("role"));
349
- }
350
-
351
- for (const el of Array.from(document.querySelectorAll("button, [role='button']"))) {
352
- if (!(el instanceof HTMLElement)) continue;
353
- const style = getComputedStyle(el);
354
- if (style.visibility === "hidden" || style.display === "none") continue;
355
- pushBlock("BUTTON", el, el.getAttribute("role"));
356
- }
357
-
358
- for (const el of Array.from(document.querySelectorAll("span, div"))) {
359
- if (!(el instanceof HTMLElement)) continue;
360
- const role = el.getAttribute("role");
361
- if (
362
- role !== "heading" &&
363
- !el.classList.contains("badge") &&
364
- el.getAttribute("data-slot") === null
365
- ) {
366
- // Only capture labeled small UI chrome (badges, pills) via short text + uppercase heuristic
367
- const style = getComputedStyle(el);
368
- if (style.visibility === "hidden" || style.display === "none") continue;
369
- const text = el.innerText?.trim().replace(/\s+/g, " ") ?? "";
370
- if (text.length < 8 || text.length > 240) continue;
371
- if (!/^[A-Z0-9\s&.,:]+$/.test(text)) continue; // ALL-CAPS-ish eyebrow labels
372
- pushBlock(el.tagName, el, role);
373
- }
374
- }
375
-
376
- const links: Array<{ href: string; text: string }> = [];
377
- for (const a of Array.from(document.querySelectorAll("a[href]"))) {
378
- const href = a.getAttribute("href") ?? "";
379
- if (!href || href.startsWith("javascript:")) continue;
380
- const text = (a.textContent ?? "").trim().replace(/\s+/g, " ");
381
- if (!text) continue;
382
- try {
383
- const abs = new URL(href, document.baseURI).href;
384
- links.push({ href: abs, text: text.slice(0, 500) });
385
- } catch {
386
- links.push({ href, text: text.slice(0, 500) });
387
- }
388
- }
389
-
390
- const media: ReferenceSnapshot["media"] = [];
391
- for (const img of Array.from(document.querySelectorAll("img"))) {
392
- const src = img.currentSrc || img.src;
393
- if (!src) continue;
394
- media.push({
395
- type: "img",
396
- src,
397
- alt: img.alt || "",
398
- width: img.naturalWidth || null,
399
- height: img.naturalHeight || null,
400
- });
401
- }
402
- for (const video of Array.from(document.querySelectorAll("video"))) {
403
- const poster = video.getAttribute("poster");
404
- let src: string | null = null;
405
- if (video.currentSrc) src = video.currentSrc;
406
- else {
407
- const s = video.querySelector("source[src]");
408
- src = s?.getAttribute("src") ?? null;
409
- }
410
- media.push({ type: "video", src, poster: poster || null });
411
- }
412
- for (const source of Array.from(document.querySelectorAll("source[src]"))) {
413
- const src = source.getAttribute("src");
414
- if (!src) continue;
415
- media.push({
416
- type: "source",
417
- src,
418
- kind: source.getAttribute("type"),
419
- });
420
- }
421
-
422
- return {
423
- title,
424
- description,
425
- canonical,
426
- lang: lang || null,
427
- visibleTextBlocks,
428
- links,
429
- media,
430
- };
431
- }