Skip to content

Commit 7d34e3b

Browse files
authored
test: add TypeScript version compatibility suite (#8180)
1 parent 64c3e50 commit 7d34e3b

9 files changed

Lines changed: 419 additions & 1 deletion

File tree

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This is the public Makefile containing some build commands.
22
# You can implement some additional personal commands such as login and sync in Makefile.private.mk (unversioned).
33

4-
.PHONY: login sync bundles test-unit test-types test-indices test-protocols test-schema test-integration test-endpoints test-e2e build build-s3-browser-bundle build-signature-v4-multi-region-browser-bundle clean-nested link-smithy unlink-smithy copy-smithy gen-auth b-auth tpk unbuilt turbo-clean server-protocols nested-clients clients static-analysis
4+
.PHONY: login sync bundles test-unit test-types test-typescript-versions test-indices test-protocols test-schema test-integration test-endpoints test-e2e build build-s3-browser-bundle build-signature-v4-multi-region-browser-bundle clean-nested link-smithy unlink-smithy copy-smithy gen-auth b-auth tpk unbuilt turbo-clean server-protocols nested-clients clients static-analysis
55

66
# fetch AWS testing credentials
77
login:
@@ -28,6 +28,7 @@ test-integration: bundles core-prebuild
2828
yarn g:vitest run -c vitest.config.integ.mts
2929
make test-protocols
3030
make test-types
31+
make test-typescript-versions
3132
make snapshot-compare
3233
make test-indices
3334
make test-endpoints
@@ -52,6 +53,10 @@ test-types: reset-test-credentials
5253
npx tsc -p tsconfig.test.json
5354
npx tsc -p tsconfig.test.index-types.json
5455

56+
# verify clients compile across supported TypeScript versions (see tests/ts-compat/README.md).
57+
test-typescript-versions:
58+
(cd ./tests/ts-compat && npm install --no-audit --no-fund && node ./run.mjs)
59+
5560
test-indices:
5661
node ./scripts/validation/client-indexes.mjs
5762

tests/ts-compat/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
package-lock.json
3+
yarn.lock
4+
.tmp/

tests/ts-compat/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# ts-compat
2+
3+
Verifies that one client per protocol compiles on every TypeScript version
4+
listed in [`typescript-versions.json`](./typescript-versions.json).
5+
6+
The clients in this repository support a range of TypeScript versions (types are
7+
downleveled for older releases). This test guards that support by type-checking a
8+
small fixture that imports and uses one client per wire protocol against each
9+
version.
10+
11+
## What it does
12+
13+
For every version in `typescript-versions.json`, the runner:
14+
15+
1. Installs that TypeScript version in isolation (under `.tmp/`).
16+
2. Type-checks [`fixtures/index.ts`](./fixtures/index.ts) against it.
17+
18+
The clients' published `.d.ts` are parsed (so downlevel _syntax_ incompatibilities
19+
surface), while `skipLibCheck` avoids failing on their internal Node references
20+
(see [`tsconfig.json`](./tsconfig.json)). Work is parallelized across a pool of
21+
worker threads sized to the number of available processors.
22+
23+
## Running
24+
25+
This suite runs as part of the `test-typescript` Make target:
26+
27+
```bash
28+
make test-typescript
29+
```
30+
31+
You can also run it directly:
32+
33+
```bash
34+
cd tests/ts-compat && npm install && node ./run.mjs
35+
```
36+
37+
Either way assumes the referenced clients are already built (their `dist-types`
38+
are present). `run.mjs` errors with guidance if they are not. It runs in
39+
isolation with its own dependency installs.
40+
41+
## Protocol coverage
42+
43+
| Protocol | Client |
44+
| ---------- | --------------------------------- |
45+
| awsJson1_0 | `@aws-sdk/client-dynamodb` |
46+
| awsJson1_1 | `@aws-sdk/client-cloudwatch-logs` |
47+
| awsQuery | `@aws-sdk/client-sts` |
48+
| ec2Query | `@aws-sdk/client-ec2` |
49+
| restJson1 | `@aws-sdk/client-lambda` |
50+
| restXml | `@aws-sdk/client-s3` |
51+
52+
No shipping client currently uses the smithy `rpcv2Cbor` protocol; add one to the
53+
fixture once such a client is published.
54+
55+
## Updating the supported versions
56+
57+
Edit `typescript-versions.json`. Each entry is `{ version, tscArgs? }`:
58+
59+
- `version` — any spec npm accepts (a bare minor like `"5.4"` installs its latest patch).
60+
- `tscArgs` — optional compiler flags for that version, passed on the command line
61+
to override `tsconfig.json` when an option changed. For example, `moduleResolution:
62+
node` (node10) became a deprecation error in TS 6.0 and was removed in TS 7.0.

