Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/published-types-skiplibcheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"livekit-client": patch
---

fix published type declarations for consumers with `skipLibCheck: false` or `exactOptionalPropertyTypes`
5 changes: 4 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
run: |
pnpm build:clean
pnpm compat


- name: Check published types in a consumer with skipLibCheck off
run: pnpm check:published-types

- name: Ensure package publish is configured correctly
run: pnpm publint
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ user-facing API.
- **Run all tests:** `npx vitest run`
- **Run specific test file:** `npx vitest run path/to/file.test.ts`
- **Format:** `npm run format`
- **Published types:** `pnpm check:published-types` (after `pnpm build`) installs the packed tarball
into a temp project outside the repo and type-checks every entry with `skipLibCheck` off and
`exactOptionalPropertyTypes` on. Declarations must name only packages in `dependencies`, and their
`@types/*` packages must be there too. Import `Throws` from `src/utils/throws.ts`, never from
`@livekit/throws-transformer` (a devDependency). If `dist` still shows an old inferred type after
a type-only change, delete `node_modules/.cache/rollup-plugin-typescript2`: its cache misses those.

## Examples

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,13 @@
"downlevel-dts": "downlevel-dts ./dist/src ./dist/ts4.2 --to=4.2 && downlevel-dts ./dist/core/types/src ./dist/core/ts4.2 --to=4.2",
"compat": "eslint --config ./eslint.config.dist.mjs --no-inline-config ./dist/livekit-client.esm.mjs",
"size-limit": "size-limit",
"check:core": "node scripts/check-core-imports.mjs && node scripts/check-core-types.mjs"
"check:core": "node scripts/check-core-imports.mjs && node scripts/check-core-types.mjs",
"check:published-types": "node scripts/check-published-types.mjs"
},
"dependencies": {
"@livekit/mutex": "1.1.1",
"@livekit/protocol": "1.52.1",
"@types/sdp-transform": "^2.15.0",
"events": "^3.3.0",
"jose": "^6.1.0",
"loglevel": "^1.9.2",
Expand Down Expand Up @@ -118,7 +120,6 @@
"@stylistic/eslint-plugin": "^5.10.0",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/events": "^3.0.3",
"@types/sdp-transform": "2.15.0",
"@types/ua-parser-js": "0.7.39",
"@typescript-eslint/eslint-plugin": "8.64.0",
"@typescript-eslint/parser": "8.64.0",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions scripts/check-published-types.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Gate for the published declarations, as a consumer sees them. Packs the built package
// (run `pnpm build` first), installs the tarball into a temp project and type-checks every
// entry with `skipLibCheck` off and the strictest options a consumer may set. That catches:
// - a declaration that imports a package the consumer does not install (a devDependency, or a
// dependency whose types live in a devDependency `@types/*` package);
// - declarations that contradict each other under `exactOptionalPropertyTypes`.
// The temp project sits outside the repo: inside it, module resolution walks up to the repo's
// node_modules and finds the devDependencies, which hides the first case.
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const tsc = path.join(REPO, 'node_modules', '.bin', 'tsc');
const tmp = mkdtempSync(path.join(os.tmpdir(), 'livekit-published-types-'));

const run = (cmd, args, cwd) => {
try {
return { ok: true, out: execFileSync(cmd, args, { cwd, encoding: 'utf8', stdio: 'pipe' }) };
} catch (err) {
return { ok: false, out: `${err.stdout ?? ''}${err.stderr ?? ''}` };
}
};

const must = (result) => {
if (!result.ok) {
console.error(result.out);
process.exit(1);
}
};

try {
must(run('pnpm', ['pack', '--pack-destination', tmp], REPO));
const tarball = readdirSync(tmp).find((file) => file.endsWith('.tgz'));
writeFileSync(
path.join(tmp, 'package.json'),
JSON.stringify({ name: 'consumer', private: true, type: 'module' }),
);
// npm, not pnpm: a flat node_modules with only what the package.json declares (and its peers).
must(run('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts', `./${tarball}`], tmp));

writeFileSync(
path.join(tmp, 'probe.ts'),
`export * as full from 'livekit-client';
export * as core from 'livekit-client/core';
export * as e2eeWorker from 'livekit-client/e2ee-worker';
export * as frameMetadataWorker from 'livekit-client/frame-metadata-worker';
`,
);
writeFileSync(
path.join(tmp, 'tsconfig.json'),
JSON.stringify({
compilerOptions: {
noEmit: true,
strict: true,
exactOptionalPropertyTypes: true,
skipLibCheck: false,
module: 'ESNext',
moduleResolution: 'bundler',
target: 'ES2022',
lib: ['DOM', 'DOM.Iterable', 'ES2022'],
},
files: ['probe.ts'],
}),
);

const check = run(tsc, ['-p', path.join(tmp, 'tsconfig.json')], tmp);
const errors = check.out.split('\n').filter((line) => /error TS\d+/.test(line));
if (!check.ok || errors.length > 0) {
console.error('published types: a consumer with skipLibCheck off gets errors:');
for (const line of errors.length > 0 ? errors : [check.out]) console.error(` ${line.trim()}`);
process.exit(1);
}
console.log('published types: all entries type-check in a consumer with skipLibCheck off');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
2 changes: 1 addition & 1 deletion src/room/PCTransportManager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Mutex } from '@livekit/mutex';
import { SignalTarget } from '@livekit/protocol';
import type { Throws } from '@livekit/throws-transformer/throws';
import log, { LoggerNames, getLogger } from '../logger';
import TypedPromise from '../utils/TypedPromise';
import type { Throws } from '../utils/throws';
import PCTransport, { PCEvents } from './PCTransport';
import { roomConnectOptionDefaults } from './defaults';
import { ConnectionError, NegotiationError } from './errors';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/depacketizer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Throws } from '@livekit/throws-transformer/throws';
import { LoggerNames, getLogger } from '../../logger';
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
import { type Throws } from '../../utils/throws';
import { LivekitReasonedError } from '../errors';
import { type DataTrackFrameInternal } from './frame';
import { DataTrackPacket, FrameMarker } from './packet';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/handle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Throws } from '@livekit/throws-transformer/throws';
import { type Throws } from '../../utils/throws';
import { LivekitReasonedError } from '../errors';
import { U16_MAX_SIZE } from './utils';

Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/incoming/IncomingDataTrackManager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type JoinResponse, type ParticipantUpdate } from '@livekit/protocol';
import { EventEmitter } from 'events';
import type { Throws } from '@livekit/throws-transformer/throws';
import type TypedEmitter from 'typed-emitter';
import type { BaseE2EEManager } from '../../../e2ee/E2eeManager';
import { LoggerNames, getLogger } from '../../../logger';
import { abortSignalAny, abortSignalTimeout } from '../../../utils/abort-signal-polyfill';
import type { Throws } from '../../../utils/throws';
import type Participant from '../../participant/Participant';
import type RemoteParticipant from '../../participant/RemoteParticipant';
import { Future } from '../../utils';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/incoming/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Throws } from '@livekit/throws-transformer/throws';
import type { BaseE2EEManager } from '../../../e2ee/E2eeManager';
import { LoggerNames, getLogger } from '../../../logger';
import type { Throws } from '../../../utils/throws';
import DataTrackDepacketizer, { DataTrackDepacketizerDropError } from '../depacketizer';
import type { DataTrackFrameInternal } from '../frame';
import { DataTrackPacket } from '../packet';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/outgoing/OutgoingDataTrackManager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EventEmitter } from 'events';
import type { Throws } from '@livekit/throws-transformer/throws';
import type TypedEmitter from 'typed-emitter';
import type { BaseE2EEManager } from '../../../e2ee/E2eeManager';
import { LoggerNames, getLogger } from '../../../logger';
import { abortSignalAny, abortSignalTimeout } from '../../../utils/abort-signal-polyfill';
import type { Throws } from '../../../utils/throws';
import { Future } from '../../utils';
import LocalDataTrack from '../LocalDataTrack';
import type { DataTrackFrameInternal } from '../frame';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/outgoing/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Throws } from '@livekit/throws-transformer/throws';
import type { BaseE2EEManager } from '../../../e2ee/E2eeManager';
import { type Throws } from '../../../utils/throws';
import { type DataTrackFrameInternal } from '../frame';
import { DataTrackPacket } from '../packet';
import { DataTrackE2eeExtension } from '../packet/extensions';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/packet/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Throws } from '@livekit/throws-transformer/throws';
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
import { type Throws } from '../../../utils/throws';
import { coerceToDataView } from '../utils';
import { EXT_TAG_PADDING, U8_LENGTH_BYTES, U64_LENGTH_BYTES } from './constants';
import { DataTrackDeserializeError, DataTrackDeserializeErrorReason } from './errors';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/packet/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Throws } from '@livekit/throws-transformer/throws';
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
import { type Throws } from '../../../utils/throws';
import { DataTrackHandle, DataTrackHandleError, DataTrackHandleErrorReason } from '../handle';
import {
DataTrackTimestamp,
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/packet/serializable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Throws } from '@livekit/throws-transformer/throws';
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
import { type Throws } from '../../../utils/throws';
import { DataTrackSerializeError } from './errors';

