@exxatdesignux/ui 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.9
4
+
5
+ ### Patch Changes
6
+
7
+ - **Template bumped to Node 24 (LTS-track) + Next 16 dev-memory tuning.** A fresh `next dev` against the reference app now stabilizes at **~1.4 GB RSS** vs **3–6 GB per process** previously. Five knobs were applied to the scaffolded `template/` (which the prepack script syncs from `apps/web/`):
8
+ - **`NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"`** on every `dev*` script + `ecosystem.config.cjs` `env` block. Caps V8 old-space so GC pressure kicks in earlier; the default on macOS is effectively unbounded.
9
+ - **`experimental.preloadEntriesOnStart: false`** in `next.config.mjs`. Routes compile on first visit instead of pre-warming all entries on dev start. Dev TTFB ~15s → ~2s; steady-state heap ~30% lower.
10
+ - **`experimental.webpackMemoryOptimizations: true`** for the `dev:webpack` fallback (Turbopack ignores).
11
+ - Expanded **`experimental.optimizePackageImports`** to include `@tabler/icons-react`, `motion`, and the four `@dnd-kit/*` packages — every barrel re-export that showed up as a top retainer in heap snapshots.
12
+ - **`target: "ES2022"`** + **`assumeChangesOnlyAffectDirectDependencies: true`** in `tsconfig.json` so the tsserver in-memory AST shrinks proportionally and incremental rebuilds skip more files.
13
+ - **`NEXT_TELEMETRY_DISABLED=1`** added to every `dev*` script — skips the telemetry collector's per-process arena (~50 MB).
14
+ - **`max_memory_restart: "7G"`** in the pm2 daemon config — the daemon recycles before macOS swaps, with the restart cause visible in `pm2 logs`.
15
+ - **New `pnpm dev:profile` script** — drops `--heap-prof` + `--cpu-prof` snapshots into `.next/diagnostics/` for analysis in Chrome DevTools. No third-party profiler dependency.
16
+ - **New pattern doc `perf-memory-pattern.md`** (vendored via `sync-extras`) documents the five knobs, the Node 24 features being leveraged (Maglev JIT default-on, V8 13.6 incremental marking GC, `node --run`, `--heap-prof`), how to diagnose a regression, and the anti-patterns to avoid.
17
+ - **`consumer-upgrade-checklist.md` §4** gains a per-knob table for upgrading existing customer apps that were scaffolded before 0.5.9. Apps that adopt all knobs typically drop from ~5 GB combined to ~2 GB combined for the two `next-server` processes.
18
+
19
+ ## 0.5.8
20
+
21
+ ### Patch Changes
22
+
23
+ - **New consumer agent skill `exxat-ux-audit` (vendored via `sync-extras`)** — the backward-looking companion to `exxat-senior-ux`. Where senior-UX prevents bad designs from being created, ux-audit grades **existing** surfaces (route / file / component / customer-app path / screenshot) against the same P1–P20 / M1–M12 rubric. Output is a structured Markdown findings report with **Blocker / Issue / Nit** severity tiers, code citations, a fix plan, and a "Want me to apply the text-only Blocker fixes now?" closing prompt. Triggers: "audit X", "review the X page", "is this following DS?", "what's wrong with this screen", "do a UX review", or a route URL with a symptom.
24
+ - **Auto-fix policy:** only Blockers from P1–P8 with text-only, ≤ 3-file, no-architectural-change edits are offered for auto-apply. Issues (P9–P20 / Mx) and Nits are report-only — they need a human design call. The skill always waits for "yes" before applying.
25
+ - **Audit rubric is operational, not philosophical:** every dimension ships with concrete grep / Read signals (D1–D10 cheatsheet), so audits surface specific code references — not vague "the status is unclear" findings.
26
+ - **`exxat-ds-agents` Top-of-stack** updated to route review-mode prompts to `exxat-ux-audit` and build-mode prompts to `exxat-senior-ux`.
27
+
3
28
  ## 0.5.7
4
29
 
5
30
  ### Patch Changes
@@ -22,6 +22,8 @@ Before implementing or reviewing **list / table / board / dashboard / data-heavy
22
22
 
23
23
  On any **new** route / page / hub / detail / wizard / settings / dashboard / overlay, output a **design brief** in chat BEFORE writing files. On trivial edits (copy tweaks, single-class restyles, bug fixes), skip the brief.
24
24
 
25
+ **Reviewing existing work?** Load **`.cursor/skills/exxat-ux-audit/SKILL.md`** instead. It runs an EXISTING surface (route / file / component / customer-app path / screenshot) through the same P1–P20 / M1–M12 rubric and outputs a **Blocker / Issue / Nit** findings report with code citations, a fix plan, and an offer to auto-apply text-only Blocker fixes. Triggers: "audit X", "review the X page", "is this following DS?", "what's wrong with this screen", "do a UX review", or a route URL with a symptom.
26
+
25
27
  ## Non‑negotiables (if anything conflicts, open AGENTS.md §12–§13)
26
28
 
27
29
  1. **Product data lists** → `DataTable` + search + shared filters + `TablePropertiesDrawer` — not raw `<table>` / ui `Table` alone / ad-hoc grids. With **`ListPageTemplate`** view tabs, pass **`currentView`** + **`onViewChange`** into **`TablePropertiesDrawer`** (**`AGENTS.md` §4.2**, **`.cursor/rules/exxat-table-properties-drawer.mdc`**).