tests/ts-compat/fixtures/index.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Type-compatibility fixture.
3+
*
4+
* One AWS client per wire protocol is imported, instantiated, and used to send
5+
* a command. Nothing here is executed - the file exists purely so that `tsc`
6+
* type-checks the clients' published `.d.ts` surface (and their transitive
7+
* `@smithy` / `@aws-sdk` type dependencies) against each TypeScript version.
8+
*
9+
* Protocol coverage:
10+
* - awsJson1_0 -> @aws-sdk/client-dynamodb
11+
* - awsJson1_1 -> @aws-sdk/client-cloudwatch-logs
12+
* - awsQuery -> @aws-sdk/client-sts
13+
* - ec2Query -> @aws-sdk/client-ec2
14+
* - restJson1 -> @aws-sdk/client-lambda
15+
* - restXml -> @aws-sdk/client-s3
16+
*
17+
* Note: no shipping client currently uses the smithy rpcv2Cbor protocol. Add
18+
* one here (with its command) once such a client is published.
19+
*/
20+
import { CloudWatchLogsClient, DescribeLogGroupsCommand } from "@aws-sdk/client-cloudwatch-logs";
21+
import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb";
22+
import { DescribeRegionsCommand, EC2Client } from "@aws-sdk/client-ec2";
23+
import { LambdaClient, ListFunctionsCommand } from "@aws-sdk/client-lambda";
24+
import { ListBucketsCommand, S3Client } from "@aws-sdk/client-s3";
25+
import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
26+
27+
// awsJson1_0
28+
export async function dynamodb(): Promise<void> {
29+
const client = new DynamoDBClient({});
30+
const output = await client.send(new ListTablesCommand({}));
31+
const names: string[] | undefined = output.TableNames;
32+
void names;
33+
}
34+
35+
// awsJson1_1
36+
export async function cloudwatchLogs(): Promise<void> {
37+
const client = new CloudWatchLogsClient({});
38+
const output = await client.send(new DescribeLogGroupsCommand({}));
39+
void output.logGroups;
40+
}
41+
42+
// awsQuery
43+
export async function sts(): Promise<void> {
44+
const client = new STSClient({});
45+
const output = await client.send(new GetCallerIdentityCommand({}));
46+
const account: string | undefined = output.Account;
47+
void account;
48+
}
49+
50+
// ec2Query
51+
export async function ec2(): Promise<void> {
52+
const client = new EC2Client({});
53+
const output = await client.send(new DescribeRegionsCommand({}));
54+
void output.Regions;
55+
}
56+
57+
// restJson1
58+
export async function lambda(): Promise<void> {
59+
const client = new LambdaClient({});
60+
const output = await client.send(new ListFunctionsCommand({}));
61+
void output.Functions;
62+
}
63+
64+
// restXml
65+
export async function s3(): Promise<void> {
66+
const client = new S3Client({});
67+
const output = await client.send(new ListBucketsCommand({}));
68+
void output.Buckets;
69+
}

tests/ts-compat/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"private": true,
3+
"name": "@aws-sdk/ts-compat-test",
4+
"description": "Verifies that one client per protocol compiles against every supported TypeScript minor version.",
5+
"type": "module",
6+
"scripts": {
7+
"test": "node ./run.mjs"
8+
},
9+
"devDependencies": {
10+
"@aws-sdk/client-cloudwatch-logs": "file:../../clients/client-cloudwatch-logs",
11+
"@aws-sdk/client-dynamodb": "file:../../clients/client-dynamodb",
12+
"@aws-sdk/client-ec2": "file:../../clients/client-ec2",
13+
"@aws-sdk/client-lambda": "file:../../clients/client-lambda",
14+
"@aws-sdk/client-s3": "file:../../clients/client-s3",
15+
"@aws-sdk/client-sts": "file:../../clients/client-sts"
16+
}
17+
}

