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([