Skip to content

Modernise stack: Vite + React 19 + TS + MUI v9#20

Open
pleb wants to merge 6 commits into
mainfrom
feat/modernise-stack
Open

Modernise stack: Vite + React 19 + TS + MUI v9#20
pleb wants to merge 6 commits into
mainfrom
feat/modernise-stack

Conversation

@pleb
Copy link
Copy Markdown

@pleb pleb commented May 15, 2026

Summary

End-to-end modernisation of the signature generator. CRA is unmaintained, the stack was mixing Bulma + Redux + class components for a tiny single-form app, the Snyk patch file dated from 2020, and every GitHub Action was pinned to a floating @v2 tag. This PR rebuilds on a current, secure toolchain while preserving the exact email-signature output that staff and Outlook care about.

Stack changes

Concern From To
Bundler react-scripts 5 (CRA, deprecated) Vite 8
Language JavaScript TypeScript 6 (strict)
React 18.2 19.2
Styling Bulma 0.9 + MUI 5 + Sass MUI v9 (Emotion) only
Form state Redux Toolkit + react-redux react-hook-form 7.75
Phone parsing react-phone-number-input libphonenumber-js (direct dep)
Tests Jest (CRA) Vitest 4 + RTL 17 + jsdom
Lint ESLint 8 (CRA preset) ESLint 9 flat config + typescript-eslint
Node 18 (CI), unpinned locally 22 LTS, pinned via .nvmrc + engines
Deploy JamesIves/github-pages-deploy-action -> gh-pages branch actions/upload-pages-artifact + actions/deploy-pages (OIDC, no gh-pages branch)
Action pinning floating @v2 full commit SHA pins, version in comment

Security & correctness

