From fb1ae491304058646cb666d1ecd33c63960d7adf Mon Sep 17 00:00:00 2001 From: larabr Date: Mon, 28 Feb 2022 18:41:23 +0100 Subject: [PATCH] TypeScript: fix definitions to have `ReadableStream` compatible with `WebStream`, add tests (#13) Update `WebStream` definition so that type inference works when using a `ReadableStream`. Specifically, we cannot rely on the `AsyncIterable` interface, because ReadableStreams are not currently implemented by all browsers as including the necessary properties. --- .github/workflows/test-type-definitions.yml | 21 +++++++++ .gitignore | 3 ++ package-lock.json | 18 ++++++++ package.json | 7 ++- test/typescript-definitions.ts | 22 ++++++++++ web-stream-tools.d.ts | 48 +++++++++++++-------- 6 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/test-type-definitions.yml create mode 100644 .gitignore create mode 100644 test/typescript-definitions.ts diff --git a/.github/workflows/test-type-definitions.yml b/.github/workflows/test-type-definitions.yml new file mode 100644 index 0000000..594e89d --- /dev/null +++ b/.github/workflows/test-type-definitions.yml @@ -0,0 +1,21 @@ +name: Types + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + lint: + name: Test type definitions + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '15' + - run: npm ci + - run: npm run test-type-definitions diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ea988c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +test/typescript-definitions.js +.DS_Store diff --git a/package-lock.json b/package-lock.json index 4c1f960..582ed21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,6 +99,12 @@ } } }, + "@types/node": { + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", + "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -123,6 +129,12 @@ "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", "dev": true }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true + }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", @@ -190,6 +202,12 @@ "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", "dev": true }, + "typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true + }, "uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", diff --git a/package.json b/package.json index 93f70a5..a123532 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,14 @@ "web-streams-polyfill": "~3.0.3" }, "devDependencies": { - "@openpgp/jsdoc": "^3.6.4" + "@openpgp/jsdoc": "^3.6.4", + "@types/node": "^17.0.18", + "esm": "^3.2.25", + "typescript": "^4.5.5" }, "scripts": { + "test-type-definitions": "tsc test/typescript-definitions.ts && node --require esm test/typescript-definitions.js", + "test": "npm run test-type-definitions", "docs": "jsdoc --configure .jsdocrc.js --destination docs --readme README.md lib", "preversion": "rm -rf docs", "version": "npm run docs && git add -A docs", diff --git a/test/typescript-definitions.ts b/test/typescript-definitions.ts new file mode 100644 index 0000000..d08d3e3 --- /dev/null +++ b/test/typescript-definitions.ts @@ -0,0 +1,22 @@ +import * as assert from 'assert'; +import { Readable as NodeReadableStream } from 'stream'; +import { ReadableStream as WebReadableStream } from 'web-streams-polyfill'; +import { WebStream, NodeStream, readToEnd } from '../'; + +(async () => { + + const nodeStream: NodeStream = new NodeReadableStream(); + assert(nodeStream instanceof NodeReadableStream); + // @ts-expect-error detect type parameter mismatch + const webStream: WebStream = new ReadableStream(); + assert(webStream instanceof WebReadableStream); + + await readToEnd(new Uint8Array([1])) as Uint8Array; + await readToEnd(new Uint8Array([1]), _ => _) as Uint8Array[]; + + console.log('TypeScript definitions are correct'); +})().catch(e => { + console.error('TypeScript definitions tests failed by throwing the following error'); + console.error(e); + process.exit(1); +}); diff --git a/web-stream-tools.d.ts b/web-stream-tools.d.ts index e35a7d5..5444280 100644 --- a/web-stream-tools.d.ts +++ b/web-stream-tools.d.ts @@ -2,25 +2,39 @@ // Contributors: // - Flowcrypt a.s. -declare module "@openpgp/web-stream-tools" { - type Data = Uint8Array | string; - - interface BaseStream extends AsyncIterable { } +export namespace ReadableStreamInternals { + // copied+simplified version of ReadableStream from lib.dom.d.ts, needed to enforce type checks in WebStream below + interface ReadableStreamDefaultReadDoneResult { + done: true; + value?: undefined; + } + interface ReadableStreamDefaultReadValueResult { + done: false; + value: T; + } + interface ReadableStreamDefaultReader { + readonly closed: Promise; + cancel(reason?: any): Promise; + read(): Promise | ReadableStreamDefaultReadDoneResult>; + releaseLock(): void; + } +} - interface WebStream extends BaseStream { // copied+simplified version of ReadableStream from lib.dom.d.ts - readonly locked: boolean; getReader: Function; pipeThrough: Function; pipeTo: Function; tee: Function; - cancel(reason?: any): Promise; - } +type Data = Uint8Array | string; - interface NodeStream extends BaseStream { // copied+simplified version of ReadableStream from @types/node/index.d.ts - readable: boolean; pipe: Function; unpipe: Function; wrap: Function; - read(size?: number): string | Uint8Array; setEncoding(encoding: string): this; pause(): this; resume(): this; - isPaused(): boolean; unshift(chunk: string | Uint8Array): void; - } +interface WebStream { // copied+simplified version of ReadableStream from lib.dom.d.ts + readonly locked: boolean; pipeThrough: Function; pipeTo: Function; tee: Function; + cancel(reason?: any): Promise; + getReader(): ReadableStreamInternals.ReadableStreamDefaultReader; +} - type Stream = WebStream | NodeStream; +interface NodeStream extends AsyncIterable { // copied+simplified version of ReadableStream from @types/node/index.d.ts + readable: boolean; pipe: Function; unpipe: Function; wrap: Function; setEncoding(encoding: string): this; pause(): this; resume(): this; + isPaused(): boolean; unshift(chunk: string | Uint8Array): void; + read(size?: number): T; +} - type MaybeStream = T | Stream; +type Stream = WebStream | NodeStream; +type MaybeStream = T | Stream; - export function readToEnd(input: MaybeStream, concat?: (list: T[]) => T): Promise; -} +export function readToEnd(input: MaybeStream, join?: (chunks: T[]) => R): Promise;