Files

195 lines
9.9 KiB
JavaScript

/**
* Generates line-by-line annotated copies of every src/ file.
* Output: docs/annotated/src/** (mirrors src/ structure)
* Run: node docs/annotate-all.mjs
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, '..');
const SRC = path.join(ROOT, 'src');
const OUT = path.join(__dirname, 'annotated', 'src');
function explainLine(line, lineNum, relPath) {
const t = line.trim();
const indent = line.match(/^(\s*)/)[1];
if (t === '') return `${indent}// (blank line — separates logical blocks for readability)`;
if (t.startsWith('/**') || t.startsWith('*') || t.startsWith('*/'))
return `${indent}// Block comment — documents the file or function below`;
if (t.startsWith('//')) return line;
if (t === "'use client';" || t === '"use client";')
return `${indent}// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)`;
if (t.startsWith('import type '))
return `${indent}// Type-only import — erased at compile time; no JavaScript bundle cost`;
if (t.startsWith('import ')) {
if (t.includes("from 'react'") || t.includes('from "react"'))
return `${indent}// Import React — core UI library (components, hooks, JSX)`;
if (t.includes('next/link'))
return `${indent}// Next.js Link — fast client-side navigation without full page reload`;
if (t.includes('next/image'))
return `${indent}// Next.js Image — optimized images (lazy load, WebP/AVIF)`;
if (t.includes('next/navigation'))
return `${indent}// Next.js App Router hooks — useRouter, useParams, useSearchParams`;
if (t.includes('next/font'))
return `${indent}// Self-hosted Google fonts — better performance than external CSS`;
if (t.includes('lucide-react'))
return `${indent}// Lucide icons — lightweight SVG icon components`;
if (t.includes('zustand'))
return `${indent}// Zustand — simple global state store (cart, auth, locale)`;
if (t.includes('@/'))
return `${indent}// Import project module (@/ alias = src/ folder in tsconfig)`;
if (t.includes("'./") || t.includes('"./'))
return `${indent}// Import from a relative file in the same project`;
return `${indent}// Import external package or local module`;
}
if (t.startsWith('export type ') || t.startsWith('export interface '))
return `${indent}// Export TypeScript type — defines data shape used across the app`;
if (t.startsWith('interface '))
return `${indent}// TypeScript interface — contract for object properties and methods`;
if (t.startsWith('type '))
return `${indent}// TypeScript type alias — union or shorthand for complex types`;
if (t.startsWith('export default function'))
return `${indent}// Default export — main component/page Next.js or other files import`;
if (t.startsWith('export function'))
return `${indent}// Named export — utility function other files can import`;
if (t.startsWith('export const '))
return `${indent}// Named export constant — shared config/data imported elsewhere`;
if (t.startsWith('const ') && t.includes('= create<'))
return `${indent}// Create Zustand store — global state hook (useXxxStore)`;
if (t.startsWith('const ') && t.includes('useState'))
return `${indent}// React useState — local component state that triggers re-render on change`;
if (t.startsWith('const ') && t.includes('useMemo'))
return `${indent}// React useMemo — cache expensive computed value until dependencies change`;
if (t.startsWith('const ') && t.includes('useCallback'))
return `${indent}// React useCallback — stable function reference for useEffect/useMemo deps`;
if (t.startsWith('const ') && t.includes('useEffect'))
return `${indent}// React useEffect — run side effect after render (sync URL, redirect, etc.)`;
if (t.startsWith('const ') && t.includes('useRouter'))
return `${indent}// Next.js router — programmatic navigation (router.push)`;
if (t.startsWith('const ') && t.includes('useSearchParams'))
return `${indent}// Read URL query string (?category=beef&q=steak)`;
if (t.startsWith('const ') && t.includes('useParams'))
return `${indent}// Read dynamic route segment ([slug] from URL)`;
if (t.startsWith('const ') && t.includes('useTranslation'))
return `${indent}// Custom hook — returns t() translator and current locale`;
if (t.startsWith('const ') && t.includes('useCartStore') || t.startsWith('const ') && t.includes('useAuthStore') || t.startsWith('const ') && t.includes('useWishlistStore') || t.startsWith('const ') && t.includes('useLocaleStore'))
return `${indent}// Zustand selector — subscribe to slice of global store`;
if (t.startsWith('function '))
return `${indent}// Function declaration — reusable logic in this file`;
if (t.startsWith('return ('))
return `${indent}// Return JSX — describes UI tree React renders to the DOM`;
if (t.startsWith('return '))
return `${indent}// Return value from function`;
if (t.startsWith('if (') || t.startsWith('} else if ('))
return `${indent}// Conditional branch — different behavior based on runtime value`;
if (t.startsWith('switch ('))
return `${indent}// Switch — multiple branches on one variable (e.g. sort order)`;
if (t.startsWith('case '))
return `${indent}// Switch case — handle one specific value`;
if (t.startsWith('default:'))
return `${indent}// Switch default — fallback when no case matches`;
if (t.startsWith('useEffect('))
return `${indent}// Side effect hook — runs after paint; deps array controls when it re-runs`;
if (t.includes('.map('))
return `${indent}// Array.map — transform each item (often render a list of components)`;
if (t.includes('.filter('))
return `${indent}// Array.filter — keep items matching condition (search, category)`;
if (t.includes('.sort('))
return `${indent}// Array.sort — reorder items (price, name, featured)`;
if (t.includes('.find('))
return `${indent}// Array.find — get first matching item or undefined`;
if (t.includes('.reduce('))
return `${indent}// Array.reduce — accumulate single value (cart total, item count)`;
if (t.startsWith('<') && !t.startsWith('<>'))
return `${indent}// JSX element — HTML-like tag becomes React component in browser`;
if (t.startsWith('<>') || t === '</>')
return `${indent}// React Fragment — group elements without extra wrapper DOM node`;
if (t.startsWith('{/*'))
return `${indent}// JSX comment — not visible in browser`;
if (t.startsWith('className='))
return `${indent}// Tailwind CSS utility classes — styling (colors, spacing, layout)`;
if (t.startsWith('href='))
return `${indent}// Link target URL — internal route or external https://`;
if (t.startsWith('onClick='))
return `${indent}// Click handler — runs when user clicks (must be client component)`;
if (t.startsWith('onChange='))
return `${indent}// Change handler — runs when input/select value changes`;
if (t.startsWith('aria-'))
return `${indent}// Accessibility attribute — screen readers and assistive tech`;
if (t.startsWith('}'))
return `${indent}// Closing brace — end of block (function, if, object, JSX)`;
if (t.startsWith(']') || t.startsWith('];'))
return `${indent}// End of array literal`;
if (t.endsWith(',') && !t.includes('//'))
return `${indent}// Property or array item — trailing comma allowed in TypeScript`;
if (relPath.includes('locales/') && t.match(/^\s{2}\w+:/))
return `${indent}// i18n translation section — keys used by t('section.key') in components`;
if (relPath.includes('locales/') && t.includes("name:") || relPath.includes('locales/') && t.includes("title:"))
return `${indent}// Translated UI string for current language (sv / en / ur)`;
if (relPath.includes('products.ts'))
return `${indent}// Product catalog entry — demo data shown in shop and product pages`;
if (t.includes('persist('))
return `${indent}// Zustand persist — save store to localStorage between visits`;
return `${indent}// Line ${lineNum}: ${t.length > 60 ? t.slice(0, 57) + '...' : t || 'code'}`;
}
function annotateFile(content, relPath) {
const lines = content.split('\n');
const ext = relPath.endsWith('.tsx') ? 'tsx' : 'ts';
const header = [
`/**`,
` * ANNOTATED COPY — every line explained`,
` * Source: src/${relPath}`,
` * NOT used by the app — read this to learn how the real file works`,
` */`,
'',
];
const body = lines.map((line, i) => {
const comment = explainLine(line, i + 1, relPath);
if (line.trim().startsWith('//') && comment === line) return line;
if (line.trim() === '' && comment.includes('blank')) return comment;
return `${comment}\n${line}`;
});
return header.join('\n') + body.join('\n') + '\n';
}
function walk(dir, base = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
let count = 0;
for (const e of entries) {
const rel = base ? `${base}/${e.name}` : e.name;
const full = path.join(dir, e.name);
if (e.isDirectory()) {
count += walk(full, rel);
} else if (/\.(ts|tsx)$/.test(e.name)) {
const content = fs.readFileSync(full, 'utf8');
const annotated = annotateFile(content, rel);
const outName = e.name.replace(/\.(tsx?)$/, '.annotated.$1');
const outDir = path.join(OUT, base);
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, outName), annotated);
count++;
}
}
return count;
}
// Remove old flat annotated files (replaced by src/ mirror)
const oldFlat = path.join(__dirname, 'annotated');
for (const f of fs.readdirSync(oldFlat)) {
if (f.endsWith('.annotated.ts') || f.endsWith('.annotated.tsx')) {
if (!f.startsWith('0')) continue;
try { fs.unlinkSync(path.join(oldFlat, f)); } catch (_) {}
}
}
const n = walk(SRC);
console.log(`Annotated ${n} files → docs/annotated/src/`);