Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/BaselAshraf81/holystitch/llms.txt

Use this file to discover all available pages before exploring further.

HolyStitch converts your Stitch screens without spending a single AI token on the conversion itself. Everything from HTML fetching to file writing is deterministic code — no LLM calls, no guesswork.

Input and output

Input: RawScreen[] Each screen is a plain object with three fields:
interface RawScreen {
  id: string;       // Stitch screen ID
  name: string;     // Human-readable display name (e.g. "Landing Page")
  html: string;     // Raw HTML string, or a signed download URL
}
Screens are either passed directly by the caller or fetched from the Stitch API via fetchScreens. Output: ConvertStitchOutput
interface ConvertStitchOutput {
  components: ConvertedComponent[];   // Every component file written
  pages: ConvertedPage[];             // Every page file written, with its route
  hasTailwindTheme: boolean;          // Whether a Tailwind theme was extracted
  hasCustomCss: boolean;              // Whether custom CSS was found in <head>
  sourceHtmlPaths: string[];          // Paths to stitch-source/*.html reference files
  sharedComponents: string[];         // Component names appearing on 2+ screens
  outputDir: string;                  // Resolved absolute output path
  filesWritten: number;               // Total files written to disk
  summary: string;                    // Human-readable summary string
  warnings: string[];                 // Structural issues for the AI to review
}

The nine-step pipeline

1

Resolve screens

The Stitch SDK sometimes returns a signed download URL in the html field instead of raw HTML. The resolver detects this by checking whether the string starts with http:// or https://, then fetches the real content before any other step touches it.
// resolver.ts
if (looksLikeUrl(screen.html)) {
  const html = await fetchHtml(screen.html.trim());
  return { ...screen, html };
}
All screens are resolved in parallel via Promise.all.
2

Parse HTML comment boundaries → component slices

parseStitchHtml reads the body HTML and scans for <!-- ComponentName --> markers using an AST built by htmlparser2. Each marker claims the next sibling block element as a named component slice, producing a full component tree.See Component extraction for the full algorithm.
3

Extract fonts and custom CSS from `<head>`

The parser scans the <head> for Google Fonts <link> tags and inline <style> blocks. Font links are split into two buckets:
  • Text fonts (Inter, JetBrains Mono, etc.) — eligible for next/font/google optimization in Next.js
  • Icon fonts (Material Symbols, Material Icons) — always loaded via CSS @import because next/font/google does not support them
Custom CSS from <style> blocks is accumulated across all screens and written into globals.css / src/index.css.
4

Extract Tailwind `theme.extend` config

Stitch embeds a tailwind.config object in a <script id="tailwind-config"> tag. The parser extracts theme.extend, plugins, and darkMode using balanced-bracket parsing (not JSON.parse — the config uses JS object syntax, not strict JSON).When multiple screens are processed, color token values are compared across screens. Conflicts are recorded as warnings:
Tailwind color conflict: "primary" is "#1a1a2e" on screen 1 but "#0f0f23"
on "Pricing" — check tailwind.config.js
5

Compile each component's HTML to JSX

compileHTMLToJSX applies a series of deterministic transforms to each component’s HTML slice:
  • Attributes renamed (classclassName, forhtmlFor, etc.)
  • Event handlers camelCased (onclickonClick)
  • Void elements self-closed (<img>, <br>, <input>, etc.)
  • Inline style strings converted to JS objects
  • Material Symbols icon-name text stripped
  • <pre><code> curly braces escaped as HTML entities
See HTML to JSX for the full transform reference.
6

Wrap each slice in a React component file

Each compiled JSX slice is wrapped in a .tsx / .jsx file with:
  • A 'use client'; directive if the component contains event handlers or <button> elements (Next.js only)
  • import statements for any child components
  • An empty typed interface ComponentNameProps {} with prop hints surfaced as comments
  • A named default export function
// Example output for a TypeScript Next.js project
'use client';
import React from 'react';
import TopNavBar from './TopNavBar';

