diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..709c196 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "typescript.disableAutomaticTypeAcquisition": true, + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/changelog.md b/changelog.md index a820d59..50a6f22 100644 --- a/changelog.md +++ b/changelog.md @@ -50,11 +50,21 @@ ``` - The API is now ESM in `.mjs` files instead of CJS in `.js` files, [accessible via `import` but not `require`](https://nodejs.org/dist/latest/docs/api/esm.html#require). +- Implemented TypeScript types via JSDoc comments. + + Types published in [`@types/apollo-upload-client`](https://npm.im/@types/apollo-upload-client) should no longer be used. + + Projects must configure [TypeScript](https://typescriptlang.org) to use types from the ECMAScript modules that have a `// @ts-check` comment: + + - [`compilerOptions.allowJs`](https://www.typescriptlang.org/tsconfig#allowJs) should be `true`. + - [`compilerOptions.maxNodeModuleJsDepth`](https://www.typescriptlang.org/tsconfig#maxNodeModuleJsDepth) should be reasonably large, e.g. `10`. + - [`compilerOptions.module`](https://www.typescriptlang.org/tsconfig#module) should be `"node16"` or `"nodenext"`. ### Patch - Updated dev dependencies. - Simplified dev dependencies and config for ESLint. +- Check TypeScript types via a new package `types` script. - Removed the [`jsdoc-md`](https://npm.im/jsdoc-md) dev dependency and the related package scripts, replacing the readme “API” section with a manually written “Exports” section. - Updated GitHub Actions CI config: - Run tests with Node.js v18, v20, v21. @@ -63,6 +73,8 @@ - Use the `node:` URL scheme for Node.js builtin module imports. - Reorganized the test file structure. - In tests, for objects with the property `headers` that as of [`@apollo/client`](https://npm.im/@apollo/client) [v3.7.0](https://github.com/apollographql/apollo-client/releases/tag/v3.7.0) is a null-prototype object, use the assertion `deepEqual` instead of `deepStrictEqual`. +- Tweaked code for type safety. +- Updated documentation. ## 17.0.0 diff --git a/createUploadLink.mjs b/createUploadLink.mjs index 3b95f5b..f7376ed 100644 --- a/createUploadLink.mjs +++ b/createUploadLink.mjs @@ -1,3 +1,5 @@ +// @ts-check + import { ApolloLink } from "@apollo/client/link/core/ApolloLink.js"; import { createSignalIfSupported } from "@apollo/client/link/http/createSignalIfSupported.js"; import { parseAndCheckHttpResponse } from "@apollo/client/link/http/parseAndCheckHttpResponse.js"; @@ -30,25 +32,36 @@ import isExtractableFile from "./isExtractableFile.mjs"; * Some of the options are similar to the * [`createHttpLink` options](https://apollographql.com/docs/react/api/link/apollo-link-http/#httplink-constructor-options). * @see [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec). - * @kind function - * @name createUploadLink * @param {object} options Options. - * @param {string} [options.uri="/graphql"] GraphQL endpoint URI. - * @param {boolean} [options.useGETForQueries] Should GET be used to fetch queries, if there are no files to upload. - * @param {ExtractableFileMatcher} [options.isExtractableFile=isExtractableFile] Customizes how files are matched in the GraphQL operation for extraction. - * @param {class} [options.FormData] [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) implementation to use, defaulting to the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) global. - * @param {FormDataFileAppender} [options.formDataAppendFile=formDataAppendFile] Customizes how extracted files are appended to the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance. - * @param {Function} [options.fetch] [`fetch`](https://fetch.spec.whatwg.org) implementation to use, defaulting to the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) global. - * @param {FetchOptions} [options.fetchOptions] [`fetch` options]{@link FetchOptions}; overridden by upload requirements. - * @param {string} [options.credentials] Overrides `options.fetchOptions.credentials`. - * @param {object} [options.headers] Merges with and overrides `options.fetchOptions.headers`. - * @param {boolean} [options.includeExtensions=false] Toggles sending `extensions` fields to the GraphQL server. - * @returns {ApolloLink} A [terminating Apollo Link](https://apollographql.com/docs/react/api/link/introduction/#the-terminating-link). - * @example How to import. - * ```js - * import createUploadLink from "apollo-upload-client/createUploadLink.mjs"; - * ``` - * @example A basic Apollo Client setup. + * @param {string} [options.uri] GraphQL endpoint URI. Defaults to `"/graphql"`. + * @param {boolean} [options.useGETForQueries] Should GET be used to fetch + * queries, if there are no files to upload. + * @param {ExtractableFileMatcher} [options.isExtractableFile] Matches + * extractable files in the GraphQL operation. Defaults to + * {@linkcode isExtractableFile}. + * @param {typeof FormData} [options.FormData] + * [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) + * class. Defaults to the {@linkcode FormData} global. + * @param {FormDataFileAppender} [options.formDataAppendFile] + * Customizes how extracted files are appended to the + * [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) + * instance. Defaults to {@linkcode formDataAppendFile}. + * @param {typeof fetch} [options.fetch] [`fetch`](https://fetch.spec.whatwg.org) + * implementation. Defaults to the {@linkcode fetch} global. + * @param {RequestInit} [options.fetchOptions] `fetch` options; overridden by + * upload requirements. + * @param {string} [options.credentials] Overrides + * {@linkcode RequestInit.credentials credentials} in + * {@linkcode fetchOptions}. + * @param {{ [headerName: string]: string }} [options.headers] Merges with and + * overrides {@linkcode RequestInit.headers headers} in + * {@linkcode fetchOptions}. + * @param {boolean} [options.includeExtensions] Toggles sending `extensions` + * fields to the GraphQL server. Defaults to `false`. + * @returns A [terminating Apollo Link](https://apollographql.com/docs/react/api/link/introduction/#the-terminating-link). + * @example + * A basic Apollo Client setup: + * * ```js * import { ApolloClient, InMemoryCache } from "@apollo/client"; * import createUploadLink from "apollo-upload-client/createUploadLink.mjs"; @@ -79,7 +92,16 @@ export default function createUploadLink({ }; return new ApolloLink((operation) => { - const context = operation.getContext(); + const context = + /** + * @type {import("@apollo/client/core/types.js").DefaultContext & { + * clientAwareness?: { + * name?: string, + * version?: string, + * }, + * }} + */ + (operation.getContext()); const { // Apollo Studio client awareness `name` and `version` can be configured // via `ApolloClient` constructor options: @@ -112,8 +134,9 @@ export default function createUploadLink({ let uri = selectURI(operation, fetchUri); if (files.size) { - // Automatically set by `fetch` when the `body` is a `FormData` instance. - delete options.headers["content-type"]; + if (options.headers) + // Automatically set by `fetch` when the `body` is a `FormData` instance. + delete options.headers["content-type"]; // GraphQL multipart request spec: // https://github.com/jaydenseric/graphql-multipart-request-spec @@ -124,7 +147,9 @@ export default function createUploadLink({ form.append("operations", serializeFetchParameter(clone, "Payload")); + /** @type {{ [key: string]: Array }} */ const map = {}; + let i = 0; files.forEach((paths) => { map[++i] = paths; @@ -133,7 +158,7 @@ export default function createUploadLink({ i = 0; files.forEach((paths, file) => { - customFormDataAppendFile(form, ++i, file); + customFormDataAppendFile(form, String(++i), file); }); options.body = form; @@ -163,7 +188,7 @@ export default function createUploadLink({ const { controller } = createSignalIfSupported(); - if (controller) { + if (typeof controller !== "boolean") { if (options.signal) // Respect the user configured abort controller signal. options.signal.aborted @@ -189,7 +214,10 @@ export default function createUploadLink({ const runtimeFetch = customFetch || fetch; return new Observable((observer) => { - // Used to track if the observable is being cleaned up. + /** + * Is the observable being cleaned up. + * @type {boolean} + */ let cleaningUp; runtimeFetch(uri, options) @@ -223,21 +251,22 @@ export default function createUploadLink({ cleaningUp = true; // Abort fetch. It’s ok to signal an abort even when not fetching. - if (controller) controller.abort(); + if (typeof controller !== "boolean") controller.abort(); }; }); }); } /** - * A function that checks if a value is an extractable file. - * @kind typedef - * @name ExtractableFileMatcher - * @type {Function} - * @param {*} value Value to check. - * @returns {boolean} Is the value an extractable file. - * @see [`isExtractableFile`]{@link isExtractableFile} has this type. - * @example How to check for the default exactable files, as well as a custom type of file. + * Checks if a value is an extractable file. + * @template [ExtractableFile=any] Extractable file. + * @callback ExtractableFileMatcher + * @param {unknown} value Value to check. + * @returns {value is ExtractableFile} Is the value an extractable file. + * @example + * How to check for the default exactable files, as well as a custom type of + * file: + * * ```js * import isExtractableFile from "apollo-upload-client/isExtractableFile.mjs"; * @@ -247,26 +276,19 @@ export default function createUploadLink({ * ``` */ -/** - * GraphQL request `fetch` options. - * @kind typedef - * @name FetchOptions - * @type {object} - * @see [Polyfillable fetch options](https://github.github.io/fetch#options). - * @prop {object} headers HTTP request headers. - * @prop {string} [credentials] Authentication credentials mode. - */ - /** * Appends a file extracted from the GraphQL operation to the * [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) - * instance used as the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) - * `options.body` for the [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec). - * @kind typedef - * @name FormDataFileAppender - * @param {FormData} formData [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance to append the specified file to. - * @param {string} fieldName Field name for the file. - * @param {*} file File to append. The file type depends on what the [`ExtractableFileMatcher`]{@link ExtractableFileMatcher} extracts. - * @see [`formDataAppendFile`]{@link formDataAppendFile} has this type. - * @see [`createUploadLink`]{@link createUploadLink} accepts this type in `options.formDataAppendFile`. + * instance used as the + * [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) + * `options.body` for the + * [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec). + * @template [ExtractableFile=any] Extractable file. + * @callback FormDataFileAppender + * @param {FormData} formData + * [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) + * instance to append the specified file to. + * @param {string} fieldName Form data field name to append the file with. + * @param {ExtractableFile} file File to append. The file type depends on what + * the extractable file matcher extracts. */ diff --git a/createUploadLink.test.mjs b/createUploadLink.test.mjs index 3f92dad..8a6ee16 100644 --- a/createUploadLink.test.mjs +++ b/createUploadLink.test.mjs @@ -1,11 +1,13 @@ +// @ts-check + import "./test/polyfillFile.mjs"; -import { deepEqual, deepStrictEqual, strictEqual } from "node:assert"; +import { deepEqual, deepStrictEqual, ok, strictEqual } from "node:assert"; import { ApolloLink } from "@apollo/client/link/core/ApolloLink.js"; import { concat } from "@apollo/client/link/core/concat.js"; import { execute } from "@apollo/client/link/core/execute.js"; -import gql from "graphql-tag"; +import { gql } from "graphql-tag"; import revertableGlobals from "revertable-globals"; import createUploadLink from "./createUploadLink.mjs"; @@ -21,6 +23,10 @@ const graphqlResponseOptions = { }, }; +/** + * Adds `createUploadLink` tests. + * @param {import("test-director").default} tests Test director. + */ export default (tests) => { tests.add("`createUploadLink` bundle size.", async () => { await assertBundleSize( @@ -32,15 +38,21 @@ export default (tests) => { tests.add( "`createUploadLink` with default options, a query, no files.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "{\n a\n}"; const payload = { data: { a: true } }; const revertGlobals = revertableGlobals({ - async fetch(uri, options) { - fetchUri = uri; + /** @satisfies {typeof fetch} */ + fetch: async function fetch(input, options) { + fetchInput = input; fetchOptions = options; return new Response(JSON.stringify(payload), graphqlResponseOptions); @@ -49,29 +61,32 @@ export default (tests) => { try { await timeLimitPromise( - new Promise((resolve, reject) => { - execute(createUploadLink(), { - query: gql(query), - }).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute(createUploadLink(), { + query: gql(query), + }).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -87,8 +102,13 @@ export default (tests) => { tests.add( "`createUploadLink` with default options, a mutation, files.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "mutation ($a: Upload!) {\n a(a: $a)\n}"; @@ -96,8 +116,9 @@ export default (tests) => { const fileName = "a.txt"; const fileType = "text/plain"; const revertGlobals = revertableGlobals({ - async fetch(uri, options) { - fetchUri = uri; + /** @satisfies {typeof fetch} */ + fetch: async function fetch(input, options) { + fetchInput = input; fetchOptions = options; return new Response(JSON.stringify(payload), graphqlResponseOptions); @@ -106,28 +127,30 @@ export default (tests) => { try { await timeLimitPromise( - new Promise((resolve, reject) => { - execute(createUploadLink(), { - query: gql(query), - variables: { - a: new File(["a"], fileName, { type: fileType }), - }, - }).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute(createUploadLink(), { + query: gql(query), + variables: { + a: new File(["a"], fileName, { type: fileType }), + }, + }).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); - strictEqual(typeof fetchOptions, "object"); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, @@ -135,23 +158,25 @@ export default (tests) => { ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); - strictEqual(fetchOptionsBody instanceof FormData, true); + ok(fetchOptionsSignal instanceof AbortSignal); + ok(fetchOptionsBody instanceof FormData); const formDataEntries = Array.from(fetchOptionsBody.entries()); strictEqual(formDataEntries.length, 3); strictEqual(formDataEntries[0][0], "operations"); + ok(typeof formDataEntries[0][1] === "string"); deepStrictEqual(JSON.parse(formDataEntries[0][1]), { query, variables: { a: null }, }); strictEqual(formDataEntries[1][0], "map"); + ok(typeof formDataEntries[1][1] === "string"); deepStrictEqual(JSON.parse(formDataEntries[1][1]), { 1: ["variables.a"], }); strictEqual(formDataEntries[2][0], "1"); - strictEqual(formDataEntries[2][1] instanceof File, true); + ok(formDataEntries[2][1] instanceof File); strictEqual(formDataEntries[2][1].name, fileName); strictEqual(formDataEntries[2][1].type, fileType); deepEqual(fetchOptionsRest, { @@ -166,8 +191,13 @@ export default (tests) => { ); tests.add("`createUploadLink` with option `uri`.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const uri = "http://localhost:3000"; @@ -175,42 +205,45 @@ export default (tests) => { const payload = { data: { a: true } }; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - uri, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; - - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + uri, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), }, - }), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, uri); + strictEqual(fetchInput, uri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -220,56 +253,64 @@ export default (tests) => { }); tests.add("`createUploadLink` with option `includeExtensions`.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "{\n a\n}"; const payload = { data: { a: true } }; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - concat( - new ApolloLink((operation, forward) => { - operation.extensions.a = true; - return forward(operation); - }), - createUploadLink({ - includeExtensions: true, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + concat( + new ApolloLink((operation, forward) => { + operation.extensions.a = true; + return forward(operation); + }), + createUploadLink({ + includeExtensions: true, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - ), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + ), + { + query: gql(query), + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -287,53 +328,61 @@ export default (tests) => { tests.add( "`createUploadLink` with option `fetchOptions.method`.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "{\n a\n}"; const payload = { data: { a: true } }; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - fetchOptions: { method: "GET" }, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + fetchOptions: { method: "GET" }, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), }, - }), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); strictEqual( - fetchUri, + fetchInput, `${defaultUri}?query=%7B%0A%20%20a%0A%7D&variables=%7B%7D`, ); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "GET", headers: { accept: "*/*", "content-type": "application/json" }, @@ -345,53 +394,61 @@ export default (tests) => { tests.add( "`createUploadLink` with option `useGETForQueries`, query, no files.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "{\n a\n}"; const payload = { data: { a: true } }; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - useGETForQueries: true, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + useGETForQueries: true, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), }, - }), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); strictEqual( - fetchUri, + fetchInput, `${defaultUri}?query=%7B%0A%20%20a%0A%7D&variables=%7B%7D`, ); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "GET", headers: { accept: "*/*", "content-type": "application/json" }, @@ -403,8 +460,13 @@ export default (tests) => { tests.add( "`createUploadLink` with option `useGETForQueries`, query, files.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "query ($a: Upload!) {\n a(a: $a)\n}"; @@ -413,43 +475,45 @@ export default (tests) => { const fileType = "text/plain"; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - useGETForQueries: true, - FormData, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + useGETForQueries: true, + FormData, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), + variables: { + a: new File(["a"], fileName, { type: fileType }), + }, }, - }), - { - query: gql(query), - variables: { - a: new File(["a"], fileName, { type: fileType }), + ).subscribe({ + next(data) { + nextData = data; }, - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); - strictEqual(typeof fetchOptions, "object"); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, @@ -457,23 +521,25 @@ export default (tests) => { ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); - strictEqual(fetchOptionsBody instanceof FormData, true); + ok(fetchOptionsSignal instanceof AbortSignal); + ok(fetchOptionsBody instanceof FormData); const formDataEntries = Array.from(fetchOptionsBody.entries()); strictEqual(formDataEntries.length, 3); strictEqual(formDataEntries[0][0], "operations"); + ok(typeof formDataEntries[0][1] === "string"); deepStrictEqual(JSON.parse(formDataEntries[0][1]), { query, variables: { a: null }, }); strictEqual(formDataEntries[1][0], "map"); + ok(typeof formDataEntries[1][1] === "string"); deepStrictEqual(JSON.parse(formDataEntries[1][1]), { 1: ["variables.a"], }); strictEqual(formDataEntries[2][0], "1"); - strictEqual(formDataEntries[2][1] instanceof File, true); + ok(formDataEntries[2][1] instanceof File); strictEqual(formDataEntries[2][1].name, fileName); strictEqual(formDataEntries[2][1].type, fileType); deepEqual(fetchOptionsRest, { @@ -533,7 +599,7 @@ export default (tests) => { ); strictEqual(fetched, false); - strictEqual(typeof observerError, "object"); + ok(typeof observerError === "object"); strictEqual(observerError.name, "Invariant Violation"); strictEqual(observerError.parseError, parseError); }, @@ -542,50 +608,58 @@ export default (tests) => { tests.add( "`createUploadLink` with option `useGETForQueries`, mutation, no files.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "mutation {\n a\n}"; const payload = { data: { a: true } }; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - useGETForQueries: true, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + useGETForQueries: true, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), }, - }), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -596,8 +670,13 @@ export default (tests) => { ); tests.add("`createUploadLink` with context `clientAwareness`.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const clientAwareness = { name: "a", version: "1.0.0" }; @@ -605,47 +684,50 @@ export default (tests) => { const payload = { data: { a: true } }; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - concat( - new ApolloLink((operation, forward) => { - operation.setContext({ clientAwareness }); - return forward(operation); - }), - createUploadLink({ - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + concat( + new ApolloLink((operation, forward) => { + operation.setContext({ clientAwareness }); + return forward(operation); + }), + createUploadLink({ + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - ), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + ), + { + query: gql(query), + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "POST", headers: { @@ -662,8 +744,13 @@ export default (tests) => { tests.add( "`createUploadLink` with context `clientAwareness`, overridden by context `headers`.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const clientAwarenessOriginal = { name: "a", version: "1.0.0" }; @@ -672,54 +759,57 @@ export default (tests) => { const payload = { data: { a: true } }; await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - concat( - new ApolloLink((operation, forward) => { - operation.setContext({ - clientAwareness: clientAwarenessOriginal, - headers: { - "apollographql-client-name": clientAwarenessOverride.name, - "apollographql-client-version": - clientAwarenessOverride.version, + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + concat( + new ApolloLink((operation, forward) => { + operation.setContext({ + clientAwareness: clientAwarenessOriginal, + headers: { + "apollographql-client-name": clientAwarenessOverride.name, + "apollographql-client-version": + clientAwarenessOverride.version, + }, + }); + return forward(operation); + }), + createUploadLink({ + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); }, - }); - return forward(operation); - }), - createUploadLink({ - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; - - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - ), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + }), + ), + { + query: gql(query), + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + ok(fetchOptionsSignal instanceof AbortSignal); deepEqual(fetchOptionsRest, { method: "POST", headers: { @@ -737,8 +827,13 @@ export default (tests) => { tests.add( "`createUploadLink` with options `isExtractableFile` and `formDataAppendFile`.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; + + /** @type {unknown} */ let nextData; const query = "mutation ($a: Upload!) {\n a(a: $a)\n}"; @@ -747,57 +842,64 @@ export default (tests) => { const fileType = "text/plain"; class TextFile { - constructor(content, fileName) { - this.file = new File([content], fileName, { type: fileType }); + /** + * @param {string} text Text. + * @param {string} fileName File name. + */ + constructor(text, fileName) { + this.file = new File([text], fileName, { type: fileType }); } } await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - isExtractableFile(value) { - return value instanceof TextFile; + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + /** @returns {value is TextFile} */ + isExtractableFile(value) { + return value instanceof TextFile; + }, + formDataAppendFile(formData, fieldName, file) { + formData.append( + fieldName, + file instanceof TextFile ? file.file : file, + ); + }, + FormData, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), + variables: { + a: new TextFile("a", fileName), + }, }, - formDataAppendFile(formData, fieldName, file) { - formData.append( - fieldName, - file instanceof TextFile ? file.file : file, - ); + ).subscribe({ + next(data) { + nextData = data; }, - FormData, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; - - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); + error() { + reject(createUnexpectedCallError()); }, - }), - { - query: gql(query), - variables: { - a: new TextFile("a", fileName), + complete() { + resolve(); }, - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }), + }); + }) + ), ); - strictEqual(fetchUri, defaultUri); - strictEqual(typeof fetchOptions, "object"); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); const { signal: fetchOptionsSignal, @@ -805,23 +907,25 @@ export default (tests) => { ...fetchOptionsRest } = fetchOptions; - strictEqual(fetchOptionsSignal instanceof AbortSignal, true); - strictEqual(fetchOptionsBody instanceof FormData, true); + ok(fetchOptionsSignal instanceof AbortSignal); + ok(fetchOptionsBody instanceof FormData); const formDataEntries = Array.from(fetchOptionsBody.entries()); strictEqual(formDataEntries.length, 3); strictEqual(formDataEntries[0][0], "operations"); + ok(typeof formDataEntries[0][1] === "string"); deepStrictEqual(JSON.parse(formDataEntries[0][1]), { query, variables: { a: null }, }); strictEqual(formDataEntries[1][0], "map"); + ok(typeof formDataEntries[1][1] === "string"); deepStrictEqual(JSON.parse(formDataEntries[1][1]), { 1: ["variables.a"], }); strictEqual(formDataEntries[2][0], "1"); - strictEqual(formDataEntries[2][1] instanceof File, true); + ok(formDataEntries[2][1] instanceof File); strictEqual(formDataEntries[2][1].name, fileName); strictEqual(formDataEntries[2][1].type, fileType); deepEqual(fetchOptionsRest, { @@ -833,7 +937,10 @@ export default (tests) => { ); tests.add("`createUploadLink` with a HTTP error, data.", async () => { + /** @type {Response | undefined} */ let fetchResponse; + + /** @type {unknown} */ let nextData; const payload = { @@ -881,6 +988,7 @@ export default (tests) => { }); tests.add("`createUploadLink` with a HTTP error, no data.", async () => { + /** @type {Response | undefined} */ let fetchResponse; const payload = { errors: [{ message: "Unauthorized." }] }; @@ -951,7 +1059,10 @@ export default (tests) => { tests.add( "`createUploadLink` with option `fetchOptions.signal`, not yet aborted.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; const query = "{\n a\n}"; @@ -964,8 +1075,8 @@ export default (tests) => { execute( createUploadLink({ fetchOptions: { signal: controller.signal }, - fetch(uri, options) { - fetchUri = uri; + fetch(input, options) { + fetchInput = input; fetchOptions = options; return new Promise((resolve, reject) => { @@ -981,7 +1092,7 @@ export default (tests) => { ); }, 4000); - options.signal.addEventListener("abort", () => { + options?.signal?.addEventListener("abort", () => { clearTimeout(timeout); reject(fetchError); }); @@ -1009,7 +1120,7 @@ export default (tests) => { const observerError = await observerErrorPromise; - strictEqual(fetchUri, defaultUri); + strictEqual(fetchInput, defaultUri); deepEqual(fetchOptions, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -1023,7 +1134,10 @@ export default (tests) => { tests.add( "`createUploadLink` with option `fetchOptions.signal`, already aborted.", async () => { - let fetchUri; + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ let fetchOptions; const query = "{\n a\n}"; @@ -1039,11 +1153,11 @@ export default (tests) => { execute( createUploadLink({ fetchOptions: { signal: controller.signal }, - async fetch(uri, options) { - fetchUri = uri; + async fetch(input, options) { + fetchInput = input; fetchOptions = options; - if (options.signal.aborted) throw fetchError; + if (options?.signal?.aborted) throw fetchError; return new Response( JSON.stringify(payload), @@ -1070,7 +1184,7 @@ export default (tests) => { const observerError = await observerErrorPromise; - strictEqual(fetchUri, defaultUri); + strictEqual(fetchInput, defaultUri); deepEqual(fetchOptions, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, diff --git a/formDataAppendFile.mjs b/formDataAppendFile.mjs index 7789e1e..73562d8 100644 --- a/formDataAppendFile.mjs +++ b/formDataAppendFile.mjs @@ -1,19 +1,16 @@ +// @ts-check + /** - * The default implementation for [`createUploadLink`]{@link createUploadLink} - * `options.formDataAppendFile` that uses the standard - * [`FormData.append`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/append) + * The default implementation for the function `createUploadLink` option + * `formDataAppendFile` that uses the standard {@linkcode FormData.append} * method. - * @kind function - * @name formDataAppendFile - * @type {FormDataFileAppender} - * @param {FormData} formData [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance to append the specified file to. + * @param {FormData} formData Form data to append the specified file to. * @param {string} fieldName Field name for the file. - * @param {*} file File to append. - * @example How to import. - * ```js - * import formDataAppendFile from "apollo-upload-client/formDataAppendFile.mjs"; - * ``` + * @param {import("./isExtractableFile.mjs").ExtractableFile} file File to + * append. */ export default function formDataAppendFile(formData, fieldName, file) { - formData.append(fieldName, file, file.name); + "name" in file + ? formData.append(fieldName, file, file.name) + : formData.append(fieldName, file); } diff --git a/formDataAppendFile.test.mjs b/formDataAppendFile.test.mjs index bc878c3..4c78df5 100644 --- a/formDataAppendFile.test.mjs +++ b/formDataAppendFile.test.mjs @@ -1,10 +1,16 @@ +// @ts-check + import "./test/polyfillFile.mjs"; -import { strictEqual } from "node:assert"; +import { ok, strictEqual } from "node:assert"; import formDataAppendFile from "./formDataAppendFile.mjs"; import assertBundleSize from "./test/assertBundleSize.mjs"; +/** + * Adds `formDataAppendFile` tests. + * @param {import("test-director").default} tests Test director. + */ export default (tests) => { tests.add("`formDataAppendFile` bundle size.", async () => { await assertBundleSize( @@ -13,7 +19,27 @@ export default (tests) => { ); }); - tests.add("`formDataAppendFile` functionality.", () => { + tests.add("`formDataAppendFile` functionality, `Blob` instance.", () => { + const formData = new FormData(); + const fieldName = "a"; + const fileType = "text/plain"; + + formDataAppendFile( + formData, + fieldName, + new Blob(["a"], { type: fileType }), + ); + + const formDataEntries = Array.from(formData.entries()); + + strictEqual(formDataEntries.length, 1); + strictEqual(formDataEntries[0][0], "a"); + ok(typeof formDataEntries[0][1] === "object"); + strictEqual(formDataEntries[0][1].name, "blob"); + strictEqual(formDataEntries[0][1].type, fileType); + }); + + tests.add("`formDataAppendFile` functionality, `File` instance.", () => { const formData = new FormData(); const fieldName = "a"; const fileName = "a.txt"; @@ -29,7 +55,7 @@ export default (tests) => { strictEqual(formDataEntries.length, 1); strictEqual(formDataEntries[0][0], "a"); - strictEqual(typeof formDataEntries[0][1], "object"); + ok(typeof formDataEntries[0][1] === "object"); strictEqual(formDataEntries[0][1].name, fileName); strictEqual(formDataEntries[0][1].type, fileType); }); diff --git a/isExtractableFile.mjs b/isExtractableFile.mjs index ee97be7..9729213 100644 --- a/isExtractableFile.mjs +++ b/isExtractableFile.mjs @@ -1,15 +1,10 @@ +// @ts-check + +export { default } from "extract-files/isExtractableFile.mjs"; + /** - * The default implementation for [`createUploadLink`]{@link createUploadLink} - * `options.isExtractableFile`. - * @kind function - * @name isExtractableFile - * @type {ExtractableFileMatcher} - * @param {*} value Value to check. - * @returns {boolean} Is the value an extractable file. - * @see [`extract-files` `isExtractableFile` docs](https://github.com/jaydenseric/extract-files#isextractablefilemjs). - * @example How to import. - * ```js - * import isExtractableFile from "apollo-upload-client/isExtractableFile.mjs"; - * ``` + * An extractable file. + * @typedef {import( + * "extract-files/isExtractableFile.mjs" + * ).ExtractableFile} ExtractableFile */ -export { default } from "extract-files/isExtractableFile.mjs"; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..7935e73 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "maxNodeModuleJsDepth": 10, + "module": "nodenext", + "noEmit": true, + "strict": true + }, + "typeAcquisition": { + "enable": false + } +} diff --git a/package.json b/package.json index 11568ec..494dfa4 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ }, "devDependencies": { "@apollo/client": "^3.8.6", + "@types/node": "^20.8.7", "coverage-node": "^8.0.0", "esbuild": "^0.19.5", "eslint": "^8.52.0", @@ -59,13 +60,15 @@ "gzip-size": "^7.0.0", "prettier": "^3.0.3", "revertable-globals": "^4.0.0", - "test-director": "^11.0.0" + "test-director": "^11.0.0", + "typescript": "^5.2.2" }, "scripts": { "eslint": "eslint .", "prettier": "prettier -c .", + "types": "tsc -p jsconfig.json", "tests": "coverage-node test.mjs", - "test": "npm run eslint && npm run prettier && npm run tests", + "test": "npm run eslint && npm run prettier && npm run types && npm run tests", "prepublishOnly": "npm test" } } diff --git a/readme.md b/readme.md index 3ebb95a..6aaebe8 100644 --- a/readme.md +++ b/readme.md @@ -123,6 +123,12 @@ Consider polyfilling: - [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) - [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) +Projects must configure [TypeScript](https://typescriptlang.org) to use types from the ECMAScript modules that have a `// @ts-check` comment: + +- [`compilerOptions.allowJs`](https://www.typescriptlang.org/tsconfig#allowJs) should be `true`. +- [`compilerOptions.maxNodeModuleJsDepth`](https://www.typescriptlang.org/tsconfig#maxNodeModuleJsDepth) should be reasonably large, e.g. `10`. +- [`compilerOptions.module`](https://www.typescriptlang.org/tsconfig#module) should be `"node16"` or `"nodenext"`. + ## Exports The [npm](https://npmjs.com) package [`apollo-upload-client`](https://npm.im/apollo-upload-client) features [optimal JavaScript module design](https://jaydenseric.com/blog/optimal-javascript-module-design). It doesn’t have a main index module, so use deep imports from the ECMAScript modules that are exported via the [`package.json`](./package.json) field [`exports`](https://nodejs.org/api/packages.html#exports): diff --git a/test.mjs b/test.mjs index e5ff964..fee28ca 100644 --- a/test.mjs +++ b/test.mjs @@ -1,3 +1,5 @@ +// @ts-check + import TestDirector from "test-director"; import test_createUploadLink from "./createUploadLink.test.mjs"; diff --git a/test/assertBundleSize.mjs b/test/assertBundleSize.mjs index cf35ce2..e7f7ab5 100644 --- a/test/assertBundleSize.mjs +++ b/test/assertBundleSize.mjs @@ -1,3 +1,5 @@ +// @ts-check + import { fail } from "node:assert"; import { fileURLToPath } from "node:url"; @@ -6,12 +8,10 @@ import { gzipSize } from "gzip-size"; /** * Asserts the minified and gzipped bundle size of a module. - * @kind function - * @name assertBundleSize * @param {URL} moduleUrl Module URL. * @param {number} limit Minified and gzipped bundle size limit (bytes). - * @returns {Promise<{ bundle: string, gzippedSize: number }>} Resolves the minified bundle and its gzipped size (bytes). - * @ignore + * @returns {Promise<{ bundle: string, gzippedSize: number }>} Resolves the + * minified bundle and its gzipped size (bytes). */ export default async function assertBundleSize(moduleUrl, limit) { if (!(moduleUrl instanceof URL)) @@ -34,7 +34,7 @@ export default async function assertBundleSize(moduleUrl, limit) { format: "esm", }); - const gzippedSize = await gzipSize(bundle.contents); + const gzippedSize = await gzipSize(bundle.text); if (gzippedSize > limit) fail( diff --git a/test/createUnexpectedCallError.mjs b/test/createUnexpectedCallError.mjs index 7fdbb1f..fb5e6e7 100644 --- a/test/createUnexpectedCallError.mjs +++ b/test/createUnexpectedCallError.mjs @@ -1,12 +1,8 @@ +// @ts-check + import { AssertionError } from "node:assert"; -/** - * Creates an assertion error that a function was unexpectedly called. - * @kind function - * @name createUnexpectedCallError - * @returns {AssertionError} Assertion error. - * @ignore - */ +/** Creates an assertion error that a function was unexpectedly called. */ export default function createUnexpectedCallError() { return new AssertionError({ message: "Unexpected function call.", diff --git a/test/polyfillFile.mjs b/test/polyfillFile.mjs index d300b84..4b28239 100644 --- a/test/polyfillFile.mjs +++ b/test/polyfillFile.mjs @@ -1,6 +1,9 @@ +// @ts-check + import { File as NodeFile } from "node:buffer"; // TODO: Delete this polyfill once all supported Node.js versions have the // global `File`: // https://nodejs.org/api/globals.html#class-file +// @ts-expect-error It’s not a perfect polyfill, but works for the tests. globalThis.File ??= NodeFile; diff --git a/test/timeLimitPromise.mjs b/test/timeLimitPromise.mjs index 4155172..ff46195 100644 --- a/test/timeLimitPromise.mjs +++ b/test/timeLimitPromise.mjs @@ -1,3 +1,5 @@ +// @ts-check + import { AssertionError } from "node:assert"; /** @@ -6,12 +8,10 @@ import { AssertionError } from "node:assert"; * that are expected to either resolve or reject, as the Node.js behavior when * neither happens is to exit the process without an error, potentially causing * the illusion that tests passed. - * @kind function - * @name timeLimitPromise - * @param {Promise} promise Promise to time limit. - * @param {number} [msTimeLimit=1000] Time limit in milliseconds. - * @returns {Promise} Time limited promise. - * @ignore + * @template T + * @param {Promise} promise Promise to time limit. + * @param {number} [msTimeLimit] Time limit in milliseconds. Defaults to 1000. + * @returns {Promise} Time limited promise. */ export default async function timeLimitPromise(promise, msTimeLimit = 1000) { if (!(promise instanceof Promise)) @@ -30,6 +30,7 @@ export default async function timeLimitPromise(promise, msTimeLimit = 1000) { stackStartFn: timeLimitPromise, }); + /** @type {ReturnType} */ let timeout; return Promise.race([