Skip to content

Commit a40796d

Browse files
authored
Migrate to Vitest (#13)
* Combine Node and browser tests thanks to Vitest * Convert to an ES Module * Add VS Code dev container definition to ease development * Follow direct upstream linting recommendations * Reduce the number of dependencies to hopefully also reduce dependabot alerts
1 parent 9e0fcb2 commit a40796d

23 files changed

Lines changed: 2956 additions & 7373 deletions

.devcontainer/devcontainer.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "Node.js & TypeScript",
3+
"image": "mcr.microsoft.com/devcontainers/typescript-node:latest",
4+
"customizations": {
5+
"vscode": {
6+
"extensions": [
7+
"dbaeumer.vscode-eslint",
8+
"github.vscode-github-actions",
9+
"ms-azuretools.vscode-containers"
10+
],
11+
"settings": {
12+
"chat.disableAIFeatures": true,
13+
"editor.formatOnSave": true,
14+
"eslint.format.enable": true,
15+
"eslint.validate": [
16+
"javascript",
17+
"json",
18+
"typescript"
19+
],
20+
"[javascript]": {
21+
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
22+
},
23+
"[json]": {
24+
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
25+
},
26+
"[typescript]": {
27+
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
28+
}
29+
}
30+
}
31+
}
32+
}

.github/workflows/ci.yml

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ jobs:
2323
- uses: actions/checkout@v6
2424
- uses: actions/setup-node@v6
2525
with:
26-
cache: yarn
2726
node-version: ${{ env.DEFAULT_NODE_VERSION }}
2827
- run: corepack enable
29-
- run: yarn install --frozen-lockfile --ignore-scripts
28+
- run: yarn install --immutable
3029
- run: yarn run lint
3130

3231
node:
@@ -35,24 +34,22 @@ jobs:
3534
strategy:
3635
matrix:
3736
node-version:
38-
- 18.x
3937
- 20.x
4038
- 22.x
4139
- 24.x
4240
steps:
4341
- uses: actions/checkout@v6
4442
- uses: actions/setup-node@v6
4543
with:
46-
cache: yarn
4744
node-version: ${{ matrix.node-version }}
4845
- run: corepack enable
49-
- run: yarn install --frozen-lockfile --ignore-scripts
46+
- run: yarn install --immutable
5047
- run: yarn run build
51-
- run: yarn run test
48+
- run: yarn vitest run --project node --coverage
5249
- uses: coverallsapp/github-action@v2
5350
with:
5451
github-token: ${{ secrets.github_token }}
55-
flag-name: run-${{ matrix.node-version }}
52+
flag-name: node-${{ matrix.node-version }}
5653
parallel: true
5754

5855
browser:
@@ -61,27 +58,27 @@ jobs:
6158
strategy:
6259
matrix:
6360
browser:
64-
- firefox
6561
- chromium
62+
- firefox
6663
- webkit
6764
steps:
6865
- uses: actions/checkout@v6
6966
- uses: actions/setup-node@v6
7067
with:
71-
cache: yarn
7268
node-version: ${{ env.DEFAULT_NODE_VERSION }}
7369
- run: corepack enable
74-
- run: yarn install --frozen-lockfile --ignore-scripts
70+
- run: yarn install --immutable
71+
- run: yarn run build
7572
- run: yarn playwright install --with-deps ${{ matrix.browser }}
76-
- run: yarn playwright test --browser ${{ matrix.browser }}
73+
- run: yarn vitest run --project ${{ matrix.browser }}
7774

7875
coveralls:
7976
needs:
8077
- node
78+
- browser
8179
runs-on: ubuntu-latest
8280
steps:
83-
- name: Consolidate test coverage from different jobs
84-
uses: coverallsapp/github-action@v2
81+
- uses: coverallsapp/github-action@v2
8582
with:
8683
github-token: ${{ secrets.github_token }}
8784
parallel-finished: true

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
node-version: ${{ env.DEFAULT_NODE_VERSION }}
3030
registry-url: https://registry.npmjs.org
3131
- run: corepack enable
32-
- run: yarn install --frozen-lockfile --ignore-scripts
32+
- run: yarn install --immutable
3333
- run: yarn run build
3434
# TODO: when Yarn supports --provenance, switch to "yarn publish"
3535
- run: yarn pack

.gitignore

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
.yarn
2-
coverage
3-
test-results
4-
node_modules
5-
1+
.yarn/
2+
coverage/
3+
node_modules/
64
*.d.ts
75
*.js
86
*.js.map
7+
!test/readable-stream-node-to-web.d.ts

.yarnrc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
compressionLevel: mixed
2+
enableGlobalCache: false
3+
enableTelemetry: false
4+
nodeLinker: node-modules

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024
3+
Copyright (c) 2026
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

