v1 of FixBluesky #1

Merged
rose merged 32 commits from devel into main 2024-10-16 16:48:34 +00:00
30 changed files with 8513 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2

View file

@ -0,0 +1,32 @@
name: Semver
on:
push:
branches:
- "**"
jobs:
semver:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Enable Corepack
run: corepack enable
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "21"
- name: Setup Yarn
run: |
yarn set version canary
yarn config set nodeLinker node-modules
- name: yarn install
run: yarn install --immutable
- name: Semantic Release
id: semantic
run: yarn run semantic-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -0,0 +1,44 @@
name: Build Docker Image
on:
push:
branches:
- "**"
tags:
- "v*.*.*"
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Docker Metadata
id: metadata
uses: docker/metadata-action@v5
with:
images: git.thornbush.dev/rose/fixbluesky
tags: |
type=ref,event=branch
type=sha,prefix=
type=edge,branch=devel
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Docker Buildx
uses: https://code.forgejo.org/docker/setup-buildx-action@v3
- name: Login to Docker Registry
if: github.event_name != 'pull_request'
uses: https://code.forgejo.org/docker/login-action@v3
with:
registry: git.thornbush.dev
username: ${{github.actor}}
password: ${{secrets.OAUTH_TOKEN}}
- name: Build Image
uses: https://code.forgejo.org/docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

130
.gitignore vendored Normal file
View file

@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

12
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"biome.enabled": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"typescript.tsdk": "node_modules\\typescript\\lib",
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}

925
.yarn/releases/yarn-4.5.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

3
.yarnrc.yml Normal file
View file

@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.0.cjs

30
Dockerfile Normal file
View file

@ -0,0 +1,30 @@
FROM node:21-alpine AS base
ENV CI=true
FROM base AS builder
RUN corepack enable
WORKDIR /app
COPY .gitignore .gitignore
COPY yarn.lock ./
COPY package.json ./
RUN yarn set version canary
RUN yarn config set nodeLinker node-modules
RUN yarn install --immutable
COPY . .
RUN yarn build
FROM base AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 fixbluesky
RUN adduser --system --uid 1001 fixbluesky
USER fixbluesky
COPY --from=builder /app .
CMD node ./dist/main.js

31
biome.json Normal file
View file

@ -0,0 +1,31 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.1/schema.json",
"organizeImports": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useTemplate": "warn",
"noNonNullAssertion": "warn"
},
"suspicious": {
"noExplicitAny": "off"
}
}
},
"formatter": {
"enabled": true,
"lineWidth": 72,
"indentStyle": "space",
"formatWithErrors": true,
"indentWidth": 2
},
"javascript": {
"formatter": {
"semicolons": "always"
}
}
}

7
commitlint.config.ts Normal file
View file

@ -0,0 +1,7 @@
export default {
extends: ["gitmoji"],
rules: {
"type-empty": [0, "never"],
"subject-empty": [0, "never"],
},
};

28
docker-compose.yml Normal file
View file

@ -0,0 +1,28 @@
services:
keydb:
container_name: keydb
image: eqalpha/keydb
restart: always
networks:
- fixbluesky
fixbluesky:
container_name: fixbluesky
build:
context: .
dockerfile: ./Dockerfile
restart: always
ports:
- 8787:8787
environment:
- REDIS_HOSTNAME=${REDIS_HOSTNAME:-keydb}
- BSKY_SERVICE_URL=${BSKY_SERVICE_URL}
- BSKY_AUTH_USERNAME=${BSKY_AUTH_USERNAME}
- BSKY_AUTH_PASSWORD=${BSKY_AUTH_PASSWORD}
- FIXBLUESKY_APP_DOMAIN=${FIXBLUESKY_APP_DOMAIN}
networks:
- fixbluesky
depends_on:
- keydb
networks:
fixbluesky:
name: fixbluesky

10
lefthook.yml Normal file
View file

@ -0,0 +1,10 @@
pre-commit:
parallel: true
commands:
"biome lint":
run: yarn run lint
commit-msg:
commands:
"lint commit message":
run: yarn run commitlint --edit {1}

43
package.json Normal file
View file

@ -0,0 +1,43 @@
{
"name": "fixbluesky",
"private": true,
"volta": {
"node": "21.7.3"
},
"type": "module",
"main": "./dist/main.js",
"module": "./dist/main.js",
"types": "./dist/main.d.ts",
"exports": {
"import": {
"types": "./dist/main.d.ts"
}
},
"scripts": {
"dev": "tsx ./src/main.ts",
"lint": "biome ci ./src/**/*",
"build": "pkgroll"
},
"devDependencies": {
"@biomejs/biome": "1.7.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@types/node": "20.12.7",
"commitlint": "^19.5.0",
"commitlint-config-gitmoji": "^2.3.1",
"lefthook": "^1.7.18",
"pkgroll": "2.0.2",
"semantic-release": "^24.1.2",
"semantic-release-gitmoji": "^1.6.8",
"tsx": "4.7.2",
"typescript": "5.4.5"
},
"dependencies": {
"@atproto/api": "0.12.5",
"@hono/node-server": "1.11.0",
"hono": "4.2.7",
"ioredis": "5.4.1"
},
"packageManager": "yarn@4.5.0"
}

