feat(medusa-file-s3,medusa-file-minio): Upgrade to TypeScript (#3740)

This commit is contained in:
Derek Wene
2023-04-23 12:31:16 +02:00
committed by GitHub
parent ba45a316a7
commit 8b6464180a
11 changed files with 661 additions and 420 deletions
+10 -17
View File
@@ -2,7 +2,10 @@
"name": "medusa-file-minio",
"version": "1.1.6",
"description": "MinIO server file connector for Medusa",
"main": "index.js",
"main": "dist/index.js",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
@@ -11,32 +14,22 @@
"author": "Edin Skeja",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.16.0",
"@babel/core": "^7.16.0",
"@babel/node": "^7.16.0",
"@babel/plugin-proposal-class-properties": "^7.16.0",
"@babel/plugin-transform-instanceof": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/register": "^7.16.0",
"@babel/runtime": "^7.16.3",
"client-sessions": "^0.8.0",
"@medusajs/medusa": "1.8.0-rc.6",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"medusa-interfaces": "^1.3.7"
"typescript": "^4.9.5"
},
"scripts": {
"build": "babel src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'",
"prepare": "cross-env NODE_ENV=production yarn run build",
"watch": "babel -w src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'",
"test": "jest --passWithNoTests src"
"test": "jest --passWithNoTests src",
"build": "tsc",
"watch": "tsc --watch"
},
"peerDependencies": {
"medusa-interfaces": "1.3.7"
},
"dependencies": {
"@babel/plugin-transform-classes": "^7.16.0",
"aws-sdk": "^2.1043.0",
"aws-sdk": "^2.983.0",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"medusa-core-utils": "^1.2.0",
@@ -1,182 +0,0 @@
import stream from "stream"
import aws from "aws-sdk"
import { parse } from "path"
import fs from "fs"
import { AbstractFileService } from "@medusajs/medusa"
import { MedusaError } from "medusa-core-utils"
class MinioService extends AbstractFileService {
constructor({}, options) {
super({}, options)
this.bucket_ = options.bucket
this.accessKeyId_ = options.access_key_id
this.secretAccessKey_ = options.secret_access_key
this.private_bucket_ = options.private_bucket
this.private_access_key_id_ =
options.private_access_key_id ?? this.accessKeyId_
this.private_secret_access_key_ =
options.private_secret_access_key ?? this.secretAccessKey_
this.endpoint_ = options.endpoint
this.s3ForcePathStyle_ = true
this.signatureVersion_ = "v4"
this.downloadUrlDuration = options.download_url_duration ?? 60 // 60 seconds
}
upload(file) {
this.updateAwsConfig_()
return this.uploadFile(file)
}
uploadProtected(file) {
this.validatePrivateBucketConfiguration_(true)
this.updateAwsConfig_(true)
return this.uploadFile(file, { isProtected: true })
}
uploadFile(file, options = { isProtected: false }) {
const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const s3 = new aws.S3()
const params = {
ACL: options.isProtected ? "private" : "public-read",
Bucket: options.isProtected ? this.private_bucket_ : this.bucket_,
Body: fs.createReadStream(file.path),
Key: fileKey,
}
return new Promise((resolve, reject) => {
s3.upload(params, (err, data) => {
if (err) {
reject(err)
return
}
resolve({ url: data.Location, key: data.Key })
})
})
}
async delete(file) {
this.updateAwsConfig_()
const s3 = new aws.S3()
const params = {
Bucket: this.bucket_,
Key: `${file.fileKey}`,
}
return await Promise.all([
new Promise((resolve, reject) =>
s3.deleteObject({ ...params, Bucket: this.bucket_ }, (err, data) => {
if (err) {
reject(err)
return
}
resolve(data)
})
),
new Promise((resolve, reject) =>
s3.deleteObject(
{ ...params, Bucket: this.private_bucket_ },
(err, data) => {
if (err) {
reject(err)
return
}
resolve(data)
}
)
),
])
}
async getUploadStreamDescriptor({ usePrivateBucket = true, ...fileData }) {
this.validatePrivateBucketConfiguration_(usePrivateBucket)
this.updateAwsConfig_(usePrivateBucket)
const pass = new stream.PassThrough()
const fileKey = `${fileData.name}.${fileData.ext}`
const params = {
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
Body: pass,
Key: fileKey,
}
const s3 = new aws.S3()
return {
writeStream: pass,
promise: s3.upload(params).promise(),
url: `${this.spacesUrl_}/${fileKey}`,
fileKey,
}
}
async getDownloadStream({ usePrivateBucket = true, ...fileData }) {
this.validatePrivateBucketConfiguration_(usePrivateBucket)
this.updateAwsConfig_(usePrivateBucket)
const s3 = new aws.S3()
const params = {
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
Key: `${fileData.fileKey}`,
}
return s3.getObject(params).createReadStream()
}
async getPresignedDownloadUrl({ usePrivateBucket = true, ...fileData }) {
this.validatePrivateBucketConfiguration_(usePrivateBucket)
this.updateAwsConfig_(usePrivateBucket, {
signatureVersion: "v4",
})
const s3 = new aws.S3()
const params = {
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
Key: `${fileData.fileKey}`,
Expires: this.downloadUrlDuration,
}
return await s3.getSignedUrlPromise("getObject", params)
}
validatePrivateBucketConfiguration_(usePrivateBucket) {
if (
usePrivateBucket &&
(!this.private_access_key_id_ || !this.private_bucket_)
) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"Private bucket is not configured"
)
}
}
updateAwsConfig_(usePrivateBucket = false, additionalConfiguration = {}) {
aws.config.setPromisesDependency(null)
aws.config.update(
{
accessKeyId: usePrivateBucket
? this.private_access_key_id_
: this.accessKeyId_,
secretAccessKey: usePrivateBucket
? this.private_secret_access_key_
: this.secretAccessKey_,
endpoint: this.endpoint_,
s3ForcePathStyle: this.s3ForcePathStyle_,
signatureVersion: this.signatureVersion_,
...additionalConfiguration,
},
true
)
}
}
export default MinioService
@@ -0,0 +1,212 @@
import stream from "stream"
import aws from "aws-sdk"
import { parse } from "path"
import fs from "fs"
import {
AbstractFileService,
DeleteFileType,
FileServiceUploadResult,
GetUploadedFileType,
IFileService,
UploadStreamDescriptorType,
} from "@medusajs/medusa"
import { MedusaError } from "medusa-core-utils"
import { ClientConfiguration, PutObjectRequest } from "aws-sdk/clients/s3"
class MinioService extends AbstractFileService implements IFileService {
protected bucket_: string
protected accessKeyId_: string
protected secretAccessKey_: string
protected private_bucket_: string
protected private_access_key_id_: string
protected private_secret_access_key_: string
protected endpoint_: string
protected s3ForcePathStyle_: boolean
protected signatureVersion_: string
protected downloadUrlDuration: string | number
constructor({}, options) {
super({}, options)
this.bucket_ = options.bucket
this.accessKeyId_ = options.access_key_id
this.secretAccessKey_ = options.secret_access_key
this.private_bucket_ = options.private_bucket
this.private_access_key_id_ =
options.private_access_key_id ?? this.accessKeyId_
this.private_secret_access_key_ =
options.private_secret_access_key ?? this.secretAccessKey_
this.endpoint_ = options.endpoint
this.s3ForcePathStyle_ = true
this.signatureVersion_ = "v4"
this.downloadUrlDuration = options.download_url_duration ?? 60 // 60 seconds
}
protected buildUrl(bucket: string, key: string) {
return `${this.endpoint_}/${bucket}/${key}`
}
async upload(file: Express.Multer.File): Promise<FileServiceUploadResult> {
return await this.uploadFile(file)
}
async uploadProtected(
file: Express.Multer.File
): Promise<FileServiceUploadResult> {
this.validatePrivateBucketConfiguration_(true)
return await this.uploadFile(file, { isProtected: true })
}
protected async uploadFile(
file: Express.Multer.File,
options: { isProtected: boolean } = { isProtected: false }
) {
const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const client = this.getClient(options.isProtected)
const params = {
ACL: options.isProtected ? "private" : "public-read",
Bucket: options.isProtected ? this.private_bucket_ : this.bucket_,
Body: fs.createReadStream(file.path),
Key: fileKey,
ContentType: file.mimetype,
}
const result = await client.upload(params).promise()
return { url: result.Location, key: result.Key }
}
async delete(file: DeleteFileType): Promise<void> {
const privateClient = this.getClient(false)
const publicClient = this.getClient(true)
const params = {
Bucket: this.bucket_,
Key: `${file.fileKey}`,
}
await Promise.all([
new Promise((resolve, reject) =>
publicClient.deleteObject(
{ ...params, Bucket: this.bucket_ },
(err, data) => {
if (err) {
reject(err)
return
}
resolve(data)
}
)
),
new Promise((resolve, reject) =>
privateClient.deleteObject(
{ ...params, Bucket: this.private_bucket_ },
(err, data) => {
if (err) {
reject(err)
return
}
resolve(data)
}
)
),
])
}
async getUploadStreamDescriptor(
fileData: UploadStreamDescriptorType & {
usePrivateBucket?: boolean
contentType?: string
}
) {
const usePrivateBucket = !!fileData.usePrivateBucket
this.validatePrivateBucketConfiguration_(usePrivateBucket)
const client = this.getClient(usePrivateBucket)
const pass = new stream.PassThrough()
const fileKey = `${fileData.name}.${fileData.ext}`
const params: PutObjectRequest = {
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
Body: pass,
Key: fileKey,
ContentType: fileData.contentType,
}
return {
writeStream: pass,
promise: client.upload(params).promise(),
url: this.buildUrl(params.Bucket, fileKey),
fileKey,
}
}
async getDownloadStream(
fileData: GetUploadedFileType & { usePrivateBucket?: boolean }
) {
const usePrivateBucket = !!fileData.usePrivateBucket
this.validatePrivateBucketConfiguration_(usePrivateBucket)
const client = this.getClient(usePrivateBucket)
const params = {
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
Key: `${fileData.fileKey}`,
}
return client.getObject(params).createReadStream()
}
async getPresignedDownloadUrl({ usePrivateBucket = true, ...fileData }) {
this.validatePrivateBucketConfiguration_(usePrivateBucket)
const client = this.getClient(usePrivateBucket, {
signatureVersion: "v4",
})
const params = {
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
Key: `${fileData.fileKey}`,
Expires: this.downloadUrlDuration,
}
return await client.getSignedUrlPromise("getObject", params)
}
validatePrivateBucketConfiguration_(usePrivateBucket) {
if (
usePrivateBucket &&
(!this.private_access_key_id_ || !this.private_bucket_)
) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"Private bucket is not configured"
)
}
}
protected getClient(
usePrivateBucket = false,
additionalConfiguration: Partial<ClientConfiguration> = {}
) {
return new aws.S3({
accessKeyId: usePrivateBucket
? this.private_access_key_id_
: this.accessKeyId_,
secretAccessKey: usePrivateBucket
? this.private_secret_access_key_
: this.secretAccessKey_,
endpoint: this.endpoint_,
s3ForcePathStyle: this.s3ForcePathStyle_,
signatureVersion: this.signatureVersion_,
...additionalConfiguration,
})
}
}
export default MinioService
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"lib": ["es5", "es6", "es2019"],
"target": "es5",
"outDir": "./dist",
"rootDir": "src",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true // to use ES5 specific tooling
},
"include": ["src"],
"exclude": [
"dist",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules"
]
}
+29 -28
View File
@@ -23,37 +23,38 @@ Store uploaded files to your Medusa backend on S3.
1\. Run the following command in the directory of the Medusa backend:
```bash
npm install medusa-file-s3
```
```bash
npm install medusa-file-s3
```
2\. Set the following environment variables in `.env`:
```bash
S3_URL=<YOUR_BUCKET_URL>
S3_BUCKET=<YOUR_BUCKET_NAME>
S3_REGION=<YOUR_BUCKET_REGION>
S3_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
S3_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
```
```bash
S3_URL=<YOUR_BUCKET_URL>
S3_BUCKET=<YOUR_BUCKET_NAME>
S3_REGION=<YOUR_BUCKET_REGION>
S3_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
S3_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
```
3\. In `medusa-config.js` add the following at the end of the `plugins` array:
```js
const plugins = [
// ...
{
resolve: `medusa-file-s3`,
options: {
s3_url: process.env.S3_URL,
bucket: process.env.S3_BUCKET,
region: process.env.S3_REGION,
access_key_id: process.env.S3_ACCESS_KEY_ID,
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
},
```js
const plugins = [
// ...
{
resolve: `medusa-file-s3`,
options: {
s3_url: process.env.S3_URL,
bucket: process.env.S3_BUCKET,
region: process.env.S3_REGION,
access_key_id: process.env.S3_ACCESS_KEY_ID,
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
aws_config_option: {},
},
]
```
},
]
```
---
@@ -61,9 +62,9 @@ Store uploaded files to your Medusa backend on S3.
1\. Run the following command in the directory of the Medusa backend to run the backend:
```bash
npm run start
```
```bash
npm run start
```
2\. Upload an image for a product using the admin dashboard or using [the Admin APIs](https://docs.medusajs.com/api/admin#tag/Upload).
@@ -71,4 +72,4 @@ Store uploaded files to your Medusa backend on S3.
## Additional Resources
- [S3 Plugin Documentation](https://docs.medusajs.com/plugins/file-service/s3)
- [S3 Plugin Documentation](https://docs.medusajs.com/plugins/file-service/s3)
+9 -15
View File
@@ -2,7 +2,10 @@
"name": "medusa-file-s3",
"version": "1.1.12",
"description": "AWS s3 file connector for Medusa",
"main": "index.js",
"main": "dist/index.js",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
@@ -11,32 +14,23 @@
"author": "Sebastian Mateos Nicolajsen",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/node": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-transform-instanceof": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.5",
"@babel/register": "^7.7.4",
"@babel/runtime": "^7.9.6",
"client-sessions": "^0.8.0",
"@medusajs/medusa": "1.8.0-rc.6",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"medusa-interfaces": "^1.3.7",
"medusa-test-utils": "^1.1.40"
"medusa-test-utils": "^1.1.40",
"typescript": "^4.9.5"
},
"scripts": {
"prepare": "cross-env NODE_ENV=production yarn run build",
"test": "jest --passWithNoTests src",
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
"build": "tsc",
"watch": "tsc --watch"
},
"peerDependencies": {
"medusa-interfaces": "1.3.7"
},
"dependencies": {
"@babel/plugin-transform-classes": "^7.15.4",
"aws-sdk": "^2.983.0",
"body-parser": "^1.19.0",
"express": "^4.17.1",
-139
View File
@@ -1,139 +0,0 @@
import fs from "fs"
import aws from "aws-sdk"
import { parse } from "path"
import { AbstractFileService } from "@medusajs/medusa"
import stream from "stream"
class S3Service extends AbstractFileService {
// eslint-disable-next-line no-empty-pattern
constructor({}, options) {
super({}, options)
this.bucket_ = options.bucket
this.s3Url_ = options.s3_url
this.accessKeyId_ = options.access_key_id
this.secretAccessKey_ = options.secret_access_key
this.region_ = options.region
this.endpoint_ = options.endpoint
this.awsConfigObject_ = options.aws_config_object
this.client_ = new aws.S3()
}
upload(file) {
this.updateAwsConfig()
return this.uploadFile(file)
}
uploadProtected(file) {
this.updateAwsConfig()
return this.uploadFile(file, { acl: "private" })
}
uploadFile(file, options = { isProtected: false, acl: undefined }) {
const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const params = {
ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
Bucket: this.bucket_,
Body: fs.createReadStream(file.path),
Key: fileKey,
}
return new Promise((resolve, reject) => {
this.client_.upload(params, (err, data) => {
if (err) {
reject(err)
return
}
resolve({ url: data.Location, key: data.Key })
})
})
}
async delete(file) {
this.updateAwsConfig()
const params = {
Bucket: this.bucket_,
Key: `${file}`,
}
return new Promise((resolve, reject) => {
this.client_.deleteObject(params, (err, data) => {
if (err) {
reject(err)
return
}
resolve(data)
})
})
}
async getUploadStreamDescriptor(fileData) {
this.updateAwsConfig()
const pass = new stream.PassThrough()
const fileKey = `${fileData.name}.${fileData.ext}`
const params = {
ACL: fileData.acl ?? "private",
Bucket: this.bucket_,
Body: pass,
Key: fileKey,
}
return {
writeStream: pass,
promise: this.client_.upload(params).promise(),
url: `${this.s3Url_}/${fileKey}`,
fileKey,
}
}
async getDownloadStream(fileData) {
this.updateAwsConfig()
const params = {
Bucket: this.bucket_,
Key: `${fileData.fileKey}`,
}
return this.client_.getObject(params).createReadStream()
}
async getPresignedDownloadUrl(fileData) {
this.updateAwsConfig({
signatureVersion: "v4",
})
const params = {
Bucket: this.bucket_,
Key: `${fileData.fileKey}`,
Expires: this.downloadUrlDuration,
}
return await this.client_.getSignedUrlPromise("getObject", params)
}
updateAwsConfig(additionalConfiguration = {}) {
aws.config.setPromisesDependency(null)
const config = {
...additionalConfiguration,
accessKeyId: this.accessKeyId_,
secretAccessKey: this.secretAccessKey_,
region: this.region_,
endpoint: this.endpoint_,
...this.awsConfigObject_,
}
aws.config.update(config, true)
}
}
export default S3Service
+157
View File
@@ -0,0 +1,157 @@
import fs from "fs"
import aws from "aws-sdk"
import { parse } from "path"
import {
AbstractFileService,
DeleteFileType,
FileServiceUploadResult,
GetUploadedFileType,
IFileService,
UploadStreamDescriptorType,
} from "@medusajs/medusa"
import stream from "stream"
import { PutObjectRequest } from "aws-sdk/clients/s3"
import { ClientConfiguration } from "aws-sdk/clients/s3"
class S3Service extends AbstractFileService implements IFileService {
protected bucket_: string
protected s3Url_: string
protected accessKeyId_: string
protected secretAccessKey_: string
protected region_: string
protected endpoint_: string
protected awsConfigObject_: any
protected downloadFileDuration_: string
constructor({}, options) {
super({}, options)
this.bucket_ = options.bucket
this.s3Url_ = options.s3_url
this.accessKeyId_ = options.access_key_id
this.secretAccessKey_ = options.secret_access_key
this.region_ = options.region
this.endpoint_ = options.endpoint
this.downloadFileDuration_ = options.download_file_duration
this.awsConfigObject_ = options.aws_config_object ?? {}
}
protected getClient(overwriteConfig: Partial<ClientConfiguration> = {}) {
const config: ClientConfiguration = {
accessKeyId: this.accessKeyId_,
secretAccessKey: this.secretAccessKey_,
region: this.region_,
endpoint: this.endpoint_,
...this.awsConfigObject_,
...overwriteConfig,
}
return new aws.S3(config)
}
async upload(file: Express.Multer.File): Promise<FileServiceUploadResult> {
return await this.uploadFile(file)
}
async uploadProtected(file: Express.Multer.File) {
return await this.uploadFile(file, { acl: "private" })
}
async uploadFile(
file: Express.Multer.File,
options: { isProtected?: boolean; acl?: string } = {
isProtected: false,
acl: undefined,
}
) {
const client = this.getClient()
const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const params = {
ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
Bucket: this.bucket_,
Body: fs.createReadStream(file.path),
Key: fileKey,
ContentType: file.mimetype,
}
const result = await client.upload(params).promise()
return {
url: result.Location,
key: result.Key,
}
}
async delete(file: DeleteFileType): Promise<void> {
const client = this.getClient()
const params = {
Bucket: this.bucket_,
Key: `${file}`,
}
return new Promise((resolve, reject) => {
client.deleteObject(params, (err, data) => {
if (err) {
reject(err)
return
}
resolve()
})
})
}
async getUploadStreamDescriptor(fileData: UploadStreamDescriptorType) {
const client = this.getClient()
const pass = new stream.PassThrough()
const fileKey = `${fileData.name}.${fileData.ext}`
const params: PutObjectRequest = {
ACL: fileData.acl ?? "private",
Bucket: this.bucket_,
Body: pass,
Key: fileKey,
ContentType: fileData.contentType as string,
}
return {
writeStream: pass,
promise: client.upload(params).promise(),
url: `${this.s3Url_}/${fileKey}`,
fileKey,
}
}
async getDownloadStream(
fileData: GetUploadedFileType
): Promise<NodeJS.ReadableStream> {
const client = this.getClient()
const params = {
Bucket: this.bucket_,
Key: `${fileData.fileKey}`,
}
return await client.getObject(params).createReadStream()
}
async getPresignedDownloadUrl(
fileData: GetUploadedFileType
): Promise<string> {
const client = this.getClient({ signatureVersion: "v4" })
const params = {
Bucket: this.bucket_,
Key: `${fileData.fileKey}`,
Expires: this.downloadFileDuration_,
}
return await client.getSignedUrlPromise("getObject", params)
}
}
export default S3Service
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"lib": ["es5", "es6", "es2019"],
"target": "es5",
"outDir": "./dist",
"rootDir": "src",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true
},
"include": ["src"],
"exclude": [
"dist",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules"
]
}