Migrate Angular 9 Hacker News PWA to React 19 + Vite#365
Conversation
- Add Vite/React-TS toolchain (package.json, vite.config.ts, tsconfig*, eslint.config.js)
- Install react-router-dom v6 and vite-plugin-pwa
- Root index.html with React HN metadata, manifest link, PWA icons
- Minimal App.tsx/main.tsx placeholder shell
- Create src/{components,shared,styles} directory skeleton (.gitkeep)
- Copy assets + manifest.json into public/
- Exclude legacy src/app (kept as porting reference) from tsc/eslint
- Switch lockfile from yarn to npm
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…nt CSS modules Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…devin/phase2b-styles' and 'origin/devin/phase2c-shared-ui' into devin/1781803653-angular-to-react-migration
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…st render Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…' and 'origin/devin/phase3e-api-hooks' into devin/1781803653-angular-to-react-migration
…-details Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…/devin/phase4g-feed', 'origin/devin/phase4h-itemdetails' and 'origin/devin/phase4i-user' into devin/1781803653-angular-to-react-migration
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…rigin/devin/phase5k-pwa' into devin/1781803653-angular-to-react-migration
- Delete angular.json, tslint.json, karma.conf.js, ngsw-config.json, browserslist, tsconfig.spec.json, e2e/, src/app/, src/environments/, src/assets/, and Angular src entry files (main.ts, polyfills.ts, test.ts, index.html, styles.scss, manifest.json, favicon.ico) - Remove dead Vite scaffold CSS (src/index.css, src/App.css) - Trim now-dead Angular paths from tsconfig.app.json exclude and eslint ignores - Update .travis.yml to Node 22 + npm ci/lint/build (drop @angular/cli) - Rewrite README build section for Vite + vite-plugin-pwa; Angular -> React references Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Original prompt from Eashan
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| a { | ||
| text-decoration: none; | ||
| font-weight: bold; | ||
| } | ||
|
|
||
| a:hover { | ||
| text-decoration: underline; | ||
| } |
There was a problem hiding this comment.
🚩 Unscoped tag selectors in CSS module files leak globally
In src/styles/FeedPage.module.css:5-8 and src/styles/FeedPage.module.css:14-17, bare tag selectors like a { ... } and ol { ... } are used. CSS modules only mangle class-based selectors; tag selectors remain global. This means these styles (e.g., a { text-decoration: none; font-weight: bold }) apply to ALL <a> and <ol> elements in the document, not just those within FeedPage. The same issue exists in src/styles/ItemDetailsPage.module.css with bare a, p, ul, and li selectors. This replicates the Angular component-scoped behavior (ViewEncapsulation) but CSS modules don't provide the same scoping for tag selectors. It works here because the old styles were broadly similar, but it could cause surprising style bleed if new components are added.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
There was a problem hiding this comment.
Acknowledged — this is intentional. The bare a/ol/li tag selectors in the .module.css files are global (CSS Modules only mangle class selectors), which deliberately reproduces the broadly-global styling the original Angular app relied on. As you note, it works correctly here because these element styles match the prior behavior. I'm leaving it as-is to keep the port faithful; tightening these into class-scoped selectors would be a reasonable cleanup follow-up if we want true per-component isolation, but it's out of scope for the 1:1 migration.
| <Route path="/:feedType" element={<FeedPage />} /> | ||
| <Route path="/:feedType/:page" element={<FeedPage />} /> |
There was a problem hiding this comment.
🚩 Route /:feedType acts as a catch-all and may match unintended paths
The route /:feedType at src/routes.tsx:29 is a catch-all dynamic segment. While React Router v6's specificity ranking ensures /item/:id and /user/:id take precedence over /:feedType for those paths, any other unknown path (e.g., /favicon.ico, /random) will match /:feedType and render FeedPage, which will attempt an API call to https://node-hnapi.herokuapp.com/random?page=1 and show an error. The old Angular app had explicit route definitions (news, newest, show, ask, jobs) so unknown paths would not accidentally render the feed component. Consider adding a catch-all 404 route or restricting /:feedType to known values.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This is intentional and matches the migration spec, which mapped Angular's {path: ':feedType/:page?'} to a dynamic React Router route rather than enumerating news|newest|show|ask|jobs. As you note, React Router specificity still ranks /item/:id and /user/:id above /:feedType, so only genuinely unknown single-segment paths fall through — and they degrade gracefully to FeedPage's existing error state (the same "could not load" UI a failed API call produces), not a crash. The Angular app's behavior here was effectively the same for an unknown feed slug. I'm leaving the dynamic route as-specified; constraining feedType to a known set (or adding an explicit 404) is a reasonable follow-up if we want unknown slugs to render a dedicated not-found page instead.
- getInitialTheme: migrate persisted 'amoledblack' -> 'amoled' so existing users keep the AMOLED theme (class is .amoled) - global.css: restore distinct visited-link color via var(--subtext-color), matching the original Angular theme mixin ($item-a-visited-color = subtext color) Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…ling The global '.nav a' color rule never matched because the <nav> element uses a CSS-module-scoped class (styles.nav). Moved the secondary-color rule into FeedPage.module.css scoped under .main-content so it applies to the mangled class and beats the global '.wrapper a:visited' specificity in both visited and unvisited states, matching the original Angular theme. Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…NTRIBUTING - FeedPage: add feedType to the scroll-to-top effect deps so browser back/forward between feed types (same page) scrolls to top, matching the original Angular component which re-ran ngOnInit on every route-params change. - CONTRIBUTING.md: replace Angular CLI / ng init / port 4200 / precache / static-serve setup with the React + Vite workflow (npm install, npm run dev, npm run lint, npm run build, npm run preview). Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
|
Re: the CONTRIBUTING.md not updated for React + Vite finding — fixed in 6dcfa5e. Replaced the Angular-specific setup (install Angular CLI,
Also updated the intro line ("little to no experience with Angular" → "React"). |
E2E test results — Angular→React migrationTested the full app on the dev server ( 7 of 8 tests passed. Lint + build green. One partial failure (T6) to decide on — details first: Warning T6 — scroll-to-top on feed-type change is only partial.
Results
T8 — PWA / offline (production build) — key evidenceService worker T5 — pagination accent color (regression fix)
T7 — mobile layout (412px), Default + NightHeader collapses, list full-width, settings panel usable, pagination floats to corner, theme switch works.
Notes
|
Summary
Full rewrite of the Angular 9 Hacker News PWA to React 19 + TypeScript + Vite 8, preserving behavior, routes, theming, and PWA/offline support. All Angular tooling (Angular CLI/Webpack, NGSW, Karma, Protractor, TSLint) is removed; the build is now
tsc -b && vite buildand the service worker + web manifest are generated byvite-plugin-pwa(Workbox). Output dir staysdist/, so Firebase hosting and deploy are unchanged.The work was orchestrated in 6 phases (parallel sub-tasks within each, merged at hard gates):
Loader,ErrorMessage)SettingsContext, API hooksArchitecture mapping
RouterModuleroutesreact-router-domv6<Routes>insrc/routes.tsxHackernewsApiService(RxJSObservable)useFeed/useItemDetails/useUserhooks (fetch+AbortController)SettingsServiceSettingsContext+useSettings()*.component.scss*.module.css(CSS Modules)_themes.scss(:root/.night/.amoled)src/styles/themes.cssCSS custom propertiesngsw-config.json)vite-plugin-pwa(WorkboxgenerateSW)Routing (
src/routes.tsx)App shell composition (
src/App.tsx):BrowserRouter > SettingsProvider > AppShell, whereAppShellapplies the theme class (<div className={settings.theme}>) and rendersHeader / Routes / Footer. GA pageview tracking is preserved viauseLocation()+useEffect.PWA (
vite.config.ts)VitePWA({ registerType: 'autoUpdate', manifest: { name: 'React HN', theme_color: '#b92b27', ... }, workbox: { runtimeCaching: [NetworkFirst for https://node-hnapi.herokuapp.com] } }). The staticpublic/manifest.jsonand the manual<link rel="manifest">are dropped in favor of the generated manifest. Build emitsdist/sw.js,dist/workbox-*.js,dist/manifest.webmanifest(27 precache entries).Notable porting decisions
crated_timefield name andtime_ago: numbertyping are carried over from the Angular models (flagged but intentionally unchanged to keep the port 1:1).'default' | 'night' | 'amoled'to match the ported CSS class names (Angular template used'amoledblack'for the AMOLED radio, but the SCSS class was.amoled).content?/text?to theStorytype — the item-details view reads these fields, which Angular's loose template typing allowed but strict TS requires.Cleanup / config
angular.json,tslint.json,karma.conf.js,ngsw-config.json,browserslist,tsconfig.spec.json,e2e/,src/app/,src/environments/,src/assets/(mirrored intopublic/assets/), and Angularsrcentry files (main.ts,polyfills.ts,test.ts,index.html,styles.scss,manifest.json,favicon.ico)..travis.yml: Node 22,npm ci && npm run lint && npm run build(dropped global@angular/cli); Firebase deploy unchanged.README.md: Angular → React, build section rewritten for Vite +vite-plugin-pwa.firebase.jsonalready targetsdistwith an SPA rewrite;package.jsonname isreact-hn.npm run lintandnpm run buildare both green.src/app/is intentionally gone (porting reference is in git history).Link to Devin session: https://app.devin.ai/sessions/4fc319e753e14842847693d72052042c
Requested by: @eashansinha
Devin Review
6dcfa5e