diff --git a/.attw.json b/.attw.json
new file mode 100644
index 0000000..4eaa422
--- /dev/null
+++ b/.attw.json
@@ -0,0 +1,5 @@
+{
+ "pack": true,
+
+ "profile": "esm-only"
+}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..7ddcf69
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,23 @@
+{
+ "root": true,
+
+ "extends": "@ljharb",
+
+ "rules": {
+ "max-statements": 0,
+ },
+
+ "overrides": [
+ {
+ "files": "pargs.mjs",
+ "extends": "@ljharb/eslint-config/node/20",
+ "rules": {
+ "max-lines-per-function": 0,
+ },
+ },
+ {
+ "files": "./bin.mjs",
+ "extends": "@ljharb/eslint-config/node/20",
+ },
+ ]
+}
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..fc821fa
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [ljharb]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: npm/has-types
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with a single custom sponsorship URL
diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml
new file mode 100644
index 0000000..88d49f9
--- /dev/null
+++ b/.github/workflows/node-pretest.yml
@@ -0,0 +1,10 @@
+name: 'Tests: pretest/posttest'
+
+on: [pull_request, push]
+
+permissions:
+ contents: read
+
+jobs:
+ tests:
+ uses: ljharb/actions/.github/workflows/pretest.yml@main
diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml
new file mode 100644
index 0000000..cd2230b
--- /dev/null
+++ b/.github/workflows/node.yml
@@ -0,0 +1,14 @@
+name: 'Tests: node.js >= 22'
+
+on: [pull_request, push]
+
+permissions:
+ contents: read
+
+jobs:
+ tests:
+ uses: ljharb/actions/.github/workflows/node.yml@main
+ with:
+ range: '^22.11 || >= 23.3'
+ type: minors
+ command: npm run tests-only
diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml
new file mode 100644
index 0000000..b9e1712
--- /dev/null
+++ b/.github/workflows/rebase.yml
@@ -0,0 +1,9 @@
+name: Automatic Rebase
+
+on: [pull_request_target]
+
+jobs:
+ _:
+ uses: ljharb/actions/.github/workflows/rebase.yml@main
+ secrets:
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/require-allow-edits.yml b/.github/workflows/require-allow-edits.yml
new file mode 100644
index 0000000..a685b8a
--- /dev/null
+++ b/.github/workflows/require-allow-edits.yml
@@ -0,0 +1,18 @@
+name: Require “Allow Edits”
+
+on: [pull_request_target]
+
+permissions:
+ contents: read
+
+jobs:
+ _:
+ permissions:
+ pull-requests: read # for ljharb/require-allow-edits to check 'allow edits' on PR
+
+ name: "Require “Allow Edits”"
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: ljharb/require-allow-edits@main
diff --git a/.gitignore b/.gitignore
index 5829e8e..9659738 100644
--- a/.gitignore
+++ b/.gitignore
@@ -133,3 +133,5 @@ dist
npm-shrinkwrap.json
package-lock.json
yarn.lock
+
+.npmignore
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
index 43c97e7..eacea13 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1 +1,3 @@
package-lock=false
+allow-same-version=true
+message=v%s
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4922334
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,16 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## 1.0.0 - 2017-11-13
+
+### Commits
+
+- Initial commit [`cafb49b`](https://github.com/elliotblackburn/has-types/commit/cafb49b7292b7a85e4aeeccfd9f0c9425b1be8a1)
+- Rename from has-types to hastypes [`8aa23de`](https://github.com/elliotblackburn/has-types/commit/8aa23deb28fbf0ada1bb47b4f1ff70b3c11847ee)
+- Force node engine to be 8.9.1 or higher [`bf908d7`](https://github.com/elliotblackburn/has-types/commit/bf908d7a1a0fcab39e22b0f408668df514839ee6)
+- Correct bin file [`162fb4a`](https://github.com/elliotblackburn/has-types/commit/162fb4af78f9cad3a2f4176d0595f081ef0143a0)
+- 1.0.1 release [`fbcee3a`](https://github.com/elliotblackburn/has-types/commit/fbcee3a04ae4a861a1a6a0d46f8a45dd1a6b4d46)
diff --git a/README.md b/README.md
index 1c579a0..fa828bc 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,48 @@
-# Has types?
+# hastypes [![Version Badge][npm-version-svg]][package-url]
-> Check if an npm package is typescript friendly!
+[![github actions][actions-image]][actions-url]
+[![coverage][codecov-image]][codecov-url]
+[![License][license-image]][license-url]
+[![Downloads][downloads-image]][downloads-url]
+
+[![npm badge][npm-badge-png]][package-url]
+
+Does the given package have TypeScript types?
**Inspired by https://github.com/ofrobots/typescript-friendly**
-## Installation
+## Example
-```
-npm i -g hastypes
+```sh
+$ hastypes hastypes # => integrated
+$ hastypes react@19 # => @types/react
+$ hastypes mdpdf@1 # => none
```
-## Usage
+```mjs
+import hasTypes from 'hastypes';
+import assert from 'assert';
-Simply call `hastypes` followed by an npm package name!
-
-```sh
-$ hastypes @sindresorhus/is # => integrated
-$ hastypes express # => @types/express
-$ hastypes mdpdf # => none
+hasTypes('hastypes').then(x => assert.equal(x, true));
+hasTypes('react@19').then(x => assert.equal(x, '@types/react'));
+hasTypes('mdpdf@1').then(x => assert.equal(x, false));
```
+
+## Tests
+Simply clone the repo, `npm install`, and run `npm test`
+
+[package-url]: https://npmjs.org/package/hastypes
+[npm-version-svg]: https://versionbadg.es/elliotblackburn/has-types.svg
+[deps-svg]: https://david-dm.org/elliotblackburn/has-types.svg
+[deps-url]: https://david-dm.org/elliotblackburn/has-types
+[dev-deps-svg]: https://david-dm.org/elliotblackburn/has-types/dev-status.svg
+[dev-deps-url]: https://david-dm.org/elliotblackburn/has-types#info=devDependencies
+[npm-badge-png]: https://nodei.co/npm/hastypes.png?downloads=true&stars=true
+[license-image]: https://img.shields.io/npm/l/hastypes.svg
+[license-url]: LICENSE
+[downloads-image]: https://img.shields.io/npm/dm/hastypes.svg
+[downloads-url]: https://npm-stat.com/charts.html?package=hastypes
+[codecov-image]: https://codecov.io/gh/elliotblackburn/has-types/branch/main/graphs/badge.svg
+[codecov-url]: https://app.codecov.io/gh/elliotblackburn/has-types/
+[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/elliotblackburn/has-types
+[actions-url]: https://github.com/elliotblackburn/has-types/actions
diff --git a/bin.mjs b/bin.mjs
new file mode 100755
index 0000000..c4a9c40
--- /dev/null
+++ b/bin.mjs
@@ -0,0 +1,63 @@
+#! /usr/bin/env node
+
+import { readFileSync } from 'fs';
+import { join } from 'path';
+
+import npa from 'npm-package-arg';
+
+import pargs from './pargs.mjs';
+
+const help = readFileSync(join(import.meta.dirname, './help.txt'), 'utf8');
+
+const {
+ positionals,
+ values: { before },
+} = pargs(help, import.meta.url, {
+ allowPositionals: true,
+ options: {
+ before: {
+ type: 'string',
+ },
+ },
+});
+
+const specifiers = positionals.slice(1);
+
+if (specifiers.length !== 1) {
+ console.error('You must provide exactly one specifier');
+ process.exit(1);
+}
+
+if (typeof before !== 'undefined' && typeof before !== 'string') {
+ console.error('`before` option must be a valid Date value');
+ process.exit(1);
+}
+
+let name, rawSpec;
+try {
+ ({ name, rawSpec } = npa(specifiers[0]));
+ if (rawSpec === '*') {
+ rawSpec = 'latest';
+ }
+} catch (e) {
+ // eslint-disable-next-line no-extra-parens
+ console.error(/** @type {Error} */ (e)?.message ?? 'Unknown error');
+ process.exit(1);
+}
+
+import hasTypes from './index.mjs';
+
+import mockProperty from 'mock-property';
+
+// eslint-disable-next-line no-empty-function, no-extra-parens
+const restore = mockProperty(/** @type {Parameters[0]} */ (/** @type {unknown} */ (console)), 'error', { value() {} });
+const promise = hasTypes(specifiers[0], { before });
+
+promise.finally(() => {
+ restore();
+}).catch((e) => {
+ console.error(e.message);
+ process.exit(1);
+}).then((r) => {
+ console.log(`${name}@${rawSpec} ${typeof r === 'string' ? r : r ? 'integrated' : 'none'}`);
+});
diff --git a/bin/index.js b/bin/index.js
deleted file mode 100755
index 93f08f0..0000000
--- a/bin/index.js
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-
-const meow = require('meow');
-const rp = require('request-promise');
-
-const cli = meow(`
- has-types will return information about an npm packages typescript support and friendliness.
-
- Usage:
- $ hastypes
-`);
-
-const pkg = cli.input[0];
-
-if (!pkg) {
- // Show help and exit
- cli.showHelp();
-}
-
-function escapePackageName(pkg) {
- return pkg.replace('/', '%2f');
-}
-
-async function check() {
- let friendliness = 'none';
-
- let res;
- try {
- res = await rp('https://registry.npmjs.com/' + escapePackageName(pkg));
- } catch (err) {
- if (err.statusCode === 404) {
- console.error("Package not found, is it part of an npm organisation?");
- process.exit(1);
- } else {
- console.error("Failed to get details of types");
- }
- }
- const manifest = JSON.parse(res);
-
- const latestVersion = manifest['dist-tags'].latest;
- const latestManifest = manifest.versions[latestVersion];
-
- if (latestManifest.types) {
- friendliness = 'integrated';
- } else {
- // Check if an @types/ package for the module
- const typesPackage = escapePackageName('@types/' + pkg);
- let res;
- try {
- res = await rp('https://registry.npmjs.com/' + typesPackage);
- } catch (err) {
- if (err.statusCode !== 404) {
- console.error("Failed to get details of types");
- process.exit(1);
- }
- }
- if (res) {
- const typesManifest = JSON.parse(res);
- friendliness = "@types/" + pkg;
- }
- }
-
- console.log(friendliness);
-}
-
-check();
\ No newline at end of file
diff --git a/help.txt b/help.txt
new file mode 100644
index 0000000..1281ff1
--- /dev/null
+++ b/help.txt
@@ -0,0 +1,8 @@
+Usage: has-types [--before=MM/DD/YYYY]
+
+`package-specifier` must be a valid registry specifier according to `npm-package-arg`.
+
+ - see https://www.npmjs.com/package/npm-package-arg
+
+ Options:
+ `--before`: npm‘s `before` option. Must be a valid date.
\ No newline at end of file
diff --git a/index.d.mts b/index.d.mts
new file mode 100644
index 0000000..240a4cd
--- /dev/null
+++ b/index.d.mts
@@ -0,0 +1,8 @@
+declare function hasTypes(
+ specifier: string,
+ options?: {
+ before?: string;
+ },
+): Promise<`@types/${string}` | boolean>;
+
+export default hasTypes;
\ No newline at end of file
diff --git a/index.mjs b/index.mjs
new file mode 100644
index 0000000..dc36556
--- /dev/null
+++ b/index.mjs
@@ -0,0 +1,67 @@
+import { existsSync } from 'fs';
+import { join, dirname, basename, extname } from 'path';
+
+import { dirSync } from 'tmp';
+import npa from 'npm-package-arg';
+import pacote from 'pacote';
+import { getDTName } from 'dts-gen/dist/names.js';
+
+/** @type {import('.')} */
+export default async function hasTypes(specifier, options = {}) {
+ let { before } = options;
+ let date = typeof before !== 'undefined' && new Date(before);
+ if (date && isNaN(Number(date))) {
+ throw new TypeError('`before` option must be a valid Date value');
+ }
+
+ const {
+ registry,
+ name,
+ fetchSpec,
+ } = npa(specifier);
+ if (!registry) {
+ throw new TypeError('specifier must be a registry package');
+ }
+
+ const { name: tmpdir, removeCallback } = dirSync({ unsafeCleanup: true });
+
+ try {
+ const pExtract = pacote.extract(specifier, tmpdir, { before: date });
+ const manifest = pacote.manifest(specifier, { before: date });
+
+ // don't bother supporting typings
+ const explicitTypes = manifest.types;
+ if (explicitTypes) {
+ if (typeof explicitTypes !== 'string') {
+ throw new TypeError('`types` field is not a string. Please report this!');
+ }
+
+ if (!explicitTypes.endsWith('.d.ts')) {
+ return false;
+ }
+ await pExtract;
+ if (!existsSync(join(tmpdir, explicitTypes))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ var index = manifest.main || 'index.js';
+ var extless = join(dirname(index), basename(index, extname(index)));
+ var dts = `./${extless}.d.ts`;
+
+ await pExtract;
+ if (existsSync(join(tmpdir, dts))) {
+ return true;
+ }
+
+ const dtSpec = `@types/${getDTName(name)}@${fetchSpec === '*' ? 'latest' : fetchSpec}`;
+
+ const result = await pacote.manifest(dtSpec, { before: date }).catch(() => null);
+
+ return result !== null && dtSpec;
+ } finally {
+ removeCallback();
+ }
+}
diff --git a/package.json b/package.json
index 98caef0..32da51e 100644
--- a/package.json
+++ b/package.json
@@ -1,38 +1,84 @@
{
- "name": "hastypes",
- "version": "1.0.0",
- "description": "Check if a package has typescript typings from the command line",
- "main": "index.js",
- "bin": {
- "hastypes": "bin/index.js"
- },
- "files": [
- "bin"
- ],
- "repository": {
- "type": "git",
- "url": "git+https://github.com/BlueHatbRit/has-types.git"
- },
- "keywords": [
- "typescript",
- "check",
- "package",
- "npm",
- "has",
- "types"
- ],
- "author": "Elliot Blackburn ",
- "license": "MIT",
- "bugs": {
- "url": "https://github.com/BlueHatbRit/has-types/issues"
- },
- "homepage": "https://github.com/BlueHatbRit/has-types#readme",
- "dependencies": {
- "meow": "3.7.0",
- "request": "2.83.0",
- "request-promise": "4.2.2"
- },
- "engines": {
- "node": ">=8.9.1"
- }
+ "name": "has-types",
+ "version": "1.0.0",
+ "description": "Does the given package have TypeScript types?",
+ "bin": "./bin.mjs",
+ "main": "./index.mjs",
+ "exports": {
+ ".": "./index.mjs",
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "scripts": {
+ "prepack": "npmignore --auto --commentLines=autogenerated",
+ "version": "auto-changelog && git add CHANGELOG.md",
+ "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"",
+ "prepublishOnly": "safe-publish-latest",
+ "prepublish": "not-in-publish || npm run prepublishOnly",
+ "lint": "eslint --ext=js,mjs .",
+ "postlint": "tsc -p . && attw -P",
+ "pretest": "npm run lint",
+ "tests-only": "tape test/*.*js",
+ "test": "npm run tests-only",
+ "posttest": "npx npm@'>=10.2' audit --production"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/elliotblackburn/has-types.git"
+ },
+ "keywords": [
+ "npm",
+ "package",
+ "typescript",
+ "ts",
+ "types",
+ "typings",
+ "definitelytyped",
+ "dt"
+ ],
+ "author": "Elliot Blackburn ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/elliotblackburn/has-types/issues"
+ },
+ "homepage": "https://github.com/elliotblackburn/has-types#readme",
+ "dependencies": {
+ "dts-gen": "^0.10.4",
+ "get-dep-tree": "^2.0.0",
+ "mock-property": "^1.1.0",
+ "npm-package-arg": "^12.0.0",
+ "pacote": "^21.0.0",
+ "tmp": "^0.2.3"
+ },
+ "devDependencies": {
+ "@arethetypeswrong/cli": "^0.17.0",
+ "@ljharb/eslint-config": "^21.1.1",
+ "@ljharb/tsconfig": "^0.2.2",
+ "@types/node": "^22.10.1",
+ "@types/npm-package-arg": "^6.1.4",
+ "@types/tape": "^5.6.5",
+ "auto-changelog": "^2.5.0",
+ "eslint": "=8.8.0",
+ "in-publish": "^2.0.1",
+ "npmignore": "^0.3.1",
+ "safe-publish-latest": "^2.0.0",
+ "tape": "^5.9.0",
+ "typescript": "next"
+ },
+ "auto-changelog": {
+ "output": "CHANGELOG.md",
+ "template": "keepachangelog",
+ "unreleased": false,
+ "commitLimit": false,
+ "backfillLimit": false,
+ "hideCredit": true
+ },
+ "publishConfig": {
+ "ignore": [
+ ".github/workflows"
+ ]
+ },
+ "engines": {
+ "node": "^22.11 || >= 23.3"
+ }
}
diff --git a/pargs.mjs b/pargs.mjs
new file mode 100644
index 0000000..a19a4bd
--- /dev/null
+++ b/pargs.mjs
@@ -0,0 +1,100 @@
+import { parseArgs } from 'util';
+import { realpathSync } from 'fs';
+
+// import groupByPolyfill from 'object.groupby/polyfill';
+
+const { groupBy } = Object;
+
+/** @typedef {import('util').ParseArgsConfig} ParseArgsConfig */
+
+/** @typedef {(Error | TypeError) & { code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' | 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' | 'ERR_INVALID_ARG_TYPE' | 'ERR_INVALID_ARG_VALUE' | 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'}} ParseArgsError */
+
+/** @type {(e: unknown) => e is ParseArgsError} */
+function isParseArgsError(e) {
+ return !!e
+ && typeof e === 'object'
+ && 'code' in e
+ && (
+ e.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
+ || e.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
+ || e.code === 'ERR_INVALID_ARG_TYPE'
+ || e.code === 'ERR_INVALID_ARG_VALUE'
+ || e.code === 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'
+ );
+}
+
+/** @type {(helpText: string, entrypointPath: string, obj: Exclude) => ReturnType} */
+export default function pargs(helpText, entrypointPath, obj) {
+ const argv = process.argv.flatMap((arg) => {
+ try {
+ const realpathedArg = realpathSync(arg);
+ if (
+ realpathedArg === process.execPath
+ || realpathedArg === entrypointPath
+ ) {
+ return [];
+ }
+ } catch (e) { /**/ }
+ return arg;
+ });
+
+ if ('help' in obj) {
+ throw new TypeError('The "help" option is reserved');
+ }
+
+ const bools = obj.options ? Object.entries(obj.options).filter(([key, { type }]) => type === 'boolean' && key !== 'help') : [];
+ const inverseBools = Object.fromEntries(bools.map(([key, value]) => [
+ `no-${key}`,
+ { default: !value.default, type: 'boolean' },
+ ]));
+ /** @type {ParseArgsConfig & { tokens: true }} */
+ const newObj = {
+ args: argv,
+ ...obj,
+ options: {
+ ...obj.options,
+ ...inverseBools,
+ help: {
+ default: false,
+ type: 'boolean',
+ },
+ },
+ tokens: true,
+ };
+
+ try {
+ const { tokens, ...results } = parseArgs(newObj);
+
+ if ('help' in results.values && results.values.help) {
+ console.log(helpText);
+ process.exit(0);
+ }
+
+ /** @typedef {Extract} OptionToken */
+ const optionTokens = tokens.filter(/** @type {(token: typeof tokens[number]) => token is OptionToken} */ (token) => token.kind === 'option');
+ const passedArgs = new Set(optionTokens.map(({ name }) => name));
+ const groups = groupBy(passedArgs, /** @param {string} x */ (x) => x.replace(/^no-/, ''));
+ bools.forEach(([key]) => {
+ if ((groups[key]?.length ?? 0) > 1) {
+ console.log(helpText);
+ console.error(`Error: Arguments --${key} and --no-${key} are mutually exclusive`);
+ process.exit(2);
+ }
+ if (passedArgs.has(`no-${key}`)) {
+ // @ts-expect-error
+ results.values[key] = !results.values[`no-${key}`];
+ }
+ // @ts-expect-error
+ delete results.values[`no-${key}`];
+ });
+
+ return obj.tokens ? { ...results, tokens } : results;
+ } catch (e) {
+ if (isParseArgsError(e)) {
+ console.log(helpText);
+ console.error(`Error: ${e.message}`);
+ process.exit(1);
+ }
+ throw e;
+ }
+}
diff --git a/test/index.mjs b/test/index.mjs
new file mode 100644
index 0000000..e749cfe
--- /dev/null
+++ b/test/index.mjs
@@ -0,0 +1,37 @@
+import test from 'tape';
+
+import hasTypes from '../index.mjs';
+
+const before = '2024-12-01'; // update this date as desired
+
+test('hasTypes', async (t) => {
+ t.comment(`date is pinned to ${before}`);
+
+ const packages = {
+ 'has-proto': true,
+ 'tape@4': '@types/tape@4',
+ 'tape@latest': '@types/tape@latest',
+ tape: '@types/tape@latest',
+ };
+
+ // eslint-disable-next-line no-extra-parens, max-len
+ const results = /** @type {[keyof packages, PromiseSettledResult<`@types/${string}` | boolean>][]} */ (await Promise.all(Object.keys(packages).map(async (specifier) => [
+ specifier,
+ (await Promise.allSettled([hasTypes(specifier, { before })]))[0],
+ ])));
+
+ results.forEach(([specifier, {
+ status,
+ // @ts-expect-error yes, it exists on one half of the union
+ value,
+ // @ts-expect-error yes, it exists on one half of the union
+ reason,
+ }]) => {
+ t.equal(status, 'fulfilled', `expected ${specifier} to be fulfilled; got ${status}`);
+ if (status === 'fulfilled') {
+ t.equal(value, packages[specifier], `expected ${specifier} to be ${packages[specifier]}; got ${value}`);
+ } else {
+ t.fail(reason);
+ }
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..a0f7417
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "@ljharb/tsconfig",
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "nodenext",
+ },
+ "exclude": [
+ "coverage"
+ ]
+}