169 lines
5.2 KiB
Org Mode
169 lines
5.2 KiB
Org Mode
#+TITLE: ParenScript Conversion Experiment
|
|
#+AUTHOR: Glenn
|
|
#+DATE: 2025-11-06
|
|
|
|
* 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]
|
|
- [X] Add ParenScript dependency to =asteroid.asd=
|
|
- [X] Create =parenscript-utils.lisp= with helper functions
|
|
- [X] Create experimental branch
|
|
|
|
** Phase 2: Convert Simple Files First
|
|
- [X] Convert =auth-ui.js= (smallest, simplest) - COMPLETE ✅
|
|
- [X] 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
|
|
|
|
- [[https://parenscript.common-lisp.dev/][ParenScript Documentation]]
|
|
- [[https://github.com/vsedach/Parenscript][ParenScript GitHub]]
|
|
|
|
* 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:*
|
|
#+BEGIN_EXAMPLE
|
|
(defparameter *my-js*
|
|
(ps:ps* '(progn ...)))
|
|
|
|
(defun generate-my-js ()
|
|
*my-js*)
|
|
#+END_EXAMPLE
|
|
*Promise chains instead of async/await:*
|
|
#+BEGIN_EXAMPLE
|
|
(ps:chain (fetch url)
|
|
(then (lambda (response) (ps:chain response (json))))
|
|
(then (lambda (data) (process data)))
|
|
(catch (lambda (error) (handle error))))
|
|
#+END_EXAMPLE
|
|
*Intercept static route:*
|
|
#+BEGIN_EXAMPLE
|
|
(define-page static #@"/static/(.*)" (:uri-groups (path))
|
|
(if (string= path "js/my-file.js")
|
|
(serve-parenscript)
|
|
(serve-static-file)))
|
|
#+END_EXAMPLE
|
|
|
|
* 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:
|
|
#+BEGIN_EXAMPLE
|
|
(defvar *popout-window* nil)
|
|
#+END_EXAMPLE
|
|
|
|
*** String Concatenation
|
|
Use =+= operator for string concatenation:
|
|
#+BEGIN_EXAMPLE
|
|
(+ "width=" width ",height=" height)
|
|
#+END_EXAMPLE
|
|
|
|
*** Conditional Logic
|
|
Use =cond= for multiple conditions in route interception:
|
|
#+BEGIN_EXAMPLE
|
|
(cond
|
|
((string= path "js/auth-ui.js") ...)
|
|
((string= path "js/front-page.js") ...)
|
|
(t ...))
|
|
#+END_EXAMPLE
|
|
|
|
*** Object Property Access
|
|
Use =ps:getprop= for dynamic property access:
|
|
#+BEGIN_EXAMPLE
|
|
(ps:getprop config encoding) ; config[encoding]
|
|
#+END_EXAMPLE
|
|
|
|
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
|