mirror of
https://github.com/oceanprotocol/ens-proxy.git
synced 2024-11-14 17:25:00 +01:00
Merge pull request #1 from oceanprotocol/initial-setup
Creating API for requests to ENS
This commit is contained in:
commit
4eba08b60c
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
||||
#INFURA_PROJECT_ID="xxx"
|
16
.eslintrc
Normal file
16
.eslintrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.json"]
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "off"
|
||||
},
|
||||
"env": { "es6": true, "node": true }
|
||||
}
|
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
||||
* @jamiehewitt15 @kremalicious
|
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: monthly
|
||||
time: '03:00'
|
||||
timezone: Europe/Berlin
|
48
.github/workflows/ci.yml
vendored
Normal file
48
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: 'CI'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-lint-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: ${{ runner.os }}-lint-${{ env.cache-name }}-
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
|
||||
test_integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-lint-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: ${{ runner.os }}-lint-${{ env.cache-name }}-
|
||||
- run: npm ci
|
||||
- run: npm run test:integration
|
27
.github/workflows/publish.yml
vendored
Normal file
27
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: 'Publish'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
npm:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm ci
|
||||
|
||||
# pre-releases, triggered by `next` as part of git tag
|
||||
- run: npm publish --tag next
|
||||
if: ${{ contains(github.ref, 'next') }}
|
||||
|
||||
# production releases
|
||||
- run: npm publish
|
||||
if: ${{ !contains(github.ref, 'next') }}
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.env
|
||||
.vercel
|
||||
dist
|
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
node_modules/.bin/pretty-quick --staged
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "auto"
|
||||
}
|
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"wix.vscode-import-cost"
|
||||
]
|
||||
}
|
17
.vscode/settings.json
vendored
Normal file
17
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"search.exclude": {
|
||||
"**/.cache": true,
|
||||
"**/public": true
|
||||
}
|
||||
}
|
100
README.md
100
README.md
@ -1 +1,101 @@
|
||||
# Proxy API for ENS requests
|
||||
|
||||
## Running Locally
|
||||
|
||||
```
|
||||
npm install
|
||||
npm i -g vercel
|
||||
vercel dev
|
||||
```
|
||||
|
||||
## Example Requests
|
||||
|
||||
### Get Ens Name
|
||||
|
||||
```
|
||||
GET http://localhost:3000/api/name?accountId=0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "jellymcjellyfish.eth"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Ens Address
|
||||
|
||||
```
|
||||
GET http://localhost:3000/api/address?name=jellymcjellyfish.eth
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{
|
||||
"address": "0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Ens Text Records
|
||||
|
||||
```
|
||||
GET http://localhost:3000/api/text?name=jellymcjellyfish.eth
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"key": "url",
|
||||
"value": "https://oceanprotocol.com"
|
||||
},
|
||||
{
|
||||
"key": "avatar",
|
||||
"value": "https://raw.githubusercontent.com/oceanprotocol/art/main/logo/favicon-white.png"
|
||||
},
|
||||
{
|
||||
"key": "com.twitter",
|
||||
"value": "oceanprotocol"
|
||||
},
|
||||
{
|
||||
"key": "com.github",
|
||||
"value": "oceanprotocol"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get ENS Profile
|
||||
|
||||
```
|
||||
GET http://localhost:3000/api/profile?address=0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{
|
||||
"profile": {
|
||||
"name": "jellymcjellyfish.eth",
|
||||
"avatar": "https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth",
|
||||
"links": [
|
||||
{
|
||||
"key": "url",
|
||||
"value": "https://oceanprotocol.com"
|
||||
},
|
||||
{
|
||||
"key": "com.twitter",
|
||||
"value": "oceanprotocol"
|
||||
},
|
||||
{
|
||||
"key": "com.github",
|
||||
"value": "oceanprotocol"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
9
api/_utils.ts
Normal file
9
api/_utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ethers } from 'ethers'
|
||||
|
||||
export async function getProvider(): Promise<any> {
|
||||
const provider = new ethers.providers.InfuraProvider(
|
||||
'homestead',
|
||||
process.env.INFURA_PROJECT_ID
|
||||
)
|
||||
return provider
|
||||
}
|
18
api/address.ts
Normal file
18
api/address.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { VercelRequest, VercelResponse } from '@vercel/node'
|
||||
import { getProvider } from './_utils'
|
||||
|
||||
export default async function getEnsAddress(
|
||||
request: VercelRequest,
|
||||
response: VercelResponse
|
||||
) {
|
||||
try {
|
||||
const ensName = request.query.name
|
||||
const provider = await getProvider()
|
||||
const address = await provider.resolveName(ensName)
|
||||
|
||||
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
|
||||
response.status(200).send({ address })
|
||||
} catch (error) {
|
||||
response.send({ error })
|
||||
}
|
||||
}
|
27
api/name.ts
Normal file
27
api/name.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { VercelRequest, VercelResponse } from '@vercel/node'
|
||||
import { getProvider } from './_utils'
|
||||
|
||||
export async function getEnsName(accountId: string) {
|
||||
const provider = await getProvider()
|
||||
let name = await provider.lookupAddress(accountId)
|
||||
|
||||
// Check to be sure the reverse record is correct.
|
||||
const reverseAccountId = await provider.resolveName(name)
|
||||
if (accountId.toLowerCase() !== reverseAccountId.toLowerCase()) name = null
|
||||
return name
|
||||
}
|
||||
|
||||
export default async function nameApi(
|
||||
request: VercelRequest,
|
||||
response: VercelResponse
|
||||
) {
|
||||
try {
|
||||
const accountId = String(request.query.accountId)
|
||||
const name = await getEnsName(accountId)
|
||||
|
||||
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
|
||||
response.status(200).send({ name })
|
||||
} catch (error) {
|
||||
response.send({ error })
|
||||
}
|
||||
}
|
72
api/profile.ts
Normal file
72
api/profile.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type { VercelRequest, VercelResponse } from '@vercel/node'
|
||||
import { getEnsName } from './name'
|
||||
import { getEnsTextRecords } from './text'
|
||||
|
||||
interface ProfileLink {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
name: string
|
||||
url?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
links?: ProfileLink[]
|
||||
}
|
||||
|
||||
function getEnsAvatar(ensName: string): string {
|
||||
return ensName
|
||||
? `https://metadata.ens.domains/mainnet/avatar/${ensName}`
|
||||
: 'null'
|
||||
}
|
||||
|
||||
export async function getEnsProfile(accountId: string): Promise<Profile> {
|
||||
const name = await getEnsName(accountId)
|
||||
if (!name) return { name: 'null' }
|
||||
|
||||
const records = await getEnsTextRecords(name)
|
||||
if (!records) return { name }
|
||||
|
||||
const avatar = records.filter((record) => record.key === 'avatar')[0]?.value
|
||||
const description = records.filter(
|
||||
(record) => record.key === 'description'
|
||||
)[0]?.value
|
||||
|
||||
// filter out what we need from the fetched text records
|
||||
const linkKeys = [
|
||||
'url',
|
||||
'com.twitter',
|
||||
'com.github',
|
||||
'org.telegram',
|
||||
'com.discord',
|
||||
'com.reddit'
|
||||
]
|
||||
|
||||
const links: ProfileLink[] = records.filter((record) =>
|
||||
linkKeys.includes(record.key)
|
||||
)
|
||||
|
||||
const profile: Profile = {
|
||||
name,
|
||||
...(avatar && { avatar: getEnsAvatar(name) }),
|
||||
...(description && { description }),
|
||||
...(links.length > 0 && { links })
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
export default async function EnsProfileApi(
|
||||
request: VercelRequest,
|
||||
response: VercelResponse
|
||||
) {
|
||||
try {
|
||||
const accountId = String(request.query.address)
|
||||
const profile = await getEnsProfile(accountId)
|
||||
|
||||
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
|
||||
response.status(200).send({ profile })
|
||||
} catch (error) {
|
||||
response.send({ error })
|
||||
}
|
||||
}
|
44
api/text.ts
Normal file
44
api/text.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { VercelRequest, VercelResponse } from '@vercel/node'
|
||||
import { getProvider } from './_utils'
|
||||
|
||||
export async function getEnsTextRecords(
|
||||
ensName: string
|
||||
): Promise<{ key: string; value: string }[] | null> {
|
||||
const texts = [
|
||||
'url',
|
||||
'avatar',
|
||||
'com.twitter',
|
||||
'com.github',
|
||||
'org.telegram',
|
||||
'com.discord',
|
||||
'com.reddit'
|
||||
]
|
||||
|
||||
const records = []
|
||||
const provider = await getProvider()
|
||||
const resolver = await provider.getResolver(ensName)
|
||||
if (!resolver) return null
|
||||
|
||||
for (let index = 0; index < texts?.length; index++) {
|
||||
const key = texts[index]
|
||||
const value = await resolver.getText(key)
|
||||
value && records.push({ key, value })
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
export default async function ensTextApi(
|
||||
request: VercelRequest,
|
||||
response: VercelResponse
|
||||
) {
|
||||
try {
|
||||
const ensName = String(request.query.name)
|
||||
const records = await getEnsTextRecords(ensName)
|
||||
|
||||
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
|
||||
response.status(200).send({ records })
|
||||
} catch (error) {
|
||||
response.send({ error })
|
||||
}
|
||||
}
|
7965
package-lock.json
generated
Normal file
7965
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "ocean-ens-proxy",
|
||||
"description": "Ocean Protocol ENS Proxy Server",
|
||||
"version": "0.0.0",
|
||||
"author": "Ocean Protocol <devops@oceanprotocol.com>",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js --ext .ts --ext .tsx . && npm run type-check",
|
||||
"clean": "rm -rf ./dist",
|
||||
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
|
||||
"test:format": "npm run format && npm run lint",
|
||||
"test:integration": "ts-mocha -p test/tsconfig.json --exit test/**/*.test.ts",
|
||||
"test": "test:format && test:integration",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"ethers": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/test-listen": "^1.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"@vercel/node": "^2.5.8",
|
||||
"axios": "^0.27.2",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"husky": "^7.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"test-listen": "^1.1.0",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"typescript": "4.7.3",
|
||||
"vercel-node-server": "^2.2.1"
|
||||
}
|
||||
}
|
143
test/api.test.ts
Normal file
143
test/api.test.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import addressApi from '../api/address'
|
||||
import nameApi from '../api/name'
|
||||
import profileApi from '../api/profile'
|
||||
import textApi from '../api/text'
|
||||
import { createServer } from 'vercel-node-server'
|
||||
import listen from 'test-listen'
|
||||
// import express from 'express'
|
||||
// import request from 'supertest'
|
||||
import axios from 'axios'
|
||||
// import type { VercelRequest, VercelResponse } from '@vercel/node'
|
||||
import { assert } from 'chai'
|
||||
|
||||
let server: any
|
||||
let url: string
|
||||
const name = 'jellymcjellyfish.eth'
|
||||
const accountId = '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0'
|
||||
const invalid = 'lkdfjslkdfjpeoijf3423'
|
||||
|
||||
describe('Testing ENS proxy API endpoints', function () {
|
||||
this.timeout(25000)
|
||||
|
||||
it('Requesting address should return the expected response', async () => {
|
||||
server = createServer(addressApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
name
|
||||
}
|
||||
})
|
||||
assert(response.status === 200)
|
||||
assert(response.data.address === accountId)
|
||||
})
|
||||
it('Requesting ENS name should return the expected response', async () => {
|
||||
server = createServer(nameApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
accountId
|
||||
}
|
||||
})
|
||||
assert(response.status === 200)
|
||||
assert(response.data.name === name)
|
||||
})
|
||||
it('Requesting text records should return the expected response', async () => {
|
||||
server = createServer(textApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
name
|
||||
}
|
||||
})
|
||||
assert(response.status === 200)
|
||||
assert(
|
||||
response.data.records[0].value === 'https://oceanprotocol.com',
|
||||
'Wrong URL'
|
||||
)
|
||||
assert(
|
||||
response.data.records[1].value ===
|
||||
'https://raw.githubusercontent.com/oceanprotocol/art/main/logo/favicon-white.png',
|
||||
'Wrong avatar'
|
||||
)
|
||||
assert(response.data.records[2].value === 'oceanprotocol', 'Wrong link 1')
|
||||
assert(response.data.records[3].value === 'oceanprotocol', 'wrong link 2')
|
||||
|
||||
assert(response.data.records[0].key === 'url', 'Wrong URL')
|
||||
assert(response.data.records[1].key === 'avatar', 'Wrong avatar')
|
||||
assert(response.data.records[2].key === 'com.twitter', 'Wrong link 1')
|
||||
assert(response.data.records[3].key === 'com.github', 'wrong link 2')
|
||||
})
|
||||
|
||||
it('Requesting user profile should return the expected response', async () => {
|
||||
server = createServer(profileApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
address: accountId
|
||||
}
|
||||
})
|
||||
|
||||
assert(response.status === 200)
|
||||
assert(response.data.profile.name === name)
|
||||
assert(
|
||||
response.data.profile.avatar ===
|
||||
'https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth'
|
||||
)
|
||||
assert(response.data.profile.links[0].value === 'https://oceanprotocol.com')
|
||||
assert(response.data.profile.links[1].value === 'oceanprotocol')
|
||||
assert(response.data.profile.links[2].value === 'oceanprotocol')
|
||||
|
||||
assert(response.data.profile.links[0].key === 'url')
|
||||
assert(response.data.profile.links[1].key === 'com.twitter')
|
||||
assert(response.data.profile.links[2].key === 'com.github')
|
||||
})
|
||||
|
||||
// Tests with incorrect address or name
|
||||
|
||||
it('Requesting address should return status 200 with invalid name', async () => {
|
||||
server = createServer(addressApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
name: invalid
|
||||
}
|
||||
})
|
||||
assert(response.status === 200)
|
||||
})
|
||||
it('Requesting name should return status 200 with invalid accountId', async () => {
|
||||
server = createServer(nameApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
accountId: invalid
|
||||
}
|
||||
})
|
||||
assert(response.status === 200)
|
||||
})
|
||||
it('Requesting text records should return status 200 with invalid name', async () => {
|
||||
server = createServer(textApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
name: invalid
|
||||
}
|
||||
})
|
||||
assert(response.status === 200)
|
||||
})
|
||||
|
||||
it('Requesting profile should return status 200 with invalid name', async () => {
|
||||
server = createServer(profileApi)
|
||||
url = await listen(server)
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
address: invalid
|
||||
}
|
||||
})
|
||||
|
||||
assert(response.status === 200)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
server.close()
|
||||
})
|
9
test/tsconfig.json
Normal file
9
test/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["es6", "es7"],
|
||||
"noUnusedLocals": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user