dudi/docs/superpowers/specs/2026-05-08-js-module-split-design.md

117 lines
3.1 KiB
Markdown
Raw Permalink Normal View History

# JS Module Split Design
## Goal
Split `public/app.js` (1025 lines, single file) into ES modules under `public/js/` — no build step, no npm. Modernize `var` to `const`/`let` in the process.
## Module Structure
```
public/js/
state.js shared mutable state object
i18n.js translations, locale logic
api.js fetch wrapper, loadGoals(), saveGoal()
goals.js pure goal calculations (calc, tOff, dcls, …)
ui.js tpl(), showToast(), overlay helpers, updateHeader()
auth.js login/register/password screens
sheets.js openNew(), openData(), openAdmin()
render.js buildCard(), render(), wire()
app.js entry point: init, URL params, stopwatch, midnight timer
```
## State Management
Single exported object in `state.js`. All modules import and mutate it directly:
```js
// state.js
export const state = {
goals: [],
userName: '',
isAdmin: false,
prefs: {},
selDay: {},
addAmt: {},
renamingId: null,
renameVal: '',
collapsed: {},
TODAY: new Date(),
};
```
Replaces all global `var` declarations at the top of `app.js`. Mutations like `goals = data` become `state.goals = data`.
## Dependency Graph
```
state.js ← all modules
i18n.js ← goals.js, ui.js, auth.js, sheets.js, render.js, app.js
api.js ← auth.js, sheets.js, app.js
goals.js ← render.js, sheets.js
ui.js ← auth.js, sheets.js, render.js
auth.js ← sheets.js, app.js
render.js ← app.js
```
## Circular Import Solution
`api.js` previously called `showLogin()` on 401 — creating a circular dependency with `auth.js`. Resolved via a Custom Event:
```js
// api.js — on 401:
document.dispatchEvent(new CustomEvent('session-expired'));
// app.js — on startup:
document.addEventListener('session-expired', () => showLogin());
```
Events are used only for this cross-cutting concern. All other communication is via direct imports.
## const/let Conversion
- ES modules run in strict mode automatically
- `var``const` where the binding is never reassigned
- `var``let` for loop counters and mutated locals
- State re-assignments (`goals = newArray`) → `state.goals = newArray`
- `TODAY` moves into `state.TODAY`, updated in `scheduleMidnight()` and `visibilitychange`
## HTML Change
`templates/app.html.twig`:
```html
<!-- before -->
<script src="{{ asset('app.js') }}"></script>
<!-- after -->
<script type="module" src="{{ asset('js/app.js') }}"></script>
```
`public/app.js` is deleted after the migration is complete.
## File Size Estimates
| File | Est. lines |
|---|---|
| state.js | ~15 |
| i18n.js | ~185 |
| api.js | ~35 |
| goals.js | ~60 |
| ui.js | ~35 |
| auth.js | ~130 |
| sheets.js | ~135 |
| render.js | ~185 |
| app.js | ~60 |
| **Total** | **~840** |
(Reduction due to removed comments and tighter const/let style.)
## Migration Strategy
One module at a time, keeping the app functional throughout:
1. Create `public/js/` directory
2. Write modules in dependency order: state → i18n → api → goals → ui → auth → sheets → render → app
3. Switch HTML to `<script type="module">` when all modules are ready
4. Delete old `public/app.js`