41
release.config.mjs Normal file
View file

@ -0,0 +1,41 @@
/**
* @type {import('semantic-release').GlobalConfig}
*/
export default {
branches: ["main", { name: "devel", prerelease: true }],
plugins: [
[
"semantic-release-gitmoji",
{
releaseRules: {
major: [":boom:"],
minor: [":sparkles:"],
patch: [
":bug:",
":ambulance:",
":lipstick:",
":lock:",
":zap:",
":chart_with_upwards_trend:",
":globe_with_meridians:",
":alien:",
":wheelchair:",
":mag:",
":children_crossing:",
":speech_balloon:",
":iphone:",
":pencil2:",
":bento:",
],
},
},
],
[
"@semantic-release/exec",
{
successCmd:
"echo 'NOTES<<EOF' >> $GITHUB_OUTPUT && echo '${nextRelease.notes}' >> $GITHUB_OUTPUT && echo 'EOF' >> $GITHUB_OUTPUT && echo 'VERSION=${nextRelease.version}' >> $GITHUB_OUTPUT",
},
],
],
};

27
src/components/Layout.tsx Normal file
View file

@ -0,0 +1,27 @@
import { html } from "hono/html";
export interface LayoutProps {
url: string;
children: any;
}
export const Layout = ({ url, children }: LayoutProps) => {
const removeLeadingSlash = url.substring(1);
const redirectUrl = removeLeadingSlash.startsWith("https://")
? removeLeadingSlash
: `https://bsky.app/${removeLeadingSlash}`;
return html`
<!DOCTYPE html>
<html>
<head>
<link rel="canonical" href="${url.substring(1)}" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta content="#0085ff" name="theme-color" />
<meta property="og:site_name" content="FixBluesky" />
${children}
<meta http-equiv="refresh" content="0;url=${redirectUrl}" />
</head>
</html>
`;
};

52
src/components/Post.tsx Normal file
View file

@ -0,0 +1,52 @@
import type { AppBskyFeedDefs } from "@atproto/api";
import { parseEmbedDescription } from "../lib/parseEmbedDescription.ts";
import { parseEmbedImage } from "../lib/parseEmbedImage.ts";
import { OEmbedTypes } from "../routes/getOEmbed.ts";
import { Layout } from "./Layout.tsx";
interface PostProps {
post: AppBskyFeedDefs.PostView;
url: string;
appDomain: string;
}
export const Post = ({ post, url, appDomain }: PostProps) => {
const image = parseEmbedImage(post);
return (
<Layout url={url}>
<meta name="twitter:creator" content={`@${post.author.handle}`} />
<meta
property="og:description"
content={parseEmbedDescription(post)}
/>
<meta
property="og:title"
content={`${post.author.displayName} (@${post.author.handle})`}
/>
{!(image === post.author.avatar) && (
<meta name="twitter:card" content="summary_large_image" />
)}
{Array.isArray(image) ? (
image.map((i) => (
<meta property="og:image" content={i} key={i} />
))
) : (
<meta property="og:image" content={image} />
)}
<link
type="application/json+oembed"
href={`https:/${appDomain}/oembed?type=${
OEmbedTypes.Post
}&replies=${post.replyCount}&reposts=${
post.repostCount
}&likes=${post.likeCount}&avatar=${encodeURIComponent(
post.author.avatar ?? "",
)}`}
/>
</Layout>
);
};

View file

@ -0,0 +1,34 @@
import type { AppBskyActorDefs } from "@atproto/api";
import { OEmbedTypes } from "../routes/getOEmbed.ts";
import { Layout } from "./Layout.tsx";
interface ProfileProps {
profile: AppBskyActorDefs.ProfileViewDetailed;
url: string;
appDomain: string;
}
export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
<Layout url={url}>
<meta name="twitter:creator" content={`@${profile.handle}`} />
<meta
property="og:description"
content={profile.description ?? ""}
/>
<meta
property="og:title"
content={`${profile.displayName} (@${profile.handle})`}
/>
<meta property="og:image" content={profile.avatar} />
<link
type="application/json+oembed"
href={`https://${appDomain}/oembed?type=${
OEmbedTypes.Profile
}&follows=${profile.followsCount}&posts=${
profile.postsCount
}&avatar=${encodeURIComponent(profile.avatar ?? "")}`}
/>
</Layout>
);

