Skip to content

Commit ad787ce

Browse files
authored
Merge pull request #12 from ismailza/chore/improve-compatibility-validation
ci: validate the published package against Angular 17-22
2 parents 26313cb + 25e3b0a commit ad787ce

34 files changed

Lines changed: 5188 additions & 4458 deletions

.github/workflows/ci.yml

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ on:
88

99
jobs:
1010
build:
11+
name: Lint, test & build
1112
runs-on: ubuntu-latest
1213

1314
steps:
1415
- uses: actions/checkout@v5
1516

1617
- uses: actions/setup-node@v5
1718
with:
18-
node-version: 22
19+
node-version: 24
1920
cache: npm
2021

2122
- run: npm ci
@@ -28,3 +29,93 @@ jobs:
2829

2930
- name: Build
3031
run: npm run build
32+
33+
# Catches an omission that would otherwise only surface after publishing.
34+
- name: Verify package contents
35+
run: npm run verify:package
36+
37+
# The compatibility matrix consumes exactly what would be published,
38+
# so pack once here instead of rebuilding per Angular version.
39+
- name: Pack
40+
run: |
41+
npm pack --pack-destination "$RUNNER_TEMP" --silent
42+
mv "$RUNNER_TEMP"/*.tgz "$GITHUB_WORKSPACE/package.tgz"
43+
working-directory: dist/ngx-api-client
44+
45+
- uses: actions/upload-artifact@v4
46+
with:
47+
name: package-tarball
48+
path: package.tgz
49+
retention-days: 1
50+
51+
versions:
52+
# Derived from the library's `@angular/core` peer range so the matrix cannot
53+
# drift from what the package claims to support.
54+
name: Resolve supported Angular versions
55+
runs-on: ubuntu-latest
56+
outputs:
57+
angular: ${{ steps.derive.outputs.angular }}
58+
59+
steps:
60+
- uses: actions/checkout@v5
61+
62+
- id: derive
63+
run: echo "angular=$(node scripts/check-angular-compat.mjs --list)" >> "$GITHUB_OUTPUT"
64+
65+
compat:
66+
# The workspace builds on the newest Angular, but the package supports a
67+
# range of majors. Install the packed tarball into a throwaway consumer
68+
# project per combination and type-check, bundle and run it there, so the
69+
# supported range is verified rather than asserted.
70+
name: Angular ${{ matrix.angular }} / Node ${{ matrix.node }}
71+
needs: [build, versions]
72+
runs-on: ubuntu-latest
73+
74+
strategy:
75+
fail-fast: false
76+
matrix:
77+
angular: ${{ fromJSON(needs.versions.outputs.angular) }}
78+
node: [22, 24]
79+
80+
steps:
81+
- uses: actions/checkout@v5
82+
83+
- uses: actions/setup-node@v5
84+
with:
85+
node-version: ${{ matrix.node }}
86+
87+
- uses: actions/download-artifact@v4
88+
with:
89+
name: package-tarball
90+
91+
- name: Verify against Angular ${{ matrix.angular }}
92+
run: |
93+
npm run compat -- ${{ matrix.angular }} \
94+
--tarball package.tgz \
95+
--report "compat-${{ matrix.angular }}-node${{ matrix.node }}.json"
96+
97+
# `always()`: a failed combination still belongs in the summary table.
98+
- if: always()
99+
uses: actions/upload-artifact@v4
100+
with:
101+
name: compat-result-${{ matrix.angular }}-node${{ matrix.node }}
102+
path: compat-*.json
103+
retention-days: 1
104+
if-no-files-found: ignore
105+
106+
report:
107+
name: Compatibility report
108+
needs: compat
109+
if: always()
110+
runs-on: ubuntu-latest
111+
112+
steps:
113+
- uses: actions/checkout@v5
114+
115+
- uses: actions/download-artifact@v4
116+
with:
117+
pattern: compat-result-*
118+
path: compat-results
119+
120+
- name: Publish compatibility table
121+
run: node scripts/compat-report.mjs compat-results >> "$GITHUB_STEP_SUMMARY"

.github/workflows/release.yml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ jobs:
2828
config-file: release-please-config.json
2929
manifest-file: .release-please-manifest.json
3030

31-
# Builds the tarball once so both registries receive identical bytes.
31+
# Builds the package once so both registries receive identical contents. Each
32+
# publish job packs its own tarball from this directory, so the two must stay
33+
# on the same Node — a differing bundled npm changes the shasum.
3234
build:
3335
name: Build
3436
needs: release-please
@@ -39,22 +41,21 @@ jobs:
3941

4042
- uses: actions/setup-node@v5
4143
with:
42-
node-version: 22
44+
node-version: 24
4345
cache: npm
4446

4547
- run: npm ci
4648

4749
- name: Test
4850
run: npm test
4951

52+
# `postbuild` stages LICENSE and CHANGELOG.md, which ng-packagr does not
53+
# copy, so this produces the same bytes CI validated.
5054
- name: Build
5155
run: npm run build
5256

53-
# ng-packagr copies README.md but not these.
54-
- name: Include LICENSE and CHANGELOG in the package
55-
run: |
56-
cp LICENSE dist/ngx-api-client/LICENSE
57-
cp projects/ngx-api-client/CHANGELOG.md dist/ngx-api-client/CHANGELOG.md
57+
- name: Verify package contents
58+
run: npm run verify:package
5859

5960
- uses: actions/upload-artifact@v4
6061
with:
@@ -77,7 +78,7 @@ jobs:
7778

7879
- uses: actions/setup-node@v5
7980
with:
80-
node-version: 22
81+
node-version: 24
8182
registry-url: https://registry.npmjs.org
8283

8384
- run: npm publish ./package --provenance --access public
@@ -99,9 +100,9 @@ jobs:
99100

100101
- uses: actions/setup-node@v5
101102
with:
102-
node-version: 22
103+
node-version: 24
103104
registry-url: https://npm.pkg.github.com
104-
scope: "@ismailza"
105+
scope: '@ismailza'
105106

106107
- run: npm publish ./package
107108
env:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
/out-tsc
77
/bazel-out
88

9+
# Tarball packed for the Angular compatibility matrix
10+
/package.tgz
11+
912
# Node
1013
/node_modules
1114
npm-debug.log

CONTRIBUTING.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ Small bug fixes, documentation improvements, and typo fixes can generally be sub
3434

3535
Make sure you have the following installed:
3636

37-
- Node.js (LTS recommended)
37+
- Node.js `^22.22.3`, `^24.15.0` or newer — the floor comes from the Angular CLI
38+
the workspace builds with, and an older release will refuse to run `ng`
3839
- npm
3940
- Git
4041

@@ -129,6 +130,37 @@ Run the linter:
129130
npm run lint
130131
```
131132

133+
### Compatibility Checks
134+
135+
The package supports a range of Angular majors, so a change that compiles
136+
against the workspace's Angular can still break consumers on an older one. CI
137+
runs these on every push; run them locally when you touch the public API, the
138+
build setup or `peerDependencies`.
139+
140+
Verify the packaged artifact contains everything it should:
141+
142+
```bash
143+
npm run build
144+
npm run verify:package
145+
```
146+
147+
Verify the package against every supported Angular major — this installs each
148+
one into a throwaway project, so expect it to take a few minutes:
149+
150+
```bash
151+
npm run compat
152+
```
153+
154+
To check a single version while iterating:
155+
156+
```bash
157+
npm run compat -- 17
158+
```
159+
160+
See [`compat/README.md`](compat/README.md) for what each check covers and how to
161+
extend the fixture. When you add a public export, reference it there — an export
162+
no fixture touches is an export the matrix does not cover.
163+
132164
## Commit Messages
133165

134166
This project follows the **Conventional Commits** specification.

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,22 @@ own `ApiSuccessHandler`.
281281

282282
## Compatibility
283283

284-
Developed and tested against **Angular 21**. The declared floor of `>=17` reflects
285-
the APIs used — signals and functional interceptors — rather than a range the CI
286-
matrix currently covers.
284+
| Requirement | Supported |
285+
| ----------------- | --------------------------------------------------------- |
286+
| Angular | 17 · 18 · 19 · 20 · 21 · 22 |
287+
| RxJS | `^7.4.0` |
288+
| Peer dependencies | `@angular/core`, `@angular/common`, `rxjs` — nothing else |
289+
290+
Every Angular major above is verified on each push, against the packed tarball
291+
rather than the source. CI installs the package into a throwaway consumer
292+
application per version and runs three checks: a type-check against that
293+
version's Angular types, a production `ng build` — which is what exercises the
294+
Angular linker over the shipped partial declarations — and a runtime test of the
295+
injector and the `exports` map. Each combination runs on both Node 22 and 24.
296+
297+
The peer range is bounded (`>=17.0.0 <23.0.0`) deliberately: a new Angular major
298+
is added only once it has been validated, so the supported range is a tested
299+
claim rather than an assumption.
287300

288301
## Contributing
289302

compat/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Angular compatibility fixture
2+
3+
The workspace builds on the newest Angular, but `@ismailza/ngx-api-client`
4+
supports a range of majors. This directory is the consumer project used to check
5+
that claim against every version in the range.
6+
7+
`scripts/check-angular-compat.mjs` copies these files into a throwaway project
8+
outside the repo, installs one Angular major plus the packed tarball, and runs
9+
three checks — each covering something the previous one cannot:
10+
11+
| Check | Catches |
12+
| ------------------------------------- | ------------------------------------------------------------------------------------------------ |
13+
| `ngc -p tsconfig.json` | type-level breaks between the shipped `.d.ts` and that version's Angular types |
14+
| `ng build --configuration production` | Angular **linker** failures over the shipped partial declarations, plus bundler/AOT breakage |
15+
| `node smoke.mjs` | JIT compilation, injector behaviour and `exports` map resolution — none of which a build reaches |
16+
17+
The supported majors are derived from the library's `@angular/core` peer range,
18+
so the matrix cannot drift from what is published.
19+
20+
## Running it
21+
22+
```bash
23+
npm run build
24+
npm run compat # every supported major
25+
npm run compat -- 17 22 # selected majors
26+
npm run compat -- --list # the derived matrix, as JSON
27+
```
28+
29+
## Layout
30+
31+
| File | Role |
32+
| ------------------- | ---------------------------------------------------------------------- |
33+
| `src/models.ts` | domain types standing in for a consumer's own |
34+
| `src/handlers.ts` | `ApiErrorHandler` / `ApiSuccessHandler` subclasses |
35+
| `src/services.ts` | a service injecting `ApiService`, calling every verb |
36+
| `src/providers.ts` | `appConfig` wiring `provideApi()` and the interceptors |
37+
| `src/index.ts` | barrel — the `ngc` entry point |
38+
| `src/browser.ts` | the `ng build` entry point |
39+
| `tsconfig.json` | `ngc` config (`skipLibCheck: false`, so the shipped types are checked) |
40+
| `tsconfig.app.json` | `ng build` config |
41+
42+
## Editing the fixture
43+
44+
These sources must stay valid on **all** supported majors, so they declare no
45+
components: standalone semantics changed between v17 and v19, and the library
46+
ships no declarables anyway. Keep to DI, the public types and the interceptors.
47+
48+
`browser.ts` parks the fixture on `globalThis` deliberately — the application
49+
builder tree-shakes aggressively, and without a live reference the library would
50+
be dropped from the bundle, leaving the linker with nothing to do and the build
51+
proving nothing. `assertBundleLinked()` in the runner guards both directions.
52+
53+
When a public export is added, reference it here — an export that no fixture
54+
touches is an export the matrix does not cover.

compat/smoke.mjs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Runtime smoke test: the published package ships partial (unlinked) Angular
3+
* declarations, so importing it under an older runtime exercises that runtime's
4+
* JIT compiler. Type-checking alone would not catch a linker/runtime break.
5+
*
6+
* Also asserts the `exports` map resolves the way consumers rely on it.
7+
*/
8+
import '@angular/compiler';
9+
import { ErrorHandler, Injector } from '@angular/core';
10+
import {
11+
API_BASE_URL,
12+
API_PREFIX,
13+
API_RETRY_CONFIG,
14+
API_VERSION,
15+
API_VERSIONING,
16+
ApiErrorHandler,
17+
ApiLoadingService,
18+
ApiSuccessHandler,
19+
provideApi,
20+
} from '@ismailza/ngx-api-client';
21+
import assert from 'node:assert/strict';
22+
import { createRequire } from 'node:module';
23+
24+
const injector = Injector.create({
25+
providers: [
26+
{ provide: ErrorHandler, useValue: new ErrorHandler() },
27+
provideApi({
28+
baseUrl: 'https://api.example.com/',
29+
prefix: 'api',
30+
version: 2,
31+
versioning: 'url',
32+
retry: { maxRetries: 5 },
33+
}),
34+
ApiLoadingService,
35+
],
36+
});
37+
38+
assert.equal(injector.get(API_BASE_URL), 'https://api.example.com');
39+
assert.equal(injector.get(API_PREFIX), 'api');
40+
assert.equal(injector.get(API_VERSION), 2);
41+
assert.equal(injector.get(API_VERSIONING).strategy, 'url');
42+
assert.equal(injector.get(API_RETRY_CONFIG).maxRetries, 5);
43+
44+
// Fallback handlers registered by provideApi() must be constructible.
45+
assert.ok(injector.get(ApiErrorHandler) instanceof ApiErrorHandler);
46+
assert.ok(injector.get(ApiSuccessHandler) instanceof ApiSuccessHandler);
47+
48+
// Signal-based loading state.
49+
const loading = injector.get(ApiLoadingService);
50+
assert.equal(loading.loading(), false);
51+
loading.start();
52+
assert.equal(loading.loading(), true);
53+
loading.stop();
54+
assert.equal(loading.loading(), false);
55+
56+
// --- exports map -----------------------------------------------------------
57+
58+
// `./package.json` is exported so tooling can read the manifest.
59+
const require = createRequire(import.meta.url);
60+
const manifest = require('@ismailza/ngx-api-client/package.json');
61+
assert.equal(manifest.name, '@ismailza/ngx-api-client');
62+
63+
// Deep imports must stay blocked: reaching past the entry point would let
64+
// consumers depend on paths that are free to change between releases.
65+
await assert.rejects(
66+
() => import('@ismailza/ngx-api-client/lib/api.service'),
67+
(error) => error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED',
68+
'deep imports should be blocked by the exports map',
69+
);
70+
71+
console.log('runtime smoke ok');

compat/src/browser.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Browser entry for the `ng build` leg of the matrix.
3+
*
4+
* The application builder tree-shakes aggressively: without a live reference the
5+
* library would be dropped from the bundle and the Angular linker would never
6+
* run over its partial declarations, making the build a vacuous check.
7+
*/
8+
import { appConfig, OrderService, publicTokens } from './index';
9+
10+
(globalThis as Record<string, unknown>)['__compatFixture'] = {
11+
appConfig,
12+
OrderService,
13+
publicTokens,
14+
};

0 commit comments

Comments
 (0)