diff --git a/.eslintrc.js b/.eslintrc.js index 7f2ba0089d..8704799ce8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,7 +44,7 @@ module.exports = { }, }, { - files: 'ably.d.ts', + files: ['ably.d.ts', 'modules.d.ts'], extends: [ 'plugin:jsdoc/recommended', ], diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 675343c945..f131a149da 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,5 +21,5 @@ jobs: - run: npm ci - run: npm run lint - run: npm run format:check - - run: npx tsc --noEmit ably.d.ts build/ably-webworker.min.d.ts + - run: npx tsc --noEmit ably.d.ts modules.d.ts build/ably-webworker.min.d.ts - run: npm audit --production diff --git a/ably.d.ts b/ably.d.ts index 126779e264..fa58110328 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -1700,32 +1700,7 @@ declare namespace Types { /** * A client that offers a simple stateless API to interact directly with Ably's REST API. */ - class Rest { - /** - * Construct a client object using an Ably {@link Types.ClientOptions} object. - * - * @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably. - */ - constructor(options: Types.ClientOptions); - /** - * Constructs a client object using an Ably API key or token string. - * - * @param keyOrToken - The Ably API key or token string used to validate the client. - */ - constructor(keyOrToken: string); - /** - * The cryptographic functions available in the library. - */ - static Crypto: Types.Crypto; - /** - * Static utilities related to messages. - */ - static Message: Types.MessageStatic; - /** - * Static utilities related to presence messages. - */ - static PresenceMessage: Types.PresenceMessageStatic; - + abstract class Rest { /** * An {@link Types.Auth} object. */ @@ -1799,31 +1774,7 @@ declare namespace Types { /** * A client that extends the functionality of {@link Rest} and provides additional realtime-specific features. */ - class Realtime { - /** - * Construct a client object using an Ably {@link Types.ClientOptions} object. - * - * @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably. - */ - constructor(options: Types.ClientOptions); - /** - * Constructs a client object using an Ably API key or token string. - * - * @param keyOrToken - The Ably API key or token string used to validate the client. - */ - constructor(keyOrToken: string); - /** - * The cryptographic functions available in the library. - */ - static Crypto: Types.Crypto; - /** - * Static utilities related to messages. - */ - static Message: Types.MessageStatic; - /** - * Static utilities related to presence messages. - */ - static PresenceMessage: Types.PresenceMessageStatic; + abstract class Realtime { /** * A client ID, used for identifying this client when publishing messages or for presence purposes. The `clientId` can be any non-empty string, except it cannot contain a `*`. This option is primarily intended to be used in situations where the library is instantiated with a key. A `clientId` may also be implicit in a token used to instantiate the library; an error will be raised if a `clientId` specified here conflicts with the `clientId` implicit in the token. */ @@ -2849,12 +2800,62 @@ declare namespace Types { /** * A client that offers a simple stateless API to interact directly with Ably's REST API. */ -export declare class Rest extends Types.Rest {} +export declare class Rest extends Types.Rest { + /** + * Construct a client object using an Ably {@link Types.ClientOptions} object. + * + * @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably. + */ + constructor(options: Types.ClientOptions); + /** + * Constructs a client object using an Ably API key or token string. + * + * @param keyOrToken - The Ably API key or token string used to validate the client. + */ + constructor(keyOrToken: string); + /** + * The cryptographic functions available in the library. + */ + static Crypto: Types.Crypto; + /** + * Static utilities related to messages. + */ + static Message: Types.MessageStatic; + /** + * Static utilities related to presence messages. + */ + static PresenceMessage: Types.PresenceMessageStatic; +} /** * A client that extends the functionality of {@link Rest} and provides additional realtime-specific features. */ -export declare class Realtime extends Types.Realtime {} +export declare class Realtime extends Types.Realtime { + /** + * Construct a client object using an Ably {@link Types.ClientOptions} object. + * + * @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably. + */ + constructor(options: Types.ClientOptions); + /** + * Constructs a client object using an Ably API key or token string. + * + * @param keyOrToken - The Ably API key or token string used to validate the client. + */ + constructor(keyOrToken: string); + /** + * The cryptographic functions available in the library. + */ + static Crypto: Types.Crypto; + /** + * Static utilities related to messages. + */ + static Message: Types.MessageStatic; + /** + * Static utilities related to presence messages. + */ + static PresenceMessage: Types.PresenceMessageStatic; +} /** * A generic Ably error object that contains an Ably-specific status code, and a generic status code. Errors returned from the Ably server are compatible with the `ErrorInfo` structure and should result in errors that inherit from `ErrorInfo`. diff --git a/modules.d.ts b/modules.d.ts new file mode 100644 index 0000000000..45e48bd18f --- /dev/null +++ b/modules.d.ts @@ -0,0 +1,45 @@ +import { Types } from './ably'; + +export declare const generateRandomKey: Types.Crypto['generateRandomKey']; +export declare const getDefaultCryptoParams: Types.Crypto['getDefaultParams']; +export declare const decodeMessage: Types.MessageStatic['fromEncoded']; +export declare const decodeEncryptedMessage: Types.MessageStatic['fromEncoded']; +export declare const decodeMessages: Types.MessageStatic['fromEncodedArray']; +export declare const decodeEncryptedMessages: Types.MessageStatic['fromEncodedArray']; +export declare const decodePresenceMessage: Types.PresenceMessageStatic['fromEncoded']; +export declare const decodePresenceMessages: Types.PresenceMessageStatic['fromEncodedArray']; +export declare const constructPresenceMessage: Types.PresenceMessageStatic['fromValues']; + +export declare const Rest: unknown; +export declare const Crypto: unknown; +export declare const MsgPack: unknown; +export declare const RealtimePresence: unknown; +export declare const WebSocketTransport: unknown; +export declare const XHRPolling: unknown; +export declare const XHRStreaming: unknown; +export declare const XHRRequest: unknown; +export declare const FetchRequest: unknown; +export declare const MessageInteractions: unknown; + +export interface ModulesMap { + Rest?: typeof Rest; + Crypto?: typeof Crypto; + MsgPack?: typeof MsgPack; + RealtimePresence?: typeof RealtimePresence; + WebSocketTransport?: typeof WebSocketTransport; + XHRPolling?: typeof XHRPolling; + XHRStreaming?: typeof XHRStreaming; + XHRRequest?: typeof XHRRequest; + FetchRequest?: typeof FetchRequest; + MessageInteractions?: typeof MessageInteractions; +} + +export declare class BaseRest extends Types.Rest { + constructor(options: Types.ClientOptions, modules: ModulesMap); +} + +export declare class BaseRealtime extends Types.Realtime { + constructor(options: Types.ClientOptions, modules: ModulesMap); +} + +export { Types }; diff --git a/package.json b/package.json index 101cbcac45..c4054bf155 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "default": "./build/ably.js" }, "./modules": { + "types": "./modules.d.ts", "default": "./build/modules/index.js" } }, @@ -21,6 +22,7 @@ "files": [ "build/**", "ably.d.ts", + "modules.d.ts", "resources/**", "src/**", "react/**" @@ -126,8 +128,8 @@ "lint": "eslint .", "lint:fix": "eslint --fix .", "prepare": "npm run build", - "format": "prettier --write --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts webpack.config.js Gruntfile.js scripts/*.js docs/chrome-mv3.md", - "format:check": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts webpack.config.js Gruntfile.js scripts/*.js", + "format": "prettier --write --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts modules.d.ts webpack.config.js Gruntfile.js scripts/*.js docs/chrome-mv3.md", + "format:check": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts modules.d.ts webpack.config.js Gruntfile.js scripts/*.js", "sourcemap": "source-map-explorer build/ably.min.js", "sourcemap:noencryption": "source-map-explorer build/ably.noencryption.min.js", "modulereport": "node scripts/moduleReport.js", diff --git a/test/package/browser/template/README.md b/test/package/browser/template/README.md index 6a8ab91e27..825c23d68d 100644 --- a/test/package/browser/template/README.md +++ b/test/package/browser/template/README.md @@ -5,7 +5,10 @@ This directory is intended to be used for testing the following aspects of the a - that its exports are correctly configured and provide access to ably-js’s functionality - that its TypeScript typings are correctly configured and can be successfully used from a TypeScript-based app that imports the package -The file `src/index.ts` imports the ably-js package and exports a function which briefly exercises its functionality. +It contains two files, each of which import ably-js in different manners, and which export a function which briefly exercises its functionality: + +- `src/index-default.ts` imports the default ably-js package (`import { Realtime } from 'ably'`). +- `src/index-modules.ts` imports the tree-shakable ably-js package (`import { BaseRealtime, WebSocketTransport, FetchRequest } from 'ably/modules'`). ## Why is `ably` not in `package.json`? @@ -15,6 +18,8 @@ The `ably` dependency gets added when we run the repository’s `test:bundle` pa This directory exposes three package scripts that are to be used for testing: -- `build`: Uses esbuild to create a bundle containing `src/index.ts` and ably-js. -- `test`: Using the bundle created by `build`, tests that the code that exercises ably-js’s functionality is working correctly in a browser. +- `build`: Uses esbuild to create: + 1. a bundle containing `src/index-default.ts` and ably-js; + 2. a bundle containing `src/index-modules.ts` and ably-js. +- `test`: Using the bundles created by `build`, tests that the code that exercises ably-js’s functionality is working correctly in a browser. - `typecheck`: Type-checks the code that imports ably-js. diff --git a/test/package/browser/template/package.json b/test/package/browser/template/package.json index 0b1e74ff9c..1c91a386eb 100644 --- a/test/package/browser/template/package.json +++ b/test/package/browser/template/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "build": "esbuild --bundle src/index.ts --outdir=dist", + "build": "esbuild --bundle src/index-default.ts --outdir=dist && esbuild --bundle src/index-modules.ts --outdir=dist", "typecheck": "tsc -noEmit", "test-support:server": "ts-node server/server.ts", "test": "playwright test", diff --git a/test/package/browser/template/server/resources/index-default.html b/test/package/browser/template/server/resources/index-default.html new file mode 100644 index 0000000000..f71012bf99 --- /dev/null +++ b/test/package/browser/template/server/resources/index-default.html @@ -0,0 +1,11 @@ + + + + + Ably NPM package test (default export) + + + + + + diff --git a/test/package/browser/template/server/resources/index-modules.html b/test/package/browser/template/server/resources/index-modules.html new file mode 100644 index 0000000000..0135008065 --- /dev/null +++ b/test/package/browser/template/server/resources/index-modules.html @@ -0,0 +1,11 @@ + + + + + Ably NPM package test (tree-shakable export) + + + + + + diff --git a/test/package/browser/template/server/resources/index.html b/test/package/browser/template/server/resources/index.html deleted file mode 100644 index 0e5fbaa1d1..0000000000 --- a/test/package/browser/template/server/resources/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Ably NPM package test - - - - - - diff --git a/test/package/browser/template/server/resources/runTest.js b/test/package/browser/template/server/resources/runTest.js new file mode 100644 index 0000000000..ccdc2d18e7 --- /dev/null +++ b/test/package/browser/template/server/resources/runTest.js @@ -0,0 +1,9 @@ +(async () => { + try { + await testAblyPackage(); + onResult(null); + } catch (error) { + console.log('Caught error', error); + onResult(error); + } +})(); diff --git a/test/package/browser/template/server/server.ts b/test/package/browser/template/server/server.ts index c371589aa3..6eefe26aba 100644 --- a/test/package/browser/template/server/server.ts +++ b/test/package/browser/template/server/server.ts @@ -3,8 +3,11 @@ import path from 'node:path'; async function startWebServer(listenPort: number) { const server = express(); + server.get('/', (req, res) => res.send('OK')); server.use(express.static(path.join(__dirname, '/resources'))); - server.use('/index.js', express.static(path.join(__dirname, '..', 'dist', 'index.js'))); + for (const filename of ['index-default.js', 'index-modules.js']) { + server.use(`/${filename}`, express.static(path.join(__dirname, '..', 'dist', filename))); + } server.listen(listenPort); } diff --git a/test/package/browser/template/src/index.ts b/test/package/browser/template/src/index-default.ts similarity index 100% rename from test/package/browser/template/src/index.ts rename to test/package/browser/template/src/index-default.ts diff --git a/test/package/browser/template/src/index-modules.ts b/test/package/browser/template/src/index-modules.ts new file mode 100644 index 0000000000..07a7751e3d --- /dev/null +++ b/test/package/browser/template/src/index-modules.ts @@ -0,0 +1,34 @@ +import { BaseRealtime, Types, WebSocketTransport, FetchRequest, generateRandomKey } from 'ably/modules'; +import { createSandboxAblyAPIKey } from './sandbox'; + +// This function exists to check that we can import the Types namespace and refer to its types. +async function attachChannel(channel: Types.RealtimeChannel) { + await channel.attach(); +} + +// This function exists to check that one of the free-standing functions (arbitrarily chosen) can be imported and does something vaguely sensible. +async function checkStandaloneFunction() { + const generatedKey = await generateRandomKey(); + if (!(generatedKey instanceof ArrayBuffer)) { + throw new Error('Expected to get an ArrayBuffer from generateRandomKey'); + } +} + +globalThis.testAblyPackage = async function () { + const key = await createSandboxAblyAPIKey(); + + const realtime = new BaseRealtime({ key, environment: 'sandbox' }, { WebSocketTransport, FetchRequest }); + + const channel = realtime.channels.get('channel'); + await attachChannel(channel); + + const receivedMessagePromise = new Promise((resolve) => { + channel.subscribe(() => { + resolve(); + }); + }); + + await channel.publish('message', { foo: 'bar' }); + await receivedMessagePromise; + await checkStandaloneFunction(); +}; diff --git a/test/package/browser/template/test/package.test.ts b/test/package/browser/template/test/package.test.ts index aa62408d77..ff04e14b05 100644 --- a/test/package/browser/template/test/package.test.ts +++ b/test/package/browser/template/test/package.test.ts @@ -1,27 +1,34 @@ import { test, expect } from '@playwright/test'; test.describe('NPM package', () => { - test('can be imported and provides access to Ably functionality', async ({ page }) => { - const pageResultPromise = new Promise((resolve, reject) => { - page.exposeFunction('onResult', (error: Error | null) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); + for (const scenario of [ + { name: 'default export', path: '/index-default.html' }, + { name: 'modular export', path: '/index-modules.html' }, + ]) { + test.describe(scenario.name, () => { + test('can be imported and provides access to Ably functionality', async ({ page }) => { + const pageResultPromise = new Promise((resolve, reject) => { + page.exposeFunction('onResult', (error: Error | null) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); - await page.goto('/'); + await page.goto(scenario.path); - let caughtError: Error | null; - try { - await pageResultPromise; - caughtError = null; - } catch (error) { - caughtError = error; - } + let caughtError: Error | null; + try { + await pageResultPromise; + caughtError = null; + } catch (error) { + caughtError = error; + } - expect(caughtError).toBe(null); - }); + expect(caughtError).toBe(null); + }); + }); + } });