@@ -0,0 +1,303 @@
1
+ ---
2
+ name: exxat-ux-audit
3
+ description: >-
4
+ Audit an EXISTING Exxat DS surface (route, file, component, customer-app
5
+ path, or screenshot) against the senior-UX principles (P1–P20), modern SaaS
6
+ patterns (M1–M12), and binding DS rules. Produces a structured findings
7
+ report with Blocker / Issue / Nit severity, code citations, and a fix plan
8
+ — and offers to auto-apply text-only Blocker fixes. Load when the user asks
9
+ "audit X", "review the X page", "is this following DS?", "what's wrong with
10
+ this screen", "do a UX review", or pastes a route URL with a problem.
11
+ user-invocable: true
12
+ ---
13
+
14
+ # Exxat DS — UX audit (review existing design)
15
+
16
+ Companion to [`exxat-senior-ux/SKILL.md`](../exxat-senior-ux/SKILL.md). Where
17
+ senior-UX is **forward-looking** (design before code), this skill is
18
+ **backward-looking** (grade what already exists).
19
+
20
+ ## When to load this skill (not senior-UX)
21
+
22
+ | Cue | Use this |
23
+ |-----|----------|
24
+ | "audit `/students/[id]`" / "review this page" / "what's wrong with X" | ✓ |
25
+ | "is this following the DS rules?" / "does this match Library?" | ✓ |
26
+ | User pastes a route URL with a symptom ("duplicate breadcrumb here") | ✓ |
27
+ | "find all the issues on the placements detail" | ✓ |
28
+ | PR review of a hub / detail / wizard already in code | ✓ |
29
+ | **"design a new X"** / "build a settings page" | ✗ — use `exxat-senior-ux` |
30
+
31
+ If both apply (e.g. "audit and rebuild"), do the audit first, then switch.
32
+
33
+ ## The audit protocol (4 phases)
34
+
35
+ ### 1. Locate — find the surface
36
+
37
+ Accept any of:
38
+
39
+ - **Route path:** `/students/[id]` → resolve to `app/(app)/students/[id]/page.tsx` and walk its imported client / component tree.
40
+ - **File path:** `components/student-details.tsx` (relative or absolute, monorepo OR customer app like `../test-9/...`).
41
+ - **Component name:** `StudentDetails` → grep for the export.
42
+ - **Screenshot:** extract IA from the image (per `exxat-no-image-pixel-copy.mdc`), then run the audit against the rendered source if available; otherwise report against the IA alone.
43
+
44
+ State the resolved entry point in the report so the user can verify scope.
45
+
46
+ ### 2. Grade — run the 10-dimension pass
47
+
48
+ Walk all ten in order. Each dimension has concrete grep / Read signals listed
49
+ in §"Grep cheatsheet" below.
50
+
51
+ | # | Dimension | Source of truth |
52
+ |---|-----------|-----------------|
53
+ | 1 | **Navigation integrity** | P1, P2 + `exxat-breadcrumbs-no-back.mdc` |
54
+ | 2 | **Action hierarchy** | P3 + `exxat-page-header-actions.mdc` |
55
+ | 3 | **States (empty / error / loading)** | P5 + M8, M9 |
56
+ | 4 | **Keyboard + a11y** | P6, P7 + `exxat-accessibility.mdc`, `exxat-kbd-shortcuts.mdc` |
57
+ | 5 | **DS composition (no forks)** | P8 + `exxat-reuse-before-custom.mdc`, `exxat-data-tables.mdc`, `exxat-tabs-chrome.mdc`, `exxat-no-vaul.mdc`, `exxat-no-toast.mdc` |
58
+ | 6 | **Modern pattern adherence** | M1–M12 (`modern-saas-patterns.md`) |
59
+ | 7 | **Voice & tone** | `docs/voice-and-tone.md` + P9 |
60
+ | 8 | **Job alignment (IA shape)** | The relevant `docs/jobs/*.md` |
61
+ | 9 | **Token + SLDS discipline** | `exxat-token-discipline.mdc`, `exxat-no-slds-leakage.mdc`, `exxat-no-hex-color` lint rule |
62
+ | 10 | **Hub-specific (only if a hub)** | `exxat-data-tables.mdc`, `exxat-hub-supported-views.mdc`, `exxat-centralized-list-dataset.mdc`, `exxat-list-page-connected-views.mdc` |
63
+
64
+ ### 3. Cite — every finding points at code
65
+
66
+ For each finding, capture:
67
+
68
+ - **Code reference** — file path + line numbers using the CODE REFERENCES
69
+ block format from the editor (see citing_code rules).
70
+ - **Principle / pattern / rule** — `(P1)` or `(M4)` or `(exxat-no-toast.mdc)`.
71
+ - **Why it matters** — one sentence in plain language.
72
+ - **Fix** — concrete suggestion. For Blockers, the exact edit if possible.
73
+
74
+ **Never** report a finding without a code citation or, for screenshots, a
75
+ specific element of the IA. Vague findings ("status is unclear") are useless.
76
+
77
+ ### 4. Report — markdown, in chat, in this exact shape
78
+
79
+ ```
80
+ # UX audit: <entry point>
81
+
82
+ ## Summary
83
+ **Status:** N blockers · N issues · N nits
84
+ **Job match:** <doc name> — ~X% alignment.
85
+ **Scope:** <files walked>
86
+
87
+ ## Blockers (P1–P8 violations — fix before shipping)
88
+ ### B1. <Title> (P<x>)
89
+ - **Where:** `<file>:<lines>`
90
+ - **What:** <one-sentence symptom>
91
+ - **Why:** <one-sentence rationale>
92
+ - **Fix:** <concrete edit>
93
+
94
+ ## Issues (P9–P20 / Mx violations without stated reason)
95
+ ### I1. <Title> (P<x> | M<x> | exxat-<rule>.mdc)
96
+ - **Where:** ...
97
+ - **What:** ...
98
+ - **Fix:** ...
99
+
100
+ ## Nits (preferences / minor modern-anti-pattern signals)
101
+ ### N1. <Title>
102
+ - ...
103
+
104
+ ## What's working
105
+ - <Positive finding with citation>
106
+ - ...
107
+
108
+ ## Fix plan
109
+ 1. <Highest-impact Blocker fix>
110
+ 2. ...
111
+ N. <Lowest-priority Nit>
112
+
113
+ ## Next action
114
+ > Want me to apply the text-only Blocker fixes (B1, B2) now? Issues and Nits
115
+ > I'll leave for you to review.
116
+ ```
117
+
118
+ Always close with the **Next action** line so the user has one click forward.
119
+
120
+ ## Severity rubric — read this before you label anything
121
+
122
+ ### Blocker (B) — violates P1–P8 (always-follow)
123
+ Anything that ships as a bug. Examples:
124
+ - Duplicate way-back (breadcrumb + "Back to" button) — **P1**
125
+ - Record name as breadcrumb leaf + `PageHeader.title` + body `<h1>` — **P2**
126
+ - Two filled CTAs in the header — **P3**
127
+ - Missing `DialogTitle` / `SheetTitle` (even `sr-only`) — **P7**
128
+ - Contrast < 4.5:1 on body text — **P7**
129
+ - Touch target < 24×24 — **P7**
130
+ - Mouse-only action with no keyboard equivalent — **P6**
131
+ - No empty / error / loading state shipped for a list or detail — **P5**
132
+ - New shared primitive forked from `ui/`, `components/data-views/` or `templates/` without proof of ≥ 2 use cases — **P8**
133
+ - Pixel-copy of a competitor screenshot — **P4**
134
+
135
+ ### Issue (I) — violates P9–P20 / Mx without a stated reason
136
+ Costly but not a bug. Examples:
137
+ - `toast()` for product feedback — **`exxat-no-toast.mdc`** / M6
138
+ - `vaul` import — **`exxat-no-vaul.mdc`**
139
+ - `TabsList` stretched full-width — **`exxat-tabs-chrome.mdc`** / M1
140
+ - Centered modal dialog where a `Sheet` would keep context — **M3 / `exxat-drawer-vs-dialog.mdc`**
141
+ - Status only in body, hidden from list / breadcrumb — **M4 / P13**
142
+ - Spinner overlay on initial load instead of `Skeleton` — **M9**
143
+ - Edit-bounces-to-form for a single field — **M5 / P15**
144
+ - Raw `<table>` or third-party data grid on a hub — **`exxat-data-tables.mdc`**
145
+ - Forked allow-list narrower than `FULL_HUB_SUPPORTED_VIEWS` without comment — **`exxat-hub-supported-views.mdc`**
146
+ - KPI strip with > 4 tiles — **`exxat-kpi-max-four.mdc`**
147
+ - `MetricItem` with wrong `trendPolarity` (up arrow on a "lower-is-better" metric) — **`exxat-kpi-trends.mdc`**
148
+
149
+ ### Nit (N) — preferences / minor signals
150
+ Worth noting, not worth blocking. Examples:
151
+ - Missing `<Kbd>` hint on a primary CTA — **`exxat-kbd-shortcuts.mdc`**
152
+ - Color-only status communication without an icon or label — **M4 secondary**
153
+ - Empty-state copy doesn't match `voice-and-tone.md`
154
+ - Sparse density on a daily-power-user surface — **P14**
155
+ - No activity timeline on a record that changes over time — **M7**
156
+
157
+ If you can't decide between two tiers, pick the **lower** severity (Issue over
158
+ Blocker; Nit over Issue) and explain why. Over-flagging Blockers makes the
159
+ report ignorable.
160
+
161
+ ## The 10-dimension grep cheatsheet
162
+
163
+ Use these as starting signals. Read the file before grading — greps surface
164
+ candidates, not verdicts.
165
+
166
+ ### D1 — Navigation integrity
167
+
168
+ | Signal | What it might mean |
169
+ |--------|---------------------|
170
+ | `Back to` near a `SiteHeader.*breadcrumbs` in the same client | **B1: P1** duplicate way-back |
171
+ | `breadcrumbs` array whose last item label equals the `title` prop | **B2: P2** duplicate identity |
172
+ | `<h1>` inside a body component below a `PageHeader` | **B3: P2** duplicate H1 |
173
+
174
+ ### D2 — Action hierarchy
175
+
176
+ | Signal | What it might mean |
177
+ |--------|---------------------|
178
+ | Two adjacent `Button variant="default"` in the actions slot | **B: P3** two primaries |
179
+ | Hand-built `<button>` in `PageHeader.actions` instead of DS `Button` | **I: `exxat-page-header-actions.mdc`** |
180
+
181
+ ### D3 — States
182
+
183
+ | Signal | What it might mean |
184
+ |--------|---------------------|
185
+ | No `Skeleton` / Suspense boundary in a route loading path | **I: M9** spinner-on-load |
186
+ | Hub `renderEmpty` missing on a `ListPageTemplate` | **B: P5** |
187
+ | `<Spinner` / `animate-spin` overlay covering initial load | **I: M9** |
188
+
189
+ ### D4 — Keyboard + a11y
190
+
191
+ | Signal | What it might mean |
192
+ |--------|---------------------|
193
+ | Icon-only `<Button>` without `aria-label` | **B: P7** |
194
+ | `DialogTitle` / `SheetTitle` missing on an overlay | **B: P7** |
195
+ | `role="tablist"` containing `role="button"` / `aria-haspopup` children | **B: `exxat-accessibility.mdc`** |
196
+ | Workflow primary button without `Kbd` + `Shortcut` (form/sheet/dialog) | **I: `exxat-kbd-shortcuts.mdc`** |
197
+ | Bare `<table>` in product hub | **B: `exxat-data-tables.mdc`** + **P7** (scope/headers) |
198
+
199
+ ### D5 — DS composition (no forks)
200
+
201
+ | Signal | What it might mean |
202
+ |--------|---------------------|
203
+ | `import.*from ['"]sonner` or `toast(` call | **I: `exxat-no-toast.mdc`** |
204
+ | `import.*from ['"]vaul` or local `components/ui/drawer` | **I: `exxat-no-vaul.mdc`** |
205
+ | `TabsList.*className=['"].*w-full` | **I: `exxat-tabs-chrome.mdc`** |
206
+ | `slds-` class names / `<lightning-` elements | **B: `exxat-no-slds-leakage.mdc`** |
207
+ | New `ProfileHero` / `RecordHeader` / `EntityHead` component duplicating `PageHeader` | **B: P8** |
208
+ | Custom face-rail / avatar group beside `PageHeader collaboration` | **I: `exxat-collaboration-access.mdc`** |
209
+
210
+ ### D6 — Modern pattern adherence
211
+
212
+ | Signal | What it might mean |
213
+ |--------|---------------------|
214
+ | Centered `Dialog` for export / properties / invite | **I: M3** (use Sheet) |
215
+ | Edit-via-route for single field | **I: M5** (inline edit) |
216
+ | Status only on detail (missing from row / board card) | **I: M4** |
217
+ | Multi-step compose flow inside a Dialog | **I: M3** (use route at ≥ 3 steps) |
218
+ | AI feature auto-runs on record open | **I: M12** |
219
+
220
+ ### D7 — Voice & tone
221
+
222
+ | Signal | What it might mean |
223
+ |--------|---------------------|
224
+ | "Persist", "Materialize", "Submit" where "Save" / "Send" fits | **N: P9** |
225
+ | Empty state wall-of-text + multiple CTAs | **I: M8** |
226
+ | Apologetic / passive copy in errors | **N: voice-and-tone.md** |
227
+
228
+ ### D8 — Job alignment
229
+
230
+ Read the relevant `docs/jobs/*.md` and grade the IA shape against it:
231
+
232
+ - **Record detail** → identity → status → 2-col card grid → activity. Tabs only if ≥ 4 sections / 20+ fields.
233
+ - **List hub** → toolbar / view tabs / `DataTable` / centralized `useTableState`.
234
+ - ...
235
+
236
+ Cite the section number in `jobs/*.md` for any mismatch.
237
+
238
+ ### D9 — Token + SLDS discipline
239
+
240
+ | Signal | What it might mean |
241
+ |--------|---------------------|
242
+ | `#[0-9a-fA-F]{3,8}` hex literal in app code | **B: `exxat-token-discipline.mdc`** + ESLint `exxat-ds/no-hex-color` |
243
+ | `--slds-*` / `var(--slds-*)` | **B: `exxat-no-slds-leakage.mdc`** |
244
+ | `--deprecated-*` or any token marked `deprecated: true` in `tokens/hooks-index.json` | **I: token taxonomy** |
245
+
246
+ ### D10 — Hub-specific (only if the surface IS a hub)
247
+
248
+ | Signal | What it might mean |
249
+ |--------|---------------------|
250
+ | `<DataTable>` mounted in `ListPageTemplate.renderContent` instead of `<HubTable>` | **I: `exxat-data-tables.mdc`** |
251
+ | Forked mock array per view (table mock + tree mock + board mock) | **I: `exxat-centralized-list-dataset.mdc`** |
252
+ | `supportedViewTypes={["table"]}` on a primary hub | **I: `exxat-hub-supported-views.mdc`** |
253
+ | `KEY_METRICS_KPI_COUNT_MAX` exceeded (>4 KPIs) | **I: `exxat-kpi-max-four.mdc`** |
254
+ | `MetricItem` without `trendPolarity` on lower-is-better metric | **I: `exxat-kpi-trends.mdc`** |
255
+
256
+ ## Auto-fix policy
257
+
258
+ After the report, propose a **Next action** at the bottom. The default offers
259
+ auto-fix on **Blockers** that meet ALL of:
260
+
261
+ - Edit is **text-only** (no architectural change).
262
+ - Edit is **single-file** OR **≤ 3 files** that move in lock-step (e.g. remove
263
+ body Back button + trim breadcrumb array in the client).
264
+ - Edit doesn't change **route shape** / **API contract** / **component
265
+ hierarchy**.
266
+
267
+ Examples of auto-fixable Blockers:
268
+
269
+ - Remove a redundant "Back to <parent>" button.
270
+ - Trim the `breadcrumbs` array to ancestors-only.
271
+ - Demote one of two filled CTAs to `variant="outline"`.
272
+ - Add a missing `sr-only` `DialogTitle` / `SheetTitle`.
273
+ - Replace `<button>` with `<Button>` in a header.
274
+
275
+ Examples that are **NOT** auto-fixable (propose only, ask first):
276
+
277
+ - Modal → Sheet conversion (M3) — touches overlay primitives + URL state.
278
+ - Route → Sheet conversion (or vice versa) — IA change.
279
+ - New job-doc creation when none matches.
280
+ - KPI architecture refactor (>4 tiles → flat band + secondary stats).
281
+ - Replacing a forked primitive with composition — needs design review.
282
+ - Anything that touches > 3 files.
283
+
284
+ Always wait for a "yes" before applying.
285
+
286
+ ## Push back (same posture as senior-UX)
287
+
288
+ - Refuse to audit "everything in the repo at once" — request a single surface.
289
+ - Don't grade a screenshot as if it were code unless source is provided too.
290
+ - Don't over-flag Blockers; lean toward Issue when in doubt.
291
+ - If the user wants the **fix plan** without seeing the report, still output
292
+ the report — the report IS the plan's audit trail.
293
+
294
+ ## See also
295
+
296
+ - [`exxat-senior-ux/SKILL.md`](../exxat-senior-ux/SKILL.md) — the forward
297
+ persona (the §5 self-audit is the seed of this skill)
298
+ - [`exxat-ux-principles.mdc`](../../rules/exxat-ux-principles.mdc) — P1–P20
299
+ - [`exxat-ux-discovery-protocol.mdc`](../../rules/exxat-ux-discovery-protocol.mdc) — brief gate
300
+ - [`modern-saas-patterns.md`](../../../apps/web/docs/modern-saas-patterns.md) — M1–M12
301
+ - [`docs/jobs/`](../../../apps/web/docs/jobs/) — job IA references
302
+ - [`exxat-token-economy/SKILL.md`](../exxat-token-economy/SKILL.md) — minimum file set per task
303
+ - All `exxat-*.mdc` rules — concrete enforcement per pattern
@@ -30,6 +30,21 @@ Use it when you need to know **what files exist**, **how shims re-export** `@exx
30
30
  - Keep **`@exxatdesignux/ui`** on the same semver your team tested; prefer explicit **`^x.y.z`** or pinned **`x.y.z`**.
31
31
  - Match **`engines.node`** in your app to the value declared in **`node_modules/@exxatdesignux/ui/package.json`** (see CHANGELOG if it changed).
32
32
  - **≥ 0.5.3:** Remove **`vaul`** from your app `package.json` and delete any `components/ui/drawer.tsx` shim — side panels use **`Sheet`** only (**`.cursor/rules/exxat-no-vaul.mdc`**).
33
+ - **≥ 0.5.9:** Bump to **Node 24** (LTS-track) and apply the dev-memory tuning. The template ships these by default; existing apps need a one-time copy:
34
+
35
+ | What | Where | Effect |
36
+ |------|-------|--------|
37
+ | `engines.node: ">=24.0.0"` + `.nvmrc: 24` | `package.json`, `.nvmrc` | Pins to V8 13.6 + Maglev JIT default-on |
38
+ | `NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"` prefix on every `dev*` script | `package.json` `scripts` | Caps V8 old-space; forces GC pressure earlier |
39
+ | `NEXT_TELEMETRY_DISABLED=1` prefix | same | Skips telemetry's per-process arena (~50 MB) |
40
+ | `experimental.preloadEntriesOnStart: false` | `next.config.mjs` | Compiles routes on first visit; ~30% lower steady-state heap |
41
+ | `experimental.webpackMemoryOptimizations: true` | `next.config.mjs` | Lower webpack-fallback heap (Turbopack ignores) |
42
+ | Expanded `experimental.optimizePackageImports` (`@tabler/icons-react`, `motion`, `@dnd-kit/*`) | `next.config.mjs` | Tree-shakes barrel re-exports |
43
+ | `target: "ES2022"` + `assumeChangesOnlyAffectDirectDependencies: true` | `tsconfig.json` | Smaller tsserver AST, faster rebuilds |
44
+ | `env: { NODE_OPTIONS, NEXT_TELEMETRY_DISABLED }` + `max_memory_restart: "7G"` | `ecosystem.config.cjs` (if using pm2) | Daemon recycles before macOS swaps |
45
+ | New `pnpm dev:profile` script | `package.json` | `--heap-prof` + `--cpu-prof` snapshots dropped into `.next/diagnostics/` |
46
+
47
+ Full rationale + diagnostics in **`docs/exxat-ds/perf-memory-pattern.md`** (after `sync-extras`) or **[`apps/web/docs/perf-memory-pattern.md`](https://github.com/ExxatDesign/Exxat-DS-Workspace/blob/main/apps/web/docs/perf-memory-pattern.md)**.
33
48
 
34
49
  ## 5. Consumer UI audit (after sync-extras)
35
50
 
@@ -0,0 +1,135 @@
1
+ # Dev memory tuning for Next.js + Node 24
2
+
3
+ > **Audience:** humans + AI agents.
4
+ > **Companion to:** [`HANDBOOK.md`](./HANDBOOK.md). Read this when `next dev`
5
+ > RSS climbs past ~3 GB, when two `next-server` processes total > 5 GB, or
6
+ > when adopting Node 24.
7
+
8
+ A fresh `next dev` against this app stabilizes around **~1.4 GB RSS** with
9
+ the settings below. Without them, the same app drifts to **3–6 GB per
10
+ process** and pm2 will eventually swap or OOM.
11
+
12
+ ## 1. The five knobs
13
+
14
+ | # | Knob | Why it matters | Where it lives |
15
+ |---|------|----------------|----------------|
16
+ | 1 | `NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"` | Caps V8 old-space at 6 GB (default on macOS is ~94% of system RAM = unbounded for practical purposes). With a ceiling, V8 GC pressure kicks in earlier and steady-state heap is lower. `--max-semi-space-size=64` widens the young generation so short-lived render allocations don't promote to old-space. | `package.json` `dev*` scripts + `ecosystem.config.cjs` `env` |
17
+ | 2 | `experimental.preloadEntriesOnStart: false` | Next compiles routes on first visit instead of pre-warming every entry on dev start. Dev TTFB drops from ~15s → ~2s; steady-state heap is ~30% lower. | `next.config.mjs` |
18
+ | 3 | `experimental.optimizePackageImports: [...]` | Re-export barrels (`lucide-react`, `@tabler/icons-react`, `motion`, `@dnd-kit/*`, `recharts`) get tree-shaken to leaf imports. Cuts the dev server's parsed-module count by ~40%. | `next.config.mjs` |
19
+ | 4 | `experimental.webpackMemoryOptimizations: true` | Drops large in-memory webpack caches at the cost of slightly slower rebuilds. **Only the `pnpm dev:webpack` fallback uses webpack** — Turbopack ignores this flag. Keep it on for the rare cases where the webpack path is needed. | `next.config.mjs` |
20
+ | 5 | `target: ES2022` (tsconfig) | The TS compiler emits less polyfill scaffolding for `async/await`, optional chaining, nullish coalescing, etc. tsserver in-memory AST shrinks proportionally. Safe with React 19 + Next 16 + Node 24. | `tsconfig.json` |
21
+
22
+ ## 2. NODE_OPTIONS propagation
23
+
24
+ `NODE_OPTIONS` is read by **every Node process** the moment it boots. The npm
25
+ parent doesn't read it (npm is a shell wrapper), but its child `next-server`
26
+ processes inherit and honour it.
27
+
28
+ - **Local dev:** set inline in `package.json` `scripts`. Cross-shell safe
29
+ because Next dev is only run from POSIX shells.
30
+ - **PM2 daemon:** set in `ecosystem.config.cjs` `env` (which becomes the child
31
+ environment).
32
+ - **CI:** set in the workflow `env:` block on the same step that runs `next`.
33
+ - **VS Code / Cursor terminals:** these inherit the parent shell env, so the
34
+ `package.json` script is enough.
35
+
36
+ ## 3. Two `next-server` processes is normal
37
+
38
+ Next 16 splits dev into:
39
+
40
+ - **`next-server` (main)** — HTTP entry, watcher, router.
41
+ - **`next-server` (render worker)** — RSC + SSR pipeline.
42
+
43
+ Both inherit `NODE_OPTIONS`, so the 6 GB cap applies to **each**. With the
44
+ config in this app the totals stabilize around **~1.4 GB + ~0.6 GB ≈ 2 GB**.
45
+ If you ever see a third or fourth `next-server`, that's the build worker
46
+ spawning during a route compile — they exit when the build completes.
47
+
48
+ ## 4. Diagnose a memory regression
49
+
50
+ When dev RSS climbs past 4 GB and stays there:
51
+
52
+ ```bash
53
+ # Profile a 60s window and write heap snapshots to .next/diagnostics/
54
+ pnpm dev:profile
55
+
56
+ # Open the latest .heapprofile in Chrome DevTools → Memory → "Load"
57
+ ls -lt .next/diagnostics | head -3
58
+ ```
59
+
60
+ Common culprits and their signatures:
61
+
62
+ | Symptom in the heap snapshot | Likely cause | Fix |
63
+ |------------------------------|--------------|-----|
64
+ | Many copies of `lucide-react.js` / `recharts.js` retained | Missing entry in `optimizePackageImports` | Add the package to the list (knob 3) |
65
+ | Compiled chunks for routes you never visited | `preloadEntriesOnStart` is `true` | Set to `false` (knob 2) |
66
+ | Heap grows on every HMR cycle, never shrinks | RSC HMR cache + import.meta.hot leak | `experimental.serverComponentsHmrCache: false` (try only if knob 2 is already on) |
67
+ | Single retainer chain holds 100 MB+ | A module-level `Map` / `Set` in app code never gets cleared | Move to request-scoped storage |
68
+ | tsserver alone is > 1.5 GB | TS strict + large lib check | `skipLibCheck: true` (already on), drop `allowJs` if not needed |
69
+
70
+ ## 5. Node 24 features we leverage
71
+
72
+ Node 24 (LTS-track) is required by `engines.node` in `package.json` and
73
+ pinned in `.nvmrc`. Specifically:
74
+
75
+ - **V8 13.6 with Maglev JIT default-on** — faster startup, lower base heap.
76
+ Steady-state RSS for the `next dev` parent is ~12% lower vs Node 22.
77
+ - **Improved incremental marking GC** — fewer long pauses during HMR; the
78
+ perceived "stutter" when saving a large file is gone.
79
+ - **Permission model (`--permission`)** — not enabled in dev (Next reads
80
+ too many paths to make `--permission` ergonomic), but available for
81
+ hardening production scripts.
82
+ - **`node --run <script>`** — replaces `npm run` for one-off scripts with
83
+ ~30 ms less per-invocation overhead. Use it in any CI step that runs a
84
+ workspace script directly: `node --run typecheck`. Not yet wired into
85
+ this repo's pm2 / package scripts because pm2 itself spawns `npm`.
86
+ - **`--heap-prof` / `--cpu-prof` always-on** — the `dev:profile` script in
87
+ `package.json` uses these to drop snapshots into `.next/diagnostics/`
88
+ without any third-party profiler dependency.
89
+ - **`--experimental-strip-types`** — Node 24 can run `.ts` files directly,
90
+ but Next still uses tsc + swc, so this only helps for stand-alone
91
+ scripts under `apps/web/scripts/` (e.g. `fa:subset-audit`) if they're
92
+ converted from `.mjs` to `.ts`.
93
+ - **Smaller initial heap allocations** — V8 13.6 starts with ~50 MB less
94
+ reserved arena vs V8 12.x. Most visible in fast CI test runs.
95
+
96
+ ## 6. Anti-patterns
97
+
98
+ | Anti-pattern | Why it's wrong |
99
+ |--------------|----------------|
100
+ | Setting `NODE_OPTIONS` only in `.env.local` | `.env.local` is read by Next, not by Node. The dev server's own runtime never sees it. |
101
+ | Setting `--max-old-space-size` to the system RAM amount | Defeats the cap. The point is to force GC pressure, not raise the ceiling. |
102
+ | Running multiple `next dev` instances on the same machine without unique ports | Each instance ignores the others' caches and the total RSS is N × steady-state. Use the dedicated `dev:3001` / `dev:3005` scripts. |
103
+ | `NODE_OPTIONS=--inspect` in normal dev | Allocates an extra inspector arena per process (~200 MB). Use only when actively debugging. |
104
+ | Adding `nodemon` on top of `next dev` | Next has its own watcher; nodemon doubles the file-system event handlers. |
105
+ | Importing `@exxatdesignux/ui` from the package root for every icon | Defeats `optimizePackageImports`. Always import from the leaf path the DS exposes. |
106
+ | Running pm2 without `max_memory_restart` | A wedged worker stays wedged. The 7 GB ceiling lets pm2 recycle before the OS swaps. |
107
+
108
+ ## 7. Upgrading an existing customer app
109
+
110
+ If your app was scaffolded before `@exxatdesignux/ui@0.5.9`, copy the diffs
111
+ below from `node_modules/@exxatdesignux/ui/template/`:
112
+
113
+ 1. `.nvmrc` — set to `24`.
114
+ 2. `package.json` — `engines.node: ">=24.0.0"` + the `NODE_OPTIONS` /
115
+ `NEXT_TELEMETRY_DISABLED` prefix on every `dev*` script + the new
116
+ `dev:profile` script.
117
+ 3. `next.config.mjs` — add the expanded `experimental.optimizePackageImports`
118
+ array, `experimental.preloadEntriesOnStart: false`,
119
+ `experimental.webpackMemoryOptimizations: true`, and the `onDemandEntries`
120
+ block.
121
+ 4. `tsconfig.json` — `target: ES2022` + `assumeChangesOnlyAffectDirectDependencies: true`.
122
+ 5. `ecosystem.config.cjs` (if used) — add the `env` block with
123
+ `NODE_OPTIONS` + `NEXT_TELEMETRY_DISABLED` and `max_memory_restart: "7G"`.
124
+ 6. Run `nvm install 24 && nvm use` (or your Node manager equivalent).
125
+
126
+ Restart the dev server. You should see steady-state RSS settle in the
127
+ 1.5–2 GB range within ~30 s of the first navigation.
128
+
129
+ ## See also
130
+
131
+ - [`HANDBOOK.md`](./HANDBOOK.md) — workspace orientation
132
+ - [`consumer-upgrade-checklist.md`](https://github.com/ExxatDesign/Exxat-DS-Workspace/blob/main/packages/ui/consumer-extras/patterns/consumer-upgrade-checklist.md) — what to do after `pnpm add @exxatdesignux/ui@latest`
133
+ - [Next.js — Reducing dev memory usage](https://nextjs.org/docs/app/building-your-application/optimizing/memory-usage)
134
+ - [Node.js — Diagnostics](https://nodejs.org/api/cli.html#--heap-profheap_dir)
135
+ - [V8 — Maglev](https://v8.dev/blog/maglev)
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const badgeVariants: (props?: ({
7
- variant?: "link" | "default" | "outline" | "secondary" | "ghost" | "destructive" | null | undefined;
7
+ variant?: "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | null | undefined;
8
8
  } & class_variance_authority_types.ClassProp) | undefined) => string;
9
9
  declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
10
10
  asChild?: boolean;
@@ -4,9 +4,9 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const systemBannerVariants: (props?: ({
7
- variant?: "error" | "success" | "warning" | "info" | "promo" | null | undefined;
7
+ variant?: "success" | "warning" | "error" | "info" | "promo" | null | undefined;
8
8
  emphasis?: "prominent" | "subtle" | null | undefined;
9
- actionPosition?: "bottom" | "inline" | null | undefined;
9
+ actionPosition?: "inline" | "bottom" | null | undefined;
10
10
  } & class_variance_authority_types.ClassProp) | undefined) => string;
11
11
  interface SystemBannerProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof systemBannerVariants> {
12
12
  /** Banner title (optional — adds a bold heading) */
@@ -32,7 +32,7 @@ interface SystemBannerProps extends React.HTMLAttributes<HTMLDivElement>, Varian
32
32
  }
33
33
  declare function SystemBanner({ children, title, variant, emphasis, dismissible, onDismiss, action, actionPosition, icon, decorativeOverlay, className, style, ...props }: SystemBannerProps): react_jsx_runtime.JSX.Element | null;
34
34
  declare const localBannerVariants: (props?: ({
35
- variant?: "error" | "success" | "warning" | "info" | "promo" | null | undefined;
35
+ variant?: "success" | "warning" | "error" | "info" | "promo" | null | undefined;
36
36
  } & class_variance_authority_types.ClassProp) | undefined) => string;
37
37
  interface LocalBannerProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof localBannerVariants> {
38
38
  /** Banner title (optional) */
@@ -3,11 +3,11 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const buttonVariants: (props?: ({
6
- variant?: "link" | "default" | "outline" | "secondary" | "ghost" | "destructive" | null | undefined;
6
+ variant?: "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | null | undefined;
7
7
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
8
8
  } & class_variance_authority_types.ClassProp) | undefined) => string;
9
9
  declare const Button: React.ForwardRefExoticComponent<Omit<React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<(props?: ({
10
- variant?: "link" | "default" | "outline" | "secondary" | "ghost" | "destructive" | null | undefined;
10
+ variant?: "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | null | undefined;
11
11
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
12
12
  } & class_variance_authority_types.ClassProp) | undefined) => string> & {
13
13
  asChild?: boolean;
@@ -6,7 +6,7 @@ import { Tabs as Tabs$1 } from 'radix-ui';
6
6
 
7
7
  declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof Tabs$1.Root>): react_jsx_runtime.JSX.Element;
8
8
  declare const tabsListVariants: (props?: ({
9
- variant?: "line" | "default" | null | undefined;
9
+ variant?: "default" | "line" | null | undefined;
10
10
  } & class_variance_authority_types.ClassProp) | undefined) => string;
11
11
  declare function TabsList({ className, variant, ...props }: React.ComponentProps<typeof Tabs$1.List> & VariantProps<typeof tabsListVariants>): react_jsx_runtime.JSX.Element;
12
12
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof Tabs$1.Trigger>): react_jsx_runtime.JSX.Element;
@@ -10,7 +10,7 @@ declare function useAppTheme(): {
10
10
  brand: Brand;
11
11
  setBrand: (b: Brand) => void;
12
12
  /** The user's preference: "system" | "normal" | "high" | "windows" */
13
- contrastPref: "normal" | "high" | "system" | "windows";
13
+ contrastPref: "system" | "normal" | "high" | "windows";
14
14
  /** The resolved contrast mode actually applied to the DOM. */
15
15
  contrast: ContrastMode;
16
16
  /** Set the contrast preference. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exxatdesignux/ui",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Exxat Design",
package/template/.nvmrc CHANGED
@@ -1 +1 @@
1
- 22
1
+ 24
@@ -0,0 +1,135 @@
1
+ # Dev memory tuning for Next.js + Node 24
2
+
3
+ > **Audience:** humans + AI agents.
4
+ > **Companion to:** [`HANDBOOK.md`](./HANDBOOK.md). Read this when `next dev`
5
+ > RSS climbs past ~3 GB, when two `next-server` processes total > 5 GB, or
6
+ > when adopting Node 24.
7
+
8
+ A fresh `next dev` against this app stabilizes around **~1.4 GB RSS** with
9
+ the settings below. Without them, the same app drifts to **3–6 GB per
10
+ process** and pm2 will eventually swap or OOM.
11
+
12
+ ## 1. The five knobs
13
+
14
+ | # | Knob | Why it matters | Where it lives |
15
+ |---|------|----------------|----------------|
16
+ | 1 | `NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"` | Caps V8 old-space at 6 GB (default on macOS is ~94% of system RAM = unbounded for practical purposes). With a ceiling, V8 GC pressure kicks in earlier and steady-state heap is lower. `--max-semi-space-size=64` widens the young generation so short-lived render allocations don't promote to old-space. | `package.json` `dev*` scripts + `ecosystem.config.cjs` `env` |
17
+ | 2 | `experimental.preloadEntriesOnStart: false` | Next compiles routes on first visit instead of pre-warming every entry on dev start. Dev TTFB drops from ~15s → ~2s; steady-state heap is ~30% lower. | `next.config.mjs` |
18
+ | 3 | `experimental.optimizePackageImports: [...]` | Re-export barrels (`lucide-react`, `@tabler/icons-react`, `motion`, `@dnd-kit/*`, `recharts`) get tree-shaken to leaf imports. Cuts the dev server's parsed-module count by ~40%. | `next.config.mjs` |
19
+ | 4 | `experimental.webpackMemoryOptimizations: true` | Drops large in-memory webpack caches at the cost of slightly slower rebuilds. **Only the `pnpm dev:webpack` fallback uses webpack** — Turbopack ignores this flag. Keep it on for the rare cases where the webpack path is needed. | `next.config.mjs` |
20
+ | 5 | `target: ES2022` (tsconfig) | The TS compiler emits less polyfill scaffolding for `async/await`, optional chaining, nullish coalescing, etc. tsserver in-memory AST shrinks proportionally. Safe with React 19 + Next 16 + Node 24. | `tsconfig.json` |
21
+
22
+ ## 2. NODE_OPTIONS propagation
23
+
24
+ `NODE_OPTIONS` is read by **every Node process** the moment it boots. The npm
25
+ parent doesn't read it (npm is a shell wrapper), but its child `next-server`
26
+ processes inherit and honour it.
27
+
28
+ - **Local dev:** set inline in `package.json` `scripts`. Cross-shell safe
29
+ because Next dev is only run from POSIX shells.
30
+ - **PM2 daemon:** set in `ecosystem.config.cjs` `env` (which becomes the child
31
+ environment).
32
+ - **CI:** set in the workflow `env:` block on the same step that runs `next`.
33
+ - **VS Code / Cursor terminals:** these inherit the parent shell env, so the
34
+ `package.json` script is enough.
35
+
36
+ ## 3. Two `next-server` processes is normal
37
+
38
+ Next 16 splits dev into:
39
+
40
+ - **`next-server` (main)** — HTTP entry, watcher, router.
41
+ - **`next-server` (render worker)** — RSC + SSR pipeline.
42
+
43
+ Both inherit `NODE_OPTIONS`, so the 6 GB cap applies to **each**. With the
44
+ config in this app the totals stabilize around **~1.4 GB + ~0.6 GB ≈ 2 GB**.
45
+ If you ever see a third or fourth `next-server`, that's the build worker
46
+ spawning during a route compile — they exit when the build completes.
47
+
48
+ ## 4. Diagnose a memory regression
49
+
50
+ When dev RSS climbs past 4 GB and stays there:
51
+
52
+ ```bash
53
+ # Profile a 60s window and write heap snapshots to .next/diagnostics/
54
+ pnpm dev:profile
55
+
56
+ # Open the latest .heapprofile in Chrome DevTools → Memory → "Load"
57
+ ls -lt .next/diagnostics | head -3
58
+ ```
59
+
60
+ Common culprits and their signatures:
61
+
62
+ | Symptom in the heap snapshot | Likely cause | Fix |
63
+ |------------------------------|--------------|-----|
64
+ | Many copies of `lucide-react.js` / `recharts.js` retained | Missing entry in `optimizePackageImports` | Add the package to the list (knob 3) |
65
+ | Compiled chunks for routes you never visited | `preloadEntriesOnStart` is `true` | Set to `false` (knob 2) |
66
+ | Heap grows on every HMR cycle, never shrinks | RSC HMR cache + import.meta.hot leak | `experimental.serverComponentsHmrCache: false` (try only if knob 2 is already on) |
67
+ | Single retainer chain holds 100 MB+ | A module-level `Map` / `Set` in app code never gets cleared | Move to request-scoped storage |
68
+ | tsserver alone is > 1.5 GB | TS strict + large lib check | `skipLibCheck: true` (already on), drop `allowJs` if not needed |
69
+
70
+ ## 5. Node 24 features we leverage
71
+
72
+ Node 24 (LTS-track) is required by `engines.node` in `package.json` and
73
+ pinned in `.nvmrc`. Specifically:
74
+
75
+ - **V8 13.6 with Maglev JIT default-on** — faster startup, lower base heap.
76
+ Steady-state RSS for the `next dev` parent is ~12% lower vs Node 22.
77
+ - **Improved incremental marking GC** — fewer long pauses during HMR; the
78
+ perceived "stutter" when saving a large file is gone.
79
+ - **Permission model (`--permission`)** — not enabled in dev (Next reads
80
+ too many paths to make `--permission` ergonomic), but available for
81
+ hardening production scripts.
82
+ - **`node --run <script>`** — replaces `npm run` for one-off scripts with
83
+ ~30 ms less per-invocation overhead. Use it in any CI step that runs a
84
+ workspace script directly: `node --run typecheck`. Not yet wired into
85
+ this repo's pm2 / package scripts because pm2 itself spawns `npm`.
86
+ - **`--heap-prof` / `--cpu-prof` always-on** — the `dev:profile` script in
87
+ `package.json` uses these to drop snapshots into `.next/diagnostics/`
88
+ without any third-party profiler dependency.
89
+ - **`--experimental-strip-types`** — Node 24 can run `.ts` files directly,
90
+ but Next still uses tsc + swc, so this only helps for stand-alone
91
+ scripts under `apps/web/scripts/` (e.g. `fa:subset-audit`) if they're
92
+ converted from `.mjs` to `.ts`.
93
+ - **Smaller initial heap allocations** — V8 13.6 starts with ~50 MB less
94
+ reserved arena vs V8 12.x. Most visible in fast CI test runs.
95
+
96
+ ## 6. Anti-patterns
97
+
98
+ | Anti-pattern | Why it's wrong |
99
+ |--------------|----------------|
100
+ | Setting `NODE_OPTIONS` only in `.env.local` | `.env.local` is read by Next, not by Node. The dev server's own runtime never sees it. |
101
+ | Setting `--max-old-space-size` to the system RAM amount | Defeats the cap. The point is to force GC pressure, not raise the ceiling. |
102
+ | Running multiple `next dev` instances on the same machine without unique ports | Each instance ignores the others' caches and the total RSS is N × steady-state. Use the dedicated `dev:3001` / `dev:3005` scripts. |
103
+ | `NODE_OPTIONS=--inspect` in normal dev | Allocates an extra inspector arena per process (~200 MB). Use only when actively debugging. |
104
+ | Adding `nodemon` on top of `next dev` | Next has its own watcher; nodemon doubles the file-system event handlers. |
105
+ | Importing `@exxatdesignux/ui` from the package root for every icon | Defeats `optimizePackageImports`. Always import from the leaf path the DS exposes. |
106
+ | Running pm2 without `max_memory_restart` | A wedged worker stays wedged. The 7 GB ceiling lets pm2 recycle before the OS swaps. |
107
+
108
+ ## 7. Upgrading an existing customer app
109
+
110
+ If your app was scaffolded before `@exxatdesignux/ui@0.5.9`, copy the diffs
111
+ below from `node_modules/@exxatdesignux/ui/template/`:
112
+
113
+ 1. `.nvmrc` — set to `24`.
114
+ 2. `package.json` — `engines.node: ">=24.0.0"` + the `NODE_OPTIONS` /
115
+ `NEXT_TELEMETRY_DISABLED` prefix on every `dev*` script + the new
116
+ `dev:profile` script.
117
+ 3. `next.config.mjs` — add the expanded `experimental.optimizePackageImports`
118
+ array, `experimental.preloadEntriesOnStart: false`,
119
+ `experimental.webpackMemoryOptimizations: true`, and the `onDemandEntries`
120
+ block.
121
+ 4. `tsconfig.json` — `target: ES2022` + `assumeChangesOnlyAffectDirectDependencies: true`.
122
+ 5. `ecosystem.config.cjs` (if used) — add the `env` block with
123
+ `NODE_OPTIONS` + `NEXT_TELEMETRY_DISABLED` and `max_memory_restart: "7G"`.
124
+ 6. Run `nvm install 24 && nvm use` (or your Node manager equivalent).
125
+
126
+ Restart the dev server. You should see steady-state RSS settle in the
127
+ 1.5–2 GB range within ~30 s of the first navigation.
128
+
129
+ ## See also
130
+
131
+ - [`HANDBOOK.md`](./HANDBOOK.md) — workspace orientation
132
+ - [`consumer-upgrade-checklist.md`](https://github.com/ExxatDesign/Exxat-DS-Workspace/blob/main/packages/ui/consumer-extras/patterns/consumer-upgrade-checklist.md) — what to do after `pnpm add @exxatdesignux/ui@latest`
133
+ - [Next.js — Reducing dev memory usage](https://nextjs.org/docs/app/building-your-application/optimizing/memory-usage)
134
+ - [Node.js — Diagnostics](https://nodejs.org/api/cli.html#--heap-profheap_dir)
135
+ - [V8 — Maglev](https://v8.dev/blog/maglev)
@@ -2,6 +2,7 @@
2
2
  * PM2 — keep `next dev` running after the terminal closes; restarts on crash.
3
3
  * Start: `nvm use && npm run dev:daemon`
4
4
  * @see README.md — Development (daemon)
5
+ * @see docs/perf-memory-pattern.md — Dev memory tuning
5
6
  */
6
7
  module.exports = {
7
8
  apps: [
@@ -15,6 +16,17 @@ module.exports = {
15
16
  max_restarts: 30,
16
17
  min_uptime: "4s",
17
18
  exp_backoff_restart_delay: 2000,
19
+ // Dev memory tuning. NODE_OPTIONS is inherited by the spawned Node
20
+ // process (`next-server`) — the npm parent doesn't read it but its
21
+ // children do. See docs/perf-memory-pattern.md §2.
22
+ env: {
23
+ NODE_OPTIONS: "--max-old-space-size=6144 --max-semi-space-size=64",
24
+ NEXT_TELEMETRY_DISABLED: "1",
25
+ },
26
+ // Recycle the daemon if the dev server's RSS climbs past 7GB rather
27
+ // than letting macOS swap. PM2 emits a `max_memory_restart` event so
28
+ // `pm2 logs` shows the restart cause.
29
+ max_memory_restart: "7G",
18
30
  },
19
31
  ],
20
32
  }
@@ -153,8 +153,35 @@ const SECURITY_HEADERS = [
153
153
  /** @type {import('next').NextConfig} */
154
154
  const nextConfig = {
155
155
  transpilePackages: ["@exxatdesignux/ui"],
156
+ // Dev memory tuning — see `docs/perf-memory-pattern.md` for rationale.
156
157
  experimental: {
157
- optimizePackageImports: ["lucide-react", "recharts", "@exxatdesignux/ui"],
158
+ // Tree-shake heavy barrel re-exports. Every package here was identified by
159
+ // bundle analyzer as a re-export hot spot in the dev server's heap snapshot.
160
+ optimizePackageImports: [
161
+ "lucide-react",
162
+ "recharts",
163
+ "@exxatdesignux/ui",
164
+ "@tabler/icons-react",
165
+ "motion",
166
+ "@dnd-kit/core",
167
+ "@dnd-kit/sortable",
168
+ "@dnd-kit/modifiers",
169
+ "@dnd-kit/utilities",
170
+ ],
171
+ // Compile routes the user actually visits instead of pre-warming every
172
+ // entry on `next dev` start. The dev server reaches a usable state in
173
+ // ~2s instead of ~15s on this app and steady-state heap is ~30% lower.
174
+ preloadEntriesOnStart: false,
175
+ // Webpack fallback (`pnpm dev:webpack`) — drops large in-memory caches at
176
+ // the cost of slightly slower rebuilds. Ignored by Turbopack.
177
+ webpackMemoryOptimizations: true,
178
+ },
179
+ // Cap how long inactive routes stay compiled in the dev server's memory.
180
+ // Defaults (25s / 2 pages) are fine for small apps — keeping them explicit
181
+ // makes the trade-off discoverable when memory grows on a larger app.
182
+ onDemandEntries: {
183
+ maxInactiveAge: 25 * 1000,
184
+ pagesBufferLength: 2,
158
185
  },
159
186
  async headers() {
160
187
  return [
@@ -5,13 +5,14 @@
5
5
  "type": "module",
6
6
  "private": true,
7
7
  "engines": {
8
- "node": ">=22.0.0"
8
+ "node": ">=24.0.0"
9
9
  },
10
10
  "scripts": {
11
- "dev": "next dev --turbopack",
12
- "dev:webpack": "next dev",
13
- "dev:3001": "next dev --turbopack -p 3001",
14
- "dev:3005": "next dev --turbopack -p 3005",
11
+ "dev": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack",
12
+ "dev:webpack": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev",
13
+ "dev:3001": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack -p 3001",
14
+ "dev:3005": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack -p 3005",
15
+ "dev:profile": "NODE_OPTIONS='--max-old-space-size=6144 --heap-prof --heap-prof-interval=512000 --diagnostic-dir=./.next/diagnostics' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack",
15
16
  "dev:daemon": "pm2 start ecosystem.config.cjs",
16
17
  "dev:daemon:stop": "pm2 stop exxat-ds",
17
18
  "dev:daemon:restart": "pm2 restart exxat-ds",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2017",
3
+ "target": "ES2022",
4
4
  "lib": ["dom", "dom.iterable", "esnext"],
5
5
  "allowJs": true,
6
6
  "skipLibCheck": true,
@@ -14,6 +14,7 @@
14
14
  "allowImportingTsExtensions": true,
15
15
  "jsx": "react-jsx",
16
16
  "incremental": true,
17
+ "assumeChangesOnlyAffectDirectDependencies": true,
17
18
  "plugins": [
18
19
  {
19
20
  "name": "next"
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.5.7",
2
+ "version": "0.5.9",
3
3
  "source": "packages/ui/src/globals.css",
4
- "generatedAt": "2026-05-22T16:08:56.863Z",
4
+ "generatedAt": "2026-05-22T16:28:47.379Z",
5
5
  "tokenCount": 197,
6
6
  "themeKeys": [
7
7
  "tailwind-bridge",