asteroid/PARENSCRIPT-EXPERIMENT.org

4.1 KiB

ParenScript Conversion Experiment

Overview

This branch experiments with converting all JavaScript files to ParenScript, allowing us to write client-side code in Common Lisp that compiles to JavaScript.

Goals

  • Replace all .js files with ParenScript equivalents
  • Maintain same functionality
  • Improve code maintainability by using one language (Lisp) for both frontend and backend
  • Take advantage of Lisp macros for client-side code generation

Current JavaScript Files

  • static/js/admin.js - Admin dashboard functionality
  • static/js/auth-ui.js - Authentication UI
  • static/js/front-page.js - Front page interactions
  • static/js/player.js - Audio player controls
  • static/js/profile.js - User profile page
  • static/js/users.js - User management

Implementation Plan

Phase 1: Setup [DONE]

  • Add ParenScript dependency to asteroid.asd
  • Create parenscript-utils.lisp with helper functions
  • Create experimental branch

Phase 2: Convert Simple Files First

  • Convert auth-ui.js (smallest, simplest) - COMPLETE
  • Convert profile.js
  • Convert users.js

Phase 3: Convert Complex Files

  • Convert player.js (audio player logic)
  • Convert front-page.js (pop-out player, frameset mode)
  • Convert admin.js (queue management, track controls)

Phase 4: Testing & Refinement

  • Test all functionality
  • Optimize generated JavaScript
  • Document ParenScript patterns used

Benefits

Code Reuse

  • Share utility functions between frontend and backend
  • Use same data structures and validation logic

Macros

  • Create domain-specific macros for common UI patterns
  • Generate repetitive JavaScript code programmatically

Type Safety

  • Catch more errors at compile time
  • Better IDE support with Lisp tooling

Maintainability

  • Single language for entire stack
  • Easier refactoring across frontend/backend boundary

Lessons Learned

auth-ui.js Conversion (2025-11-06)

Challenge 1: Route Precedence

Problem: Radiance routes are matched in load order, not definition order. The general static file route (/static/(.*)) was intercepting our specific ParenScript route.

Solution: Intercept the static file route and check if path is js/auth-ui.js. If yes, serve ParenScript; otherwise serve regular file.

Challenge 2: Async/Await Syntax

Problem: ParenScript doesn't support async/await syntax. Using (async lambda ...) generated invalid JavaScript.

Solution: Use promise chains with .then() instead of async/await.

Challenge 3: Compile Time vs Runtime

Problem: ParenScript compiler (ps:ps*) isn't available in saved binary at runtime.

Solution: Compile JavaScript at load time and store in a parameter. The function just returns the pre-compiled string.

Success Metrics

  • JavaScript compiles correctly (1386 characters)
  • No browser console errors
  • Auth UI works perfectly (show/hide elements based on login status)
  • Generated code is readable and maintainable

Key Patterns

Compile at load time:

(defparameter *my-js*
  (ps:ps* '(progn ...)))

(defun generate-my-js ()
  *my-js*)

Promise chains instead of async/await:

(ps:chain (fetch url)
          (then (lambda (response) (ps:chain response (json))))
          (then (lambda (data) (process data)))
          (catch (lambda (error) (handle error))))

Intercept static route:

(define-page static #@"/static/(.*)" (:uri-groups (path))
  (if (string= path "js/my-file.js")
      (serve-parenscript)
      (serve-static-file)))

Notes

This is an EXPERIMENTAL branch. The goal is to evaluate ParenScript for this project, not to immediately replace all JavaScript.

If successful, we can merge incrementally, one file at a time.

First Conversion Complete!

auth-ui.js successfully converted to ParenScript on 2025-11-06. All functionality working, no errors.