195 lines
9.9 KiB
JavaScript
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/`); |