Skip to content

Core upgrade: Angular 9 → Angular 20 (framework + build config)#417

Open
tobydrinkall wants to merge 2 commits into
masterfrom
devin/1782947720-angular20-core-upgrade
Open

Core upgrade: Angular 9 → Angular 20 (framework + build config)#417
tobydrinkall wants to merge 2 commits into
masterfrom
devin/1782947720-angular20-core-upgrade

Conversation

@tobydrinkall

@tobydrinkall tobydrinkall commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Foundational wave of the Angular 9 → 20 upgrade. Jumps the core framework and build toolchain directly to Angular 20 (TypeScript 5.8, zone.js 0.15, tslib 2), keeping code changes to the minimum needed to make ng build (dev + production, incl. service worker) compile.

Key decisions/changes:

  • @angular/platform-browser-dynamic no longer ships with v20 — main.ts bootstraps via platformBrowser() from @angular/platform-browser; dropped unused @angular/animations.
  • Angular 19+ defaults components/pipes to standalone — added explicit standalone: false to all 12 declarables so existing NgModules keep working (NgModule → standalone conversion is deferred to a later wave).
  • angular.json: removed defunct options (extractCss, aot, es5-era flags, defaultProject), browserTargetbuildTarget; test target drops main/src/test.ts (CLI auto-discovers specs now).
  • karma: karma-coverage-istanbul-reporterkarma-coverage; jasmine/karma stack bumped for Angular 20's karma builder.
  • tsconfig: target/module es2022, moduleResolution: bundler, skipLibCheck (needed for unfetch's d.ts), useDefineForClassFields: false.
  • Switched to npm lockfile (removed stale yarn.lock, committed package-lock.json).

Deliberately deferred to follow-up waves: RxJS 7 + removal of rxjs-compat/deep imports, deprecated subscribe(next, err, complete) signatures, tslint→eslint migration, protractor removal, adding unit tests, Sass @import/division deprecations.

Verified: npm run build and ng build --configuration production succeed on Node 22.

Link to Devin session: https://app.devin.ai/sessions/7d72297591cd4aaa8bfa196a17d071fa
Requested by: @tobydrinkall


Open in Devin Review

devin-ai-integration Bot and others added 2 commits July 1, 2026 23:18
Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
@tobydrinkall tobydrinkall self-assigned this Jul 1, 2026
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

Open in Devin Review

Comment thread src/main.ts
Comment on lines 8 to +11
enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
platformBrowser().bootstrapModule(AppModule)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Bootstrap method changed from JIT-capable to AOT-only

The switch from platformBrowserDynamic().bootstrapModule() to platformBrowser().bootstrapModule() removes the JIT compiler from the bundle. This works correctly because Angular CLI uses AOT by default (mandatory since Angular 13), and Ivy compiles decorators into static fields at build time. However, this means any future attempt to use JIT compilation (e.g., runtime template compilation via Compiler.compileModuleAsync()) would silently fail. Since the app doesn't use JIT features, this is safe and produces a slightly smaller bundle, but it's a subtle behavioral contract change worth noting.

(Refers to lines 2-11)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread tsconfig.json
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"module": "es2022",
"moduleResolution": "bundler",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Deep rxjs imports rely on rxjs-compat with new moduleResolution

The tsconfig.json changes moduleResolution from "node" to "bundler". Two files still use legacy deep imports: import { Subscription } from 'rxjs/Subscription' at src/app/item-details/item-details.component.ts:4 and src/app/user/user.component.ts:4, plus import { Observable } from 'rxjs/Observable' at src/app/shared/services/hackernews-api.service.ts:2. These resolve via the rxjs-compat package. Since rxjs 6.6.7 has no exports field in its package.json, TypeScript with moduleResolution: "bundler" falls back to file-based resolution, which should find the compat shims. However, if rxjs-compat is ever removed, all three imports would break. The canonical imports (from 'rxjs') are already used in src/app/feeds/feed/feed.component.ts:2-3, making the inconsistency evident.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional for this foundational wave — rxjs-compat is kept so the deep imports keep resolving. The follow-up RxJS wave (based on this branch) upgrades to rxjs 7, removes rxjs-compat, and rewrites all deep imports to from 'rxjs'.

Comment thread package.json
Comment on lines +14 to +25
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@angular/service-worker": "^20.0.0",
"rxjs": "~6.6.7",
"rxjs-compat": "^6.6.7",
"tslib": "^2.6.0",
"unfetch": "^4.1.0",
"zone.js": "~0.10.2"
"zone.js": "~0.15.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Removed @angular/animations dependency is an optional peer dep

The PR removes @angular/animations from package.json dependencies. @angular/platform-browser lists it as a peer dependency, but the package-lock.json confirms it's marked optional: true in peerDependenciesMeta. A codebase-wide grep confirms no imports of @angular/animations, BrowserAnimationsModule, or NoopAnimationsModule anywhere in src/. So this removal is safe. If animations support is needed in the future, it must be re-added explicitly.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread tsconfig.json
Comment on lines +8 to +13
"module": "es2022",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"importHelpers": true,
"target": "es2015",
"target": "es2022",
"useDefineForClassFields": false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Removed emitDecoratorMetadata may affect non-Angular DI libraries

The tsconfig.json removes emitDecoratorMetadata: true. Angular with Ivy does not need this setting — it uses its own compilation pipeline to generate metadata. However, if any third-party library in the project relies on TypeScript's reflect-metadata for decorator-based dependency injection (e.g., some testing utilities or IoC containers), those would break silently. A quick check of the codebase shows only Angular decorators are used, so this is safe for the current codebase.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration

Copy link
Copy Markdown

E2E test results — full migration (waves #417 + #418 + #419 + #420 merged locally)

Merged all four wave branches into a local integration branch, ran lint/build/test, then served the app and clicked through the golden paths.

Results

  • ✅ Bootstrap + news feed renders (//news/1, 30 numbered stories)
  • ✅ Tab navigation + pagination ("More ›" → /news/2, numbering continues at 31)
  • ✅ Item details with nested comments (lazy /item module)
  • ⚠️ User profile — untested: upstream node-hnapi.herokuapp.com/user/:id returns 404 for every user (verified via curl; external API issue, not a migration break — lazy module loads and error UI renders gracefully)
  • ✅ Theme switch to AMOLED Black (validates Wave 3 Sass @use/color.adjust rework)
Screenshots
🟢 News feed (default theme) 🟢 Page 2 numbering starts at 31
News feed News page 2
🟢 Item details with comments 🟢 AMOLED Black theme
Item details AMOLED feed
Shell evidence (integration branch)
npm run lint  → All files pass linting.
npm run build → exit 0
npm test      → Chrome Headless 137: Executed 6 of 6 SUCCESS

Notes: merging #418 after #419/#420 produces a package-lock.json conflict — regenerate with npm install.

Devin session

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