eslint.config.cjs

Lines changed: 0 additions & 20 deletions
This file was deleted.

eslint.config.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { defineConfig, globalIgnores } from 'eslint/config'
2+
import { configs } from '@eslint/js'
3+
import tseslint from 'typescript-eslint'
4+
5+
export default defineConfig(
6+
configs.all,
7+
tseslint.configs.eslintRecommended,
8+
tseslint.configs.strict,
9+
tseslint.configs.stylistic,
10+
globalIgnores([
11+
'**/*.d.ts',
12+
'**/*.js',
13+
'**/*.js.map',
14+
'**/coverage/',
15+
'**/node_modules/'
16+
]),
17+
{
18+
rules: {
19+
'array-bracket-spacing': [ 'error', 'always' ],
20+
'comma-dangle': [ 'error', 'never' ],
21+
'computed-property-spacing': [ 'error', 'never' ],
22+
// eslint-disable-next-line no-magic-numbers
23+
'indent': [ 'error', 2, { SwitchCase: 1 } ],
24+
'object-curly-spacing': [ 'error', 'always' ],
25+
'one-var': [ 'error', 'never' ],
26+
'quotes': [ 'error', 'single' ],
27+
'semi': [ 'error', 'never' ]
28+
}
29+
}
30+
)

jest.config.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

lib/ReadableFromWeb.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,56 @@
1-
import { Readable, type ReadableOptions } from 'readable-stream';
1+
import { Readable, type ReadableOptions } from 'readable-stream'
22

3+
/**
4+
* Wrapper for ReadableStream that converts it into a readable-stream Readable.
5+
* The implementation loosely follows the example laid out by Node's internal implementation:
6+
* https://github.com/nodejs/node/blob/0b676736a0e9ab4939c195a516aa7e82fcd839aa/lib/internal/webstreams/adapters.js#L512
7+
*/
38
class ReadableFromWeb<T> extends Readable {
4-
private readonly reader: ReadableStreamDefaultReader<T>;
5-
private readerClosed: boolean;
9+
private readonly reader: ReadableStreamDefaultReader<T>
10+
private readerClosed: boolean
611

7-
public constructor(stream: ReadableStream<T>, options?: ReadableOptions) {
8-
super(options);
9-
this.reader = stream.getReader();
10-
this.readerClosed = false;
11-
this.reader.closed.then(() => {
12-
this.readerClosed = true;
13-
}).catch((error: Error) => {
14-
this.readerClosed = true;
15-
this.destroy(error);
16-
});
12+
public constructor(stream: Readonly<ReadableStream<T>>, options?: Readonly<ReadableOptions>) {
13+
super(options)
14+
this.reader = stream.getReader()
15+
this.readerClosed = false
16+
this.reader.closed.catch((error: unknown) => {
17+
this.destroy(error)
18+
}).finally(() => {
19+
this.readerClosed = true
20+
})
1721
}
1822

19-
// eslint-disable-next-line ts/naming-convention
2023
public _read(): void {
2124
this.reader.read()
22-
.then(chunk => this.push(chunk.done ? null : chunk.value))
23-
.catch((error: Error) => this.destroy(error));
25+
.then((chunk: Readonly<ReadableStreamReadResult<T>>) => {
26+
if (chunk.done) {
27+
this.push(null)
28+
} else {
29+
this.push(chunk.value)
30+
}
31+
})
32+
.catch((error: unknown) => {
33+
this.destroy(error)
34+
})
2435
}
2536

26-
public destroy(error?: Error): this {
37+
public destroy(error: unknown): this {
38+
let finalError: unknown = error
2739
if (!this.readerClosed) {
28-
this.reader.cancel(error).then().catch(() => {
29-
// Ideally, the error from cancel should be handled here.
30-
// However, an error thrown in cancel does not seem to reach this callback.
31-
// Therefore, the error is simply not handled here.
32-
});
40+
this.reader.cancel(error).then().catch((cancelError: unknown) => {
41+
finalError = cancelError
42+
})
3343
}
34-
return super.destroy(error);
44+
if (finalError instanceof Error) {
45+
return super.destroy(finalError)
46+
}
47+
return super.destroy()
3548
}
3649
}
3750

38-
function readableFromWeb<T>(stream: ReadableStream<T>, options?: ReadableOptions): Readable {
39-
return new ReadableFromWeb<T>(stream, options);
40-
}
51+
const readableFromWeb = <T>(
52+
stream: Readonly<ReadableStream<T>>,
53+
options?: Readonly<ReadableOptions>
54+
): Readable => new ReadableFromWeb<T>(stream, options)
4155

42-
export { ReadableFromWeb, readableFromWeb };
56+
export { ReadableFromWeb, readableFromWeb }

0 commit comments

Comments
 (0)