Surfaced and fixed during the migration:

  • Removed dangerouslySetInnerHTML in both Signature.tsx and RepliesAndForwards.tsx. Phone numbers now render as React text nodes with nbsp spans; no user-controlled string flows through __html any more.
  • Validate the email before interpolating into href={\mailto:${email}`}` (regex check; fall through to plain text on failure).
  • Async Clipboard API replaces document.execCommand('copy'). Returns Promise<boolean> so the Copy button can show an "error" state on rejection instead of silently flashing "Copied!".
  • CI gating: npm audit --audit-level=high --omit=dev now runs on every PR; production dep graph is clean (0 vulnerabilities).
  • Least privilege workflows: contents: read on the build job, id-token: write confined to the deploy job only.
  • Latent bugs fixed by strict TS: constants.brandGPTWLogo (undefined) -> constants.brandInfo.brandGPTWLogo; nested <tr> collapsed; parseMobile null guards.

Performance

  • Signature and RepliesAndForwards wrapped in React.memo.
  • SignatureContainer subscribes per-field via useWatch and defers the preview with useDeferredValue so typing stays responsive.
  • <link rel=\"preload\"> for the Azure-hosted brand logos in index.html.
  • Bundle: 236 KB gzipped main chunk (within target).

UX

  • Required * asterisks visible from initial render.
  • Clipboard rejection now surfaces a visible "Copy failed" state.
  • ?embedded=1 query param hides the AppBar for iframing.
  • Custom MUI theme matches the previous dark-navy AppBar (#000c28) and MakerX blue (#003FB5).

Test plan

  • npm run lint exits 0
  • npm run typecheck exits 0
  • npm test -- --run -> 22 / 22 passing (parseMobile, isValidEmail, stripObject, clipboard success+rejection, Form required asterisks, Button state transitions, Signature + RepliesAndForwards golden HTML)
  • npm run build clean (236 KB gzipped)
  • npm audit --audit-level=high --omit=dev -> 0 vulnerabilities
  • Dev server smoke-tested locally (HTTP 200, preload tags emit, ?embedded=1 hides AppBar)
  • Reviewer: paste the copied signature into Outlook desktop, Outlook web, and Gmail and confirm logo, links, and GPTW logo render correctly. This is the only thing automated tests cannot verify.

One-time deploy setup required

Before the first merge to main can deploy successfully, in repo Settings -> Pages -> Source, choose GitHub Actions (the native flow replaces the gh-pages branch). The CD workflow will fail with a clear error until this is done. README documents this in its own section.

The custom domain (signatures.makerx.tech) is preserved by public/CNAME, which ships in every artifact.

Out of scope / follow-ups

  • ESLint stays on v9.x for now because eslint-plugin-react has not yet published a v10-compatible release. Dependabot will pick this up when it lands.
  • MUI v10 (when it ships) will need a deliberate upgrade PR; the v9 -> v10 jump removes more deprecated APIs.

pleb added 6 commits May 15, 2026 14:18
Replace Create React App 5 with Vite 8, add TypeScript 6 strict mode,
ESLint 9 flat config, Prettier 3, and Node 22 pinning. Drop the stale
Snyk patch file (Lodash is gone) and the legacy .eslintrc / public/index.html
(now at repo root for Vite).
Replace the Bulma + Redux + class-component stack with MUI v9 components
and react-hook-form. Custom MUI theme matches the previous dark-navy AppBar
and MakerX blue.

Security and correctness fixes surfaced during the migration:
- remove dangerouslySetInnerHTML from Signature and RepliesAndForwards;
  phone numbers now render as React text nodes with nbsp spans
- validate the email before interpolating into the mailto href
- replace document.execCommand('copy') with the async Clipboard API,
  returning Promise<boolean> so the Button can show success vs error state
- collapse the latent nested <tr> bug in the standard signature
- fix constants.brandGPTWLogo (was undefined) to constants.brandInfo.brandGPTWLogo
- add null guards on parseMobile callers

Performance: Signature components wrapped in React.memo, SignatureContainer
subscribes per-field via useWatch and defers the preview render with
useDeferredValue so typing stays responsive.

Embed mode: ?embedded=1 hides the AppBar for iframing.

Inline-styled <table> markup in the signature components is intentionally
preserved; comments explaining the email-client constraints are kept.
Replaces the single Button snapshot from the old Jest setup with:

- parseMobile, isValidEmail, stripObject unit tests
- Clipboard helper tests covering both write success and write rejection
  paths, so the Button error state has coverage
- Form regression guard that the required asterisks render on initial mount
- Button state transition tests (success label, error label, auto-revert)
- Golden HTML assertions for Signature and RepliesAndForwards against a
  checked-in fixture; this is the primary regression gate against silent
  email-client output changes on future dep bumps
- Every action pinned to a full commit SHA with the version tag as a
  comment (checkout v6.0.2, setup-node v6.4.0, configure-pages v6.0.0,
  upload-pages-artifact v5.0.0, deploy-pages v5.0.0). SHAs re-verified
  via gh api before writing the workflows.
- CI gains audit, lint, typecheck, and test steps before build; permissions
  reduced to contents:read.
- CD migrates from JamesIves/github-pages-deploy-action + gh-pages branch
  to the native GitHub Pages flow (actions/configure-pages +
  upload-pages-artifact + deploy-pages). OIDC id-token:write is scoped to
  the deploy job only. Concurrency group prevents overlapping deploys.
- public/CNAME preserves the signatures.makerx.tech custom domain in
  every artifact, replacing the gh-pages branch that previously held it.
- Dependabot watches both npm and github-actions weekly, with minor+patch
  updates grouped per ecosystem.

First-time setup required: repo Settings -> Pages -> Source = "GitHub Actions".
Documented in README.
Drops the CRA boilerplate, documents the new Vite scripts, Node 22
requirement, embed mode (?embedded=1), and the one-time Pages source
toggle required for first deploy. Notes the absence of pre-commit hooks
and warns that MUI majors are not handled by grouped Dependabot patches.

Also commits docs/upgrading.md, the modernisation process notes kept by
the maintainer to drive future stack upgrades.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant