git3 uri protocol

master
cyhhao 2 years ago
parent c107ace2a1
commit e93e72c47e

@ -0,0 +1,3 @@
export default {
"ETHStorage": '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"chunkId","type":"uint256"}],"name":"chunkStakeTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"name","type":"bytes"}],"name":"countChunks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"}],"name":"createRepo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"name","type":"bytes"}],"name":"delRef","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"path","type":"bytes"}],"name":"download","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isOptimize","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"}],"name":"listRefs","outputs":[{"components":[{"internalType":"bytes20","name":"hash","type":"bytes20"},{"internalType":"bytes","name":"name","type":"bytes"}],"internalType":"struct Git3.refData[]","name":"list","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"","type":"bytes"}],"name":"nameToRefInfo","outputs":[{"internalType":"bytes20","name":"hash","type":"bytes20"},{"internalType":"uint96","name":"index","type":"uint96"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"path","type":"bytes"}],"name":"remove","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"","type":"bytes"}],"name":"repoNameToOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"repoNameToRefs","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes20","name":"refHash","type":"bytes20"}],"name":"setRef","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"name","type":"bytes"}],"name":"size","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"path","type":"bytes"}],"name":"stakeTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upload","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes","name":"repoName","type":"bytes"},{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"chunkId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"uploadChunk","outputs":[],"stateMutability":"payable","type":"function"}]'
}

@ -0,0 +1,43 @@
// from https://chainid.network/chains.json
const evmNetworks: Record<number, any> = {
1: {
"name": "ethereum",
"nativeCurrency": {
"name": "Ether",
"symbol": "ETH",
"decimals": 18
},
"rpc": ["https://rpc.flashbots.net", "https://singapore.rpc.blxrbdn.com", "https://rpc.ankr.com/eth"],
"explorers": [
{
"name": "etherscan",
"url": "https://etherscan.io",
"standard": "EIP3091"
}
],
"contracts": { "git3": "" }
},
3334: {
"name": "Web3Q Galileo",
"nativeCurrency": {
"name": "Web3Q",
"symbol": "W3Q",
"decimals": 18
},
"rpc": [
"https://galileo.web3q.io:8545"
],
"explorers": [
{
"name": "w3q-galileo",
"url": "https://explorer.galileo.web3q.io",
"standard": "EIP3091"
}
],
"contracts": { "git3": "0x0068bD3ec8D16402690C1Eddff06ACb913A209ef" }
}
}
export default evmNetworks

@ -0,0 +1,12 @@
const ns: Record<string, any> = {
"eth": {
"chainId": 1,
"resolver": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
},
"w3q": {
"chainId": 3334,
"resolver": "0x076B3e04dd300De7db95Ba3F5db1eD31f3139aE0"
}
}
export default ns;

@ -1,366 +0,0 @@
[
{
"inputs": [
{
"internalType": "uint8",
"name": "slotLimit",
"type": "uint8"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"stateMutability": "nonpayable",
"type": "fallback"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "changeOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "chunkId",
"type": "uint256"
}
],
"name": "chunkSize",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
}
],
"name": "countChunks",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "defaultFile",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "destruct",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "chunkId",
"type": "uint256"
}
],
"name": "getChunkHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isOptimize",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
}
],
"name": "read",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
},
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "chunkId",
"type": "uint256"
}
],
"name": "readChunk",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
},
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "refund",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
}
],
"name": "remove",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "chunkId",
"type": "uint256"
}
],
"name": "removeChunk",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "resolveMode",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_defaultFile",
"type": "bytes"
}
],
"name": "setDefault",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
}
],
"name": "size",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "chunkId",
"type": "uint256"
}
],
"name": "truncate",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "write",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "chunkId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "writeChunk",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
]

