Skip to content

Commit 4bc5ddd

Browse files
authored
CCM-16068 - Migration to PNPM (#102)
* CCM-16068 - Migration to PNPM * CCM-16068 - Removed eslintignore as this is deprecated, eslint ignores are now handled in the eslint config file * CCM-16068 - Revised AGENTS.md * CCM-16068 - Update any remaining npm references
1 parent 09def6f commit 4bc5ddd

23 files changed

Lines changed: 7703 additions & 11323 deletions

File tree

.github/dependabot.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ updates:
1212
schedule:
1313
interval: "daily"
1414

15-
- package-ecosystem: "npm"
15+
- package-ecosystem: "pnpm"
1616
directory: "/"
1717
schedule:
1818
interval: "daily"

.github/workflows/stage-2-test.yaml

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,21 @@ jobs:
4747
steps:
4848
- name: "Checkout code"
4949
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
50+
- name: "Setup pnpm"
51+
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
52+
with:
53+
version: 10.33.0
54+
- name: "Use Node.js"
55+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
56+
with:
57+
node-version-file: '.tool-versions'
58+
cache: 'pnpm'
5059
- name: "Repo setup"
5160
run: |
52-
npm ci
61+
pnpm install --frozen-lockfile
5362
- name: "Generate dependencies"
5463
run: |
55-
npm run generate-dependencies --workspaces --if-present
64+
pnpm run generate-dependencies
5665
git diff --exit-code
5766
test-unit:
5867
name: "Unit tests"
@@ -61,12 +70,21 @@ jobs:
6170
steps:
6271
- name: "Checkout code"
6372
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
73+
- name: "Setup pnpm"
74+
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
75+
with:
76+
version: 10.33.0
77+
- name: "Use Node.js"
78+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
79+
with:
80+
node-version-file: '.tool-versions'
81+
cache: 'pnpm'
6482
- name: "Repo setup"
6583
run: |
66-
npm ci
84+
pnpm install --frozen-lockfile
6785
- name: "Generate dependencies"
6886
run: |
69-
npm run generate-dependencies --workspaces --if-present
87+
pnpm run generate-dependencies
7088
- name: "Run unit test suite"
7189
run: |
7290
make test-unit
@@ -89,12 +107,21 @@ jobs:
89107
steps:
90108
- name: "Checkout code"
91109
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
110+
- name: "Setup pnpm"
111+
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
112+
with:
113+
version: 10.33.0
114+
- name: "Use Node.js"
115+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
116+
with:
117+
node-version-file: '.tool-versions'
118+
cache: 'pnpm'
92119
- name: "Repo setup"
93120
run: |
94-
npm ci
121+
pnpm install --frozen-lockfile
95122
- name: "Generate dependencies"
96123
run: |
97-
npm run generate-dependencies --workspaces --if-present
124+
pnpm run generate-dependencies
98125
- name: "Run linting"
99126
run: |
100127
make test-lint
@@ -105,12 +132,21 @@ jobs:
105132
steps:
106133
- name: "Checkout code"
107134
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
135+
- name: "Setup pnpm"
136+
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
137+
with:
138+
version: 10.33.0
139+
- name: "Use Node.js"
140+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
141+
with:
142+
node-version-file: '.tool-versions'
143+
cache: 'pnpm'
108144
- name: "Repo setup"
109145
run: |
110-
npm ci
146+
pnpm install --frozen-lockfile
111147
- name: "Generate dependencies"
112148
run: |
113-
npm run generate-dependencies --workspaces --if-present
149+
pnpm run generate-dependencies
114150
- name: "Run typecheck"
115151
run: |
116152
make test-typecheck

.tool-versions

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
act 0.2.64
22
gitleaks 8.24.0
33
jq 1.6
4-
nodejs 22.11.0
4+
nodejs 24.14.1
5+
pnpm 10.33.0
56
pre-commit 3.6.0
67
terraform 1.14.3
78
terraform-docs 0.21.0

AGENTS.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ Keep anything language or tool-specific in nested `AGENTS.md` files (for example
1111

1212
At a glance, the main areas are:
1313

14+
- `pnpm-workspace.yaml` - Defines packages, dependency versions, and package installation options.
1415
- `infrastructure/terraform/` – Terraform components, and shared modules for AWS accounts and environments.
15-
- `lambdas/` – TypeScript lambda projects (each with their own `package.json`, Jest config, etc.). Root level packages.json defines workspaces and scripts. Tests for the lambda are stored in `lambdas/{name}/src/__test`.
16+
- `lambdas/` – TypeScript lambda projects (each with their own `package.json`, Jest config, etc.). Root level packages.json defines scripts. Tests for the lambda are stored in `lambdas/{name}/src/__test`.
1617
- `src/` and `utils/` – Shared code and utilities (for example `utils/logger`).
1718
- `docs/` – Documentation site, ADRs, RFCS, and other long‑form docs.
1819
- `.github/workflows/` and `.github/actions/` – GitHub Actions workflows and composite actions.
@@ -21,27 +22,40 @@ At a glance, the main areas are:
2122

2223
Agents should look for a nested `AGENTS.md` in or near these areas before making non‑trivial changes.
2324

25+
## Root pnpm-workspace.yaml - role and usage
26+
27+
The root `pnpm-workspace.yaml` is the manifest for configuring the pnpm tool, and any workspace packages (if added).
28+
29+
- Packages: Declares the set of workspace packages (e.g. under `lambdas/`, `utils/`, `tests/`, `scripts/`). Agents should add a new package path here when introducing a new workspace package if the packages entry is present.
30+
- Catalogs: Defines named version catalogs (`lint`, `test`, `tools`) that centralise dependency version ranges. Workspace packages reference these with the `catalog:<name>` protocol (e.g. `"jest": "catalog:test"`) instead of hardcoding version ranges in each `package.json`.
31+
32+
Agent guidance for catalogs:
33+
34+
- When adding a dependency that belongs to an existing catalog category, add the version range to the appropriate catalog in `pnpm-workspace.yaml` and reference it as `"catalog:<name>"` in the consuming `package.json`.
35+
- Do not hardcode version ranges in workspace `package.json` files for dependencies that already exist in a catalog — always use the `catalog:` protocol.
36+
- When updating a dependency version, change it in the catalog entry only; all workspace packages referencing that catalog entry will pick up the new version automatically.
37+
- If a dependency does not fit any existing catalog, create a new catalog with a suitable name and add the version range there. All dependencies must be managed through catalogs — never hardcode version ranges directly in a `package.json`.
38+
2439
## Root package.json – role and usage
2540

26-
The root `package.json` is the orchestration manifestgit co for this repo. It does not ship application code; it wires up shared dev tooling and delegates to workspace-level projects.
41+
The root `package.json` is the orchestration manifest for this repo. It does not ship application code; it wires up shared dev tooling and delegates to workspace-level projects.
2742

28-
- Workspaces: Declares the set of npm workspaces (e.g. under `lambdas/`, `utils/`, `tests/`, `scripts/`). Agents should add a new workspace path here when introducing a new npm project.
29-
- Scripts: Provides top-level commands that fan out across workspaces using `--workspaces` (lint, typecheck, unit tests) and project-specific runners (e.g. `build-archive`).
43+
- Scripts: Provides top-level commands that fan out across workspaces using `--recursive` / `-r` (lint, typecheck, unit tests) and project-specific runners (e.g. `build-archive`).
3044
- Dev tool dependencies: Centralises Jest, TypeScript, ESLint configurations and plugins to keep versions consistent across workspaces. Workspace projects should rely on these unless a local override is strictly needed.
3145
- Overrides/resolutions: Pins transitive dependencies (e.g. Jest/react-is) to avoid ecosystem conflicts. Agents must not remove overrides without verifying tests across all workspaces.
3246

3347
Agent guidance:
3448

35-
- Before adding or removing a workspace, update the root `workspaces` array and ensure CI scripts still succeed with `npm run lint`, `npm run typecheck`, and `npm run test:unit` at the repo root.
36-
- When adding repo-wide scripts, keep names consistent with existing patterns (e.g. `lint`, `lint:fix`, `typecheck`, `test:unit`, `build-archive`) and prefer `--workspaces` fan-out.
49+
- Before adding or removing a workspace, update the root `packages` array in `pnpm-workspace.yaml` and ensure CI scripts still succeed with `pnpm run lint`, `pnpm run typecheck`, and `pnpm run test:unit` at the repo root.
50+
- When adding repo-wide scripts, keep names consistent with existing patterns (e.g. `lint`, `lint:fix`, `typecheck`, `test:unit`, `build-archive`) and prefer `pnpm -r run` fan-out.
3751
- Do not publish from the root. If adding a new workspace intended for publication, mark that workspace package as `private: false` and keep the root as private.
3852
- Validate changes by running the repo pre-commit hooks: `make githooks-run`.
3953

4054
Success criteria for changes affecting the root `package.json`:
4155

42-
- `npm run lint`, `npm run typecheck`, and `npm run test:unit` pass at the repo root.
43-
- Workspace discovery is correct (new projects appear under `npm run typecheck --workspaces`).
44-
- No regression in lambda build tooling (`npm run build-archive`).
56+
- `pnpm run lint`, `pnpm run typecheck`, and `pnpm run test:unit` pass at the repo root.
57+
- Workspace discovery is correct (new projects appear under `pnpm run typecheck`).
58+
- No regression in lambda build tooling (`pnpm run build:archive`).
4559

4660
## What Agents Can / Can’t Do
4761

@@ -81,7 +95,7 @@ When proposing a change, agents should:
8195

8296
to catch formatting and basic lint issues. Domain specific checks will be defined in appropriate nested AGENTS.md files.
8397

84-
- Suggest at least one extra validation step (for example `npm test` in a lambda, or triggering a specific workflow).
98+
- Suggest at least one extra validation step (for example `pnpm test` in a lambda, or triggering a specific workflow).
8599
- Any required follow up activites which fall outside of the current task's scope should be clearly marked with a 'TODO: CCM-12345' comment. The human user should be prompted to create and provide a JIRA ticket ID to be added to the comment.
86100

87101
## Security & Safety

containers/example-app/build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -euo pipefail
44

55
rm -rf dist
66

7-
npx esbuild \
7+
pnpm exec esbuild \
88
--bundle \
99
--minify \
1010
--sourcemap \
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { Config } from 'jest';
1+
import type { Config } from "jest";
22

33
const config: Config = {
4-
preset: 'ts-jest',
4+
preset: "ts-jest",
55

66
// Automatically clear mock calls, instances, contexts and results before every test
77
clearMocks: true,
@@ -10,10 +10,10 @@ const config: Config = {
1010
collectCoverage: true,
1111

1212
// The directory where Jest should output its coverage files
13-
coverageDirectory: './.reports/unit/coverage',
13+
coverageDirectory: "./.reports/unit/coverage",
1414

1515
// Indicates which provider should be used to instrument code for coverage
16-
coverageProvider: 'babel',
16+
coverageProvider: "babel",
1717

1818
coverageThreshold: {
1919
global: {
@@ -24,26 +24,26 @@ const config: Config = {
2424
},
2525
},
2626

27-
coveragePathIgnorePatterns: ['/__tests__/'],
28-
transform: { '^.+\\.ts$': 'ts-jest' },
29-
testPathIgnorePatterns: ['.build'],
30-
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
27+
coveragePathIgnorePatterns: ["/__tests__/"],
28+
transform: { "^.+\\.ts$": "ts-jest" },
29+
testPathIgnorePatterns: [".build"],
30+
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
3131

3232
// Use this configuration option to add custom reporters to Jest
3333
reporters: [
34-
'default',
34+
"default",
3535
[
36-
'jest-html-reporter',
36+
"jest-html-reporter",
3737
{
38-
pageTitle: 'Test Report',
39-
outputPath: './.reports/unit/test-report.html',
38+
pageTitle: "Test Report",
39+
outputPath: "./.reports/unit/test-report.html",
4040
includeFailureMsg: true,
4141
},
4242
],
4343
],
4444

4545
// The test environment that will be used for testing
46-
testEnvironment: 'node',
46+
testEnvironment: "node",
4747
};
4848

4949
export default config;

containers/example-app/package.json

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
{
22
"devDependencies": {
3-
"@tsconfig/node22": "^22.0.2",
4-
"@types/jest": "^29.5.14",
5-
"@types/node": "^22.0.0",
6-
"jest": "^29.7.0",
7-
"jest-mock-extended": "^3.0.7",
8-
"typescript": "^5.8.2"
3+
"@tsconfig/node22": "catalog:tools",
4+
"@types/jest": "catalog:test",
5+
"@types/node": "catalog:tools",
6+
"jest": "catalog:test",
7+
"jest-mock-extended": "catalog:test",
8+
"typescript": "catalog:tools"
9+
},
10+
"engines": {
11+
"node": ">=24.14.1"
912
},
1013
"name": "nhs-notify-admail-example-app",
1114
"private": true,
1215
"unused-scripts": {
13-
"build:archive": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts"
16+
"build:archive": "rm -rf dist && pnpm exec esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts"
1417
},
1518
"scripts": {
16-
"build:container": "cd ../.. && make docker-build-and-push base_image=node:22-alpine dir=containers/example-app",
19+
"build:container": "cd ../.. && make docker-build-and-push base_image=node:24-alpine dir=containers/example-app",
1720
"lint": "eslint .",
1821
"lint:fix": "eslint . --fix",
1922
"test:unit": "jest",
Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import http from 'node:http';
2-
import { createRequestHandler, startServer } from '../server';
1+
import http from "node:http";
2+
import { createRequestHandler, startServer } from "../server";
33

4-
describe('example-app server', () => {
5-
describe('createRequestHandler', () => {
6-
it('returns a request handler function', () => {
4+
describe("example-app server", () => {
5+
describe("createRequestHandler", () => {
6+
it("returns a request handler function", () => {
77
const handler = createRequestHandler();
8-
expect(typeof handler).toBe('function');
8+
expect(typeof handler).toBe("function");
99
});
1010

11-
it('responds with 200 status and JSON body', (done) => {
11+
it("responds with 200 status and JSON body", () => {
1212
const handler = createRequestHandler();
1313
const mockReq = {} as http.IncomingMessage;
1414
const mockRes = {
@@ -18,44 +18,56 @@ describe('example-app server', () => {
1818

1919
handler(mockReq, mockRes);
2020

21-
expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
22-
expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ status: 'ok' }));
23-
done();
21+
expect(mockRes.writeHead).toHaveBeenCalledWith(200, {
22+
"Content-Type": "application/json",
23+
});
24+
expect(mockRes.end).toHaveBeenCalledWith(
25+
JSON.stringify({ status: "ok" }),
26+
);
2427
});
2528
});
2629

27-
describe('startServer', () => {
30+
describe("startServer", () => {
2831
let server: http.Server;
2932
const port = 8888;
3033

31-
afterEach((done) => {
32-
if (server) {
33-
server.close(done);
34-
} else {
35-
done();
36-
}
34+
afterEach(async () => {
35+
await new Promise<void>((resolve) => {
36+
if (server) {
37+
server.close(() => {
38+
resolve();
39+
});
40+
} else {
41+
resolve();
42+
}
43+
});
3744
});
3845

39-
it('starts server on specified port and responds correctly', (done) => {
46+
it("starts server on specified port and responds correctly", async () => {
4047
server = startServer(port);
4148

42-
// Wait a bit for server to start
43-
setTimeout(() => {
44-
http.get(`http://localhost:${port}`, (res) => {
45-
expect(res.statusCode).toBe(200);
46-
expect(res.headers['content-type']).toBe('application/json');
49+
await new Promise<void>((resolve, reject) => {
50+
setTimeout(() => {
51+
http
52+
.get(`http://localhost:${port}`, (res) => {
53+
expect(res.statusCode).toBe(200);
54+
expect(res.headers["content-type"]).toBe("application/json");
4755

48-
let body = '';
49-
res.on('data', (chunk) => {
50-
body += chunk;
51-
});
56+
let body = "";
57+
res.on("data", (chunk: Buffer) => {
58+
body += chunk.toString();
59+
});
5260

53-
res.on('end', () => {
54-
expect(JSON.parse(body)).toEqual({ status: 'ok' });
55-
done();
56-
});
57-
});
58-
}, 100);
61+
res.on("end", () => {
62+
expect(JSON.parse(body)).toEqual({ status: "ok" });
63+
resolve();
64+
});
65+
66+
res.on("error", reject);
67+
})
68+
.on("error", reject);
69+
}, 100);
70+
});
5971
});
6072
});
6173
});

0 commit comments

Comments
 (0)