/** An abstract class implementing common behavior related to data track binary serialization. */
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/packetizer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Throws } from '@livekit/throws-transformer/throws';
import type { Throws } from '../../utils/throws';
import { LivekitReasonedError } from '../errors';
import { type DataTrackFrameInternal } from './frame';
import { DataTrackHandle } from './handle';
Expand Down
2 changes: 1 addition & 1 deletion src/room/data-track/track-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const DataTrackSymbol: symbol = /* @__PURE__ */ Symbol.for('lk.data-track
export interface IDataTrack extends ITrack {
readonly typeSymbol: typeof DataTrackSymbol;

readonly info?: DataTrackInfo;
readonly info?: DataTrackInfo | undefined;
}

export function isDataTrack(subject: unknown): subject is IDataTrack {
Expand Down
2 changes: 1 addition & 1 deletion src/room/track/LocalTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default abstract class LocalTrack<

protected processorElement?: HTMLMediaElement;

protected processor?: TrackProcessor<TrackKind, any>;
protected processor?: TrackProcessor<TrackKind, any> | undefined;

protected audioContext?: AudioContext;

Expand Down
2 changes: 1 addition & 1 deletion src/room/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
DisconnectReason,
Transcription as TranscriptionModel,
} from '@livekit/protocol';
import { type Throws } from '@livekit/throws-transformer/throws';
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
import TypedPromise from '../utils/TypedPromise';
import { getBrowser } from '../utils/browserParser';
import type { BrowserDetails } from '../utils/browserParser';
import { type Throws } from '../utils/throws';
import { clientProtocol, protocolVersion, version } from '../version';
import { type ConnectionError, ConnectionErrorReason } from './errors';
import type { LocalParticipant } from './participant/LocalParticipant';
Expand Down
2 changes: 1 addition & 1 deletion src/utils/deferrable-map.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Throws } from '@livekit/throws-transformer/throws';
import { Future } from '../room/utils';
import { type Throws } from './throws';

/** An error which is thrown if a {@link DeferrableMap#getDeferred} call is aborted midway
* through. */
Expand Down
15 changes: 15 additions & 0 deletions src/utils/throws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// A copy of `Throws` from `@livekit/throws-transformer/throws`. The published declarations name
// this type, and the transformer is a devDependency, so a consumer could not resolve an import
// of it. The transformer matches the `__throws` brand, not the import path: keep the brand as is.

type Primitives = void | null | undefined | string | number | bigint | boolean | symbol;

/**
* Branded type that encodes possible thrown errors in the return type. The error types are
* phantom types for static analysis; the runtime value is just T.
*/
export type Throws<T, E extends Error> =
| (T & {
readonly __throws?: E;
})
| Extract<T, Primitives>;
Comment on lines +1 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Is there some way these could pull from the npm package implementation still, but get resolved at build time into effectively the contents of this file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I'm not particularly happy about just copying this either.

The potential viable options as I see them:

  • have a dedicated types package for the throws transformer that's installed as a regular dependency
  • keep the copy and add a build time check that the types actually match

Loading