Skip to content

feat: upgrade Angular 9 → 20 (Wave 1 — core framework + build config)#426

Open
tobydrinkall wants to merge 3 commits into
masterfrom
upgrade/angular-core
Open

feat: upgrade Angular 9 → 20 (Wave 1 — core framework + build config)#426
tobydrinkall wants to merge 3 commits into
masterfrom
upgrade/angular-core

Conversation

@tobydrinkall

@tobydrinkall tobydrinkall commented Jul 2, 2026

Copy link
Copy Markdown

Summary

Foundational PR for the Angular 9→20 migration. Incrementally upgraded through every major version (9→10→11→12→13→14→15→16→17→18→19→20), verifying ng build at each step.

Final versions

Package Version
@angular/* 20.3.25
@angular/cli 20.3.31
@angular-devkit/build-angular 20.3.31
typescript 5.8.3
zone.js 0.15.0
tslib ^2.6.0
rxjs 6.5.4 (unchanged — Wave 2 handles RxJS 7)

Key config changes

angular.json — migrated to modern application builder (esbuild):

- "builder": "@angular-devkit/build-angular:browser"
- "main": "src/main.ts"
- "polyfills": "src/polyfills.ts"
- "browserTarget": "angular-hnpwa:build"
+ "builder": "@angular-devkit/build-angular:application"
+ "browser": "src/main.ts"
+ "polyfills": ["zone.js"]
+ "buildTarget": "angular-hnpwa:build"

Removed deprecated options: extractCss, aot, vendorChunk, buildOptimizer, defaultProject. Service worker config consolidated to "serviceWorker": "ngsw-config.json".

tsconfig.json — modernized to Angular 20 defaults:

- "target": "es2015"
- "module": "esnext"
- "emitDecoratorMetadata": true
- "experimentalDecorators": true
+ "target": "ES2022"
+ "module": "es2022"
+ "useDefineForClassFields": false
+ angularCompilerOptions { strictInjectionParameters, strictInputAccessModifiers }

browserslist.browserslistrc — dropped IE9-11 targeting, modern browser list only.

polyfills.ts — removed entirely; zone.js polyfill now in angular.json array.

test.tszone.js/dist/zone-testingzone.js/testing; test polyfills also moved to angular.json.

Minimum code changes for compilation

  • All 11 components + 1 pipe: added standalone: false (Angular 19+ defaults standalone to true).
  • hackernews-api.service.ts: fixed old-style RxJS imports (rxjs/Observablerxjs, rxjs/Subscriptionrxjs), removed rxjs-compat dependency, fixed TS7030 (not all code paths return a value).

Remaining waves (separate PRs rebased off this branch)

  • Wave 2: RxJS 6→7 migration, TypeScript strict-mode fixes
  • Wave 3: Component/template API breaking changes, Sass modernization
  • Wave 4: TSLint→ESLint, Karma/Jasmine updates, Protractor removal

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


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)
Open in Devin Review

Incrementally upgraded through all major versions: 9→10→11→12→13→14→15→16→17→18→19→20.

Package versions reached:
- @angular/* packages: 20.3.25
- @angular/cli: 20.3.31
- @angular-devkit/build-angular: 20.3.31
- TypeScript: 5.8.3
- zone.js: 0.15.0
- tslib: 2.6.0
- rxjs: 6.5.4 (unchanged, Wave 2 handles RxJS 7 migration)

Config changes:
- tsconfig.json: target→ES2022, module→es2022, added angularCompilerOptions,
  removed emitDecoratorMetadata/experimentalDecorators (Ivy does not need them)
- angular.json: removed deprecated extractCss, removed defaultProject
- browserslist → .browserslistrc with modern browser targets
- polyfills.ts: simplified, zone.js/dist/zone → zone.js
- test.ts: zone.js/dist/zone-testing → zone.js/testing
- Removed rxjs-compat (rxjs/Observable→rxjs, rxjs/Subscription→rxjs)
- Removed yarn.lock, added package-lock.json (npm)

Code changes (minimum for compilation):
- All components/pipes: added standalone: false (Angular 19+ default)
- hackernews-api.service.ts: fixed TS7030 (not all code paths return)

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
@tobydrinkall tobydrinkall self-assigned this Jul 2, 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 6 potential issues.

Open in Devin Review

Comment thread angular.json
Comment thread package.json
Comment on lines +53 to +58
if (cancelToken) { return undefined; }
return res.json()
.then(data => {
fetchObserver.next(data);
fetchObserver.complete();
});

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: The lazyFetch refactoring is logically equivalent to the original

The refactored lazyFetch function (hackernews-api.service.ts:53-58) inverts the condition from if (!cancelToken) { ... } to if (cancelToken) { return undefined; } followed by the main logic. This is a correct early-return transformation that preserves identical behavior. The explicit return undefined also satisfies the new noImplicitReturns: true tsconfig setting (tsconfig.json:10).

Open in Devin Review

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

Comment thread src/app/app.component.ts
standalone: false,
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']

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: All components use deprecated 'styleUrls' (plural) instead of 'styleUrl'

All 11 components in the project use styleUrls: ['./foo.component.scss'] (e.g., src/app/app.component.ts:13). Angular 17+ deprecated styleUrls in favor of styleUrl (singular string) when there is only one stylesheet. While styleUrls still works, since this PR is upgrading to Angular 20, it would be a good opportunity to modernize these to styleUrl. Similarly, templateUrl could be checked against the newer template inline option, though templateUrl remains fully supported.

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.

styleUrlsstyleUrl modernization is scoped to Wave 3 (component/template fixes, child session on branch upgrade/component-template).

Comment thread package.json
Comment on lines 44 to +47
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.7.5"
"typescript": "~5.8.3"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Incomplete upgrade: dev tooling dependencies left at very old versions

While Angular and TypeScript were upgraded, several dev dependencies remain at versions that are incompatible with Angular 20 and TypeScript 5.8: protractor (~5.4.0, deprecated and removed from Angular CLI), tslint (~5.15.0, deprecated in favor of ESLint), codelyzer (^5.1.2, depends on tslint), karma (~4.1.0), and @types/jasmine (~3.3.8). The angular.json still references @angular-devkit/build-angular:tslint for linting (angular.json:106) and @angular-devkit/build-angular:protractor for e2e (angular.json:119), both of which were removed from the Angular CLI build-angular package. Running ng lint or ng e2e will fail.

(Refers to lines 34-47)

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 — TSLint→ESLint migration, Karma/Jasmine updates, and Protractor removal are scoped to Wave 4 (child session on branch upgrade/test-lint). The lint/e2e builders in angular.json will be updated in that wave.

Comment thread package.json
"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.

🚩 zone.js version may not match Angular 20 requirements

The PR specifies zone.js: ~0.15.0 (package.json:27). Angular versions typically ship with a specific compatible zone.js version. Angular 18 used zone.js 0.14.x, and Angular 19+ made zone.js optional. For Angular 20, the required zone.js version should be verified against Angular's peer dependencies to ensure compatibility. The polyfill import path change from zone.js/dist/zone to zone.js (src/polyfills.ts:9) and zone.js/dist/zone-testing to zone.js/testing (src/test.ts:3) are correct for zone.js 0.11+.

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.

zone.js ~0.15.0 is correct — verified via npm show @angular/core@20.3.25 peerDependencies which specifies "zone.js": "~0.15.0".

- angular.json: builder browser → application, main → browser
- Polyfills moved to angular.json array (zone.js), removed polyfills.ts
- Removed deprecated options: aot, vendorChunk, buildOptimizer (automatic in esbuild)
- browserTarget → buildTarget in serve/extract-i18n configs
- serviceWorker + ngswConfigPath → serviceWorker: 'ngsw-config.json'
- Test polyfills also moved to angular.json array
- tsconfig.app.json: removed polyfills.ts from files array

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>

@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 3 new potential issues.

Open in Devin Review

Comment thread tsconfig.app.json
Comment thread package.json
"@angular/router": "~20.3.25",
"@angular/service-worker": "~20.3.25",
"node-fetch": "^2.6.0",
"rxjs": "~6.5.4",

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: RxJS 6.5 is old but still within Angular 20's peer dependency range

The PR keeps rxjs at ~6.5.4 while upgrading Angular to 20.x. Angular 20.3.25's @angular/core declares peerDependencies of rxjs: ^6.5.3 || ^7.4.0 (verified in package-lock.json), so this is technically compatible. However, RxJS 6.5 is quite old (released 2019) and lacks many bug fixes and features from RxJS 7.x. The removal of rxjs-compat (line 25 of old package.json) is correct since the codebase already uses RxJS 6-style imports. Consider upgrading to RxJS 7.x for long-term maintainability.

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.

Acknowledged — RxJS 7 upgrade is handled in Wave 2 (child session on upgrade/rxjs-ts-api).

Comment thread angular.json
Comment on lines +18 to +22
"builder": "@angular-devkit/build-angular:application",
"options": {
"aot": true,
"outputPath": "dist/angular-hnpwa",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"browser": "src/main.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Migration to application builder changes output directory structure

The builder was changed from @angular-devkit/build-angular:browser to @angular-devkit/build-angular:application (angular.json:18), and the entry point key changed from main to browser (line 22). The application builder (esbuild-based) produces a different output directory structure than the old browser builder (webpack-based): output goes into dist/angular-hnpwa/browser/ subdirectory rather than directly into dist/angular-hnpwa/. Any deployment scripts, CI pipelines (.travis.yml), or Firebase hosting config (firebase.json) that reference the old output path may need updating.

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.

Good catch — updated firebase.json public path from "dist" to "dist/angular-hnpwa/browser" to match the application builder's output structure. Fixed in ae50b40.

…base output path

- tsconfig.spec.json: removed src/polyfills.ts from files array (file was deleted)
- firebase.json: updated public path to dist/angular-hnpwa/browser (application builder output)

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>

@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 1 new potential issue.

Open in Devin Review

Comment thread angular.json
@@ -129,6 +128,5 @@
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Angular lint/e2e architect targets reference removed/deprecated builders

The angular.json still contains lint and e2e architect targets using @angular-devkit/build-angular:tslint and @angular-devkit/build-angular:protractor respectively. Both builders were removed from @angular-devkit/build-angular after Angular 12. Running ng lint or ng e2e will fail with Angular 20's build tooling. These targets should either be removed or migrated to their modern equivalents (ESLint and Cypress/Playwright).

(Refers to lines 104-128)

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 deferral — TSLint→ESLint and Protractor removal (including these angular.json targets) are scoped to Wave 4 (child session on branch upgrade/test-lint). See earlier reply.

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