This commit is contained in:
암냥 2026-04-15 00:56:08 +09:00
commit 6786cb6148
No known key found for this signature in database
31 changed files with 2352 additions and 0 deletions

34
apps/backend/Dockerfile Normal file
View file

@ -0,0 +1,34 @@
FROM oven/bun AS build
WORKDIR /app
# Cache packages installation
COPY package.json package.json
COPY bun.lock bun.lock
RUN bun install
COPY ./src ./src
ENV NODE_ENV=production
RUN bun build \
--compile \
--minify-whitespace \
--minify-syntax \
--outfile server \
src/index.ts
FROM gcr.io/distroless/base
USER mizuki:mizuki
WORKDIR /app
COPY --from=build --chown=mizuki:mizuki /app/server server
ENV NODE_ENV=production
CMD ["./server"]
EXPOSE 3000

15
apps/backend/README.md Normal file
View file

@ -0,0 +1,15 @@
# Elysia with Bun runtime
## Getting Started
To get started with this template, simply paste this command into your terminal:
```bash
bun create elysia ./elysia-example
```
## Development
To start the development server run:
```bash
bun run dev
```
Open http://localhost:3000/ with your browser to see the result.

56
apps/backend/bun.lock Normal file
View file

@ -0,0 +1,56 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "elysia",
"dependencies": {
"elysia": "latest",
},
"devDependencies": {
"bun-types": "latest",
},
},
},
"packages": {
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
}
}

18
apps/backend/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "elysia",
"version": "1.0.50",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1030.0",
"@elysiajs/openapi": "^1.4.14",
"elysia": "latest",
"mongoose": "^9.4.1"
},
"devDependencies": {
"bun-types": "latest"
},
"module": "src/index.js"
}

397
apps/backend/src/index.ts Normal file
View file