@ -12,6 +12,7 @@ class Git {
pushed: Map<string, string> = new Map()
head: string | null
constructor(info: ApiBaseParams, storage: Storage) {
this.gitdir = info.gitdir
this.remoteName = info.remoteName
@ -22,9 +23,9 @@ class Git {
this.head = null
}
async do_list(forPush: boolean) {
async doList(forPush: boolean) {
let outLines: string[] = []
let refs = await this.get_refs(forPush)
let refs = await this.getRefs(forPush)
for (let ref of refs) {
if (ref.ref == "HEAD") {
if (!forPush) outLines.push(`@${ref.sha} HEAD\n`)
@ -40,28 +41,31 @@ class Git {
return outLines.join("") + "\n"
}
async do_fetch(refs: { ref: string, oid: string }[]) {
async doFetch(refs: { ref: string, oid: string }[]) {
for (let ref of refs) {
await this.fetch(ref.oid)
}
return "\n\n"
}
async do_push(refs: {
async doPush(refs: {
src: string
dst: string
force: boolean
}[]): Promise<string> {
let outLines: string[] = []
// let remoteHead = null
for (let ref of refs) {
if (!await this.storage.hasPermission(ref.dst)) {
return `error ${ref.dst} refusing to push to remote ${this.remoteUrl} (permission denied)` + "\n\n"
}
if (ref.src == "") {
if (this.refs.get("HEAD") == ref.dst) {
return `error ${ref.dst} refusing to delete the current branch: ${ref.dst}` + "\n\n"
}
log("deleting ref", ref.dst)
this.storage.removeRef(ref.dst)
this.refs.delete(ref.dst)
this.pushed.delete(ref.dst)
} else {
@ -131,7 +135,7 @@ class Git {
let present = Array.from(this.refs.values())
present.push(...Array.from(this.pushed.values()))
let objects = GitUtils.listObjects(src, present)
let pendings = []
let pendings: Promise<string>[] = []
for (let obj of objects) {
pendings.push(this.putObject(obj))
}
@ -174,12 +178,13 @@ class Git {
}
async putObject(sha: string) {
async putObject(sha: string): Promise<string> {
let data = GitUtils.encodeObject(sha)
let path = this.objectPath(sha)
log("writing...", path, sha)
log("writing...", path)
let status = await this.storage.upload(path, data)
log("status", status)
return status
}
objectPath(name: string): string {
@ -188,7 +193,7 @@ class Git {
return join("objects", prefix, suffix)
}
async get_refs(forPush: boolean): Promise<Ref[]> {
async getRefs(forPush: boolean): Promise<Ref[]> {
let refs = await this.storage.listRefs()
for (let item of refs) {
if (item.sha == "0000000000000000000000000000000000001ead") {

@ -2,54 +2,69 @@
import GitRemoteHelper from './git/git-remote-helper'
import { ApiBaseParams } from './git/git-remote-helper'
import Git from './git/git'
import { log } from './git/log'
// import { log } from './git/log'
import { ETHStorage } from './storage/ETHStorage'
import nameServices from './config/name-services'
let git: Git;
GitRemoteHelper({
env: process.env,
stdin: process.stdin,
stdout: process.stdout,
api: {
/**
* This will always be invoked when the remote helper is invoked
*/
init: async (p: ApiBaseParams) => {
git = new Git(p, new ETHStorage(p.remoteUrl))
const url = new URL(p.remoteUrl)
let repoName
let git3Address
let chainId = url.port ? parseInt(url.port) : 3334
if (url.hostname.indexOf(".") < 0) {
if (url.hostname.startsWith("0x")) {
git3Address = url.hostname
repoName = url.pathname.slice(1)
} else {
// use Default git3Address
git3Address = null
repoName = url.hostname
}
}
else {
let nsSuffix = url.hostname.split(".")[1] // Todo: support sub domain
let ns = nameServices[nsSuffix]
if (!ns) throw new Error("invalid name service")
// Todo: resolve name service
git3Address = null // ns parse address
chainId = chainId || ns.chainId
repoName = url.pathname.slice(1)
}
let sender = url.username || null
let storage = new ETHStorage(repoName, chainId, { git3Address, sender })
git = new Git(p, storage)
return
},
/**
* This needs to return a list of git refs.
*/
list: async (p: {
gitdir: string
remoteName: string
remoteUrl: string
forPush: boolean
}) => {
log('list', p)
let out = await git.do_list(p.forPush)
log("list out:\n", out)
// log('list', p)
let out = await git.doList(p.forPush)
// log("list out:\n", out)
return out
},
/**
* This should put the requested objects into the `.git`
*/
handleFetch: async (p: {
gitdir: string
remoteName: string
remoteUrl: string
refs: { ref: string, oid: string }[]
}) => {
log("fetch", p)
let out = await git.do_fetch(p.refs)
log("fetch out:\n", out)
// log("fetch", p)
let out = await git.doFetch(p.refs)
// log("fetch out:\n", out)
return out
},
/**
* This should copy objects from `.git`
*/
handlePush: async (p: {
gitdir: string
remoteName: string
@ -60,9 +75,9 @@ GitRemoteHelper({
force: boolean
}[]
}) => {
log("push", p)
let out = await git.do_push(p.refs)
log("push out:\n", out)
// log("push", p)
let out = await git.doPush(p.refs)
// log("push out:\n", out)
return out
},
},

@ -2,35 +2,54 @@ import { Ref, Status, Storage } from "./storage"
import { getWallet } from "../wallet/index"
import { ethers, Signer } from "ethers"
import { NonceManager } from "@ethersproject/experimental"
const abi = '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"countChunks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"delRef","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"}],"name":"download","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"listRefs","outputs":[{"components":[{"internalType":"bytes20","name":"hash","type":"bytes20"},{"internalType":"string","name":"name","type":"string"}],"internalType":"struct Git3.refData[]","name":"list","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"nameToRefInfo","outputs":[{"internalType":"bytes20","name":"hash","type":"bytes20"},{"internalType":"uint96","name":"index","type":"uint96"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"refs","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"val","type":"uint256"}],"name":"refund","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"refund1","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"}],"name":"remove","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"bytes20","name":"refHash","type":"bytes20"}],"name":"setRef","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"size","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"storageManager","outputs":[{"internalType":"contract IFileOperator","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upload","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"chunkId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"uploadChunk","outputs":[],"stateMutability":"payable","type":"function"}]'
import abis from "../config/abis"
import network from "../config/evm-network"
export class ETHStorage implements Storage {
repoURI: string
repoName: string
wallet: Signer
contract: ethers.Contract
provider: ethers.providers.JsonRpcProvider
constructor(repoURI: string) {
this.repoURI = repoURI
this.wallet = getWallet()
this.provider = new ethers.providers.JsonRpcProvider('https://galileo.web3q.io:8545')
constructor(repoName: string, chainId: number, options: { git3Address: string | null, sender: string | null }) {
let net = network[chainId]
if (!net) throw new Error("chainId not supported")
this.repoName = repoName
this.wallet = getWallet(options.sender)
let rpc = net.rpc[Math.floor(Math.random() * net.rpc.length)] //random get rpc
this.provider = new ethers.providers.JsonRpcProvider(rpc)
this.wallet = this.wallet.connect(this.provider)
this.wallet = new NonceManager(this.wallet)
this.contract = new ethers.Contract('0x01d2681e3F4dED1750359F066a42a768Adaa142F', abi, this.wallet)
let repoAddress = options.git3Address || net.contracts.git3
this.contract = new ethers.Contract(repoAddress, abis.ETHStorage, this.wallet)
}
async repoRoles(): Promise<string[]> {
let owner = await this.contract.repoNameToOwner(Buffer.from(this.repoName))
if (owner === ethers.constants.AddressZero) return []
return [owner]
}
async hasPermission(ref: string): Promise<boolean> {
let member = await this.repoRoles()
return member.indexOf(await this.wallet.getAddress()) >= 0
}
async download(path: string): Promise<[Status, Buffer]> {
const res = await this.contract.download(Buffer.from(path))
const res = await this.contract.download(Buffer.from(this.repoName), Buffer.from(path))
const buffer = Buffer.from(res[0].slice(2), 'hex')
console.error(`=== download file ${path} result ===`)
console.error(buffer.toString('utf-8'))
// console.error(buffer.toString('utf-8'))
return [Status.SUCCEED, buffer]
}
async upload(path: string, file: Buffer): Promise<Status> {
const uploadResult = await this.contract.upload(Buffer.from(path), file)
console.error(`=== upload file ${path} result ===`)
console.error(uploadResult)
const uploadResult = await this.contract.upload(Buffer.from(this.repoName), Buffer.from(path), file)
console.error(`=== upload file ${path} ===`)
console.error("upload done:", uploadResult.hash)
return Status.SUCCEED
}
@ -39,20 +58,21 @@ export class ETHStorage implements Storage {
}
async listRefs(): Promise<Ref[]> {
const res: string[][] = await this.contract.listRefs()
const res: string[][] = await this.contract.listRefs(Buffer.from(this.repoName))
let refs = res.map(i => ({
ref: i[1],
ref: Buffer.from(i[1].slice(2), "hex").toString("utf8").slice(this.repoName.length + 1),
sha: i[0].slice(2)
}))
return refs
}
async setRef(path: string, sha: string): Promise<Status> {
await this.contract.setRef(path, '0x' + sha)
await this.contract.setRef(Buffer.from(this.repoName), Buffer.from(path), '0x' + sha)
return Status.SUCCEED
}
removeRef(path: string): Promise<Status> {
throw new Error("Method not implemented.")
async removeRef(path: string): Promise<Status> {
await this.contract.delRef(Buffer.from(this.repoName), Buffer.from(path))
return Status.SUCCEED
}
}

@ -8,10 +8,14 @@ const log = console.error
log("mock path", mockPath)
export class MockStorage implements Storage {
repoURI: string
repoName: string
constructor(repoURI: string) {
this.repoURI = repoURI
constructor() {
this.repoName = "mock"
}
async hasPermission(ref: string): Promise<boolean> {
return true
}
async listRefs(): Promise<Ref[]> {

@ -12,8 +12,9 @@ export type Ref = {
}
export interface Storage {
repoURI: string
repoName: string
hasPermission(ref: string): Promise<boolean>
download(path: string): Promise<[Status, Buffer]>
upload(path: string, file: Buffer): Promise<Status>
remove(path: string): Promise<Status>

@ -1,8 +1,10 @@
import { mkdirSync, readFileSync } from "fs"
import { ethers } from 'ethers'
export function getWallet(): ethers.Wallet {
export function getWallet(sender: string | null): ethers.Wallet {
// Todo: according sender address to select wallet, if sender==null then use default wallet
const wallet = 'default'
const keyPath = process.env.HOME + "/.git3/keys"
mkdirSync(keyPath, { recursive: true })

@ -41,7 +41,7 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "baseUrl": "src", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [

Loading…
Cancel
Save