mirror of
https://github.com/raphaelsalaja/userinterface-wiki.git
synced 2026-09-14 20:06:29 +08:00
201 lines
5.9 KiB
TypeScript
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}`);
|
|
}
|
|
}
|