@ -0,0 +1,397 @@
import { Elysia, t } from "elysia";
import config from "../config.toml";
import { S3Client } from "bun";
import {
HeadObjectCommand,
PutObjectCommand,
S3Client as AwsS3Client,
} from "@aws-sdk/client-s3";
import * as mongoose from "mongoose";
import openapi from "@elysiajs/openapi";
await mongoose.connect(config.mongodb.uri);
const mediaUploadSchema = new mongoose.Schema({
tweetId: { type: String, required: true },
tweet: { type: Object, required: true },
mediaIndex: { type: Number, required: true },
mediaUrl: { type: String, required: true },
s3Key: { type: String, required: true },
tags: { type: [String], default: [] },
author: { type: String, required: true },
}, {
timestamps: true,
});
const tagSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
usageCount: { type: Number, default: 0 },
lastUsedAt: { type: Date, default: Date.now },
}, {
timestamps: true,
});
const MediaUpload = mongoose.models.MediaUpload || mongoose.model("MediaUpload", mediaUploadSchema);
const Tag = mongoose.models.Tag || mongoose.model("Tag", tagSchema);
const inFlightUploads = new Set<string>();
const client = new S3Client({
accessKeyId: config.s3.access_key,
secretAccessKey: config.s3.secret_key,
bucket: config.s3.bucket,
endpoint: config.s3.endpoint,
});
const awsClient = new AwsS3Client({
region: "auto",
endpoint: config.s3.endpoint,
forcePathStyle: true,
credentials: {
accessKeyId: config.s3.access_key,
secretAccessKey: config.s3.secret_key,
},
});
async function checkTweetData(url: string, selected: Array<boolean>) {
// get tweet id from url
const match = url.match(/\/status\/(\d+)/);
if (!match) {
throw new Error("Invalid tweet URL");
}
const tweetId = match[1];
// find in mongodb if there is already a record with the same tweet id and media index in selected
const existing = await MediaUpload.findOne({ tweetId, mediaIndex: { $in: selected.map((s, i) => s ? i : -1).filter(i => i >= 0) } });
if (existing) {
return true;
}
return null;
}
async function fetchTweetData(url: string) {
const apiUrl = `https://api.fxtwitter.com/${url.replace(/^https?:\/\/(www\.)?(x\.com|twitter\.com|fxtwitter\.com|fixupx\.com|vxwitter\.com)\//, "")}`;
const response = await fetch(apiUrl);
if (response.ok) {
// const dataText = await response.text();
// console.log("Raw API response:", dataText);
const data = await response.json();
return data;
} else {
throw new Error(`Failed to fetch tweet data: ${response.status} ${response.statusText}`);
}
}
function makeS3FileName(authorId: string, tweetId: string, mediaUrl: string, index: number) {
const rawName = mediaUrl.split("/").pop() || `media_${Date.now()}_${index}`;
const withoutQuery = rawName.split("?")[0]?.split("#")[0] || `media_${Date.now()}_${index}`;
const safeName = withoutQuery.replace(/[^a-zA-Z0-9._-]/g, "_");
return `twitter/${authorId}/${tweetId}/${safeName || `media_${Date.now()}_${index}`}`;
}
async function uploadToS3WithRetry(fileName: string, mediaUrl: string, maxRetry = 3) {
async function existsInS3(key: string) {
try {
return await client.exists(key);
} catch {
try {
await awsClient.send(new HeadObjectCommand({
Bucket: config.s3.bucket,
Key: key,
}));
return true;
} catch {
return false;
}
}
}
async function writeToS3(key: string, body: Uint8Array, mediaType?: string | null) {
try {
await client.write(key, body);
return;
} catch (bunWriteError) {
console.warn(`[S3 bun write failed, fallback to aws-sdk] key=${key}`, bunWriteError);
await awsClient.send(new PutObjectCommand({
Bucket: config.s3.bucket,
Key: key,
Body: body,
ContentType: mediaType ?? undefined,
}));
}
}
async function recoverByPollingExists(reason: string) {
for (let probe = 1; probe <= 4; probe++) {
await Bun.sleep(probe * 600);
try {
if (await existsInS3(fileName)) {
console.warn(`[S3 upload recovered-${reason}] key=${fileName} probe=${probe}`);
return true;
}
} catch (existsError) {
console.error(`[S3 exists probe failed] key=${fileName} probe=${probe}`, existsError);
}
}
return false;
}
let lastError: unknown;
for (let attempt = 1; attempt <= maxRetry; attempt++) {
try {
const response = await fetch(mediaUrl);
if (!response.ok) {
throw new Error(`Failed to fetch media from ${mediaUrl}: ${response.status} ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
await writeToS3(fileName, buffer, response.headers.get("content-type"));
return;
} catch (error) {
lastError = error;
console.error(`[S3 upload attempt ${attempt}/${maxRetry}] key=${fileName} url=${mediaUrl}`, error);
const errorCode =
typeof error === "object" && error !== null && "code" in error
? String((error as { code?: unknown }).code)
: "";
// Some S3 providers return UnknownError even when the object is eventually persisted.
if (errorCode === "UnknownError") {
if (await recoverByPollingExists("unknown")) {
return;
}
}
if (attempt < maxRetry) {
await Bun.sleep(attempt * 800);
}
}
}
// Final guard: do one last exists check before surfacing failure.
if (await recoverByPollingExists("final")) {
return;
}
throw lastError;
}
function normalizeTags(tags: string[]) {
const unique = new Set(
tags
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0),
);
if (unique.size === 0) {
return ["미분류"];
}
return Array.from(unique);
}
function normalizeQueryTags(tags?: string | string[]) {
if (!tags) {
return [];
}
const rawTags = Array.isArray(tags) ? tags : [tags];
return normalizeTags(rawTags);
}
function buildUploadKey(url: string, selected: boolean[]) {
const match = url.match(/\/status\/(\d+)/);
const tweetId = match?.[1] ?? url;
const selectedIndices = selected
.map((isSelected, index) => (isSelected ? index : -1))
.filter((index) => index >= 0)
.join(",");
return `${tweetId}:${selectedIndices}`;
}
async function saveTags(tags: string[]) {
await Promise.all(
tags.map((tag) =>
Tag.updateOne(
{ name: tag },
{
$inc: { usageCount: 1 },
$set: { lastUsedAt: new Date() },
$setOnInsert: { name: tag },
},
{ upsert: true },
),
),
);
}
const app = new Elysia()
.use(openapi())
.get("/", () => "어...")
.get("/total", async ({ query }) => {
const filterTags = normalizeQueryTags(query.tags);
const filter = filterTags.length > 0
? { tags: { $in: filterTags } }
: {};
const count = await MediaUpload.countDocuments(filter);
return count;
}, {
query: t.Object({
tags: t.Optional(t.Union([t.String(), t.Array(t.String())])),
})
})
.get("/list", async ({ query }) => {
const page = query.page;
const pageSize = 20;
const filterTags = normalizeQueryTags(query.tags);
const filter = filterTags.length > 0
? { tags: { $in: filterTags } }
: {};
const uploads = await MediaUpload.find(filter)
.sort({ createdAt: -1 })
.skip((page - 1) * pageSize)
.limit(pageSize);
return uploads;
}, {
query: t.Object({
page: t.Number({default: 1, minimum: 1}),
tags: t.Optional(t.Union([t.String(), t.Array(t.String())])),
})
})
.get("/tags", async () => {
const tags = await Tag.find().sort({ usageCount: -1, lastUsedAt: -1 });
return tags;
})
.post("/upload", async ({ body, status }) => {
if (body.url.startsWith("https://www.pixiv.net/")) {
return "저는 저능아입니다";
} else if (body.url.startsWith("https://x.com/") || body.url.startsWith("https://twitter.com/") || body.url.startsWith("https://fxtwitter.com/") || body.url.startsWith("https://fixupx.com/") || body.url.startsWith("https://vxwitter.com/")) {
const requestId = crypto.randomUUID();
const uploadKey = buildUploadKey(body.url, body.selected);
if (inFlightUploads.has(uploadKey)) {
console.warn(`[Upload skipped-duplicate] requestId=${requestId} key=${uploadKey}`);
return status(202, "이미 처리 중인 업로드입니다.");
}
inFlightUploads.add(uploadKey);
console.log(`[Upload started] requestId=${requestId} key=${uploadKey}`);
if (await checkTweetData(body.url, body.selected)) {
inFlightUploads.delete(uploadKey);
console.log(`[Upload skipped-existing] requestId=${requestId} key=${uploadKey}`);
return "이미 저장된 트윗입니다.";
}
try {
const tweetData = await fetchTweetData(body.url);
if (tweetData.tweet) {
const media = tweetData.tweet.media.photos || [];
if (media.length > 0) {
const mediaUrls = media.map((m: any) => m.url);
// Upload to S3
let savedCount = 0;
let failedCount = 0;
const hasExplicitSelection = body.selected.length > 0;
for (const [index, url] of mediaUrls.entries()) {
const isSelected = hasExplicitSelection
? body.selected[index] === true
: true;
if (!isSelected) {
continue;
}
const fileName = makeS3FileName(tweetData.tweet.author.id, tweetData.tweet.id, url, index);
try {
if (await client.exists(fileName)) {
console.log(`File ${fileName} already exists in S3, skipping upload.`);
} else {
await uploadToS3WithRetry(fileName, url);
console.log(`Uploaded ${fileName} to S3`);
}
const { media: _media, ...tweetWithoutMedia } = tweetData.tweet;
const normalizedTags = normalizeTags(body.tag || ["미분류"]);
await MediaUpload.create({
type: "twitter",
tweetId: tweetData.tweet.id,
tweet: tweetWithoutMedia,
mediaIndex: index,
mediaUrl: `${config.s3.endpoint}/${config.s3.bucket}/${fileName}`,
s3Key: fileName,
tags: normalizedTags,
author: body.author ? body.author : tweetData.tweet.author.name,
});
await saveTags(normalizedTags);
savedCount += 1;
} catch (error) {
failedCount += 1;
console.error(`[Upload failed] index=${index} url=${url} key=${fileName}`, error);
}
}
if (savedCount === 0 && failedCount === 0) {
console.warn("No media uploaded: selected[] did not include any upload target.");
}
console.log(`Saved ${savedCount} media records to MongoDB. Failed: ${failedCount}`);
} else {
console.log("No media found in the tweet.");
}
}
console.log(`[Upload finished] requestId=${requestId} key=${uploadKey}`);
console.log(tweetData);
} catch (error) {
console.error(`[Upload aborted] requestId=${requestId} key=${uploadKey}`, error);
console.error(error);
return status(500, "Failed to fetch tweet data");
} finally {
inFlightUploads.delete(uploadKey);
}
} else {
return status(400, "어...");
}
return "아...";
}, {
body: t.Object({
url: t.String(),
tag: t.Optional(t.Array(t.String({default: "미분류"}))),
author: t.Optional(t.String()),
selected: t.Array(t.Boolean()),
})
})
.get("/fetch/tweet", async ({ query, status }) => {
try {
const tweetData = await fetchTweetData(query.url);
return tweetData;
} catch (error) {
console.error(error);
return status(500, "Failed to fetch tweet data");
}
}, {
query: t.Object({
url: t.String(),
})
})
.listen(config.server.port)
;
console.log(
`🎀 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
);

103
apps/backend/tsconfig.json Normal file
View file

@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES2022", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}