interface HeroSectionProps {
  // Potential prop: title (currently "Get started for free")
  // Potential prop: imageSrc — replace hardcoded Stitch image URLs
}

export default function HeroSection(_props: HeroSectionProps) {
  return (
    // compiled JSX here
  );
}
7

Generate page files and all framework config files

For each screen, a page file is generated that imports its root components and renders them. The page body uses pageBodyHtml — the original body HTML with root component blocks replaced by <ComponentName /> placeholders.Config files generated alongside components and pages:
FileDescription
package.jsonDependencies for Next.js 14 or Vite 5
tailwind.config.jsExtracted theme.extend, plugins, darkMode
postcss.config.jsTailwind + autoprefixer
tsconfig.jsonTypeScript config (when language is typescript)
next.config.jsMinimal Next.js config (Next.js only)
app/layout.tsxRoot layout with next/font/google imports (Next.js only)
app/globals.cssTailwind directives + font imports + custom CSS
src/App.tsxHash-based router with all page routes (Vite only)
src/main.tsxReactDOM entry point (Vite only)
For Vite projects, routing is hash-based (/#/route-name). src/App.tsx maps every route to its page component using a plain object lookup — no router library needed.
8

Write everything to disk

writeProjectFiles writes all accumulated ProjectFile objects to the output directory. It creates intermediate directories automatically and supports base64-encoded binary files.
// writer.ts
for (const file of files) {
  const fullPath = path.join(resolvedDir, file.path);
  await fs.mkdir(path.dirname(fullPath), { recursive: true });
  if (file.binary) {
    await fs.writeFile(fullPath, Buffer.from(file.content, "base64"));
  } else {
    await fs.writeFile(fullPath, file.content, "utf8");
  }
}
The original Stitch HTML for each screen is also written to stitch-source/<screen-name>.html as a reference file for post-conversion AI work.
9

Write `project-context.md`

The final step writes project-context.md to the output directory. This file is designed to be read by an AI assistant in a follow-up session and contains:
  • A source map linking each route to its stitch-source/*.html reference file
  • A fix checklist (unclosed tags, style objects, routing, font token normalization)
  • A full component list with file paths
  • All structural warnings generated during conversion
project-context.md is the handoff point between HolyStitch and your AI assistant. Open it after conversion to understand exactly what was generated and what still needs attention.

Why no AI tokens are needed

Every transform in the pipeline is a deterministic algorithm:
  • Attribute renaming — a static lookup table (ATTR_MAP)
  • Style conversion — a balanced-bracket CSS parser
  • Component detection — an AST scan for uppercase HTML comments
  • Shared component deduplication — Jaccard similarity over token sets
  • Tailwind config extraction — balanced-bracket object parsing
  • File writingfs.writeFile with directory creation
There are no calls to any LLM, no embedding lookups, and no probabilistic steps. The same input always produces the same output. If the generated JSX has issues (unclosed tags from malformed Stitch HTML, hardcoded href="#" links), project-context.md documents exactly what needs fixing and where to find the source HTML.

Shared component detection

When the same component name appears on multiple screens, HolyStitch computes Jaccard similarity over word tokens from both HTML strings:
function tokenSimilarity(a: string, b: string): number {
  const tokenize = (s: string): Set<string> =>
    new Set(s.match(/[a-zA-Z][a-zA-Z0-9-]{2,}/g) ?? []);
  const ta = tokenize(a);
  const tb = tokenize(b);
  let intersection = 0;
  for (const t of ta) { if (tb.has(t)) intersection++; }
  return intersection / (ta.size + tb.size - intersection);
}
  • Similarity ≥ 0.7 — the component is treated as shared; the existing file is reused
  • Similarity < 0.7 — the component is treated as a different widget that happens to share a name; a screen-scoped copy is created as ComponentName + PascalCasedScreenName, and a warning is emitted

Build docs developers (and LLMs) love