feat: add typescript client

This commit is contained in:
Alan 2024-12-17 13:59:20 +08:00
parent 3a2d02923e
commit e481fea2b9
23 changed files with 2725 additions and 2960 deletions

View File

@ -0,0 +1 @@
* text eol=lf

View File

@ -7,15 +7,15 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['10.x', '12.x', '14.x']
node: ['14.x', '16.x']
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- name: Checkout repo
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

View File

@ -6,7 +6,7 @@ jobs:
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,103 +1,44 @@
# TSDX User Guide
# IP2Region.Net
Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Lets get you oriented with whats here and how to use it.
TypeScript node client library for IP2Region
> This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If youre looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`.
## Installation
> If youre new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript)
## Commands
TSDX scaffolds your new library inside `/src`.
To run TSDX, use:
Install the package with NPM
```bash
npm start # or yarn start
npm install @types/ip2region
# or
yarn add @types/ip2region
```
This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`.
## Usage
To do a one-off build, use `npm run build` or `yarn build`.
```typescript
import { Searcher, CachePolicy, ISearcher } from 'ip2region';
To run tests, use `npm test` or `yarn test`.
const dbPath = './ip2region.xdb';
const searcher: ISearcher = new Searcher(CachePolicy.Content, dbPath);
## Configuration
const ip = '8.8.8.8';
const regionInfo = await searcher.search(ip);
console.log(`IP: ${ip}, Region: ${regionInfo}`);
Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly.
### Jest
Jest tests are set up to run with `npm test` or `yarn test`.
### Bundle Analysis
[`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`.
#### Setup Files
This is the folder structure we set up for you:
```txt
/src
index.tsx # EDIT THIS
/test
blah.test.tsx # EDIT THIS
.gitignore
package.json
README.md # EDIT THIS
tsconfig.json
await searcher.close();
```
### Cache Policy Description
| Cache Policy | Description | Thread Safe |
|-------------------------|------------------------------------------------------------------------------------------------------------|-------------|
| CachePolicy.Content | Cache the entire `xdb` data. | Yes |
| CachePolicy.VectorIndex | Cache `vecotorIndex` to speed up queries and reduce system io pressure by reducing one fixed IO operation. | Yes |
| CachePolicy.File | Completely file-based queries | Yes |
### XDB File Description
Generate using [maker](https://github.com/lionsoul2014/ip2region/tree/master/maker/csharp), or [download](https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb) pre-generated xdb files
### Rollup
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details.
Please make sure to update tests as appropriate.
### TypeScript
`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs.
## Continuous Integration
### GitHub Actions
Two actions are added by default:
- `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix
- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit)
## Optimizations
Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations:
```js
// ./types/index.d.ts
declare var __DEV__: boolean;
// inside your code...
if (__DEV__) {
console.log('foo');
}
```
You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions.
## Module Formats
CJS, ESModules, and UMD module formats are supported.
The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found.
## Named Exports
Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library.
## Including Styles
There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like.
For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader.
## Publishing to NPM
We recommend using [np](https://github.com/sindresorhus/np).
## License
[Apache License 2.0](https://github.com/lionsoul2014/ip2region/blob/master/LICENSE.md)

View File

@ -1,28 +1,27 @@
{
"name": "@types/ip2region",
"version": "0.1.0",
"license": "MIT",
"license": "Apache-2.0",
"author": "Alan",
"main": "dist/index.js",
"module": "dist/ip2region.esm.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test",
"lint": "tsdx lint",
"prepare": "tsdx build",
"analyze": "size-limit --why",
"build": "dts build",
"lint": "dts lint",
"prepare": "dts build",
"size": "size-limit",
"analyze": "size-limit --why"
"start": "dts watch",
"test": "dts test"
},
"peerDependencies": {},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
"pre-commit": "dts lint"
}
},
"prettier": {
@ -31,9 +30,13 @@
"singleQuote": true,
"trailingComma": "es5"
},
"name": "ip2region",
"author": "Alan",
"module": "dist/ip2region.esm.js",
"jest": {
"testEnvironment": "node"
},
"peerDependencies": {},
"engines": {
"node": ">=12"
},
"size-limit": [
{
"path": "dist/ip2region.cjs.production.min.js",
@ -46,9 +49,10 @@
],
"devDependencies": {
"@size-limit/preset-small-lib": "^11.1.6",
"@tsconfig/recommended": "^1.0.8",
"dts-cli": "^2.0.5",
"husky": "^9.1.7",
"size-limit": "^11.1.6",
"tsdx": "^0.14.1",
"tslib": "^2.8.1",
"typescript": "^5.7.2"
}

View File

@ -0,0 +1,93 @@
import * as fs from 'fs/promises';
export abstract class AbstractCacheStrategy {
protected static readonly HEADER_INFO_LENGTH = 256;
protected static readonly VECTOR_INDEX_ROWS = 256;
protected static readonly VECTOR_INDEX_COLS = 256;
protected static readonly VECTOR_INDEX_SIZE = 8;
private fileHandle: fs.FileHandle | null = null;
private fileSize: number = 0;
private ioCounter = 0;
protected constructor(protected readonly xdbPath: string) {
// 构造不立即打开文件,懒加载
}
public get ioCount(): number {
return this.ioCounter;
}
protected incrementIoCount() {
this.ioCounter++;
}
/** 打开文件,若已打开则跳过 */
protected async openFile(): Promise<void> {
if (this.fileHandle === null) {
const fh = await fs.open(this.xdbPath, 'r');
this.fileHandle = fh;
const stat = await fh.stat();
this.fileSize = stat.size;
}
}
protected getFileSize(): number {
return this.fileSize;
}
/** 关闭文件 */
public async closeFile(): Promise<void> {
if (this.fileHandle) {
await this.fileHandle.close();
this.fileHandle = null;
}
}
/**
*
* @param ip uint32
*/
protected getVectorIndexStartPos(ip: number): number {
const il0 = (ip >>> 24) & 0xFF;
const il1 = (ip >>> 16) & 0xFF;
const idx = il0 * AbstractCacheStrategy.VECTOR_INDEX_COLS * AbstractCacheStrategy.VECTOR_INDEX_SIZE
+ il1 * AbstractCacheStrategy.VECTOR_INDEX_SIZE;
return idx;
}
public abstract getVectorIndex(ip: number): Promise<Buffer>;
/**
* Node.js可安全并发读
* @param offset
* @param length
*/
public async getData(offset: number, length: number): Promise<Buffer> {
await this.openFile();
if (!this.fileHandle) {
throw new Error('File handle is not available.');
}
// Node.js 通常会一次性读完,但仍保留循环以防偶发读不足
let totalBytesRead = 0;
const buffer = Buffer.allocUnsafe(length);
while (totalBytesRead < length) {
const { bytesRead } = await this.fileHandle.read({
buffer,
offset: totalBytesRead,
length: length - totalBytesRead,
position: offset + totalBytesRead
});
this.incrementIoCount();
if (bytesRead === 0) {
break; // 文件结束或出错
}
totalBytesRead += bytesRead;
}
return buffer.subarray(0, totalBytesRead);
}
}

View File

@ -0,0 +1,22 @@
import { CachePolicy } from "../models/cache-policy";
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
import { ContentCacheStrategy } from "./content-cache-strategy";
import { FileCacheStrategy } from "./file-cache-strategy";
import { VectorIndexCacheStrategy } from "./vector-index-cache-strategy";
export class CacheStrategyFactory {
constructor(private readonly xdbPath: string) {}
public createCacheStrategy(cachePolicy: CachePolicy): AbstractCacheStrategy {
switch (cachePolicy) {
case CachePolicy.Content:
return new ContentCacheStrategy(this.xdbPath);
case CachePolicy.VectorIndex:
return new VectorIndexCacheStrategy(this.xdbPath);
case CachePolicy.File:
return new FileCacheStrategy(this.xdbPath);
default:
throw new Error(`Unknown cachePolicy: ${cachePolicy}`);
}
}
}

View File

@ -0,0 +1,45 @@
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
export class ContentCacheStrategy extends AbstractCacheStrategy {
private cacheData: Buffer | null = null;
constructor(xdbPath: string) {
super(xdbPath);
}
public async getVectorIndex(ip: number): Promise<Buffer> {
if (!this.cacheData) {
await this.ensureCacheLoaded();
}
if (!this.cacheData) {
throw new Error('Cache data is not loaded.');
}
const idx = this.getVectorIndexStartPos(ip);
return this.cacheData.subarray(
AbstractCacheStrategy.HEADER_INFO_LENGTH + idx,
AbstractCacheStrategy.HEADER_INFO_LENGTH + idx + AbstractCacheStrategy.VECTOR_INDEX_SIZE
);
}
public override async getData(offset: number, length: number): Promise<Buffer> {
if (!this.cacheData) {
await this.ensureCacheLoaded();
}
if (!this.cacheData) {
throw new Error('Cache data is not loaded.');
}
return this.cacheData.subarray(offset, offset + length);
}
private async ensureCacheLoaded(): Promise<void> {
if (this.cacheData !== null) {
return;
}
await this.openFile();
const fileSize = this.getFileSize();
this.cacheData = await super.getData(0, fileSize);
// 全量读取后立即关闭文件
await this.closeFile();
}
}

View File

@ -0,0 +1,15 @@
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
export class FileCacheStrategy extends AbstractCacheStrategy {
public constructor(xdbPath: string) {
super(xdbPath);
}
public async getVectorIndex(ip: number): Promise<Buffer> {
const idx = this.getVectorIndexStartPos(ip);
return this.getData(
AbstractCacheStrategy.HEADER_INFO_LENGTH + idx,
AbstractCacheStrategy.VECTOR_INDEX_SIZE
);
}
}

View File

@ -0,0 +1,32 @@
import { AbstractCacheStrategy } from "./abstract-cache-strategy";
export class VectorIndexCacheStrategy extends AbstractCacheStrategy {
private vectorIndex: Buffer | null = null;
constructor(xdbPath: string) {
super(xdbPath);
}
public async getVectorIndex(ip: number): Promise<Buffer> {
if (!this.vectorIndex) {
await this.ensureVectorIndexLoaded();
}
if (!this.vectorIndex) {
throw new Error('Vector index data not loaded.');
}
const idx = this.getVectorIndexStartPos(ip);
return this.vectorIndex.subarray(idx, idx + AbstractCacheStrategy.VECTOR_INDEX_SIZE);
}
private async ensureVectorIndexLoaded(): Promise<void> {
if (this.vectorIndex !== null) {
return;
}
await this.openFile();
const vectorLength = AbstractCacheStrategy.VECTOR_INDEX_ROWS
* AbstractCacheStrategy.VECTOR_INDEX_COLS
* AbstractCacheStrategy.VECTOR_INDEX_SIZE;
this.vectorIndex = await super.getData(AbstractCacheStrategy.HEADER_INFO_LENGTH, vectorLength);
// 保留 fileHandle 打开,因为后续可能还要继续读段数据
}
}

View File

@ -1,6 +1,3 @@
export const sum = (a: number, b: number) => {
if ('development' === process.env.NODE_ENV) {
console.log('boop');
}
return a + b;
};
export * from './searcher/searcher';
export * from './searcher/ISearcher';
export * from './models/cache-policy';

View File

@ -0,0 +1,5 @@
export enum CachePolicy {
File = 'File',
VectorIndex = 'VectorIndex',
Content = 'Content'
}

View File

@ -0,0 +1,6 @@
export interface ISearcher {
search(ipStr: string): Promise<string | undefined>;
searchByUint32(ip: number): Promise<string | undefined>;
ioCount: number;
close(): Promise<void>;
}

View File

@ -0,0 +1,84 @@
import { AbstractCacheStrategy } from "../cache/abstract-cache-strategy";
import { CacheStrategyFactory } from "../cache/cache-strategy-factory";
import { CachePolicy } from "../models/cache-policy";
import { Util } from "../util/ip-util";
import { ISearcher } from "./ISearcher";
export class Searcher implements ISearcher {
private readonly cacheStrategy: AbstractCacheStrategy;
constructor(cachePolicy: CachePolicy, dbPath: string) {
const factory = new CacheStrategyFactory(dbPath);
this.cacheStrategy = factory.createCacheStrategy(cachePolicy);
}
public get ioCount(): number {
return this.cacheStrategy.ioCount;
}
/** 主动关闭 */
public async close(): Promise<void> {
await this.cacheStrategy.closeFile();
}
public async search(ipStr: string): Promise<string | undefined> {
const ip = Util.ipAddressToUInt32(ipStr);
return this.searchByUint32(ip);
}
/**
* sPtr~ePtr getData()
*/
public async searchByUint32(ip: number): Promise<string | undefined> {
const index = await this.cacheStrategy.getVectorIndex(ip);
if (index.length < 8) {
return undefined;
}
// 读取 sPtr, ePtruint32, 小端序)
const sPtr = index.readUInt32LE(0);
const ePtr = index.readUInt32LE(4);
const SEGMENT_INDEX_SIZE = 14;
const totalCount = Math.floor((ePtr - sPtr) / SEGMENT_INDEX_SIZE) + 1;
if (totalCount <= 0) {
return undefined;
}
// 一次性批量读取 [sPtr, ePtr] 范围内的所有 segment
const blockSize = totalCount * SEGMENT_INDEX_SIZE;
const blockBuff = await this.cacheStrategy.getData(sPtr, blockSize);
// 在内存中完成二分查找
let l = 0;
let h = totalCount - 1;
let dataLen = 0;
let dataPtr = 0;
while (l <= h) {
const mid = (l + h) >>> 1; // 中点
const offset = mid * SEGMENT_INDEX_SIZE;
const sip = blockBuff.readUInt32LE(offset);
const eip = blockBuff.readUInt32LE(offset + 4);
if (ip < sip) {
h = mid - 1;
} else if (ip > eip) {
l = mid + 1;
} else {
dataLen = blockBuff.readUInt16LE(offset + 8);
dataPtr = blockBuff.readUInt32LE(offset + 10);
break;
}
}
if (dataLen === 0) {
return undefined;
}
// 读取 region 数据
const regionBuff = await this.cacheStrategy.getData(dataPtr, dataLen);
return regionBuff.toString('utf8');
}
}

View File

@ -0,0 +1,33 @@
export class Util {
public static isValidIpAddress(ipStr: string): boolean {
const octets = ipStr.split('.');
if (octets.length !== 4) {
return false;
}
for (const octet of octets) {
if (isNaN(Number(octet)) || Number(octet) < 0 || Number(octet) > 255) {
return false;
}
}
return true;
}
/**
* IPv4 uint32
* "1.2.3.4" -> 0x04030201
*/
public static ipAddressToUInt32(ipStr: string): number {
if (!Util.isValidIpAddress(ipStr)) {
throw new Error(`Invalid IPv4: ${ipStr}`);
}
const octets = ipStr.split('.').map(Number);
// 数据库是小端存储,如 1.2.3.4 => 0x04030201
return (
((octets[3] & 0xff) << 24) |
((octets[2] & 0xff) << 16) |
((octets[1] & 0xff) << 8) |
(octets[0] & 0xff)
) >>> 0;
}
}

View File

@ -1,7 +0,0 @@
import { sum } from '../src';
describe('blah', () => {
it('works', () => {
expect(sum(1, 1)).toEqual(2);
});
});

View File

@ -0,0 +1,31 @@
import { ContentCacheStrategy } from '../../src/cache/content-cache-strategy';
describe('ContentCacheStrategy Tests', () => {
let strategy: ContentCacheStrategy;
const dbPath = "../../../../data/ip2region.xdb";
beforeAll(() => {
strategy = new ContentCacheStrategy(dbPath);
});
afterAll(async () => {
await strategy.closeFile();
});
it('should load the entire file into memory', async () => {
// 首次访问会触发 ensureCacheLoaded
const vectorIndex = await strategy.getVectorIndex(0x01020304);
expect(vectorIndex.length).toBe(8); // VECTOR_INDEX_SIZE = 8
// 再次读取不会增加IO次数太多
const initIo = strategy.ioCount;
await strategy.getVectorIndex(0x05060708);
const newIo = strategy.ioCount;
expect(newIo).toBe(initIo); // 全量缓存后几乎不增加IO
});
it('should read arbitrary data after caching', async () => {
const data = await strategy.getData(0, 32); // 读取前32字节
expect(data.length).toBe(32);
});
});

View File

@ -0,0 +1,36 @@
import { FileCacheStrategy } from '../../src/cache/file-cache-strategy';
describe('FileCacheStrategy Tests', () => {
let strategy: FileCacheStrategy;
//const dbPath = join(__dirname, '..', 'data', 'ip2region.xdb');
const dbPath = "../../../../data/ip2region.xdb";
beforeAll(() => {
strategy = new FileCacheStrategy(dbPath);
});
afterAll(async () => {
await strategy.closeFile();
});
it('should read vector index from file each time', async () => {
// 每次获取矢量索引,都会触发一次文件读取
const initIo = strategy.ioCount;
const vecIndex1 = await strategy.getVectorIndex(0x01020304);
expect(vecIndex1.length).toBe(8);
const afterIo1 = strategy.ioCount;
expect(afterIo1).toBeGreaterThan(initIo);
// 第二次读取仍增加IO因为不缓存
const vecIndex2 = await strategy.getVectorIndex(0x05060708);
expect(vecIndex2.length).toBe(8);
const afterIo2 = strategy.ioCount;
expect(afterIo2).toBeGreaterThan(afterIo1);
});
it('should read arbitrary data directly from file', async () => {
const offset = 0;
const length = 64;
const data = await strategy.getData(offset, length);
expect(data.length).toBe(length);
});
});

View File

@ -0,0 +1,40 @@
import { VectorIndexCacheStrategy } from '../../src/cache/vector-index-cache-strategy';
describe('VectorIndexCacheStrategy Tests', () => {
let strategy: VectorIndexCacheStrategy;
//const dbPath = join(__dirname, '..', 'data', 'ip2region.xdb');
const dbPath = "../../../../data/ip2region.xdb";
beforeAll(() => {
strategy = new VectorIndexCacheStrategy(dbPath);
});
afterAll(async () => {
await strategy.closeFile();
});
it('should cache the vector index in memory', async () => {
// 首次调用会把矢量索引读到内存
const initIo = strategy.ioCount;
const vecIndex1 = await strategy.getVectorIndex(0x01020304);
expect(vecIndex1.length).toBe(8);
const afterIo1 = strategy.ioCount;
expect(afterIo1).toBeGreaterThan(initIo);
// 第二次获取矢量索引不会增加IO
const vecIndex2 = await strategy.getVectorIndex(0x05060708);
expect(vecIndex2.length).toBe(8);
const afterIo2 = strategy.ioCount;
expect(afterIo2).toBe(afterIo1);
});
it('should still read segment data from file as needed', async () => {
// 模拟读取部分数据段
const offset = 1024; // 示例偏移
const length = 32;
const segmentData = await strategy.getData(offset, length);
expect(segmentData.length).toBe(length);
// 这会增加IO
expect(strategy.ioCount).toBeGreaterThan(0);
});
});

View File

@ -0,0 +1,39 @@
import { Searcher } from '../../src/searcher/searcher';
import { CachePolicy } from '../../src/models/cache-policy';
describe('Searcher Tests', () => {
let searcher: Searcher;
beforeAll(() => {
// 指向一个真实存在的 xdb 文件(测试需要)
const dbPath = "../../../../data/ip2region.xdb";
// 测试用 ContentCacheStrategy一次性载入文件
searcher = new Searcher(CachePolicy.Content, dbPath);
});
afterAll(async () => {
await searcher.close();
});
it('should return valid data for a known IP', async () => {
const ipStr = '1.2.3.4';
const regionInfo = await searcher.search(ipStr);
expect(regionInfo).toBeDefined();
});
it('should return undefined for an invalid IP', async () => {
const invalidIp = '999.999.999.999';
await expect(searcher.search(invalidIp)).rejects.toThrow();
});
it('should track ioCount properly', async () => {
// ioCount 取决于缓存策略和查询次数
const initialCount = searcher.ioCount;
await searcher.search('8.8.8.8');
const afterCount = searcher.ioCount;
// ContentCacheStrategy只读取一次所以 ioCount 不变
expect(afterCount).toEqual(initialCount);
});
});

View File

@ -0,0 +1,16 @@
import { Util } from '../../src/util/ip-util';
describe('IP Util Tests', () => {
it('should convert valid IPv4 string to uint32 correctly', () => {
const ipStr = '1.2.3.4';
const uint32 = Util.ipAddressToUInt32(ipStr);
// 1.2.3.4 小端序 => 0x04030201 => 67305985
expect(uint32).toBe(67305985);
});
it('should throw error for invalid IPv4 string', () => {
expect(() => {
Util.ipAddressToUInt32('256.256.256.256');
}).toThrow();
});
});

View File

@ -1,35 +1,8 @@
{
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"extends": "@tsconfig/recommended/tsconfig.json",
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"lib": ["dom", "esnext"],
"importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true,
// match output dir to input dir. e.g. dist/index instead of dist/src/index
"rootDir": "./src",
// stricter type-checking for stronger correctness. Recommended by TS
"strict": true,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": true,
"noUnusedParameters": true,
// use Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "node",
// transpile JSX to React.createElement
"jsx": "react",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true,
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
"noEmit": true,
"allowImportingTsExtensions": false
}
}

File diff suppressed because it is too large Load Diff