UNPKG

boardcast

Version:

Animation library for tabletop game rules on hex boards with CLI tools and game extensions

228 lines (187 loc) 7.43 kB
import { chromium } from 'playwright'; import express from 'express'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); async function recordTutorial(tutorialFile) { // Validate tutorial file exists if (!fs.existsSync(tutorialFile)) { throw new Error(`Tutorial file not found: ${tutorialFile}`); } // Validate it's a JavaScript file if (!tutorialFile.endsWith('.js')) { throw new Error('Tutorial file must be a .js file'); } console.log(`🎬 Recording tutorial: ${tutorialFile}`); // Get paths - look for boardcast library in node_modules or peer dependency const tutorialPath = path.resolve(tutorialFile); const cliRoot = path.resolve(__dirname, '..'); const runtimeDir = path.join(cliRoot, 'runtime'); // Try to find boardcast library let boardcastPath; const possiblePaths = [ // Try peer dependency in parent project path.resolve(process.cwd(), 'node_modules/boardcast/dist'), // Try in CLI package node_modules path.join(cliRoot, 'node_modules/boardcast/dist'), // Try relative to working directory path.resolve(process.cwd(), 'dist') ]; for (const testPath of possiblePaths) { if (fs.existsSync(testPath)) { boardcastPath = testPath; break; } } if (!boardcastPath) { throw new Error('Boardcast library not found. Please install boardcast as a dependency or run "npm run build" in a boardcast project.'); } console.log(`📦 Using boardcast library from: ${boardcastPath}`); // Basic tutorial file validation and config extraction let tutorialConfig; try { const tutorialContent = fs.readFileSync(tutorialPath, 'utf8'); // Basic syntax validation if (!tutorialContent.includes('runTutorial')) { throw new Error('Tutorial file must export a runTutorial function'); } if (!tutorialContent.includes('config')) { throw new Error('Tutorial file must export a config object'); } // Extract config using regex const configMatch = tutorialContent.match(/export\s+const\s+config\s*=\s*({[^}]*})/); if (configMatch) { try { tutorialConfig = JSON.parse(configMatch[1].replace(/'/g, '"')); } catch { tutorialConfig = { gridRadius: 8, title: "Tutorial" }; } } else { tutorialConfig = { gridRadius: 8, title: "Tutorial" }; } console.log(`📋 Tutorial config:`, tutorialConfig); } catch (error) { throw new Error(`Tutorial file validation failed: ${error.message}`); } // Create videos directory const videosDir = path.join(process.cwd(), 'videos'); if (!fs.existsSync(videosDir)) { fs.mkdirSync(videosDir, { recursive: true }); } // Create Express server const app = express(); const port = 3001; // Serve runtime files (HTML template) - both at root and /boardcast/ path app.use(express.static(runtimeDir)); app.use('/boardcast', express.static(runtimeDir)); // Serve the built library files - boardcastPath already points to dist/ app.use('/dist', express.static(boardcastPath)); app.use('/boardcast/dist/lib', express.static(path.join(boardcastPath, 'lib'))); // Serve boardcast-contrib from the built dist directory const contribPath = path.resolve(boardcastPath, '../dist/contrib'); if (fs.existsSync(contribPath)) { app.use('/boardcast-contrib', express.static(contribPath)); app.use('/boardcast/boardcast-contrib', express.static(contribPath)); console.log(`📦 Using boardcast-contrib from: ${contribPath}`); } else { console.log(`⚠️ boardcast-contrib not found at: ${contribPath}`); } // Serve the user's tutorial file app.get('/user-tutorial.js', (req, res) => { res.sendFile(tutorialPath); }); app.get('/boardcast/user-tutorial.js', (req, res) => { res.sendFile(tutorialPath); }); const server = app.listen(port, () => { console.log(`📡 Server running at http://localhost:${port}`); }); // Launch browser for recording const browser = await chromium.launch({ headless: false, // Show browser for debugging }); const context = await browser.newContext({ recordVideo: { dir: videosDir, size: { width: 1920, height: 1080 } }, viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); try { console.log('📱 Loading tutorial...'); await page.goto(`http://localhost:${port}/boardcast/tutorial-runner.html`); // Wait for the page to load await page.waitForSelector('#chart', { state: 'visible' }); console.log('✅ Tutorial page loaded'); // Wait for tutorial to start await page.waitForTimeout(2000); console.log('🎮 Tutorial started...'); // Wait for tutorial completion or error console.log('⏳ Recording in progress...'); let completed = false; let attempts = 0; const maxAttempts = 300; // 5 minutes max while (!completed && attempts < maxAttempts) { try { // Check for completion const isComplete = await page.evaluate(() => { return window.isTutorialComplete && window.isTutorialComplete(); }); if (isComplete) { // Check if there was an error const error = await page.evaluate(() => { return window.getTutorialError && window.getTutorialError(); }); if (error) { throw new Error(`Tutorial runtime error: ${error.message || error}`); } completed = true; console.log('✅ Tutorial completed successfully'); } else { await page.waitForTimeout(1000); attempts++; // Log progress every 10 seconds if (attempts % 10 === 0) { console.log(`⏳ Still recording... (${attempts} seconds elapsed)`); } } } catch (error) { if (error.message.includes('Tutorial runtime error')) { throw error; // Re-throw tutorial errors } // Other errors might be temporary, continue waiting await page.waitForTimeout(1000); attempts++; } } if (!completed) { throw new Error('Tutorial did not complete within timeout (5 minutes)'); } // Give a little extra time for final animations await page.waitForTimeout(2000); } finally { await context.close(); await browser.close(); server.close(); // Find and rename the recorded video file const videoFiles = fs.readdirSync(videosDir).filter(file => file.endsWith('.webm')); if (videoFiles.length > 0) { const videoPath = path.join(videosDir, videoFiles[videoFiles.length - 1]); const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, ''); const baseName = path.basename(tutorialFile, '.js'); const finalPath = path.join(videosDir, `${baseName}-${timestamp}.webm`); // Rename to timestamped name fs.renameSync(videoPath, finalPath); console.log(`🎥 Video saved: ${finalPath}`); } else { throw new Error('No video file was created'); } console.log('🏁 Recording complete!'); } } export { recordTutorial };