9
src/globals.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
import type { BskyAgent } from "@atproto/api";
declare global {
interface HonoEnv {
Variables: {
Agent: BskyAgent;
};
}
}

18
src/lib/fetchPostData.ts Normal file
View file

@ -0,0 +1,18 @@
import type { BskyAgent } from "@atproto/api";
export interface fetchPostOptions {
user: string;
post: string;
}
export async function fetchPost(
agent: BskyAgent,
{ user, post }: fetchPostOptions,
) {
const { data: userData } = await agent.getProfile({
actor: user,
});
return agent.getPosts({
uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
});
}

14
src/lib/fetchProfile.ts Normal file
View file

@ -0,0 +1,14 @@
import type { BskyAgent } from "@atproto/api";
export interface fetchProfileOptions {
user: string;
}
export async function fetchProfile(
agent: BskyAgent,
{ user }: fetchProfileOptions,
) {
return agent.getProfile({
actor: user,
});
}

View file

@ -0,0 +1,46 @@
import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
type AppBskyFeedDefs,
} from "@atproto/api";
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
if (AppBskyFeedPost.isRecord(post.record)) {
if (AppBskyEmbedRecord.isView(post.embed)) {
const { success: isView } = AppBskyEmbedRecord.validateView(
post.embed,
);
if (
isView &&
AppBskyEmbedRecord.isViewRecord(post.embed.record)
) {
const { success: isViewRecord } =
AppBskyEmbedRecord.validateViewRecord(post.embed.record);
if (isViewRecord) {
// @ts-expect-error For some reason the original post value is typed as {}
return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥ ${post.embed.record.value.text}`;
}
}
}
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
const { success: isView } =
AppBskyEmbedRecordWithMedia.validateView(post.embed);
if (
isView &&
AppBskyEmbedRecord.isViewRecord(post.embed.record.record)
) {
const { success: isViewRecord } =
AppBskyEmbedRecord.validateViewRecord(
post.embed.record.record,
);
if (isViewRecord) {
// @ts-expect-error For some reason the original post value is typed as {}
return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥ ${post.embed.record.record.value.text}`;
}
}
}
return post.record.text;
}
return "";
}

View file

@ -0,0 +1,58 @@
import {
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
type AppBskyFeedDefs,
} from "@atproto/api";
export function parseEmbedImage(post: AppBskyFeedDefs.PostView) {
if (AppBskyEmbedRecord.isView(post.embed)) {
const { success: isView } = AppBskyEmbedRecord.validateView(
post.embed,
);
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
const { success: isViewRecord } =
AppBskyEmbedRecord.validateViewRecord(post.embed.record);
if (
isViewRecord &&
post.embed.record.embeds &&
post.embed.record.embeds.every((v) =>
AppBskyEmbedImages.isView(v),
)
) {
const validImageView = post.embed.record.embeds.every((v) =>
AppBskyEmbedImages.validateView(v),
);
if (validImageView) {
// return post.embed.record.embeds[0].images[0].fullsize;
const embeds = post.embed.record
.embeds as AppBskyEmbedImages.View[];
return embeds.flatMap((e) => e.images.map((i) => i.fullsize));
}
}
}
}
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
const { success: isView } =
AppBskyEmbedRecordWithMedia.validateView(post.embed);
if (isView && AppBskyEmbedImages.isView(post.embed.media)) {
const { success: isImageView } = AppBskyEmbedImages.validateView(
post.embed.media,
);
if (isImageView) {
// return post.embed.media.images[0].fullsize;
return post.embed.media.images.map((i) => i.fullsize);
}
}
}
if (AppBskyEmbedImages.isView(post.embed)) {
const { success: isImageView } = AppBskyEmbedImages.validateView(
post.embed,
);
if (isImageView) {
// return post.embed.images[0].fullsize;
return post.embed.images.map((i) => i.fullsize);
}
}
return post.author.avatar ?? "";
}

76
src/main.ts Normal file
View file

