@fabasoad/sarif-to-slack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/FUNDING.yml +9 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +26 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +59 -0
- package/.github/workflows/linting.yml +18 -0
- package/.github/workflows/release.yml +75 -0
- package/.github/workflows/security.yml +19 -0
- package/.github/workflows/sync-labels.yml +13 -0
- package/.github/workflows/unit-tests.yml +22 -0
- package/.github/workflows/update-license.yml +12 -0
- package/.markdownlint.yml +9 -0
- package/.markdownlintignore +1 -0
- package/.pre-commit-config.yaml +105 -0
- package/.tool-versions +1 -0
- package/.yamllint.yml +7 -0
- package/CONTRIBUTING.md +61 -0
- package/LICENSE +21 -0
- package/Makefile +42 -0
- package/README.md +34 -0
- package/api-extractor.json +454 -0
- package/biome.json +81 -0
- package/dist/sarif-to-slack.d.ts +175 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/etc/sarif-to-slack.api.md +61 -0
- package/jest.config.json +33 -0
- package/package.json +54 -0
- package/sample.png +0 -0
- package/src/Logger.ts +34 -0
- package/src/Processors.ts +100 -0
- package/src/SarifToSlackService.ts +106 -0
- package/src/SlackMessageBuilder.ts +176 -0
- package/src/index.ts +52 -0
- package/src/types.ts +94 -0
- package/tests/Processors.spec.ts +115 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { AnyBlock } from '@slack/types'
|
|
2
|
+
import { HeaderBlock, ContextBlock } from '@slack/types/dist/block-kit/blocks'
|
|
3
|
+
import { IncomingWebhook } from '@slack/webhook'
|
|
4
|
+
import type { Run, Result, ReportingDescriptor } from 'sarif'
|
|
5
|
+
import { Sarif, SlackMessage } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for the SlackMessageBuilder.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export type SlackMessageBuilderOptions = {
|
|
12
|
+
username?: string
|
|
13
|
+
iconUrl?: string
|
|
14
|
+
color?: string
|
|
15
|
+
sarif: Sarif
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RuleData = { id?: string, index?: number }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Class for building and sending Slack messages based on SARIF logs.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export class SlackMessageBuilder implements SlackMessage {
|
|
25
|
+
private readonly webhook: IncomingWebhook
|
|
26
|
+
private readonly gitHubServerUrl: string
|
|
27
|
+
private readonly color?: string
|
|
28
|
+
|
|
29
|
+
private header?: HeaderBlock
|
|
30
|
+
private footer?: ContextBlock
|
|
31
|
+
private actor?: string
|
|
32
|
+
private runId?: string
|
|
33
|
+
|
|
34
|
+
public readonly sarif: Sarif
|
|
35
|
+
|
|
36
|
+
constructor(url: string, opts: SlackMessageBuilderOptions) {
|
|
37
|
+
this.webhook = new IncomingWebhook(url, {
|
|
38
|
+
username: opts.username || 'SARIF results',
|
|
39
|
+
icon_url: opts.iconUrl
|
|
40
|
+
})
|
|
41
|
+
this.color = opts.color
|
|
42
|
+
this.sarif = opts.sarif
|
|
43
|
+
this.gitHubServerUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
withHeader(header?: string): void {
|
|
47
|
+
this.header = {
|
|
48
|
+
type: 'header',
|
|
49
|
+
text: {
|
|
50
|
+
type: 'plain_text',
|
|
51
|
+
text: header || process.env.GITHUB_REPOSITORY || 'SARIF results'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
withActor(actor?: string): void {
|
|
57
|
+
this.actor = actor || process.env.GITHUB_ACTOR
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
withRun(): void {
|
|
61
|
+
this.runId = process.env.GITHUB_RUN_ID
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
withFooter(footer?: string): void {
|
|
65
|
+
const repoName = 'fabasoad/sarif-to-slack-action'
|
|
66
|
+
this.footer = {
|
|
67
|
+
type: 'context',
|
|
68
|
+
elements: [{
|
|
69
|
+
type: footer ? 'plain_text' : 'mrkdwn',
|
|
70
|
+
text: footer || `Generated by <${this.gitHubServerUrl}/${repoName}|${repoName}>`
|
|
71
|
+
}],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async send(): Promise<string> {
|
|
76
|
+
const blocks: AnyBlock[] = []
|
|
77
|
+
if (this.header) {
|
|
78
|
+
blocks.push(this.header)
|
|
79
|
+
}
|
|
80
|
+
blocks.push({
|
|
81
|
+
type: 'section',
|
|
82
|
+
text: {
|
|
83
|
+
type: 'mrkdwn',
|
|
84
|
+
text: this.buildText()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
if (this.footer) {
|
|
88
|
+
blocks.push(this.footer)
|
|
89
|
+
}
|
|
90
|
+
const { text } = await this.webhook.send({
|
|
91
|
+
attachments: [{ color: this.color, blocks }]
|
|
92
|
+
})
|
|
93
|
+
return text
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private buildText(): string {
|
|
97
|
+
const text: string[] = []
|
|
98
|
+
if (this.actor) {
|
|
99
|
+
const actorUrl = `${this.gitHubServerUrl}/${this.actor}`
|
|
100
|
+
text.push(`_Triggered by <${actorUrl}|${this.actor}>_`)
|
|
101
|
+
}
|
|
102
|
+
text.push(this.composeSummary())
|
|
103
|
+
if (this.runId) {
|
|
104
|
+
let runText: string = 'Job '
|
|
105
|
+
if (process.env.GITHUB_REPOSITORY) {
|
|
106
|
+
runText += `<${this.gitHubServerUrl}/${process.env.GITHUB_REPOSITORY}/actions/runs/${this.runId}|#${this.runId}>`
|
|
107
|
+
} else {
|
|
108
|
+
runText += `#${this.runId}`
|
|
109
|
+
}
|
|
110
|
+
text.push(runText)
|
|
111
|
+
}
|
|
112
|
+
return text.join('\n')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private composeRunSummary(toolName: string, map: Map<string, number>): string {
|
|
116
|
+
const levelsText: string[] = []
|
|
117
|
+
for (const [level, count] of map.entries()) {
|
|
118
|
+
const levelCapitalized = level.charAt(0).toUpperCase() + level.slice(1)
|
|
119
|
+
levelsText.push(`*${levelCapitalized}*: ${count}`)
|
|
120
|
+
}
|
|
121
|
+
return `*${toolName}*\n${levelsText.join(', ')}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private composeSummary(): string {
|
|
125
|
+
const data = new Map<string, Map<string, number>>()
|
|
126
|
+
for (const run of this.sarif.runs) {
|
|
127
|
+
const toolName = run.tool.driver.name
|
|
128
|
+
if (!data.has(toolName)) {
|
|
129
|
+
data.set(toolName, new Map<string, number>())
|
|
130
|
+
}
|
|
131
|
+
const results: Result[] = run.results ?? []
|
|
132
|
+
for (const result of results) {
|
|
133
|
+
const level: string = this.tryGetLevel(run, result)
|
|
134
|
+
const count: number = data.get(toolName)?.get(level) || 0
|
|
135
|
+
data.get(toolName)?.set(level, count + 1)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const summaries: string[] = []
|
|
139
|
+
for (const [toolName, map] of data.entries()) {
|
|
140
|
+
summaries.push(this.composeRunSummary(toolName, map))
|
|
141
|
+
}
|
|
142
|
+
return summaries.join('\n')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private tryGetLevel(run: Run, result: Result): string {
|
|
146
|
+
if (result.level) {
|
|
147
|
+
return result.level
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const ruleData: RuleData = {}
|
|
151
|
+
|
|
152
|
+
if (result.rule) {
|
|
153
|
+
if (result.rule?.index) {
|
|
154
|
+
ruleData.index = result.rule.index
|
|
155
|
+
}
|
|
156
|
+
if (result.rule?.id) {
|
|
157
|
+
ruleData.id = result.rule.id
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!ruleData.index && result.ruleIndex) {
|
|
162
|
+
ruleData.index = result.ruleIndex
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ruleData.index
|
|
166
|
+
&& run.tool.driver?.rules
|
|
167
|
+
&& ruleData.index < run.tool.driver.rules.length) {
|
|
168
|
+
const rule: ReportingDescriptor = run.tool.driver.rules[ruleData.index]
|
|
169
|
+
if (rule.properties && 'problem.severity' in rule.properties) {
|
|
170
|
+
return rule.properties['problem.severity'] as string
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return 'unknown'
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright (c) Yevhen Fabizhevskyi. All rights reserved. Licensed under the MIT license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sarif to Slack message converter library.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* This library provides a service to send a Slack messages based on the provided
|
|
8
|
+
* SARIF (Static Analysis Results Interchange Format) files.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { SarifToSlackService } from 'sarif-to-slack';
|
|
13
|
+
*
|
|
14
|
+
* const service = new SarifToSlackService({
|
|
15
|
+
* webhookUrl: 'https://hooks.slack.com/services/your/webhook/url',
|
|
16
|
+
* sarifPath: 'path/to/your/sarif/file.sarif',
|
|
17
|
+
* logLevel: 'info',
|
|
18
|
+
* username: 'SARIF Bot',
|
|
19
|
+
* iconUrl: 'https://example.com/icon.png',
|
|
20
|
+
* color: '#36a64f',
|
|
21
|
+
* header: {
|
|
22
|
+
* include: true,
|
|
23
|
+
* value: 'SARIF Analysis Results'
|
|
24
|
+
* },
|
|
25
|
+
* footer: {
|
|
26
|
+
* include: true,
|
|
27
|
+
* value: 'Generated by @fabasoad/sarif-to-slack'
|
|
28
|
+
* },
|
|
29
|
+
* actor: {
|
|
30
|
+
* include: true,
|
|
31
|
+
* value: 'fabasoad'
|
|
32
|
+
* },
|
|
33
|
+
* run: {
|
|
34
|
+
* include: true
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
* await service.sendAll();
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @see {@link SarifToSlackService}
|
|
41
|
+
*
|
|
42
|
+
* @packageDocumentation
|
|
43
|
+
*/
|
|
44
|
+
export { SarifToSlackService } from './SarifToSlackService'
|
|
45
|
+
export {
|
|
46
|
+
IncludeAwareProps,
|
|
47
|
+
IncludeAwareWithValueProps,
|
|
48
|
+
LogLevel,
|
|
49
|
+
Sarif,
|
|
50
|
+
SarifToSlackServiceOptions,
|
|
51
|
+
SlackMessage,
|
|
52
|
+
} from './types'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Log } from 'sarif'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type representing a SARIF log.
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export type Sarif = Log
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interface for a Slack message that can be sent.
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export interface SlackMessage {
|
|
14
|
+
/**
|
|
15
|
+
* Sends the Slack message.
|
|
16
|
+
* @returns A promise that resolves to the response from the Slack webhook.
|
|
17
|
+
*/
|
|
18
|
+
send: () => Promise<string>
|
|
19
|
+
/**
|
|
20
|
+
* The SARIF log associated with this Slack message.
|
|
21
|
+
*/
|
|
22
|
+
sarif: Sarif
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enum representing log levels for the service.
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export enum LogLevel {
|
|
30
|
+
/**
|
|
31
|
+
* Represents the most verbose logging level, typically used for detailed debugging information.
|
|
32
|
+
*/
|
|
33
|
+
Silly = 0,
|
|
34
|
+
/**
|
|
35
|
+
* Represents a logging level for tracing the flow of the application.
|
|
36
|
+
*/
|
|
37
|
+
Trace = 1,
|
|
38
|
+
/**
|
|
39
|
+
* Represents a logging level for debugging information that is less verbose than silly.
|
|
40
|
+
*/
|
|
41
|
+
Debug = 2,
|
|
42
|
+
/**
|
|
43
|
+
* Represents a logging level for general informational messages that highlight the progress of the application.
|
|
44
|
+
*/
|
|
45
|
+
Info = 3,
|
|
46
|
+
/**
|
|
47
|
+
* Represents a logging level for potentially harmful situations that require attention.
|
|
48
|
+
*/
|
|
49
|
+
Warning = 4,
|
|
50
|
+
/**
|
|
51
|
+
* Represents a logging level for error conditions that do not require immediate action but should be noted.
|
|
52
|
+
*/
|
|
53
|
+
Error = 5,
|
|
54
|
+
/**
|
|
55
|
+
* Represents a logging level for critical errors that require immediate attention and may cause the application to terminate.
|
|
56
|
+
*/
|
|
57
|
+
Fatal = 6
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Type representing properties that indicate whether to include certain information
|
|
62
|
+
* in the Slack message.
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export type IncludeAwareProps = {
|
|
66
|
+
include: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Type representing properties that indicate whether to include certain information
|
|
71
|
+
* in the Slack message, along with an optional value.
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export type IncludeAwareWithValueProps = IncludeAwareProps & {
|
|
75
|
+
value?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Options for the SarifToSlackService.
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
export type SarifToSlackServiceOptions = {
|
|
83
|
+
// The Slack webhook URL to send messages to.
|
|
84
|
+
webhookUrl: string,
|
|
85
|
+
sarifPath: string,
|
|
86
|
+
username?: string,
|
|
87
|
+
iconUrl?: string,
|
|
88
|
+
color?: string,
|
|
89
|
+
logLevel?: LogLevel | string,
|
|
90
|
+
header?: IncludeAwareWithValueProps,
|
|
91
|
+
footer?: IncludeAwareWithValueProps,
|
|
92
|
+
actor?: IncludeAwareWithValueProps,
|
|
93
|
+
run?: IncludeAwareProps,
|
|
94
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { processColor, processSarifPath, processLogLevel } from '../src/Processors'
|
|
4
|
+
|
|
5
|
+
jest.mock('fs')
|
|
6
|
+
const mockedFs = fs as jest.Mocked<typeof fs>
|
|
7
|
+
|
|
8
|
+
jest.mock('../src/Logger', () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
default: { info: jest.fn(), debug: jest.fn() }
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
describe('processColor', () => {
|
|
14
|
+
test('returns correct hex for success', () => {
|
|
15
|
+
expect(processColor('success')).toBe('#008000')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('returns correct hex for failure', () => {
|
|
19
|
+
expect(processColor('failure')).toBe('#ff0000')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('returns correct hex for cancelled', () => {
|
|
23
|
+
expect(processColor('cancelled')).toBe('#0047ab')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('returns correct hex for skipped', () => {
|
|
27
|
+
expect(processColor('skipped')).toBe('#808080')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('returns input for unknown color', () => {
|
|
31
|
+
expect(processColor('other')).toBe('other')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('returns undefined for undefined input', () => {
|
|
35
|
+
expect(processColor(undefined)).toBeUndefined()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('processSarifPath', () => {
|
|
40
|
+
const fakeDir = '/fake/dir'
|
|
41
|
+
const fakeFile = '/fake/file.sarif'
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
jest.resetAllMocks()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('throws if path does not exist', () => {
|
|
48
|
+
mockedFs.existsSync.mockReturnValue(false)
|
|
49
|
+
expect(() => processSarifPath(fakeFile)).toThrow(/does not exist/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('returns .sarif files in directory', () => {
|
|
53
|
+
mockedFs.existsSync.mockReturnValue(true)
|
|
54
|
+
mockedFs.statSync.mockReturnValue({ isDirectory: () => true, isFile: () => false } as any)
|
|
55
|
+
// @ts-ignore: mocking readdirSync with a specific return value
|
|
56
|
+
mockedFs.readdirSync.mockReturnValue(['a.sarif', 'b.SARIF', 'c.txt'])
|
|
57
|
+
const result: string[] = processSarifPath(fakeDir)
|
|
58
|
+
expect(result).toEqual(
|
|
59
|
+
['a.sarif', 'b.SARIF'].map((file: string) => path.join(fakeDir, file))
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('returns file path if it is a file', () => {
|
|
64
|
+
mockedFs.existsSync.mockReturnValue(true)
|
|
65
|
+
mockedFs.statSync.mockReturnValue({ isDirectory: () => false, isFile: () => true } as any)
|
|
66
|
+
const result: string[] = processSarifPath(fakeFile)
|
|
67
|
+
expect(result).toHaveLength(1)
|
|
68
|
+
expect(result[0]).toEqual(fakeFile)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('throws if path is neither file nor directory', () => {
|
|
72
|
+
mockedFs.existsSync.mockReturnValue(true)
|
|
73
|
+
mockedFs.statSync.mockReturnValue({ isDirectory: () => false, isFile: () => false } as any)
|
|
74
|
+
expect(() => processSarifPath('/weird/path')).toThrow(/neither a file nor a directory/)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('processLogLevel', () => {
|
|
79
|
+
test('returns 0 for silly', () => {
|
|
80
|
+
expect(processLogLevel('silly')).toBe(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('returns 1 for trace', () => {
|
|
84
|
+
expect(processLogLevel('trace')).toBe(1)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('returns 2 for debug', () => {
|
|
88
|
+
expect(processLogLevel('debug')).toBe(2)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('returns 3 for info', () => {
|
|
92
|
+
expect(processLogLevel('info')).toBe(3)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('returns 4 for warning', () => {
|
|
96
|
+
expect(processLogLevel('warning')).toBe(4)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('returns 5 for error', () => {
|
|
100
|
+
expect(processLogLevel('error')).toBe(5)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('returns 6 for fatal', () => {
|
|
104
|
+
expect(processLogLevel('fatal')).toBe(6)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('is case-insensitive', () => {
|
|
108
|
+
expect(processLogLevel('ERROR')).toBe(5)
|
|
109
|
+
expect(processLogLevel('Info')).toBe(3)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('throws for unknown log level', () => {
|
|
113
|
+
expect(() => processLogLevel('unknown')).toThrow(/Unknown log level/)
|
|
114
|
+
})
|
|
115
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "es2024",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"stripInternal": true,
|
|
10
|
+
"types": ["node", "jest"],
|
|
11
|
+
"newLine": "lf",
|
|
12
|
+
"lib": [
|
|
13
|
+
"es2024"
|
|
14
|
+
],
|
|
15
|
+
"outDir": "lib"
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src/**/*.ts"
|
|
19
|
+
],
|
|
20
|
+
"exclude": ["node_modules", "tests"]
|
|
21
|
+
}
|