feat: modernize and harden Lettia bot
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
tsconfig.json
|
||||||
+7
-8
@@ -1,13 +1,12 @@
|
|||||||
FROM node:22-alpine
|
FROM node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json /app
|
COPY --chown=node:node package.json package-lock.json ./
|
||||||
COPY package-lock.json /app
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
COPY .env /app
|
|
||||||
COPY tsconfig.json /app
|
|
||||||
COPY src /app/src
|
|
||||||
|
|
||||||
RUN npm i
|
COPY --chown=node:node src ./src
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
USER node
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# discord-lettia
|
||||||
|
|
||||||
|
A Discord message context-menu bot that converts message text into selectable Unicode font styles.
|
||||||
|
|
||||||
|
## Discord setup
|
||||||
|
|
||||||
|
Create a bot application with the Guilds, Guild Messages, and privileged Message Content intents.
|
||||||
|
The bot needs View Channel, Read Message History, and Send Messages permissions in channels where
|
||||||
|
Fontify is used.
|
||||||
|
|
||||||
|
Set the application credentials and start the bot:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp default.env .env
|
||||||
|
npm ci
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The global `Fontify` message command is refreshed at startup. In Discord, right-click a message,
|
||||||
|
open Apps, select Fontify, and choose a font. Only the user who opened the menu can choose from it.
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm ci
|
||||||
|
npm run check
|
||||||
|
docker build -t discord-lettia .
|
||||||
|
```
|
||||||
|
|
||||||
|
Secrets are supplied at runtime and are not copied into the Docker image.
|
||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
TOKEN = 0 # Your bot token
|
TOKEN= # Your bot token
|
||||||
CLIENT_ID = 0 # Your bot client ID
|
CLIENT_ID= # Your bot client ID
|
||||||
|
|||||||
Generated
+597
-269
File diff suppressed because it is too large
Load Diff
+9
-5
@@ -1,14 +1,18 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/index.ts"
|
"start": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "node --import tsx --test src/*_test.ts",
|
||||||
|
"check": "npm run typecheck && npm test"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord.js": "^14.16.3",
|
"discord.js": "14.27.0",
|
||||||
"dotenv": "^16.4.5"
|
"dotenv": "17.4.2",
|
||||||
|
"tsx": "4.23.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsx": "^4.19.1",
|
"@types/node": "24.10.15",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "7.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApplicationCommandType, ContextMenuCommandBuilder, ContextMenuCommandType } from "discord.js";
|
import { ApplicationCommandType, ContextMenuCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
new ContextMenuCommandBuilder()
|
new ContextMenuCommandBuilder()
|
||||||
.setName("Fontify")
|
.setName("Fontify")
|
||||||
.setType(ApplicationCommandType.Message as ContextMenuCommandType)
|
.setType(ApplicationCommandType.Message),
|
||||||
]
|
];
|
||||||
|
|||||||
+7
-9
@@ -1,6 +1,6 @@
|
|||||||
export class Font {
|
export class Font {
|
||||||
preprocesser?: (text: string) => string
|
preprocesser?: (text: string) => string;
|
||||||
letters: { [key: string]: number }
|
letters: { [key: string]: number };
|
||||||
constructor(patterns: { [key: string]: number | number[] }, preprocesser?: (text: string) => string) {
|
constructor(patterns: { [key: string]: number | number[] }, preprocesser?: (text: string) => string) {
|
||||||
this.preprocesser = preprocesser;
|
this.preprocesser = preprocesser;
|
||||||
this.letters = {};
|
this.letters = {};
|
||||||
@@ -9,26 +9,24 @@ export class Font {
|
|||||||
if (v.length === 1) return v.charCodeAt(0);
|
if (v.length === 1) return v.charCodeAt(0);
|
||||||
const [start, end] = v.split("-");
|
const [start, end] = v.split("-");
|
||||||
if (start === undefined || end === undefined || end.length !== 1 || start.length !== 1)
|
if (start === undefined || end === undefined || end.length !== 1 || start.length !== 1)
|
||||||
return
|
return;
|
||||||
return [...Array(end.charCodeAt(0) - start.charCodeAt(0) + 1).keys()].map((i) => start.charCodeAt(0) + i);
|
return [...Array(end.charCodeAt(0) - start.charCodeAt(0) + 1).keys()].map((i) => start.charCodeAt(0) + i);
|
||||||
}).filter((v) => v !== undefined);
|
}).filter((v) => v !== undefined);
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (Array.isArray(key)) {
|
if (Array.isArray(key)) {
|
||||||
if (Array.isArray(value)) for (let [i, v] of key.entries()) {
|
if (Array.isArray(value)) for (const [i, v] of key.entries()) {
|
||||||
if (value[i] === undefined) continue;
|
if (value[i] === undefined) continue;
|
||||||
this.letters[String.fromCharCode(v)] = value[i];
|
this.letters[String.fromCharCode(v)] = value[i];
|
||||||
}
|
}
|
||||||
else for (let [i, v] of key.entries()) {
|
else for (const [i, v] of key.entries()) {
|
||||||
this.letters[String.fromCharCode(v)] = value + i;
|
this.letters[String.fromCharCode(v)] = value + i;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let v = Array.isArray(value) ? value[0] : value;
|
const v = Array.isArray(value) ? value[0] : value;
|
||||||
this.letters[String.fromCharCode(key)] = v;
|
this.letters[String.fromCharCode(key)] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(Object.entries(this.letters).map(([k, v]) => `${k}`).join(""));
|
|
||||||
console.log(Object.entries(this.letters).map(([k, v]) => `${String.fromCodePoint(v)}`).join(""));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-24
@@ -1,47 +1,73 @@
|
|||||||
import 'dotenv/config'
|
import "dotenv/config";
|
||||||
const TOKEN = process.env.TOKEN;
|
const TOKEN = process.env.TOKEN;
|
||||||
const CLIENT_ID = process.env.CLIENT_ID;
|
const CLIENT_ID = process.env.CLIENT_ID;
|
||||||
if (!TOKEN || !CLIENT_ID) {
|
if (!TOKEN || !CLIENT_ID) {
|
||||||
console.error(`No ${TOKEN ? "CLIENT_ID" : "TOKEN"} provided`);
|
console.error(`Missing required environment variable: ${TOKEN ? "CLIENT_ID" : "TOKEN"}`);
|
||||||
process.exit();
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Client,
|
||||||
|
Events,
|
||||||
|
GatewayIntentBits,
|
||||||
REST,
|
REST,
|
||||||
Routes,
|
Routes,
|
||||||
Client,
|
|
||||||
GatewayIntentBits,
|
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
|
|
||||||
import commands from "./context_commands.ts";
|
import commands from "./context_commands.ts";
|
||||||
const rest = new REST({ version: "10" }).setToken(TOKEN);
|
const rest = new REST({ version: "10" }).setToken(TOKEN);
|
||||||
try {
|
console.log("Refreshing application commands...");
|
||||||
console.log("refreshing slash commands...");
|
await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands });
|
||||||
await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands });
|
console.log("Application commands refreshed.");
|
||||||
console.log("OK");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.GuildMessageReactions,
|
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("ready", () => {
|
client.once(Events.ClientReady, (readyClient) => {
|
||||||
if (!client.user) {
|
console.log(
|
||||||
console.error("Failed to login");
|
`Logged in as ${readyClient.user.tag}; serving ${readyClient.guilds.cache.size} guild(s).`,
|
||||||
process.exit();
|
);
|
||||||
}
|
readyClient.user.setActivity("kawaii fonts!");
|
||||||
console.log(`Logged in as ${client.user.tag}!`);
|
|
||||||
client.user.setActivity("kawaii fonts!");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
import interactionCreate from './listener/interaction_create.ts';
|
import interactionCreate from "./listener/interaction_create.ts";
|
||||||
client.on("interactionCreate", interactionCreate);
|
client.on(Events.InteractionCreate, interactionCreate);
|
||||||
|
client.on(Events.Error, (error) => console.error("Discord client error:", error));
|
||||||
|
client.on(Events.Warn, (message) => console.warn("Discord client warning:", message));
|
||||||
|
client.on(Events.ShardError, (error, shardId) => {
|
||||||
|
console.error(`Discord shard ${shardId} error:`, error);
|
||||||
|
});
|
||||||
|
client.on(Events.ShardDisconnect, (event, shardId) => {
|
||||||
|
console.warn(`Discord shard ${shardId} disconnected with code ${event.code}.`);
|
||||||
|
});
|
||||||
|
client.on(Events.ShardReconnecting, (shardId) => {
|
||||||
|
console.warn(`Discord shard ${shardId} reconnecting.`);
|
||||||
|
});
|
||||||
|
client.on(Events.ShardResume, (shardId, replayedEvents) => {
|
||||||
|
console.log(`Discord shard ${shardId} resumed; replayed ${replayedEvents} event(s).`);
|
||||||
|
});
|
||||||
|
|
||||||
client.login(TOKEN);
|
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
||||||
|
process.once(signal, () => {
|
||||||
|
console.log(`Received ${signal}; closing Discord connection.`);
|
||||||
|
client.destroy();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
console.error("Unhandled promise rejection:", reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
console.error("Uncaught exception:", error);
|
||||||
|
client.destroy();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.login(TOKEN);
|
||||||
|
|||||||
@@ -1,65 +1,70 @@
|
|||||||
import { CacheType, Interaction } from "discord.js";
|
import { CacheType, Interaction } from "discord.js";
|
||||||
|
|
||||||
import selection_compoent from "../selection_compoent.ts";
|
import fontSelectionComponent, {
|
||||||
|
parseFontSelectionId,
|
||||||
|
} from "../selection_component.ts";
|
||||||
import list from "../fontlist.ts";
|
import list from "../fontlist.ts";
|
||||||
|
import { formatTranslation, translateText } from "../translation.ts";
|
||||||
|
|
||||||
const proc_cache: {
|
const FONTIFY_COMMAND = "Fontify";
|
||||||
[id: string]: {
|
|
||||||
resolve?: (value: string) => void,
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
export default async (interaction: Interaction<CacheType>) => {
|
export default async function interactionCreate(
|
||||||
if (interaction.isMessageContextMenuCommand()) {
|
interaction: Interaction<CacheType>,
|
||||||
if (interaction.commandName === "Fontify") {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await interaction.deferReply({ ephemeral: false });
|
if (
|
||||||
await interaction.followUp({
|
interaction.isMessageContextMenuCommand() &&
|
||||||
content: "-# " + interaction.targetMessage.id,
|
interaction.commandName === FONTIFY_COMMAND
|
||||||
components: [selection_compoent],
|
) {
|
||||||
})
|
await interaction.reply({
|
||||||
proc_cache[interaction.targetMessage.id] = { text: interaction.targetMessage.content };
|
content: `-# ${interaction.targetMessage.id}`,
|
||||||
new Promise<string>((resolve) => {
|
components: [
|
||||||
proc_cache[interaction.targetMessage.id].resolve = resolve;
|
fontSelectionComponent(interaction.targetMessage.id, interaction.user.id),
|
||||||
}).then((value) => {
|
],
|
||||||
interaction.editReply({
|
});
|
||||||
content: "```\n" + value + "\n```",
|
return;
|
||||||
components: [],
|
|
||||||
});
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (interaction.isStringSelectMenu()) {
|
|
||||||
if (interaction.customId === "font_selection") {
|
if (!interaction.isStringSelectMenu()) return;
|
||||||
try {
|
const selection = parseFontSelectionId(interaction.customId);
|
||||||
await interaction.reply({
|
if (!selection) return;
|
||||||
content: "Processing...",
|
|
||||||
ephemeral: true,
|
if (selection.requesterId !== interaction.user.id) {
|
||||||
});
|
await interaction.reply({
|
||||||
const selected = interaction.values[0];
|
content: "Only the person who opened Fontify can choose this font.",
|
||||||
const font = list.find((f) => f.value === selected)?.font;
|
ephemeral: true,
|
||||||
if (!font) {
|
});
|
||||||
await interaction.followUp({
|
return;
|
||||||
content: "Invalid font selected!",
|
}
|
||||||
});
|
|
||||||
return;
|
const selected = interaction.values[0];
|
||||||
}
|
const font = list.find((item) => item.value === selected)?.font;
|
||||||
const id = interaction.message.content.slice(3);
|
if (!font) {
|
||||||
const text = proc_cache[id].text;
|
await interaction.reply({ content: "That font is no longer available.", ephemeral: true });
|
||||||
// console.log(text);
|
return;
|
||||||
const result = text.split("").map((c) => font.letters[c] ? String.fromCodePoint(font.letters[c]) : c).join("");
|
}
|
||||||
// console.log(result);
|
|
||||||
await interaction.deleteReply();
|
const sourceMessage = await interaction.message.channel.messages.fetch(
|
||||||
if (proc_cache[id].resolve) proc_cache[id].resolve(result);
|
selection.sourceMessageId,
|
||||||
else console.error(`Resolve function not found for id: ${id}`);
|
);
|
||||||
} catch (e) {
|
const result = translateText(sourceMessage.content, font);
|
||||||
console.error(e);
|
await interaction.update({ content: formatTranslation(result), components: [] });
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Interaction ${interaction.id} failed in guild ${interaction.guildId ?? "DM"}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
if (!interaction.isRepliable()) return;
|
||||||
|
|
||||||
|
const payload = { content: "Fontify failed. Please try again.", ephemeral: true } as const;
|
||||||
|
if (interaction.deferred || interaction.replied) {
|
||||||
|
await interaction.followUp(payload).catch((replyError) => {
|
||||||
|
console.error(`Failed to report interaction ${interaction.id} error:`, replyError);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await interaction.reply(payload).catch((replyError) => {
|
||||||
|
console.error(`Failed to report interaction ${interaction.id} error:`, replyError);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js';
|
|
||||||
|
|
||||||
import list from './fontlist.ts';
|
|
||||||
|
|
||||||
export default new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
|
||||||
new StringSelectMenuBuilder()
|
|
||||||
.setCustomId('font_selection')
|
|
||||||
.setPlaceholder('Select a font')
|
|
||||||
.addOptions(list.map(({ label, value }) => ({ label, value })))
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ActionRowBuilder, StringSelectMenuBuilder } from "discord.js";
|
||||||
|
|
||||||
|
import list from "./fontlist.ts";
|
||||||
|
|
||||||
|
const CUSTOM_ID_PREFIX = "font_selection";
|
||||||
|
|
||||||
|
export default function fontSelectionComponent(
|
||||||
|
sourceMessageId: string,
|
||||||
|
requesterId: string,
|
||||||
|
): ActionRowBuilder<StringSelectMenuBuilder> {
|
||||||
|
return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||||
|
new StringSelectMenuBuilder()
|
||||||
|
.setCustomId(`${CUSTOM_ID_PREFIX}:${sourceMessageId}:${requesterId}`)
|
||||||
|
.setPlaceholder("Select a font")
|
||||||
|
.addOptions(list.map(({ label, value }) => ({ label, value }))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFontSelectionId(
|
||||||
|
customId: string,
|
||||||
|
): { sourceMessageId: string; requesterId: string } | undefined {
|
||||||
|
const match = customId.match(/^font_selection:(\d{17,20}):(\d{17,20})$/);
|
||||||
|
if (!match) return undefined;
|
||||||
|
return { sourceMessageId: match[1], requesterId: match[2] };
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
|
||||||
|
import { parseFontSelectionId } from "./selection_component.ts";
|
||||||
|
|
||||||
|
test("parseFontSelectionId extracts source and requester IDs", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
parseFontSelectionId("font_selection:12345678901234567:12345678901234567890"),
|
||||||
|
{
|
||||||
|
sourceMessageId: "12345678901234567",
|
||||||
|
requesterId: "12345678901234567890",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseFontSelectionId rejects unrelated or malformed component IDs", () => {
|
||||||
|
assert.equal(parseFontSelectionId("other:12345678901234567:12345678901234567"), undefined);
|
||||||
|
assert.equal(parseFontSelectionId("font_selection:not-an-id:12345678901234567"), undefined);
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Font } from "./font.ts";
|
||||||
|
|
||||||
|
const MAX_DISCORD_MESSAGE_LENGTH = 2_000;
|
||||||
|
const CODE_BLOCK_PREFIX = "```\n";
|
||||||
|
const CODE_BLOCK_SUFFIX = "\n```";
|
||||||
|
|
||||||
|
export function translateText(text: string, font: Font): string {
|
||||||
|
const input = font.preprocesser?.(text) ?? text;
|
||||||
|
return [...input]
|
||||||
|
.map((character) => {
|
||||||
|
const codePoint = font.letters[character];
|
||||||
|
return codePoint === undefined ? character : String.fromCodePoint(codePoint);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTranslation(value: string): string {
|
||||||
|
const escaped = value.replaceAll("```", "``\u200b`");
|
||||||
|
const available = MAX_DISCORD_MESSAGE_LENGTH - CODE_BLOCK_PREFIX.length -
|
||||||
|
CODE_BLOCK_SUFFIX.length;
|
||||||
|
if (escaped.length <= available) {
|
||||||
|
return `${CODE_BLOCK_PREFIX}${escaped}${CODE_BLOCK_SUFFIX}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let truncated = escaped.slice(0, available - 1);
|
||||||
|
const finalCodeUnit = truncated.charCodeAt(truncated.length - 1);
|
||||||
|
if (finalCodeUnit >= 0xD800 && finalCodeUnit <= 0xDBFF) truncated = truncated.slice(0, -1);
|
||||||
|
return `${CODE_BLOCK_PREFIX}${truncated}…${CODE_BLOCK_SUFFIX}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
|
||||||
|
import { Font } from "./font.ts";
|
||||||
|
import { formatTranslation, translateText } from "./translation.ts";
|
||||||
|
|
||||||
|
test("translateText replaces configured characters and preserves the rest", () => {
|
||||||
|
const font = new Font({ "a-c": 0x1D5EE });
|
||||||
|
assert.equal(translateText("abc!", font), "𝗮𝗯𝗰!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("translateText applies a font preprocessor", () => {
|
||||||
|
const font = new Font({ a: 0x1D5EE }, (value) => value.toLowerCase());
|
||||||
|
assert.equal(translateText("A", font), "𝗮");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatTranslation stays within Discord's message limit", () => {
|
||||||
|
const output = formatTranslation("𝗮".repeat(2_000));
|
||||||
|
assert.ok(output.length <= 2_000);
|
||||||
|
assert.match(output, /…\n```$/);
|
||||||
|
});
|
||||||
+5
-6
@@ -4,17 +4,16 @@
|
|||||||
"lib": [
|
"lib": [
|
||||||
"ES2023"
|
"ES2023"
|
||||||
],
|
],
|
||||||
"module": "node16",
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noEmit": true,
|
"noEmit": true
|
||||||
|
|
||||||
"outDir": "./dist",
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.ts"
|
||||||
, "src/font_translater.js" ]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user