tests/ts-compat/run.mjs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* TypeScript compatibility test runner.
3+
*
4+
* For every TypeScript version listed in typescript-versions.json, this script:
5+
* 1. Installs that TypeScript version in isolation (under .tmp/).
6+
* 2. Type-checks fixtures/index.ts - which imports and uses one client per
7+
* protocol - against it. The clients' published .d.ts are parsed (so
8+
* downlevel *syntax* incompatibilities surface), while skipLibCheck avoids
9+
* failing on their internal Node references (see tsconfig.json).
10+
*
11+
* Work is parallelized across a pool of worker threads sized to the number of
12+
* available processors (os.cpus().length).
13+
*
14+
* Usage: node ./run.mjs
15+
*/
16+
import { execFileSync } from "node:child_process";
17+
import { existsSync, readFileSync } from "node:fs";
18+
import os from "node:os";
19+
import path from "node:path";
20+
import { Worker } from "node:worker_threads";
21+
22+
const root = import.meta.dirname;
23+
24+
// The versions under test live in typescript-versions.json (oldest -> newest)
25+
// so the support range can be updated without touching runner logic. Each entry
26+
// is { version: string, tscArgs?: string[] } where `version` is any spec npm
27+
// accepts (a bare minor like "5.4" installs its latest patch) and `tscArgs`
28+
// holds compiler options that CHANGED for that version and must be passed on
29+
// the command line (overriding tsconfig.json) so the shared base config stays
30+
// valid everywhere. Past examples of why an entry needed tscArgs:
31+
// - `moduleResolution: node` (node10) became a deprecation *error* in TS 6.0.
32+
// - `moduleResolution: node` (node10) was removed in TS 7.0; `bundler`
33+
// resolution requires `module: preserve` (or esnext).
34+
const VERSIONS_FILE = path.join(root, "typescript-versions.json");
35+
36+
/**
37+
* Load the version specs under test.
38+
* @returns {{ version: string, tscArgs: string[] }[]}
39+
*/
40+
function loadVersions() {
41+
/** @type {{ version: string, tscArgs?: string[] }[]} */
42+
const specs = JSON.parse(readFileSync(VERSIONS_FILE, "utf8"));
43+
return specs.map(({ version, tscArgs = [] }) => ({ version, tscArgs }));
44+
}
45+
46+
/**
47+
* Ensure the workspace clients are installed and built (dist-types present).
48+
* The clients are consumed via file: links, so their .d.ts must exist on disk.
49+
*/
50+
function ensureClientsReady() {
51+
if (!existsSync(path.join(root, "node_modules"))) {
52+
console.log("Installing fixture dependencies (clients) ...");
53+
execFileSync("npm", ["install", "--no-audit", "--no-fund"], { cwd: root, stdio: "inherit" });
54+
}
55+
56+
const clients = [
57+
"@aws-sdk/client-dynamodb",
58+
"@aws-sdk/client-cloudwatch-logs",
59+
"@aws-sdk/client-sts",
60+
"@aws-sdk/client-ec2",
61+
"@aws-sdk/client-lambda",
62+
"@aws-sdk/client-s3",
63+
];
64+
const missing = clients.filter(
65+
(name) => !existsSync(path.join(root, "node_modules", ...name.split("/"), "dist-types", "index.d.ts"))
66+
);
67+
if (missing.length > 0) {
68+
console.error(
69+
`\nThe following clients are not built (dist-types missing): ${missing.join(", ")}.\n` +
70+
`Build them first, e.g.:\n` +
71+
` node ./scripts/turbo build -F=@aws-sdk/client-dynamodb -F=@aws-sdk/client-cloudwatch-logs \\\n` +
72+
` -F=@aws-sdk/client-sts -F=@aws-sdk/client-ec2 -F=@aws-sdk/client-lambda -F=@aws-sdk/client-s3\n`
73+
);
74+
process.exit(1);
75+
}
76+
}
77+
78+
/**
79+
* Run all compile jobs across a worker pool.
80+
* @param {{ minor: string, version: string }[]} versions
81+
*/
82+
async function runPool(versions) {
83+
const poolSize = Math.max(1, Math.min(os.cpus().length, versions.length));
84+
85+
const queue = [...versions];
86+
/** @type {{ version: string, ok: boolean, output: string }[]} */
87+
const results = [];
88+
89+
await new Promise((resolve, reject) => {
90+
let active = 0;
91+
92+
const workerPath = path.join(root, "worker.mjs");
93+
94+
const spawnWorker = () => {
95+
const job = queue.shift();
96+
if (!job) {
97+
return;
98+
}
99+
active++;
100+
const worker = new Worker(workerPath, { workerData: { root, job } });
101+
102+
worker.once("message", (res) => {
103+
results.push(res);
104+
worker.terminate();
105+
});
106+
worker.once("error", reject);
107+
worker.once("exit", () => {
108+
active--;
109+
if (queue.length > 0) {
110+
spawnWorker();
111+
} else if (active === 0) {
112+
resolve(undefined);
113+
}
114+
});
115+
};
116+
117+
for (let i = 0; i < poolSize; i++) {
118+
spawnWorker();
119+
}
120+
});
121+
122+
return results;
123+
}
124+
125+
ensureClientsReady();
126+
const versions = loadVersions();
127+
const results = await runPool(versions);
128+
129+
results.sort(
130+
(a, b) => versions.findIndex((v) => v.version === a.version) - versions.findIndex((v) => v.version === b.version)
131+
);
132+
133+
const failures = results.filter((r) => !r.ok);
134+
135+
console.log("\n" + "=".repeat(60));
136+
console.log("TypeScript compatibility summary");
137+
console.log("=".repeat(60));
138+
for (const r of results) {
139+
console.log(` ${r.ok ? "PASS" : "FAIL"} typescript@${r.version}`);
140+
}
141+
142+
if (failures.length > 0) {
143+
for (const f of failures) {
144+
console.error(`\nFAIL typescript@${f.version}:\n${f.output.trim()}`);
145+
}
146+
process.exit(1);
147+
}

