mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
Initial commit
This commit is contained in:
parent
20c1ea12b6
commit
27f32fdf6f
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
lib/
|
||||
config.yml
|
10
config.sample.yml
Normal file
10
config.sample.yml
Normal file
@ -0,0 +1,10 @@
|
||||
github:
|
||||
auth: someauthtoken
|
||||
webhook:
|
||||
secret: webhooksecret
|
||||
port: 7775
|
||||
bridge:
|
||||
domain: example.com
|
||||
url: http://localhost:8008
|
||||
port: 9993 # Port the appservice is binding to
|
||||
bindAddress: 127.0.0.1 # Not the port the appservice is binding to
|
1530
package-lock.json
generated
Normal file
1530
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "matrix-github",
|
||||
"version": "0.0.1",
|
||||
"description": "A bridge that displays GitHub issues/PRs as rooms.",
|
||||
"main": "lib/app.js",
|
||||
"repository": "https://github.com/Half-Shot/matrix-github",
|
||||
"author": "Half-Shot",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node lib/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^16.28.7",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/markdown-it": "^0.0.8",
|
||||
"express": "^4.17.1",
|
||||
"markdown-it": "^9.0.1",
|
||||
"matrix-bot-sdk": "^0.3.9",
|
||||
"winston": "^3.2.1",
|
||||
"yaml": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.6.9",
|
||||
"@types/yaml": "^1.0.2",
|
||||
"typescript": "^3.5.3"
|
||||
}
|
||||
}
|
10
src/AdminRoom.ts
Normal file
10
src/AdminRoom.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const BRIDGE_ADMIN_ROOM_TYPE = "uk.half-shot.matrix-github.adminroom";
|
||||
|
||||
export class AdminRoom {
|
||||
|
||||
constructor () {
|
||||
|
||||
}
|
||||
// Initiate oauth
|
||||
// Relinquish oauth
|
||||
}
|
12
src/BridgeState.ts
Normal file
12
src/BridgeState.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const BRIDGE_STATE_TYPE = "uk.half-shot.matrix-github.bridge";
|
||||
|
||||
export interface IBridgeRoomState {
|
||||
state_key: string;
|
||||
content: {
|
||||
org: string;
|
||||
repo: string;
|
||||
state: string;
|
||||
issues: string[];
|
||||
comments_processed: number,
|
||||
},
|
||||
}
|
39
src/CommentProcessor.ts
Normal file
39
src/CommentProcessor.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { IssuesGetCommentResponse } from "@octokit/rest";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import markdown from "markdown-it";
|
||||
|
||||
const md = new markdown();
|
||||
const REGEX_MENTION = /(^|\s)(@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})(\s|$)/ig;
|
||||
|
||||
interface IMatrixCommentEvent {
|
||||
msgtype: string;
|
||||
body: string;
|
||||
formatted_body: string;
|
||||
format: string;
|
||||
external_url:string;
|
||||
"uk.half-shot.matrix-github.comment": {
|
||||
id: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class CommentProcessor {
|
||||
constructor (private as: Appservice) {}
|
||||
|
||||
public getEventBodyForComment(comment: IssuesGetCommentResponse): IMatrixCommentEvent {
|
||||
let body = comment.body;
|
||||
body = body.replace(REGEX_MENTION, (_match: string, _part1: string, githubId: string) => {
|
||||
const userId = this.as.getUserIdForSuffix(githubId.substr(1));
|
||||
return `[$2](https://matrix.to/#/${userId})`;
|
||||
});
|
||||
return {
|
||||
body,
|
||||
formatted_body: md.render(body),
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
external_url: comment.html_url,
|
||||
"uk.half-shot.matrix-github.comment": {
|
||||
id: comment.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
33
src/Config.ts
Normal file
33
src/Config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import YAML from "yaml";
|
||||
import { promises as fs } from "fs"
|
||||
import { IAppserviceRegistration } from "matrix-bot-sdk";
|
||||
|
||||
export interface BridgeConfig {
|
||||
github: {
|
||||
auth: string
|
||||
webhook: {
|
||||
port: number
|
||||
bindAddress: string
|
||||
secret: string
|
||||
},
|
||||
userTokens: {
|
||||
[userId: string]: string
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
domain: string
|
||||
url: string
|
||||
port: number,
|
||||
bindAddress: string
|
||||
},
|
||||
}
|
||||
|
||||
export async function parseRegistrationFile(filename: string) {
|
||||
const file = await fs.readFile(filename, "utf-8");
|
||||
return YAML.parse(file) as IAppserviceRegistration;
|
||||
}
|
||||
|
||||
export async function parseConfig(filename: string) {
|
||||
const file = await fs.readFile(filename, "utf-8");
|
||||
return YAML.parse(file) as BridgeConfig;
|
||||
}
|
71
src/GithubWebhooks.ts
Normal file
71
src/GithubWebhooks.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { BridgeConfig } from "./Config";
|
||||
import { Application, default as express, Request, Response } from "express";
|
||||
import { createHmac } from "crypto";
|
||||
import { IssuesGetResponse, ReposGetResponse, IssuesGetResponseUser, IssuesGetCommentResponse } from "@octokit/rest";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export interface IWebhookEvent {
|
||||
action: string;
|
||||
issue?: IssuesGetResponse, // more or less
|
||||
comment?: IssuesGetCommentResponse,
|
||||
repository?: ReposGetResponse,
|
||||
sender?: IssuesGetResponseUser,
|
||||
}
|
||||
|
||||
export class GithubWebhooks extends EventEmitter {
|
||||
private expressApp: Application;
|
||||
constructor(private config: BridgeConfig) {
|
||||
super();
|
||||
this.expressApp = express();
|
||||
this.expressApp.use(express.json({
|
||||
verify: this.verifyRequest.bind(this),
|
||||
}));
|
||||
this.expressApp.post("/", this.onPayload.bind(this));
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.expressApp.listen(
|
||||
this.config.github.webhook.port,
|
||||
this.config.github.webhook.bindAddress
|
||||
);
|
||||
}
|
||||
|
||||
onPayload(req: Request, res: Response) {
|
||||
const body = req.body as IWebhookEvent;
|
||||
console.log("Got", body.action);
|
||||
try {
|
||||
console.log(body);
|
||||
if (body.action === "created" && body.comment) {
|
||||
this.emit(`comment.created`, body);
|
||||
} else {
|
||||
this.emit(`${body.action}`, body);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error("Failed to emit");
|
||||
}
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
onEvent() {
|
||||
|
||||
}
|
||||
|
||||
// Calculate the X-Hub-Signature header value.
|
||||
private getSignature (buf: Buffer) {
|
||||
var hmac = createHmac("sha1", this.config.github.webhook.secret);
|
||||
hmac.update(buf);
|
||||
return "sha1=" + hmac.digest("hex");
|
||||
}
|
||||
|
||||
// Verify function compatible with body-parser to retrieve the request payload.
|
||||
// Read more: https://github.com/expressjs/body-parser#verify
|
||||
private verifyRequest (req: Request, res: Response, buf: Buffer) {
|
||||
const expected = req.headers['x-hub-signature'];
|
||||
const calculated = this.getSignature(buf);
|
||||
if (expected !== calculated) {
|
||||
res.sendStatus(403);
|
||||
throw new Error("Invalid signature.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
333
src/app.ts
Normal file
333
src/app.ts
Normal file
@ -0,0 +1,333 @@
|
||||
import { Appservice, LogService } from "matrix-bot-sdk";
|
||||
import Octokit, { IssuesGetResponseUser } from "@octokit/rest";
|
||||
import winston from "winston";
|
||||
import markdown from "markdown-it";
|
||||
import { IBridgeRoomState, BRIDGE_STATE_TYPE } from "./BridgeState";
|
||||
import { BridgeConfig, parseConfig, parseRegistrationFile } from "./Config";
|
||||
import { GithubWebhooks, IWebhookEvent } from "./GithubWebhooks";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
|
||||
const md = new markdown();
|
||||
|
||||
const log = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.simple(),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
]
|
||||
});
|
||||
|
||||
LogService.setLogger(log);
|
||||
|
||||
export class GithubBridge {
|
||||
private config!: BridgeConfig;
|
||||
private octokit!: Octokit;
|
||||
private as!: Appservice;
|
||||
private roomIdtoBridgeState: Map<string, IBridgeRoomState[]>;
|
||||
private orgRepoIssueToRoomId: Map<string, string>;
|
||||
private matrixHandledEvents: Set<string>;
|
||||
private webhookHandler?: GithubWebhooks;
|
||||
private commentProcessor!: CommentProcessor;
|
||||
|
||||
constructor () {
|
||||
this.roomIdtoBridgeState = new Map();
|
||||
this.orgRepoIssueToRoomId = new Map();
|
||||
this.matrixHandledEvents = new Set();
|
||||
}
|
||||
|
||||
public async start() {
|
||||
const configFile = process.argv[2] || "./config.yml";
|
||||
const registrationFile = process.argv[3] || "./registration.yml";
|
||||
this.config = await parseConfig(configFile);
|
||||
const registration = await parseRegistrationFile(registrationFile);
|
||||
this.octokit = new Octokit({
|
||||
auth: this.config.github.auth,
|
||||
userAgent: "matrix-github v0.0.1"
|
||||
});
|
||||
|
||||
this.as = new Appservice({
|
||||
homeserverName: this.config.bridge.domain,
|
||||
homeserverUrl: this.config.bridge.url,
|
||||
port: this.config.bridge.port,
|
||||
bindAddress: this.config.bridge.bindAddress,
|
||||
registration,
|
||||
});
|
||||
this.commentProcessor = new CommentProcessor(this.as);
|
||||
|
||||
this.as.on("query.room", (roomAlias, cb) => {
|
||||
cb(this.onQueryRoom(roomAlias));
|
||||
});
|
||||
|
||||
this.as.on("room.event", (roomId, event) => {
|
||||
this.onRoomEvent(roomId, event);
|
||||
});
|
||||
|
||||
if (this.config.github.webhook) {
|
||||
this.webhookHandler = new GithubWebhooks(this.config);
|
||||
this.webhookHandler.listen();
|
||||
this.webhookHandler.on("comment.created", this.onCommentCreated.bind(this));
|
||||
}
|
||||
|
||||
// Fetch all room state
|
||||
const joinedRooms = await this.as.botIntent.underlyingClient.getJoinedRooms();
|
||||
for (const roomId of joinedRooms) {
|
||||
log.info("Fetching state for " + roomId);
|
||||
await this.getRoomBridgeState(roomId);
|
||||
}
|
||||
|
||||
await this.as.begin();
|
||||
log.info("Started bridge");
|
||||
}
|
||||
|
||||
private async getRoomBridgeState(roomId: string, existingState?: any) {
|
||||
if (this.roomIdtoBridgeState.has(roomId) && !existingState) {
|
||||
return this.roomIdtoBridgeState.get(roomId)!;
|
||||
}
|
||||
try {
|
||||
log.info("Updating state cache for " + roomId)
|
||||
const state: any = existingState ? [existingState] : (await this.as.botIntent.underlyingClient.getRoomState(roomId))
|
||||
const bridgeEvents: IBridgeRoomState[] = state.filter((e: any) =>
|
||||
e.type === BRIDGE_STATE_TYPE
|
||||
);
|
||||
this.roomIdtoBridgeState.set(roomId, bridgeEvents);
|
||||
for (const event of bridgeEvents) {
|
||||
this.orgRepoIssueToRoomId.set(`${event.content.org}/${event.content.repo}#${event.content.issues[0]}`, roomId);
|
||||
}
|
||||
return bridgeEvents;
|
||||
} catch (ex) {
|
||||
log.error(`Failed to get room state for ${roomId}:` + ex);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async onRoomEvent(roomId: string, event: any) {
|
||||
const isOurUser = this.as.isNamespacedUser(event.sender);
|
||||
// if (isOurUser) {
|
||||
// log.debug("Not handling our own events.");
|
||||
// // We don't handle any IRC side stuff yet.
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (event.type === BRIDGE_STATE_TYPE) {
|
||||
log.info(`Got new state for ${roomId}`);
|
||||
this.getRoomBridgeState(roomId, event);
|
||||
// Get current state of issue.
|
||||
await this.syncIssueState(roomId, event);
|
||||
}
|
||||
|
||||
const bridgeState = await this.getRoomBridgeState(roomId);
|
||||
if (bridgeState.length === 0) {
|
||||
log.info("Room has no state for bridge");
|
||||
return;
|
||||
}
|
||||
if (bridgeState.length > 1) {
|
||||
log.error("Can't handle multiple bridges yet");
|
||||
return;
|
||||
}
|
||||
// Get a client for the IRC user.
|
||||
const githubRepo = bridgeState[0].content;
|
||||
log.info(`Got new request for ${githubRepo.org}${githubRepo.repo}#${githubRepo.issues.join("|")}`);
|
||||
if (!isOurUser) {
|
||||
if (event.content.body === "!sync") {
|
||||
await this.syncIssueState(roomId, bridgeState[0]);
|
||||
}
|
||||
if (event.type === "m.room.message") {
|
||||
await this.onMatrixIssueComment(event, bridgeState[0]);
|
||||
}
|
||||
}
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
private async getIntentForUser(user: IssuesGetResponseUser) {
|
||||
const intent = this.as.getIntentForSuffix(user.login);
|
||||
const displayName = `${user.login}`;
|
||||
// Verify up-to-date profile
|
||||
let profile;
|
||||
await intent.ensureRegistered();
|
||||
try {
|
||||
profile = await intent.underlyingClient.getUserProfile(intent.userId);
|
||||
if (profile.displayname !== displayName || (!profile.avatar_url && user.avatar_url)) {
|
||||
log.info(`${intent.userId}'s profile is out of date`);
|
||||
// Also set avatar
|
||||
const buffer = await this.octokit.request(user.avatar_url);
|
||||
log.info(`uploading ${user.avatar_url}`);
|
||||
// This does exist, but headers is silly and doesn't have content-type.
|
||||
const contentType = (buffer.headers as any)['content-type'];
|
||||
const mxc = await intent.underlyingClient.uploadContent(Buffer.from(buffer.data as ArrayBuffer), contentType);
|
||||
await intent.underlyingClient.setAvatarUrl(mxc);
|
||||
await intent.underlyingClient.setDisplayName(displayName);
|
||||
}
|
||||
} catch (ex) {
|
||||
profile = {};
|
||||
}
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private async syncIssueState(roomId: string, repoState: IBridgeRoomState) {
|
||||
const issue = await this.octokit.issues.get({
|
||||
owner: repoState.content.org,
|
||||
repo: repoState.content.repo,
|
||||
issue_number: parseInt(repoState.content.issues[0]),
|
||||
});
|
||||
issue.data.user
|
||||
if (repoState.content.comments_processed === issue.data.comments) {
|
||||
return;
|
||||
}
|
||||
const creatorIntent = await this.getIntentForUser(issue.data.user);
|
||||
if (repoState.content.comments_processed === -1) {
|
||||
// We've not sent any messages into the room yet, let's do it!
|
||||
await creatorIntent.sendEvent(roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: `created ${issue.data.pull_request ? "a pull request" : "an issue"} at ${issue.data.created_at}`,
|
||||
});
|
||||
if (issue.data.body) {
|
||||
await creatorIntent.sendEvent(roomId, {
|
||||
msgtype: "m.text",
|
||||
external_url: issue.data.html_url,
|
||||
body: `${issue.data.body} (${issue.data.updated_at})`,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: md.render(issue.data.body),
|
||||
});
|
||||
}
|
||||
if (issue.data.pull_request) {
|
||||
// Send a patch in
|
||||
}
|
||||
repoState.content.comments_processed = 0;
|
||||
}
|
||||
const comments = (await this.octokit.issues.listComments({
|
||||
owner: repoState.content.org,
|
||||
repo: repoState.content.repo,
|
||||
issue_number: parseInt(repoState.content.issues[0]),
|
||||
// TODO: Use since to get a subset
|
||||
})).data.slice(repoState.content.comments_processed);
|
||||
for (const comment of comments) {
|
||||
this.onCommentCreated({
|
||||
comment,
|
||||
action: "fake",
|
||||
}, roomId, false);
|
||||
repoState.content.comments_processed++;
|
||||
}
|
||||
|
||||
if (repoState.content.state !== issue.data.state) {
|
||||
if (issue.data.state === "closed") {
|
||||
const closedIntent = await this.getIntentForUser(issue.data.closed_by);
|
||||
await closedIntent.sendEvent(roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: `closed the ${issue.data.pull_request ? "pull request" : "issue"} at ${issue.data.closed_at}`,
|
||||
external_url: issue.data.closed_by.html_url,
|
||||
});
|
||||
}
|
||||
repoState.content.state = issue.data.state;
|
||||
}
|
||||
|
||||
await this.as.botIntent.underlyingClient.sendStateEvent(
|
||||
roomId,
|
||||
BRIDGE_STATE_TYPE,
|
||||
repoState.state_key,
|
||||
repoState.content,
|
||||
);
|
||||
}
|
||||
|
||||
private async onQueryRoom(roomAlias: string) {
|
||||
log.info("Got room query request:", roomAlias);
|
||||
const match = /#github_(.+)_(.+)_(\d+):.*/.exec(roomAlias);
|
||||
if (!match || match.length < 4) {
|
||||
throw Error("Alias is in an incorrect format");
|
||||
}
|
||||
const parts = match!.slice(1);
|
||||
const issueNumber = parseInt(parts[2]);
|
||||
|
||||
const issue = await this.octokit.issues.get({
|
||||
owner: parts[0],
|
||||
repo: parts[1],
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
if (issue.status !== 200) {
|
||||
throw Error("Could not find issue");
|
||||
}
|
||||
|
||||
const orgRepoName = issue.data.repository_url.substr("https://api.github.com/repos/".length);
|
||||
|
||||
return {
|
||||
visibility: "public",
|
||||
name: `${orgRepoName}#${issue.data.number}: ${issue.data.title}`,
|
||||
topic: `${issue.data.title} | Status: ${issue.data.state} | ${issue.data.html_url}`,
|
||||
preset: "public_chat",
|
||||
initial_state: [
|
||||
{
|
||||
type: BRIDGE_STATE_TYPE,
|
||||
content: {
|
||||
org: orgRepoName.split("/")[0],
|
||||
repo: orgRepoName.split("/")[1],
|
||||
issues: [String(issue.data.number)],
|
||||
comments_processed: -1,
|
||||
state: "open",
|
||||
},
|
||||
state_key: issue.data.url,
|
||||
} as IBridgeRoomState
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private async onCommentCreated (event: IWebhookEvent, roomId?: string, updateState: boolean = true) {
|
||||
if (!roomId) {
|
||||
const issueKey = `${event.repository!.owner.login}/${event.repository!.name}#${event.issue!.number}`;
|
||||
roomId = this.orgRepoIssueToRoomId.get(issueKey);
|
||||
if (!roomId) {
|
||||
console.log("No room id for repo");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const comment = event.comment!;
|
||||
if (event.repository) {
|
||||
// Delay to stop comments racing sends
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const dupeKey = `${event.repository.owner.login}/${event.repository.name}#${event.issue!.number}~${comment.id}`.toLowerCase();
|
||||
console.log("dupekey:", dupeKey);
|
||||
if (this.matrixHandledEvents.has(dupeKey)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const commentIntent = await this.getIntentForUser(comment.user);
|
||||
await commentIntent.sendEvent(roomId, this.commentProcessor.getEventBodyForComment(comment));
|
||||
if (!updateState) {
|
||||
return;
|
||||
}
|
||||
const state = (await this.getRoomBridgeState(roomId))[0];
|
||||
state.content.comments_processed++;
|
||||
await this.as.botIntent.underlyingClient.sendStateEvent(
|
||||
roomId,
|
||||
BRIDGE_STATE_TYPE,
|
||||
state.state_key,
|
||||
state.content,
|
||||
);
|
||||
}
|
||||
|
||||
private async onMatrixIssueComment (event: any, bridgeState: IBridgeRoomState) {
|
||||
// TODO: Someone who is not lazy should make this work with oauth.
|
||||
const senderToken = this.config.github.userTokens[event.sender];
|
||||
if (!senderToken) {
|
||||
log.warn("Cannot handle event from " + event.sender + ". No user token configured");
|
||||
}
|
||||
const clientKit = new Octokit({
|
||||
auth: senderToken,
|
||||
userAgent: "matrix-github v0.0.1"
|
||||
});
|
||||
|
||||
const result = await clientKit.issues.createComment({
|
||||
repo: bridgeState.content.repo,
|
||||
owner: bridgeState.content.org,
|
||||
body: event.content.body,
|
||||
issue_number: parseInt(bridgeState.content.issues[0]),
|
||||
});
|
||||
const key = `${bridgeState.content.org}/${bridgeState.content.repo}#${bridgeState.content.issues[0]}~${result.data.id}`.toLowerCase();
|
||||
console.log("Original dupe key", key);
|
||||
this.matrixHandledEvents.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
new GithubBridge().start().catch((ex) => {
|
||||
console.error("Bridge encountered an error and has stopped:", ex);
|
||||
});
|
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"target": "ESNext",
|
||||
"module": "commonjs",
|
||||
"allowJs": false,
|
||||
"declaration": false,
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user