ajhahn.de
← Flash
JavaScript 132 lines
import express from 'express';
import { exec } from 'child_process';
import { promises as fs, watch } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = process.env.PORT || 3000;

// Path to the flashc compiler built in the Flash workspace.
// Tutorial lives at Flash/tutorial/, so the compiler is one level up.
const COMPILER_PATH = process.env.FLASHC
  || path.join(__dirname, '..', 'zig-out', 'bin', 'flashc');
const TEMP_DIR = path.join(__dirname, 'temp');
const PUBLIC_DIR = path.join(__dirname, 'public');

// Middleware
app.use(express.json());
app.use(express.static(PUBLIC_DIR));

// Ensure temp directory exists
async function ensureTempDir() {
  try {
    await fs.mkdir(TEMP_DIR, { recursive: true });
  } catch (err) {
    console.error('Failed to create temp directory:', err);
  }
}
ensureTempDir();

// API: Transpile Flash code to Zig
app.post('/api/transpile', async (req, res) => {
  const { code } = req.body;

  if (typeof code !== 'string') {
    return res.status(400).json({
      success: false,
      error: 'Code must be a string.',
    });
  }

  // Create unique filename to avoid collision if multiple requests arrive
  const tempFileName = `try_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.flash`;
  const tempFilePath = path.join(TEMP_DIR, tempFileName);

  try {
    // Write temporary Flash source file
    await fs.writeFile(tempFilePath, code, 'utf-8');

    // Run the local flashc compiler
    // We run it and capture output
    exec(`"${COMPILER_PATH}" "${tempFilePath}"`, async (error, stdout, stderr) => {
      // Clean up the temp file
      try {
        await fs.unlink(tempFilePath);
      } catch (unlinkErr) {
        console.error('Failed to clean up temp file:', unlinkErr);
      }

      if (error) {
        // compiler returned exit code != 0, return compilation errors from stderr
        return res.json({
          success: false,
          output: stdout,
          error: stderr || error.message,
        });
      }

      // Transpilation succeeded
      res.json({
        success: true,
        output: stdout,
        error: stderr, // might contain warnings
      });
    });

  } catch (err) {
    console.error('Transpilation error:', err);
    res.status(500).json({
      success: false,
      error: `Internal Server Error: ${err.message}`,
    });
  }
});

// API: Get available chapters
app.get('/api/chapters', async (req, res) => {
  const chaptersPath = path.join(__dirname, 'public', 'chapters.json');
  try {
    const data = await fs.readFile(chaptersPath, 'utf-8');
    res.json(JSON.parse(data));
  } catch (err) {
    res.status(500).json({ error: 'Failed to read chapters metadata' });
  }
});

// ---------------------------------------------------------------------------
// Live reload (dev): a Server-Sent Events stream that fires whenever anything
// under public/ changes. The client (index.html) opens an EventSource to this
// endpoint and reloads the page on each message. Zero dependencies — uses the
// built-in fs.watch and the browser's native EventSource.
// ---------------------------------------------------------------------------
const liveClients = new Set();

app.get('/api/livereload', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  res.write('retry: 1000\n\n'); // tell EventSource to auto-reconnect after 1s
  liveClients.add(res);
  req.on('close', () => liveClients.delete(res));
});

// Watch public/ recursively (macOS supports recursive fs.watch). Debounce the
// burst of events an editor emits on a single save, then notify every browser.
let reloadTimer = null;
watch(PUBLIC_DIR, { recursive: true }, () => {
  clearTimeout(reloadTimer);
  reloadTimer = setTimeout(() => {
    for (const res of liveClients) res.write('data: reload\n\n');
  }, 100);
});

app.listen(PORT, 'localhost', () => {
  console.log(`Flash Language Tutorial running at http://localhost:${PORT}`);
});