#+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 ✅ - [X] Convert =profile.js= (user profile, stats, history) - COMPLETE ✅ - [X] Convert =users.js= (user management, admin) - COMPLETE ✅ ** 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. - *profile.js* (2025-11-06): Successfully converted. Profile data, listening stats, recent tracks, top artists, password change all working. - *users.js* (2025-11-06): Successfully converted. User stats, user list, role changes, activate/deactivate, create user 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 ** profile.js and users.js Conversion Notes *** Modulo Operator ParenScript doesn't support =%= for modulo. Use =rem= (remainder) instead: #+BEGIN_EXAMPLE ;; WRONG: (% seconds 3600) ;; CORRECT: (rem seconds 3600) #+END_EXAMPLE *** Property Access with Hyphens For properties with hyphens (like ="last-login"=), use =ps:getprop=: #+BEGIN_EXAMPLE (ps:getprop user "last-login") ;; Instead of (ps:@ user last-login) #+END_EXAMPLE *** Template Literals in HTML Generation Build HTML strings with =+= concatenation: #+BEGIN_EXAMPLE (+ "