Skip to content

Commit

Permalink
INN-1009 Add CI tests for different TS versions to ensure back compat (
Browse files Browse the repository at this point in the history
…#151)

## Summary INN-1009

Providing a TS library means supporting various versions of TypeScript.
Above just watching public API changes, this means:

- Ensuring we have an explicit earliest supported TS version
- Ensuring we have an explicit earliest supported Node version
- Ensuring that earlier TS versions maintain the same level of inference
across all tooling

TypeScript introduce breaking changes with minor increments, which seems
to be the norm among tooling with a large surface area (see [Follow
SemVer · Issue #2888 · jashkenas/backbone
(github.com)](jashkenas/backbone#2888 (comment))).
This also means that TS changes faster than it appears to and that it's
harder for users to upgrade TS versions, even though semver might
suggest otherwise.

Most TS configs will also type-check any installed libraries every time
code is built, which results in errors on TS `<4.7.0` as the earlier
compiler can't even parse the file due to unexpected characters (see
#150).

## Actions

There are some fantastic resources covering this problem (see
https://www.semver-ts.org/), but even tooling such as
[downlevel-dts](https://github.com/sandersn/downlevel-dts) takes a long
time to support the latest TS releases (it currently still doesn't
support some 4.7 features) _and_ some of the "down-leveling" just falls
back to wider types such as `any` or `unknown`, which could be
insufficient depending on where it is.

The easiest step to take is to add some CI testing for various
TypeScript versions and ensure that tests pass and compile. We have a
lot of type-only unit tests to assert inference functionality, so
testing that against earlier TS versions should do the trick.

With this, we can pretty easily support >=4.7.0, though going any
further would require some chonky rewrites. The main culprit being
[instantiation
expressions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#instantiation-expressions)
released in [TypeScript
4.7](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html).
This PR is mostly about adding a framework for asserting and committing
to a specific TS version support, then support for earlier versions can
be added in further PRs.

Time-wise, it means we can comfortably support TS versions within the
last 10 months, and specify that fact in our `peerDependencies`.

- ✅ **TypeScript 5.0** - March 2023 (<1mo)
- ✅ **TypeScript 4.9** - November 2022 (~4mo)
- ✅ **TypeScript 4.8** - August 2022 (~7mo)
- ✅ **TypeScript 4.7** - May 2022 (~10mo)
- ❌ **TypeScript 4.6** - Feb 2022 (~1y1mo)
- ❌ **TypeScript 4.5** - November 2021 (~1y4mo)
- ❌ **TypeScript 4.4** - August 2021 (~1y7mo)
- ❌ **TypeScript 4.3** - May 2021 (~1y10mo)
- ❌ **TypeScript 4.2** - Feb 2021 (~2y1m)
- ❌ **TypeScript 4.1** - November 2020 (~2y4m)
- ❌ **TypeScript 4.0** - August 2020 (~2y7mo)

To test types, we'll do a couple of actions:

- [x] Add a new `test:types` script that will test that `dist/` and
`src/**/*.test.ts` compile correctly.
This will ensure that users with the default of `skipLibCheck: false`
can properly build the code, and compiling test files ensures that all
type tests are also working.
- [x] Add CI matrix to test multiple TS versions against the above files

## Related

- #150

---------

Co-authored-by: Igor Gassmann <igor@igassmann.me>
  • Loading branch information
jpwilliams and IGassmann committed Mar 27, 2023
1 parent ca7d79e commit 906aca5
Show file tree
Hide file tree
Showing 16 changed files with 154 additions and 79 deletions.
7 changes: 7 additions & 0 deletions .changeset/popular-dogs-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"inngest": patch
---

INN-1009 Show warnings when using the package with TS versions `<4.7.2` and Node versions `<14`

This includes tests to assert we appropriately support these versions now and in the future.
27 changes: 25 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,40 @@ env:

jobs:
test:
name: Test
name: Runtime
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
nodeVersion: [14, 16, 18, lts]
nodeVersion:
- 14
- 16
- 18
- lts
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-and-build
- run: volta run --node ${{ matrix.nodeVersion }} yarn test

types:
name: Types
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
tsVersion:
- 'latest'
- '~5.0.0'
- '~4.9.0'
- '~4.8.0'
- "~4.7.0"
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-and-build
- run: yarn prelink
- run: yarn add -D typescript@${{ matrix.tsVersion }}
- run: yarn test:types

api_diff:
name: Local API diff
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"deno.enablePaths": ["./deno_compat"]
"deno.enablePaths": ["./deno_compat"],
"typescript.tsdk": "node_modules/typescript/lib"
}
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["<rootDir>/src/**/*.test.ts", "!**/examples/**/*.test.ts"],
roots: ["<rootDir>/src"],
moduleNameMapper: {
inngest: "<rootDir>/src",
},
};
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build": "yarn run clean && tsc --project tsconfig.build.json",
"test": "node --expose-gc --max-old-space-size=4096 ./node_modules/.bin/jest --silent --logHeapUsage --maxWorkers=8 --coverage --ci --verbose",
"test:examples": "node --expose-gc --max-old-space-size=4096 ./node_modules/.bin/jest --logHeapUsage --maxWorkers=8 --testMatch \"**/examples/**/*.test.ts\" --ci --verbose",
"test:types": "tsc --noEmit --project tsconfig.types.json",
"clean": "rm -rf ./dist",
"lint": "eslint .",
"postversion": "yarn run build && yarn run build:copy",
Expand Down Expand Up @@ -95,5 +96,11 @@
"volta": {
"node": "18.12.1",
"yarn": "1.22.19"
},
"peerDependencies": {
"typescript": ">=4.7.2"
},
"engines": {
"node": ">=14"
}
}
31 changes: 16 additions & 15 deletions src/components/Inngest.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { EventPayload } from "inngest";
import { assertType } from "type-plus";
import { envKeys } from "../helpers/consts";
import { IsAny } from "../helpers/types";
import { EventPayload } from "../types";
import { eventKeyWarning, Inngest } from "./Inngest";
import { createClient } from "../test/helpers";
import { eventKeyWarning } from "./Inngest";

const testEvent: EventPayload = {
name: "test",
Expand Down Expand Up @@ -32,18 +33,18 @@ describe("instantiation", () => {
});

test("should log a warning if event key not specified", () => {
new Inngest({ name: "test" });
createClient({ name: "test" });
expect(warnSpy).toHaveBeenCalledWith(eventKeyWarning);
});

test("should not log a warning if event key is specified", () => {
new Inngest({ name: "test", eventKey: testEventKey });
createClient({ name: "test", eventKey: testEventKey });
expect(warnSpy).not.toHaveBeenCalled();
});

test("should not log a warning if event key is specified in env", () => {
process.env[envKeys.EventKey] = testEventKey;
new Inngest({ name: "test" });
createClient({ name: "test" });
expect(warnSpy).not.toHaveBeenCalled();
});
});
Expand Down Expand Up @@ -84,15 +85,15 @@ describe("send", () => {
});

test("should fail to send if event key not specified at instantiation", async () => {
const inngest = new Inngest({ name: "test" });
const inngest = createClient({ name: "test" });

await expect(() => inngest.send(testEvent)).rejects.toThrowError(
"Could not find an event key"
);
});

test("should succeed if event key specified at instantiation", async () => {
const inngest = new Inngest({ name: "test", eventKey: testEventKey });
const inngest = createClient({ name: "test", eventKey: testEventKey });

await expect(inngest.send(testEvent)).resolves.toBeUndefined();

Expand All @@ -107,7 +108,7 @@ describe("send", () => {

test("should succeed if event key specified in env", async () => {
process.env[envKeys.EventKey] = testEventKey;
const inngest = new Inngest({ name: "test" });
const inngest = createClient({ name: "test" });

await expect(inngest.send(testEvent)).resolves.toBeUndefined();

Expand All @@ -121,7 +122,7 @@ describe("send", () => {
});

test("should succeed if event key given at runtime", async () => {
const inngest = new Inngest({ name: "test" });
const inngest = createClient({ name: "test" });
inngest.setEventKey(testEventKey);

await expect(inngest.send(testEvent)).resolves.toBeUndefined();
Expand All @@ -136,15 +137,15 @@ describe("send", () => {
});

test("should succeed if an event name is given with an empty list of payloads", async () => {
const inngest = new Inngest({ name: "test" });
const inngest = createClient({ name: "test" });
inngest.setEventKey(testEventKey);

await expect(inngest.send("test", [])).resolves.toBeUndefined();
expect(global.fetch).not.toHaveBeenCalled();
});

test("should succeed if an empty list of payloads is given", async () => {
const inngest = new Inngest({ name: "test" });
const inngest = createClient({ name: "test" });
inngest.setEventKey(testEventKey);

await expect(inngest.send([])).resolves.toBeUndefined();
Expand All @@ -154,7 +155,7 @@ describe("send", () => {

describe("types", () => {
describe("no custom types", () => {
const inngest = new Inngest({ name: "test", eventKey: testEventKey });
const inngest = createClient({ name: "test", eventKey: testEventKey });

test("allows sending a single event with a string", () => {
const _fn = () => inngest.send("anything", { data: "foo" });
Expand All @@ -174,7 +175,7 @@ describe("send", () => {
});

describe("multiple custom types", () => {
const inngest = new Inngest<{
const inngest = createClient<{
foo: {
name: "foo";
data: { foo: string };
Expand Down Expand Up @@ -256,7 +257,7 @@ describe("send", () => {
describe("createFunction", () => {
describe("types", () => {
describe("no custom types", () => {
const inngest = new Inngest({ name: "test" });
const inngest = createClient({ name: "test" });

test("allows name to be a string", () => {
inngest.createFunction("test", { event: "test" }, ({ event }) => {
Expand Down Expand Up @@ -331,7 +332,7 @@ describe("createFunction", () => {
});

describe("multiple custom types", () => {
const inngest = new Inngest<{
const inngest = createClient<{
foo: {
name: "foo";
data: { title: string };
Expand Down
12 changes: 5 additions & 7 deletions src/components/Inngest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,11 @@ export class Inngest<
/**
* Grab our payloads straight from the args.
*/
payloads = (
Array.isArray(nameOrPayload)
? nameOrPayload
: nameOrPayload
? [nameOrPayload]
: []
) as typeof payloads;
payloads = (Array.isArray(nameOrPayload)
? nameOrPayload
: nameOrPayload
? [nameOrPayload]
: []) as unknown as typeof payloads;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/components/InngestCommHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import { serve } from "../next";
import { Inngest } from "./Inngest";
import { createClient } from "../test/helpers";

describe("#153", () => {
test('does not throw "type instantiation is excessively deep and possibly infinite" for looping type', () => {
Expand All @@ -13,7 +13,7 @@ describe("#153", () => {
type Literal = z.infer<typeof literalSchema>;
type Json = Literal | { [key: string]: Json } | Json[];

const inngest = new Inngest<{
const inngest = createClient<{
foo: {
name: "foo";
data: {
Expand Down
12 changes: 7 additions & 5 deletions src/components/InngestCommHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { devServerAvailable, devServerUrl } from "../helpers/devserver";
import { allProcessEnv, isProd } from "../helpers/env";
import { serializeError } from "../helpers/errors";
import { strBoolean } from "../helpers/scalar";
import { stringifyUnknown } from "../helpers/strings";
import type { MaybePromise } from "../helpers/types";
import { landing } from "../landing";
import {
Expand Down Expand Up @@ -518,7 +519,7 @@ export class InngestCommHandler<H extends Handler, TransformedRes> {
this.upsertSigningKeyFromEnv(env);

const showLandingPage = this.shouldShowLandingPage(
env[envKeys.LandingPage]?.toString()
stringifyUnknown(env[envKeys.LandingPage])
);

if (this._isProd || !showLandingPage) {
Expand All @@ -532,8 +533,9 @@ export class InngestCommHandler<H extends Handler, TransformedRes> {
if (viewRes.isIntrospection) {
const introspection: IntrospectRequest = {
...this.registerBody(this.reqUrl(actions.url)),
devServerURL: devServerUrl(env[envKeys.DevServerUrl]?.toString())
.href,
devServerURL: devServerUrl(
stringifyUnknown(env[envKeys.DevServerUrl])
).href,
hasSigningKey: Boolean(this.signingKey),
};

Expand Down Expand Up @@ -563,7 +565,7 @@ export class InngestCommHandler<H extends Handler, TransformedRes> {

const { status, message } = await this.register(
this.reqUrl(actions.url),
env[envKeys.DevServerUrl]?.toString(),
stringifyUnknown(env[envKeys.DevServerUrl]),
registerRes.deployId
);

Expand Down Expand Up @@ -870,7 +872,7 @@ export class InngestCommHandler<H extends Handler, TransformedRes> {

private upsertSigningKeyFromEnv(env: Record<string, unknown>) {
if (!this.signingKey && env[envKeys.SigningKey]) {
this.signingKey = env[envKeys.SigningKey].toString();
this.signingKey = String(env[envKeys.SigningKey]);
}
}

Expand Down
Loading

0 comments on commit 906aca5

Please sign in to comment.