@ -0,0 +1,76 @@
import "./util/checkEnv.ts";
import { BskyAgent } from "@atproto/api";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";
import { Redis } from "ioredis";
import { getOEmbed } from "./routes/getOEmbed.ts";
import { getPost } from "./routes/getPost.tsx";
import { getProfile } from "./routes/getProfile.tsx";
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
const redis = new Redis(6379, process.env.REDIS_HOSTNAME!);
const agent = new BskyAgent({
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
service: process.env.BSKY_SERVICE_URL!,
persistSession: async (_evt, session) => {
if (session) {
await redis.set("session", JSON.stringify(session));
}
},
});
const app = new Hono<HonoEnv>();
app.use("*", async (c, next) => {
const session = await redis.get("session");
try {
if (session) {
const login = await agent.resumeSession(JSON.parse(session));
if (!login.success) {
await agent.login({
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
identifier: process.env.BSKY_AUTH_USERNAME!,
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
password: process.env.BSKY_AUTH_PASSWORD!,
});
}
} else {
await agent.login({
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
identifier: process.env.BSKY_AUTH_USERNAME!,
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
password: process.env.BSKY_AUTH_PASSWORD!,
});
}
c.set("Agent", agent);
} catch (error) {
const err = new Error("Failed to login to Bluesky!", {
cause: error,
});
throw new HTTPException(500, {
message: `${err.message} \n\n ${err.cause} \n\n ${err.stack}`,
});
}
return next();
});
app.get("/profile/:user/post/:post", getPost);
app.get("/https://bsky.app/profile/:user/post/:post", getPost);
app.get("/profile/:user", getProfile);
app.get("/https://bsky.app/profile/:user", getProfile);
app.get("/oembed", getOEmbed);
serve(
{
...app,
port: Number(process.env.PORT ?? 8787),
},
(addr) => {
console.log(`Listening on http://localhost:${addr.port}`);
},
);

35
src/routes/getOEmbed.ts Normal file
View file

@ -0,0 +1,35 @@
import type { Handler } from "hono";
export enum OEmbedTypes {
Post = 1,
Profile = 2,
}
export const getOEmbed: Handler<HonoEnv, "/oembed"> = async (c) => {
const type = +(c.req.query("type") ?? 0);
const avatar = c.req.query("avatar");
const defaults = {
provider_name: "FixBluesky",
provider_url: "https://bsyy.app/",
thumbnail_url: avatar,
thumbnail_width: 1000,
thumbnail_height: 1000,
};
if (type === OEmbedTypes.Post) {
const { replies, reposts, likes } = c.req.query();
return c.json({
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
...defaults,
});
}
if (type === OEmbedTypes.Profile) {
const { follows, posts } = c.req.query();
return c.json({
author_name: `👤 ${follows} followers\n🗨 ${posts} skeets`,
...defaults,
});
}
return c.json(defaults, 400);
};

27
src/routes/getPost.tsx Normal file
View file

@ -0,0 +1,27 @@
import type { Handler } from "hono";
import { HTTPException } from "hono/http-exception";
import { Post } from "../components/Post.tsx";
import { fetchPost } from "../lib/fetchPostData.ts";
export const getPost: Handler<
HonoEnv,
| "/profile/:user/post/:post"
| "/https://bsky.app/profile/:user/post/:post"
> = async (c) => {
const { user, post } = c.req.param();
const agent = c.get("Agent");
const { data, success } = await fetchPost(agent, { user, post });
if (!success) {
throw new HTTPException(500, {
message: "Failed to fetch the post!",
});
}
return c.html(
<Post
post={data.posts[0]}
url={c.req.path}
appDomain={process.env.FIXBLUESKY_APP_DOMAIN ?? "bsyy.app"}
/>,
);
};

25
src/routes/getProfile.tsx Normal file
View file

@ -0,0 +1,25 @@
import type { Handler } from "hono";
import { HTTPException } from "hono/http-exception";
import { Profile } from "../components/Profile.tsx";
import { fetchProfile } from "../lib/fetchProfile.ts";
export const getProfile: Handler<
HonoEnv,
"/profile/:user" | "/https://bsky.app/profile/:user"
> = async (c) => {
const { user } = c.req.param();
const agent = c.get("Agent");
const { data, success } = await fetchProfile(agent, { user });
if (!success) {
throw new HTTPException(500, {
message: "Failed to fetch the profile!",
});
}
return c.html(
<Profile
profile={data}
url={c.req.path}
appDomain={process.env.FIXBLUESKY_APP_DOMAIN ?? "bsyy.app"}
/>,
);
};

15
src/util/checkEnv.ts Normal file
View file

@ -0,0 +1,15 @@
if (!process.env.REDIS_HOSTNAME) {
throw new Error("REDIS_HOSTNAME is not defined");
}
if (!process.env.BSKY_SERVICE_URL) {
throw new Error("BSKY_SERVICE_URL is not defined");
}
if (!process.env.BSKY_AUTH_USERNAME) {
throw new Error("BSKY_AUTH_USERNAME is not defined");
}
if (!process.env.BSKY_AUTH_PASSWORD) {
throw new Error("BSKY_AUTH_PASSWORD is not defined");
}

33
tsconfig.json Normal file
View file

@ -0,0 +1,33 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"importHelpers": false,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"lib": ["esnext"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"newLine": "lf",
"noEmit": true,
"noEmitHelpers": false,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"pretty": true,
"removeComments": false,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"target": "ESNext",
"useDefineForClassFields": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist/**/*"]
}

6694
yarn.lock Normal file

File diff suppressed because it is too large Load diff