Files
2026-02-01 00:16:13 +00:00

201 lines
5.9 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import {
DIM,
type GeneratedFile,
Generator,
p,
pc,
RESET,
TEXT,
} from "../../lib/generator-base";
const CONTENT_DIR = path.join(process.cwd(), "content");
/**
* Scans for demo folders containing index.tsx and styles.module.css with a playgrounds folder,
* then generates playground files.
*/
function findDemoFolders(dir: string): string[] {
const demos: string[] = [];
function walk(currentDir: string) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "demos") {
const demoEntries = fs.readdirSync(fullPath, { withFileTypes: true });
for (const demoEntry of demoEntries) {
if (demoEntry.isDirectory()) {
const demoPath = path.join(fullPath, demoEntry.name);
const playgroundsPath = path.join(demoPath, "playgrounds");
const indexPath = path.join(demoPath, "index.tsx");
const stylesPath = path.join(demoPath, "styles.module.css");
if (
fs.existsSync(playgroundsPath) &&
fs.existsSync(indexPath) &&
fs.existsSync(stylesPath)
) {
demos.push(demoPath);
}
}
}
} else {
walk(fullPath);
}
}
}
}
walk(dir);
return demos;
}
function escapeTemplateString(str: string): string {
return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
}
function transformToPlaygroundCode(code: string): string {
let transformed = code;
// Remove "use client" directive
transformed = transformed.replace(/^["']use client["'];?\s*\n/m, "");
// Convert named export function to default export App
const namedExportRegex = /export function \w+(\([^)]*\))\s*\{/;
const match = transformed.match(namedExportRegex);
if (match) {
transformed = transformed.replace(
namedExportRegex,
`export default function App${match[1]} {`,
);
}
return transformed;
}
function toPascalCase(folderName: string): string {
const withoutPrefix = folderName.replace(/^\d+-/, "");
return `${withoutPrefix
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join("")}Playground`;
}
function generatePlayground(demoPath: string): string {
const playgroundsDir = path.join(demoPath, "playgrounds");
const indexPath = path.join(demoPath, "index.tsx");
const stylesPath = path.join(demoPath, "styles.module.css");
const outputPath = path.join(playgroundsDir, "index.ts");
const demoName = path.basename(demoPath);
const exportName = toPascalCase(demoName);
const rawAppCode = fs.readFileSync(indexPath, "utf-8");
const appCode = transformToPlaygroundCode(rawAppCode);
const stylesCode = fs.readFileSync(stylesPath, "utf-8");
const output = `// Auto-generated by scripts/generators/playgrounds
export const ${exportName} = {
files: {
"/App.tsx": \`${escapeTemplateString(appCode)}\`,
"/styles.module.css": \`${escapeTemplateString(stylesCode)}\`,
},
};
`;
fs.writeFileSync(outputPath, output);
return path.relative(process.cwd(), outputPath);
}
export class PlaygroundsGenerator extends Generator {
constructor() {
super({ name: "playgrounds", label: "playgrounds" });
}
protected async generate(): Promise<GeneratedFile[]> {
const demos = findDemoFolders(CONTENT_DIR);
if (demos.length === 0) {
this.warn("No demos found");
this.info("Expected structure:");
console.log(` ${DIM}→${RESET} content/*/demos/*/index.tsx`);
console.log(` ${DIM}→${RESET} content/*/demos/*/styles.module.css`);
console.log(` ${DIM}→${RESET} content/*/demos/*/playgrounds/`);
return [];
}
const files: GeneratedFile[] = [];
for (const demoPath of demos) {
const relativePath = generatePlayground(demoPath);
const stats = fs.statSync(relativePath);
files.push({
name: relativePath
.replace(/^content\//, "")
.replace(/\/playgrounds\/index\.ts$/, ""),
path: relativePath,
size: stats.size,
});
}
return files;
}
async watch(): Promise<void> {
await this.run();
const demos = findDemoFolders(CONTENT_DIR);
for (const demoPath of demos) {
const playgroundsDir = path.join(demoPath, "playgrounds");
// Watch demo folder for changes
fs.watch(demoPath, { recursive: false }, (_eventType, filename) => {
if (filename === "index.tsx" || filename === "styles.module.css") {
const relativePath = generatePlayground(demoPath);
const stats = fs.statSync(relativePath);
const displayName = relativePath
.replace(/^content\//, "")
.replace(/\/playgrounds\/index\.ts$/, "");
p.log.success(
`${pc.green("↻")} ${TEXT}${displayName}${RESET} ${DIM}(${(stats.size / 1024).toFixed(1)} kB)${RESET}`,
);
}
});
// Watch playgrounds folder
if (fs.existsSync(playgroundsDir)) {
fs.watch(
playgroundsDir,
{ recursive: false },
(_eventType, filename) => {
if (
filename &&
(filename.endsWith(".tsx") || filename.endsWith(".css"))
) {
const relativePath = generatePlayground(demoPath);
const stats = fs.statSync(relativePath);
const displayName = relativePath
.replace(/^content\//, "")
.replace(/\/playgrounds\/index\.ts$/, "");
p.log.success(
`${pc.green("↻")} ${TEXT}${displayName}${RESET} ${DIM}(${(stats.size / 1024).toFixed(1)} kB)${RESET}`,
);
}
},
);
}
}
p.log.info(`${DIM}Watching ${demos.length} directories...${RESET}`);
}
}