dudi/docs/superpowers/specs/2026-05-08-js-module-split-design.md
Simon Kühn d28f87a3c4 Add design spec for JS module split
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:22:44 +02:00

3.1 KiB

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:

// 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:

// 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
  • varconst where the binding is never reassigned
  • varlet 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:

<!-- 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