tests/ts-compat/tsconfig.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
// Base config. Options here must be valid across every TypeScript version
3+
// listed in typescript-versions.json. Options that changed between versions
4+
// (e.g. moduleResolution handling in newer releases) are supplied per-version
5+
// by run.mjs.
6+
"compilerOptions": {
7+
"target": "es2018",
8+
"module": "commonjs",
9+
"moduleResolution": "node",
10+
"lib": ["es2020", "dom"],
11+
// types:[] keeps @types/node out of the program. The fixture uses no Node
12+
// APIs, and no single @types/node version parses across all the TypeScript
13+
// versions under test.
14+
"types": [],
15+
"strict": true,
16+
"esModuleInterop": true,
17+
// skipLibCheck lets the clients' internal Node references pass without
18+
// @types/node; the .d.ts are still parsed, so downlevel *syntax*
19+
// incompatibilities are still caught.
20+
"skipLibCheck": true,
21+
"forceConsistentCasingInFileNames": true,
22+
"noEmit": true
23+
},
24+
"include": ["fixtures/**/*.ts"]
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[
2+
{ "version": "4.5" },
3+
{ "version": "4.6" },
4+
{ "version": "4.7" },
5+
{ "version": "4.8" },
6+
{ "version": "4.9" },
7+
{ "version": "5.0" },
8+
{ "version": "5.1" },
9+
{ "version": "5.2" },
10+
{ "version": "5.3" },
11+
{ "version": "5.4" },
12+
{ "version": "5.5" },
13+
{ "version": "5.6" },
14+
{ "version": "5.7" },
15+
{ "version": "5.8" },
16+
{ "version": "5.9" },
17+
{ "version": "6.0", "tscArgs": ["--ignoreDeprecations", "6.0"] },
18+
{ "version": "7.0", "tscArgs": ["--moduleResolution", "bundler", "--module", "preserve"] }
19+
]

0 commit comments

Comments
 (0)