Commit f85f6ea5 authored by nanahira's avatar nanahira

first

parent 5568a190
webpack.config.js
dist/*
build/*
test/*
\ No newline at end of file
...@@ -105,5 +105,6 @@ dist ...@@ -105,5 +105,6 @@ dist
/build /build
/output /output
/dest
/config.yaml /config.yaml
.idea .idea
stages: stages:
- build - build
- combine
- deploy - deploy
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
CONTAINER_TEST_ARM_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
CONTAINER_TEST_X86_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
before_script: build:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build-x86:
stage: build stage: build
tags: tags:
- docker - linux
script: script:
- docker build --pull -t $CONTAINER_TEST_X86_IMAGE . - npm ci
- docker push $CONTAINER_TEST_X86_IMAGE - npm run build
artifacts:
paths:
- dist/
build-arm: upload_to_minio:
stage: build
tags:
- docker-arm
script:
- docker build --pull -t $CONTAINER_TEST_ARM_IMAGE .
- docker push $CONTAINER_TEST_ARM_IMAGE
combine:
stage: combine
tags:
- docker
script:
- docker pull $CONTAINER_TEST_X86_IMAGE
- docker pull $CONTAINER_TEST_ARM_IMAGE
- docker manifest create $CONTAINER_TEST_IMAGE --amend $CONTAINER_TEST_X86_IMAGE --amend $CONTAINER_TEST_ARM_IMAGE
- docker manifest push $CONTAINER_TEST_IMAGE
deploy_latest:
stage: deploy stage: deploy
tags: dependencies:
- docker - build
tags:
- linux
script: script:
- docker pull $CONTAINER_TEST_IMAGE - aws s3 --endpoint=https://minio.mycard.moe:9000 sync --delete dist/ s3://nanahira/koishi-plugin/hisoutensoku-jammer
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
- docker push $CONTAINER_RELEASE_IMAGE
only: only:
- master - master
deploy_tag: deploy_npm:
stage: deploy stage: deploy
tags: dependencies:
- docker - build
variables: tags:
CONTAINER_TAG_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG - linux
script: script:
- docker pull $CONTAINER_TEST_IMAGE - apt update;apt -y install coreutils
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_TAG_IMAGE - echo $NPMRC | base64 --decode > ~/.npmrc
- docker push $CONTAINER_TAG_IMAGE - npm publish . || true
only: only:
- tags - master
...@@ -5,4 +5,10 @@ ...@@ -5,4 +5,10 @@
/config.yaml /config.yaml
.idea .idea
.dockerignore .dockerignore
Dockerfile Dockerfile
\ No newline at end of file /test
/src
/test-playbook*
/playbooks
/.eslint*
/webpack.config.js
FROM node:buster-slim
LABEL Author="Nanahira <nanahira@momobako.com>"
RUN apt update && apt -y install python3 build-essential && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /usr/src/app
COPY ./package*.json ./
RUN npm ci
COPY . ./
RUN npm run build
CMD ["npm", "run", "start"]
This diff is collapsed.
# koishi-plugin-act # koishi-plugin-init
#!/bin/bash #!/bin/bash
npm install --save \ npm install --save \
lodash source-map-support
npm install --save-dev \ npm install --save-dev \
lodash \
@types/node \ @types/node \
@types/lodash \ @types/lodash \
typescript \ typescript \
...@@ -11,4 +12,9 @@ npm install --save-dev \ ...@@ -11,4 +12,9 @@ npm install --save-dev \
'eslint@^7.30.0' \ 'eslint@^7.30.0' \
'eslint-config-prettier@^8.3.0' \ 'eslint-config-prettier@^8.3.0' \
'eslint-plugin-prettier@^3.4.0' \ 'eslint-plugin-prettier@^3.4.0' \
prettier prettier \
raw-loader \
ts-loader \
webpack \
webpack-cli \
koishi-core
This diff is collapsed.
...@@ -2,27 +2,42 @@ ...@@ -2,27 +2,42 @@
"name": "koishi-plugin-act", "name": "koishi-plugin-act",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "build/index.js", "main": "dist/index.js",
"dependencies": { "dependencies": {
"class-transformer": "^0.4.0", "source-map-support": "^0.5.19"
"delay": "^5.0.0", },
"lodash": "^4.17.21", "peerDependencies": {
"mustache": "^4.2.0" "koishi-adapter-onebot": "^3.1.0",
"koishi-core": "^3.13.0"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.14.172", "@types/lodash": "^4.14.172",
"@types/mustache": "^4.1.2", "@types/mustache": "^4.1.2",
"@types/node": "^16.4.10", "@types/node": "^16.4.11",
"@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0", "@typescript-eslint/parser": "^4.29.0",
"class-transformer": "^0.4.0",
"delay": "^5.0.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"koishi-adapter-onebot": "^3.1.0",
"koishi-core": "^3.13.0",
"load-json-file": "^6.2.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mustache": "^4.2.0",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"typescript": "^4.3.5" "raw-loader": "^4.0.2",
"reflect-metadata": "^0.1.13",
"ts-loader": "^9.2.5",
"typescript": "^4.3.5",
"webpack": "^5.48.0",
"webpack-cli": "^4.7.2"
}, },
"scripts": { "scripts": {
"lint": "echo \"Error: no test specified\" && exit 1" "lint": "eslint --fix .",
"build": "webpack"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
......
import 'source-map-support/register';
import type { Context } from 'koishi-core';
import { Config, MyPlugin } from './plugin';
export { Config } from './plugin';
export const name = 'act-index';
export function apply(ctx: Context, config: Config) {
ctx.plugin(new MyPlugin(), config);
}
...@@ -4,12 +4,10 @@ export class Character { ...@@ -4,12 +4,10 @@ export class Character {
playFun: (line: PlaybookLine, text: string) => Promise<void>; playFun: (line: PlaybookLine, text: string) => Promise<void>;
constructor( constructor(
public id: number, public id: number,
public name: string, public name: string, //public displayName?: string,
public displayName?: string, ) {}
) { getDisplayName() {
if (!displayName) { return /*this.displayName || */ this.name;
this.displayName = name;
}
} }
async play(line: PlaybookLine, text: string) { async play(line: PlaybookLine, text: string) {
if (this.playFun) { if (this.playFun) {
......
import { PlaybookLine } from './PlaybookLine'; import { PlaybookLine } from './PlaybookLine';
import { matchHeader, TuneTimeOptions } from './utility'; import { matchHeader, TuneTimeOptions } from './utility';
import { Character } from './Character'; import { Character } from './Character';
import { Transform } from 'class-transformer'; import { Type } from 'class-transformer';
import 'reflect-metadata';
export class Playbook { export class Playbook {
@Type(() => Character)
characters: Character[] = []; characters: Character[] = [];
@Type(() => PlaybookLine)
lines: PlaybookLine[] = []; lines: PlaybookLine[] = [];
constructor(public characterNameMap: Record<string, string> = {}) {} constructor(/*public characterNameMap: Record<string, string> = {}*/) {}
getCharacterDisplayName(name: string) { /*getCharacterDisplayName(name: string) {
if (this.characterNameMap[name]) { if (this.characterNameMap[name]) {
return this.characterNameMap[name]; return this.characterNameMap[name];
} else { } else {
return name; return name;
} }
} }*/
resetTiming() { resetTiming() {
const firstLineTime = this.lines[0].time; const firstLineTime = this.lines[0].time;
if (!firstLineTime) { if (!firstLineTime) {
...@@ -28,13 +31,7 @@ export class Playbook { ...@@ -28,13 +31,7 @@ export class Playbook {
let currentCharacterIndex = 0; let currentCharacterIndex = 0;
for (const line of this.lines) { for (const line of this.lines) {
if (!characterMap.has(line.name)) { if (!characterMap.has(line.name)) {
this.characters.push( this.characters.push(new Character(currentCharacterIndex++, line.name));
new Character(
currentCharacterIndex++,
line.name,
this.getCharacterDisplayName(line.name),
),
);
characterMap.set(line.name, true); characterMap.set(line.name, true);
} }
} }
...@@ -51,7 +48,7 @@ export class Playbook { ...@@ -51,7 +48,7 @@ export class Playbook {
continue; continue;
} }
if (matchHeader(line)) { if (matchHeader(line)) {
if (lastLineIndex > 0 && i - lastLineIndex > 1) { if (lastLineIndex >= 0 && i - lastLineIndex > 1) {
this.lines.push( this.lines.push(
PlaybookLine.FromLines(lines.slice(lastLineIndex, i)), PlaybookLine.FromLines(lines.slice(lastLineIndex, i)),
); );
...@@ -68,7 +65,7 @@ export class Playbook { ...@@ -68,7 +65,7 @@ export class Playbook {
this.resetTiming(); this.resetTiming();
this.fixupCharacters(); this.fixupCharacters();
} }
private findPreviousSelfLine(i) { private findPreviousSelfLine(i: number) {
const currentLine = this.lines[i]; const currentLine = this.lines[i];
for (let j = i - 1; j >= 0; j--) { for (let j = i - 1; j >= 0; j--) {
const tryLine = this.lines[j]; const tryLine = this.lines[j];
...@@ -87,7 +84,7 @@ export class Playbook { ...@@ -87,7 +84,7 @@ export class Playbook {
const currentLine = this.lines[i]; const currentLine = this.lines[i];
const previousLine = this.lines[i - 1]; const previousLine = this.lines[i - 1];
const selfPreviousLine = this.findPreviousSelfLine(i); const selfPreviousLine = this.findPreviousSelfLine(i);
const randomAddValue = Math.ceil(Math.random() * options.maxRandomDelay); const randomAddValue = Math.random() * options.maxRandomDelay;
const possibleReadTiming = const possibleReadTiming =
previousLine.time + previousLine.time +
options.readSlope * previousLine.renderContent(this.characters).length + options.readSlope * previousLine.renderContent(this.characters).length +
...@@ -100,8 +97,9 @@ export class Playbook { ...@@ -100,8 +97,9 @@ export class Playbook {
currentLine.renderContent(this.characters).length + currentLine.renderContent(this.characters).length +
options.writeIntercept; options.writeIntercept;
} }
currentLine.time = currentLine.time = Math.ceil(
Math.max(possibleReadTiming, possibleWriteTiming) + randomAddValue; Math.max(possibleReadTiming, possibleWriteTiming) + randomAddValue,
);
} }
} }
async play(tick: number) { async play(tick: number) {
......
...@@ -4,8 +4,8 @@ import { Character } from './Character'; ...@@ -4,8 +4,8 @@ import { Character } from './Character';
export class PlaybookLine { export class PlaybookLine {
characterId?: number; characterId?: number;
name?: string; name?: string;
content: string; content = '';
time: number; time = 0;
static FromLines(lines: string[]) { static FromLines(lines: string[]) {
const l = new PlaybookLine(); const l = new PlaybookLine();
l.fromLines(lines); l.fromLines(lines);
...@@ -39,16 +39,23 @@ export class PlaybookLine { ...@@ -39,16 +39,23 @@ export class PlaybookLine {
for (let i = 0; i < characters.length; ++i) { for (let i = 0; i < characters.length; ++i) {
this.content = this.content.replace( this.content = this.content.replace(
new RegExp(characters[i].name, 'g'), new RegExp(characters[i].name, 'g'),
`{{${i}.displayName}}`, `{{${i}}}`,
); );
} }
this.name = undefined; this.name = undefined;
} }
renderContent(characters: Character[]) { renderContent(characters: Character[]) {
return render(this.content, characters); const view: Record<number, string> = {};
for (const c of characters) {
view[c.id] = c.name;
}
return render(this.content, view);
} }
play(characters: Character[]) { play(characters: Character[]) {
const character = characters.find((c) => c.id === this.characterId); const character = characters.find((c) => c.id === this.characterId);
if (!character) {
return;
}
return character.play(this, this.renderContent(characters)); return character.play(this, this.renderContent(characters));
} }
} }
import { Playbook } from './Playbook';
import type { CQBot } from 'koishi-adapter-onebot';
import moment, { Moment } from 'moment';
export enum ShowStatus {
Idle,
Running,
Finished,
}
export class Show {
status: ShowStatus = ShowStatus.Idle;
private characterBotMap = new Map<number, CQBot>();
// eslint-disable-next-line @typescript-eslint/no-empty-function
onFinish: (message: string, show: Show) => void = () => {};
private step = 0;
private launchTime: Moment;
constructor(
public groupId: number,
private playbook: Playbook,
private autoChangeName = false,
) {
for (const character of playbook.characters) {
character.playFun = async (line, text) => {
const bot = this.characterBotMap.get(character.id);
await bot.$sendGroupMsg(this.groupId, text, true);
};
}
}
private getPerformingBots() {
return Array.from(this.characterBotMap.values());
}
useCharacter(id: number, bot: CQBot) {
this.characterBotMap.set(id, bot);
}
isCharacterFull() {
return this.playbook.characters.every((c) =>
this.characterBotMap.has(c.id),
);
}
async autoCharacters(bots: CQBot[]) {
let availableBots: CQBot[] = [];
const botNameMap = new Map<string, string>();
for (const bot of bots) {
if (!(bot.type && bot.type.startsWith('onebot'))) {
continue;
}
const groups = await bot.$getGroupList();
const matchGroup = groups.find((g) => g.groupId === this.groupId);
if (matchGroup) {
availableBots.push(bot);
botNameMap.set(
bot.selfId,
(await bot.$getGroupMemberInfo(this.groupId, bot.selfId)).nickname,
);
}
}
for (const character of this.playbook.characters) {
// exact
let matchBot = availableBots.find(
(b) => botNameMap.get(b.selfId) === character.name,
);
// match 1
if (!matchBot) {
matchBot = availableBots.find((b) =>
botNameMap.get(b.selfId).includes(character.name),
);
}
// match 2
if (!matchBot) {
matchBot = availableBots.find((b) =>
character.name.includes(botNameMap.get(b.selfId)),
);
}
if (matchBot) {
this.useCharacter(character.id, matchBot);
availableBots = availableBots.filter(
(b) => !this.getPerformingBots().includes(b),
);
}
}
for (const character of this.playbook.characters) {
if (this.characterBotMap.has(character.id)) {
continue;
}
const matchBot = availableBots.find((b) => true);
if (matchBot) {
this.useCharacter(character.id, matchBot);
availableBots = availableBots.filter(
(b) => !this.getPerformingBots().includes(b),
);
}
}
return this.isCharacterFull();
}
private showInterval: NodeJS.Timer;
async launchShow() {
if (this.status !== ShowStatus.Idle || !this.isCharacterFull()) {
return 'Characters missing or not idle.';
}
this.status = ShowStatus.Running;
if (this.autoChangeName) {
for (const character of this.playbook.characters) {
const bot = this.characterBotMap.get(character.id);
try {
await bot.$setGroupCard(this.groupId, bot.selfId, character.name);
} catch (e) {
return `Change name for ${bot.selfId} ${
character.name
} failed: ${e.toString()}`;
}
}
}
this.launchTime = moment();
this.showInterval = setInterval(async () => {
await this.showProcess();
}, 1000);
return null;
}
private async showProcess() {
try {
const exitShow = await this.playbook.play(this.step++);
if (exitShow) {
this.endShow('Show ended.');
}
return exitShow;
} catch (e) {
this.endShow(`Show errored: ${e.toString()}`);
return true;
}
}
endShow(message: string) {
if (this.status !== ShowStatus.Running) {
return false;
}
clearInterval(this.showInterval);
this.showInterval = undefined;
this.onFinish(message, this);
return true;
}
getCharacterList(): string {
return Array.from(this.characterBotMap.entries())
.map(
([characterId, bot]) =>
`${this.playbook.characters
.find((c) => c.id === characterId)
.getDisplayName()} => ${bot.selfId}`,
)
.join('\n');
}
getStatus(): string {
return `群: ${this.groupId}\n开始时间: ${this.launchTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n人物: ${this.getCharacterList()}`;
}
}
...@@ -3,7 +3,7 @@ import Mustache from 'mustache'; ...@@ -3,7 +3,7 @@ import Mustache from 'mustache';
export function matchHeader(input: string) { export function matchHeader(input: string) {
return input return input
.trim() .trim()
.match(/^(.+?) ((上午|下午) ?)?(\d{1,2}):(\d{1,2}):(\d{1,2})( (AM|PM))?$/); .match(/^(.+?) +((上午|下午) ?)?(\d{1,2}):(\d{1,2}):(\d{1,2})( (AM|PM))?$/);
} }
export function render(template: string, view: any) { export function render(template: string, view: any) {
......
import 'source-map-support/register';
import type { Context, Session } from 'koishi-core';
import { Show } from './playbook/Show';
import { Playbook } from './playbook/Playbook';
import { plainToClass } from 'class-transformer';
import loadJsonFile from 'load-json-file';
import type { CQBot } from 'koishi-adapter-onebot';
export interface Config {
adminContext: (ctx: Context) => Context;
autoChangeName: boolean;
dropHelp: boolean;
playbookPathPrefix: string;
}
export class MyPlugin {
config: Config;
ctx: Context;
adminCtx: Context;
shows = new Map<number, Show>();
name = 'act';
apply(ctx: Context, config: Config) {
this.ctx = ctx;
this.config = {
adminContext: (ctx) => ctx.private(),
autoChangeName: false,
dropHelp: false,
playbookPathPrefix: './playbooks',
...config,
};
if (this.config.dropHelp) {
ctx.command('help').dispose();
}
this.adminCtx = this.config.adminContext(ctx);
const showComamnd = this.adminCtx
.command('act [groupId:number]', '获取公演状态')
.usage('不带参数获取所有正在公演的群,带参数则获取特定群。')
.action((argv, groupId) => {
if (!groupId) {
return `当前正在公演的群有:\n${Array.from(this.shows.keys()).join(
' ',
)}`;
}
if (!this.shows.has(groupId)) {
return `群 ${groupId} 并不在公演哦。`;
}
const show = this.shows.get(groupId);
return show.getStatus();
});
showComamnd
.subcommand(
'.create <groupId:number> <playbookFilename:string> [...specificCharacters]',
'创建公演',
)
.usage(
'groupId 目标群。playbookFilename 是公演剧本文件,需要在服务器放好。后面的参数可以以 人物名=帐号 的形式指定特定的人物,否则随机分配。',
)
.example(
'act.create 11111111 play1 幽幽子=2222222 在群 11111111 创建一个公演,剧本是 play1,同时指定人物幽幽子为 2222222 扮演。',
)
.action(
async (argv, groupId, playbookFilename, ...specificCharacters) => {
if (!groupId || !playbookFilename) {
return '缺少参数。';
}
return this.createShow(
argv.session,
groupId,
playbookFilename,
specificCharacters,
);
},
);
showComamnd
.subcommand('.delete <groupId:number>', '停止')
.usage('groupId 目标群。')
.example('act.delete 11111111 停止群 11111111 的公演。')
.action((argv, groupId) => {
if (!groupId) {
return '缺少参数。';
}
if (!this.shows.has(groupId)) {
return `群 ${groupId} 并不在公演哦。`;
}
const show = this.shows.get(groupId);
show.endShow(`Ended by user ${argv.session.userId}`);
return '停止成功。';
});
}
async createShow(
session: Session,
groupId: number,
playbookFilename: string,
specificCharacters: string[] = [],
) {
if (this.shows.has(groupId)) {
return `群 ${groupId} 正在演呢!`;
}
const playbookPath = `${this.config.playbookPathPrefix}/${playbookFilename}.json`;
let playbook: Playbook;
try {
playbook = plainToClass(
Playbook,
(await loadJsonFile(playbookPath)) as any,
);
} catch (e) {
return `无法加载剧本文件 ${playbookPath}: ${e.toString()}`;
}
const bots: CQBot[] = (this.ctx.bots.filter(
(b) => b.type && b.type.startsWith('onebot'),
) as unknown) as CQBot[];
const show = new Show(groupId, playbook, this.config.autoChangeName);
for (const specificCharacter of specificCharacters) {
const [characterName, botId] = specificCharacter.split('=');
if (!botId) {
return `非法指定人物格式: ${specificCharacter}`;
}
const bot = bots.find((b) => b.selfId === botId);
if (!bot) {
return `没有找到机器人 ${botId}`;
}
const character = playbook.characters.find(
(c) => c.id === parseInt(characterName) || c.name === characterName,
);
if (!character) {
return `没有找到人物 ${characterName}`;
}
show.useCharacter(character.id, bot);
}
if (!(await show.autoCharacters(bots))) {
return '有人物缺位。';
}
show.onFinish = async (message, _show) => {
this.shows.delete(_show.groupId);
await session.send(`群 ${_show.groupId} 的公演结束了: ${message}`);
};
const launchErrorMessage = await show.launchShow();
if (launchErrorMessage) {
return `创建公演失败: ${launchErrorMessage}`;
}
this.shows.set(groupId, show);
return `成功创建群 ${groupId} 的公演 ${playbookFilename}\n出演名单:\n${show.getCharacterList()}`;
}
}
This diff is collapsed.
import * as fs from 'fs'; import * as fs from 'fs';
import { Playbook } from '../src/Playbook'; import { Playbook } from '../src/playbook/Playbook';
import { classToPlain, plainToClass } from 'class-transformer'; import { classToPlain, plainToClass } from 'class-transformer';
import delay from 'delay'; import delay from 'delay';
...@@ -8,7 +8,7 @@ async function main() { ...@@ -8,7 +8,7 @@ async function main() {
'./test/test-playbook.txt', './test/test-playbook.txt',
'utf-8', 'utf-8',
); );
const playbook = new Playbook({ Andria: 'Nanahira' }); const playbook = new Playbook();
playbook.loadText(content); playbook.loadText(content);
playbook.tuneTime({ playbook.tuneTime({
readSlope: 0, readSlope: 0,
...@@ -18,11 +18,10 @@ async function main() { ...@@ -18,11 +18,10 @@ async function main() {
maxRandomDelay: 5, maxRandomDelay: 5,
}); });
const plain = classToPlain(playbook); const plain = classToPlain(playbook);
console.log(JSON.stringify(plain, null, 2));
playbook.characters.forEach( playbook.characters.forEach(
(c) => (c) =>
(c.playFun = async (l, text) => (c.playFun = async (l, text) =>
console.error(`${c.displayName}: ${text}`)), console.error(`${c.getDisplayName()}: ${text}`)),
); );
for (let i = 0; ; i++) { for (let i = 0; ; i++) {
if (await playbook.play(i)) { if (await playbook.play(i)) {
......
{ {
"compilerOptions": { "compilerOptions": {
"outDir": "build", "outDir": "dist",
"module": "commonjs", "module": "commonjs",
"target": "esnext", "target": "esnext",
"esModuleInterop": true, "esModuleInterop": true,
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
"allowJs": true, "allowJs": true,
"include": [ "include": [
"*.ts", "*.ts",
"src/**/*.ts", "src/**/*.ts"
"test/**/*.ts"
] ]
} }
const path = require("path");
module.exports = {
entry: "./src/index.ts",
mode: "production",
target: "node",
devtool: "source-map",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{ test: /\.mustache$/, use: "raw-loader" },
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "index.js",
library: {
type: "commonjs",
},
path: path.resolve(__dirname, "dist"),
},
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment