├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── buildDeleteHook.ts ├── buildUploadHook.ts ├── index.ts ├── plugin.ts └── types.ts ├── test ├── dog.png ├── payload.config.ts ├── plugin.test.ts └── server.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "airbnb-base", 6 | "airbnb-typescript/base", 7 | "prettier", 8 | "plugin:import/recommended", 9 | "plugin:import/typescript" 10 | ], 11 | "env": { 12 | "es2022": true, 13 | "node": true 14 | }, 15 | "plugins": ["prettier"], 16 | "parserOptions": { 17 | "project": "./tsconfig.json" 18 | }, 19 | "rules": { 20 | "@typescript-eslint/lines-between-class-members": "off", 21 | "indent": ["error", 2], 22 | "no-await-in-loop": 0, 23 | "no-param-reassign": ["error", { "props": false }], 24 | "prettier/prettier": [ 25 | "error", 26 | { 27 | "singleQuote": true, 28 | "endOfLine": "auto" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jean-Baptiste Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upload files to S3 in Payload CMS 2 | 3 | This plugin sends uploaded files to Amazon S3 instead of writing them to the server file system. 4 | Resized images are properly supported. 5 | 6 | ## Why should I use this module? 7 | 8 | Payload team supports an official cloud storage plugin, different from this one. 9 | 10 | The main difference is that this plugin allows configuring collection logic on the collection itself. 11 | 12 | Payload implementation requires to define collection-specific stuff from plugins inside the global payload configuration file, which is (imho) bad design. 13 | 14 | ## Install 15 | 16 | `npm install payload-s3-upload --legacy-peer-deps` 17 | 18 | Payload requires `legacy-peer-deps` because of conflicts on React and GraphQL dependencies (see Payload [docs](https://payloadcms.com/docs/getting-started/installation)). 19 | 20 | ## Getting Started 21 | 22 | ### Enable plugin in Payload CMS config 23 | 24 | ```js 25 | import { S3Client } from '@aws-sdk/client-s3'; 26 | import { buildConfig } from 'payload/config'; 27 | import s3Upload from 'payload-s3-upload'; 28 | 29 | export default buildConfig({ 30 | // ... 31 | plugins: [ 32 | s3Upload(new S3Client({ 33 | region: process.env.AWS_REGION, 34 | credentials: { 35 | accessKeyId: process.env.AWS_KEY, 36 | secretAccessKey: process.env.AWS_SECRET, 37 | }, 38 | })), 39 | ], 40 | }); 41 | ``` 42 | 43 | ### Configure your upload collections 44 | 45 | ```js 46 | import { S3UploadCollectionConfig } from 'payload-s3-upload'; 47 | 48 | const Media: S3UploadCollectionConfig = { 49 | slug: 'media', 50 | upload: { 51 | staticURL: '/assets', 52 | staticDir: 'assets', 53 | disableLocalStorage: true, 54 | s3: { 55 | bucket: 'my-bucket', 56 | prefix: 'images/xyz', // files will be stored in bucket folder images/xyz 57 | // prefix: ({ doc }) => `assets/${doc.type}`, // dynamic prefixes are possible too 58 | commandInput: { 59 | // optionally, use here any valid PutObjectCommandInput property 60 | // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/putobjectcommandinput.html 61 | ACL: 'public-read', 62 | }, 63 | }, 64 | adminThumbnail: ({ doc }) => 65 | `https://my-bucket.s3.eu-west-3.amazonaws.com/images/xyz/${doc.filename}`, 66 | }, 67 | // create a field to access uploaded files in s3 from payload api 68 | fields: [ 69 | { 70 | name: 'url', 71 | type: 'text', 72 | access: { 73 | create: () => false, 74 | }, 75 | admin: { 76 | disabled: true, 77 | }, 78 | hooks: { 79 | afterRead: [ 80 | ({ data: doc }) => 81 | `https://my-bucket.s3.eu-west-3.amazonaws.com/images/${doc.type}/${doc.filename}`, 82 | ], 83 | }, 84 | }, 85 | ], 86 | }; 87 | 88 | export default Media; 89 | ``` 90 | 91 | ### Recipe for handling different sizes 92 | 93 | This plugin automatically uploads image variants in S3. 94 | 95 | However, in order to retrieve correct URLs for the different sizes in the API, additional hooks should be implemented. 96 | 97 | ```js 98 | import { S3UploadCollectionConfig } from 'payload-s3-upload'; 99 | 100 | const Media: S3UploadCollectionConfig = { 101 | slug: 'media', 102 | upload: { 103 | // ... 104 | imageSizes: [ 105 | { 106 | name: 'thumbnail', 107 | width: 400, 108 | height: 300, 109 | crop: 'center' 110 | }, 111 | { 112 | name: 'card', 113 | width: 768, 114 | height: 1024, 115 | crop: 'center' 116 | }, 117 | { 118 | name: 'tablet', 119 | width: 1024, 120 | height: null, 121 | crop: 'center' 122 | } 123 | ], 124 | adminThumbnail: 'thumbnail', 125 | }, 126 | hooks: { 127 | afterRead: [ 128 | ({ doc }) => { 129 | // add a url property on the main image 130 | doc.url = `${myBucketUrl}/${doc.filename}` 131 | 132 | // add a url property on each imageSize 133 | Object.keys(doc.sizes) 134 | .forEach(k => doc.sizes[k].url = `${myBucketUrl}/${doc.sizes[k].filename}`) 135 | } 136 | ] 137 | }, 138 | fields: [] 139 | }; 140 | 141 | export default Media; 142 | ``` 143 | 144 | ## Working Example 145 | 146 | Please refer to the test files! 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-s3-upload", 3 | "version": "2.1.1", 4 | "description": "Send Payload CMS uploads to Amazon S3", 5 | "keywords": [ 6 | "storage", 7 | "s3", 8 | "payload", 9 | "payloadcms", 10 | "payload-cms", 11 | "payloadplugin", 12 | "payload-plugin" 13 | ], 14 | "main": "dist/index.js", 15 | "files": [ 16 | "dist/**/*" 17 | ], 18 | "author": "Jean-Baptiste Martin", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/jeanbmar/payload-s3-upload.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/jeanbmar/payload-s3-upload/issues" 26 | }, 27 | "homepage": "https://github.com/jeanbmar/payload-s3-upload#readme", 28 | "scripts": { 29 | "build": "tsc -p tsconfig.build.json", 30 | "watch": "tsc -w", 31 | "test": "ts-node -T ./test/plugin.test.ts" 32 | }, 33 | "dependencies": { 34 | "sanitize-filename": "^1.6.3" 35 | }, 36 | "peerDependencies": { 37 | "@aws-sdk/client-s3": "^3.x.x", 38 | "payload": "^1.x.x" 39 | }, 40 | "devDependencies": { 41 | "@aws-sdk/client-s3": "^3.332.0", 42 | "@typescript-eslint/eslint-plugin": "^5.59.6", 43 | "@typescript-eslint/parser": "^5.59.6", 44 | "babel-eslint": "^10.1.0", 45 | "eslint": "^8.40.0", 46 | "eslint-config-airbnb-base": "^15.0.0", 47 | "eslint-config-airbnb-typescript": "^17.0.0", 48 | "eslint-config-prettier": "^8.8.0", 49 | "eslint-plugin-import": "^2.27.5", 50 | "eslint-plugin-prettier": "^4.2.1", 51 | "express": "^4.18.2", 52 | "graphql": "^16.6.0", 53 | "mongodb-memory-server": "^8.12.2", 54 | "payload": "^1.8.2", 55 | "prettier": "^2.8.8", 56 | "ts-node": "^10.9.1", 57 | "typescript": "^5.0.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/buildDeleteHook.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { 3 | S3Client, 4 | DeleteObjectCommand, 5 | DeleteObjectCommandInput, 6 | } from '@aws-sdk/client-s3'; 7 | import { CollectionAfterDeleteHook } from 'payload/types'; 8 | import { FileData } from 'payload/dist/uploads/types'; 9 | import { S3UploadCollectionConfig } from './types'; 10 | 11 | const getFilesToDelete: CollectionAfterDeleteHook = (afterDeleteOptions) => { 12 | const { doc } = afterDeleteOptions; 13 | const files: string[] = [doc.filename]; 14 | if (doc.mimeType?.includes('image') && doc.sizes != null) { 15 | Object.values(doc.sizes).forEach((fileData) => { 16 | if (fileData.filename != null) files.push(fileData.filename); 17 | }); 18 | } 19 | return files; 20 | }; 21 | 22 | const buildDeleteHook = ( 23 | s3Client: S3Client, 24 | collection: S3UploadCollectionConfig 25 | ) => { 26 | const { s3 } = collection.upload; 27 | const deleteHook: CollectionAfterDeleteHook = async (afterDeleteOptions) => { 28 | const filenames = getFilesToDelete(afterDeleteOptions); 29 | // eslint-disable-next-line no-restricted-syntax 30 | for (const filename of filenames) { 31 | let key = filename; 32 | if (s3.prefix) { 33 | key = 34 | s3.prefix instanceof Function 35 | ? path.posix.join(s3.prefix({ doc: afterDeleteOptions.doc }), key) 36 | : path.posix.join(s3.prefix, key); 37 | } 38 | await s3Client.send( 39 | new DeleteObjectCommand({ 40 | Bucket: s3.bucket, 41 | Key: key, 42 | } as DeleteObjectCommandInput) 43 | ); 44 | } 45 | }; 46 | return deleteHook; 47 | }; 48 | 49 | export default buildDeleteHook; 50 | -------------------------------------------------------------------------------- /src/buildUploadHook.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { 3 | S3Client, 4 | PutObjectCommand, 5 | PutObjectCommandInput, 6 | } from '@aws-sdk/client-s3'; 7 | import { CollectionBeforeChangeHook } from 'payload/types'; 8 | import { FileData } from 'payload/dist/uploads/types'; 9 | import { S3UploadCollectionConfig, File } from './types'; 10 | 11 | const getFilesToUpload: CollectionBeforeChangeHook = ({ 12 | data, 13 | req, 14 | }): File[] => { 15 | const reqFile = req.files?.file ?? req.file ?? null; 16 | if (reqFile == null) return []; 17 | const files: File[] = [ 18 | { 19 | filename: data.filename, 20 | mimeType: data.mimeType, 21 | buffer: reqFile.data, 22 | }, 23 | ]; 24 | if (data.mimeType?.includes('image') && data.sizes != null) { 25 | Object.entries(data.sizes).forEach(([key, sizeData]) => { 26 | const buffer = req.payloadUploadSizes[key]; 27 | const { filename } = sizeData; 28 | 29 | if (buffer != null || filename != null) { 30 | files.push({ 31 | buffer, 32 | filename, 33 | mimeType: data.mimeType, 34 | }); 35 | } 36 | }); 37 | } 38 | return files; 39 | }; 40 | 41 | const buildUploadHook = ( 42 | s3Client: S3Client, 43 | collection: S3UploadCollectionConfig 44 | ): CollectionBeforeChangeHook => { 45 | const { s3 } = collection.upload; 46 | const uploadHook: CollectionBeforeChangeHook = async ( 47 | beforeChangeOptions 48 | ) => { 49 | const files = getFilesToUpload(beforeChangeOptions); 50 | // eslint-disable-next-line no-restricted-syntax 51 | for (const file of files) { 52 | let key = file.filename; 53 | if (s3.prefix) { 54 | key = 55 | s3.prefix instanceof Function 56 | ? path.posix.join(s3.prefix({ doc: beforeChangeOptions.data }), key) 57 | : path.posix.join(s3.prefix, key); 58 | } 59 | let putObjectCommandInput: PutObjectCommandInput = { 60 | Bucket: s3.bucket, 61 | Key: key, 62 | Body: file.buffer, 63 | }; 64 | if (file.mimeType) { 65 | putObjectCommandInput.ContentType = file.mimeType; 66 | } 67 | if (s3.commandInput) { 68 | const commandInputEntries = Object.entries(s3.commandInput).map( 69 | ([property, value]) => [ 70 | property, 71 | typeof value === 'function' ? value(beforeChangeOptions) : value, 72 | ] 73 | ); 74 | putObjectCommandInput = { 75 | ...putObjectCommandInput, 76 | ...Object.fromEntries(commandInputEntries), 77 | }; 78 | } 79 | await s3Client.send(new PutObjectCommand(putObjectCommandInput)); 80 | } 81 | }; 82 | return uploadHook; 83 | }; 84 | 85 | export default buildUploadHook; 86 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import plugin from './plugin'; 2 | 3 | export * from './types'; 4 | export default plugin; 5 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3'; 2 | import buildUploadHook from './buildUploadHook'; 3 | import buildDeleteHook from './buildDeleteHook'; 4 | import { S3UploadCollectionConfig } from './types'; 5 | 6 | const pluginPayloadS3Upload = (s3Client?: S3Client | S3ClientConfig) => { 7 | const client = 8 | s3Client instanceof S3Client ? s3Client : new S3Client(s3Client); 9 | return (payloadConfig) => { 10 | const uploadCollections = payloadConfig.collections.filter( 11 | (collection) => collection.upload?.s3 != null 12 | ); 13 | uploadCollections.forEach((collection: S3UploadCollectionConfig) => { 14 | if (collection.hooks == null) collection.hooks = {}; 15 | if (collection.hooks.beforeChange == null) 16 | collection.hooks.beforeChange = []; 17 | if (collection.hooks.afterDelete == null) 18 | collection.hooks.afterDelete = []; 19 | collection.hooks.beforeChange.push(buildUploadHook(client, collection)); 20 | collection.hooks.afterDelete.push(buildDeleteHook(client, collection)); 21 | // comply with payload strict checking 22 | delete collection.upload.s3; 23 | }); 24 | return payloadConfig; 25 | }; 26 | }; 27 | 28 | export default pluginPayloadS3Upload; 29 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { CollectionConfig } from 'payload/types'; 3 | import { IncomingUploadType } from 'payload/dist/uploads/types'; 4 | 5 | export type S3UploadConfig = { 6 | bucket: string; 7 | prefix?: string | Function; 8 | commandInput?: any; 9 | }; 10 | 11 | export type S3IncomingUploadType = { 12 | s3: S3UploadConfig; 13 | } & IncomingUploadType; 14 | 15 | export type S3UploadCollectionConfig = { 16 | upload: S3IncomingUploadType; 17 | } & CollectionConfig; 18 | 19 | export type File = { 20 | filename: string; 21 | mimeType?: string; 22 | buffer: Buffer; 23 | }; 24 | -------------------------------------------------------------------------------- /test/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanbmar/payload-s3-upload/3e0417cfd871bfe00d742ae3cd79997efe17c261/test/dog.png -------------------------------------------------------------------------------- /test/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { buildConfig } from 'payload/config'; 2 | import { S3UploadCollectionConfig } from '../dist/types'; 3 | import s3Upload from '../dist'; 4 | 5 | export default buildConfig({ 6 | collections: [ 7 | { 8 | slug: 'media', 9 | access: { 10 | create: () => true, 11 | read: () => true, 12 | update: () => true, 13 | delete: () => true, 14 | }, 15 | fields: [ 16 | { 17 | name: 'type', 18 | type: 'select', 19 | options: [ 20 | { label: 'Dog', value: 'images/dogs' }, 21 | { label: 'Car', value: 'images/cars' }, 22 | ], 23 | }, 24 | { 25 | name: 'url', 26 | type: 'text', 27 | access: { 28 | create: () => false, 29 | }, 30 | admin: { 31 | disabled: true, 32 | }, 33 | hooks: { 34 | beforeChange: [() => undefined], 35 | afterRead: [ 36 | ({ data: doc }) => 37 | `https://payloadcms.com/${doc.type}/${doc.filename}`, 38 | ], 39 | }, 40 | }, 41 | ], 42 | upload: { 43 | staticURL: '/assets', 44 | staticDir: 'assets', 45 | disableLocalStorage: true, 46 | s3: { 47 | bucket: 'payload-s3-upload', 48 | prefix: ({ doc }) => doc.type, 49 | }, 50 | adminThumbnail: ({ doc }) => 51 | `https://cdn.payloadcms.com/${doc.type}/${doc.filename}`, 52 | imageSizes: [ 53 | { 54 | name: 'small', 55 | width: 400, 56 | height: 400, 57 | position: 'centre', 58 | }, 59 | { 60 | name: 'medium', 61 | width: 800, 62 | height: undefined, 63 | position: 'centre', 64 | }, 65 | ], 66 | }, 67 | } as S3UploadCollectionConfig, 68 | ], 69 | plugins: [s3Upload()], 70 | }); 71 | -------------------------------------------------------------------------------- /test/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import fsp from 'node:fs/promises'; 3 | import path from 'node:path'; 4 | import { Blob } from 'node:buffer'; 5 | import assert from 'node:assert/strict'; 6 | import { start, stop } from './server'; 7 | 8 | test('tests', async (t) => { 9 | const dogFile = await fsp.readFile(path.join(__dirname, 'dog.png')); 10 | await start(); 11 | 12 | await t.test('upload image', async () => { 13 | const form = new FormData(); 14 | form.append( 15 | 'file', 16 | // @ts-ignore 17 | new Blob([dogFile], { type: 'image/png' }), 18 | 'goodboy.png' 19 | ); 20 | form.append('type', 'images/dogs'); 21 | // @ts-ignore 22 | const response = await fetch('http://localhost:3000/api/media', { 23 | method: 'POST', 24 | body: form, 25 | }); 26 | assert(response.ok); 27 | const createData = await response.json(); 28 | assert.strictEqual(createData.doc?.filename, 'goodboy.png'); 29 | }); 30 | 31 | await t.test('upload image again', async () => { 32 | const form = new FormData(); 33 | form.append( 34 | 'file', 35 | // @ts-ignore 36 | new Blob([dogFile], { type: 'image/png' }), 37 | 'goodboy.png' 38 | ); 39 | form.append('type', 'images/dogs'); 40 | // @ts-ignore 41 | const response = await fetch('http://localhost:3000/api/media', { 42 | method: 'POST', 43 | body: form, 44 | }); 45 | assert(response.ok); 46 | const createData = await response.json(); 47 | assert.strictEqual(createData.doc?.filename, 'goodboy-1.png'); 48 | }); 49 | 50 | await t.test('delete image', async () => { 51 | const form = new FormData(); 52 | form.append( 53 | 'file', 54 | // @ts-ignore 55 | new Blob([dogFile], { type: 'image/png' }), 56 | 'goodboy.png' 57 | ); 58 | form.append('type', 'images/dogs'); 59 | let response = await fetch('http://localhost:3000/api/media', { 60 | method: 'POST', 61 | body: form, 62 | }); 63 | const createData = await response.json(); 64 | const id = createData.doc?.id; 65 | response = await fetch(`http://localhost:3000/api/media/${id}`, { 66 | method: 'DELETE', 67 | }); 68 | assert(response.ok); 69 | }); 70 | 71 | await t.test('update a document', async () => { 72 | // https://github.com/jeanbmar/payload-s3-upload/issues/10 73 | // @ts-ignore 74 | const form = new FormData(); 75 | form.append( 76 | 'file', 77 | // @ts-ignore 78 | new Blob([dogFile], { type: 'image/png' }), 79 | 'goodboy.png' 80 | ); 81 | form.append('type', 'images/dogs'); 82 | let response = await fetch('http://localhost:3000/api/media', { 83 | method: 'POST', 84 | body: form, 85 | }); 86 | const createData = await response.json(); 87 | const id = createData.doc?.id; 88 | response = await fetch(`http://localhost:3000/api/media/${id}`, { 89 | method: 'PATCH', 90 | headers: { 'Content-Type': 'application/json' }, 91 | body: JSON.stringify({ 92 | type: 'images/cars', 93 | }), 94 | }); 95 | assert(response.ok); 96 | const updateData = await response.json(); 97 | assert.strictEqual(updateData.doc?.type, 'images/cars'); 98 | }); 99 | 100 | await stop(); 101 | }); 102 | -------------------------------------------------------------------------------- /test/server.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import process from 'node:process'; 3 | import express from 'express'; 4 | import payload from 'payload'; 5 | import { MongoMemoryServer } from 'mongodb-memory-server'; 6 | 7 | let mongod; 8 | let app; 9 | let server; 10 | 11 | const start = async () => { 12 | mongod = await MongoMemoryServer.create(); 13 | process.env.PAYLOAD_CONFIG_PATH = path.join(__dirname, 'payload.config.ts'); 14 | app = express(); 15 | await payload.init({ 16 | secret: 's3-upload', 17 | mongoURL: mongod.getUri(), 18 | express: app, 19 | }); 20 | server = app.listen(3000, () => { 21 | console.log('test server started'); 22 | }); 23 | }; 24 | 25 | const stop = async () => { 26 | await mongod.stop(); 27 | server.close(); 28 | console.log('test server stopped'); 29 | process.exit(0); 30 | }; 31 | 32 | export { start, stop }; 33 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["test/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "module": "commonjs", 7 | "outDir": "./dist", 8 | "removeComments": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "target": "es6" 12 | }, 13 | "include": ["src/**/*", "test/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | --------------------------------------------------------------------------------