5.2 KiB
ParenScript Conversion Experiment
- Overview
- Goals
- Current JavaScript Files
- Implementation Plan
- Benefits
- ParenScript Resources
- Lessons Learned
- Notes
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
.jsfiles 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 functionalitystatic/js/auth-ui.js- Authentication UIstatic/js/front-page.js- Front page interactionsstatic/js/player.js- Audio player controlsstatic/js/profile.js- User profile pagestatic/js/users.js- User management
Implementation Plan
Phase 1: Setup [DONE]
- Add ParenScript dependency to
asteroid.asd - Create
parenscript-utils.lispwith helper functions - Create experimental branch
Phase 2: Convert Simple Files First
- Convert
auth-ui.js(smallest, simplest) - COMPLETE ✅ - Convert
front-page.js(stream quality, now playing, pop-out, frameset) - COMPLETE ✅ - Convert
profile.js - Convert
users.js
Phase 3: Convert Complex Files
- Convert
player.js(audio player logic) - 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
ParenScript Resources
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.
Conversion Progress
- auth-ui.js (2025-11-06): Successfully converted. 1386 chars. All functionality working.
- front-page.js (2025-11-06): Successfully converted. 6900 chars. Stream quality, now playing, pop-out player, frameset mode all working.
front-page.js Conversion Notes
This was a more complex file with multiple features. Key learnings:
Global Variables
ParenScript uses (defvar *variable-name* value) for global variables:
(defvar *popout-window* nil)
String Concatenation
Use + operator for string concatenation:
(+ "width=" width ",height=" height)
Conditional Logic
Use cond for multiple conditions in route interception:
(cond ((string= path "js/auth-ui.js") ...) ((string= path "js/front-page.js") ...) (t ...))
Object Property Access
Use ps:getprop for dynamic property access:
(ps:getprop config encoding) ; config[encoding]
All features tested and working:
- Stream quality selector changes stream correctly
- Now playing updates every 10 seconds
- Pop-out player functionality works
- Frameset mode toggle works
- Auto-reconnect on stream errors works