feat: ESM, webtorrent v2

This commit is contained in:
ThaUnknown 2023-06-04 23:18:30 +02:00
parent 7fb6a83138
commit 764c5a1519
18 changed files with 2805 additions and 876 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
# Build output
dist/
build/
# Dependencies
node_modules/

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,26 +1,32 @@
{
"name": "Miru",
"version": "3.11.6",
"version": "4.0.0",
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
"description": "Stream anime torrents, real-time with no waiting for downloads.",
"main": "src/index.js",
"main": "build/main.js",
"homepage": "https://github.com/ThaUnknown/miru#readme",
"scripts": {
"start": "SET NODE_ENV=development & concurrently --kill-others \"pnpm run web:watch\" \"pnpm run electron:start\"",
"web:watch": "vite",
"electron:start": "electron src",
"build": "vite build && electron-builder",
"publish": "vite build && electron-builder -p always"
"start": "SET NODE_ENV=development & concurrently --kill-others \"npm run web:watch\" \"npm run electron:start\"",
"web:watch": "webpack serve",
"web:build": "SET NODE_ENV=production & webpack build",
"electron:start": "electron ./build/main.js",
"build": "npm run web:build && electron-builder",
"publish": "npm run web:build && electron-builder -p always"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.4.0",
"concurrently": "^7.6.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"electron": "23.1.0",
"electron-builder": "^23.6.0",
"electron-notarize": "^1.2.2",
"svelte": "^3.55.0",
"vite": "4.2.1",
"vite-plugin-commonjs": "^0.5.3"
"html-webpack-plugin": "^5.5.1",
"mini-css-extract-plugin": "^2.7.6",
"svelte": "^3.59.1",
"svelte-loader": "^3.1.8",
"webpack": "^5.85.0",
"webpack-cli": "^5.1.3",
"webpack-dev-server": "^4.15.0"
},
"standard": {
"ignore": [
@ -33,6 +39,9 @@
]
},
"build": {
"directories": {
"buildResources": "buildResources"
},
"asarUnpack": "**/*.node",
"electronDownload": {
"mirror": "https://github.com/aa910d571134/feb7c2e1a10f/releases/download/",
@ -52,20 +61,17 @@
"repo": "miru"
}
],
"afterSign": "./build/notarize.js",
"afterSign": "./buildResources/notarize.js",
"appId": "com.github.thaunknown.miru",
"productName": "Miru",
"files": [
"src/*",
"src/main/*",
"src/renderer/dist/**/*",
"!node_modules/**/*.{mk,a,o,h}"
"build/**/*"
],
"mac": {
"artifactName": "${os}-${name}-${version}.${ext}",
"singleArchFiles": "node_modules/+(register-scheme|utp-native)/**",
"category": "public.app-category.video",
"icon": "build/icon.icns",
"icon": "buildResources/icon.icns",
"target": [
{
"arch": "universal",
@ -105,23 +111,21 @@
}
},
"dependencies": {
"anitomyscript": "github:ThaUnknown/anitomyscript",
"anitomyscript": "github:ThaUnknown/anitomyscript#42290c4b3f256893be08a4e89051f448ff5e9d00",
"bottleneck": "^2.19.5",
"browser-event-target-emitter": "^1.0.0",
"discord-rpc": "4.0.1",
"ebml-iterator": "^1.0.2",
"electron-log": "^4.4.8",
"electron-updater": "^4.6.5",
"jassub": "1.7.1",
"js-levenshtein": "^1.1.6",
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles#redist",
"mime": "^3.0.0",
"pako": "^2.1.0",
"perfect-seekbar": "^1.1.0",
"pump": "^3.0.0",
"quartermoon": "^1.2.3",
"range-parser": "^1.2.1",
"simple-store-svelte": "^1.0.0",
"svelte-keybinds": "1.0.5",
"svelte-miniplayer": "1.0.3",
"webtorrent": "^1.9.6"
"webtorrent": "^2.0.29"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,6 @@
const WebTorrent = require('webtorrent')
const http = require('http')
const pump = require('pump')
const rangeParser = require('range-parser')
const mime = require('mime')
const { SubtitleParser, SubtitleStream } = require('matroska-subtitles')
const { ipcRenderer } = require('electron')
import WebTorrent from 'webtorrent'
import { SubtitleParser, SubtitleStream } from './matroska.js'
import { ipcRenderer } from 'electron'
class TorrentClient extends WebTorrent {
constructor (settings) {
@ -33,56 +29,7 @@ class TorrentClient extends WebTorrent {
}, 2000)
this.on('torrent', this.handleTorrent.bind(this))
this.server = http.createServer((request, response) => {
if (!request.url) return null
let [infoHash, ...filePath] = request.url.slice(request.url.indexOf('/webtorrent/') + 12).split('/')
filePath = decodeURI(filePath.join('/'))
if (!infoHash || !filePath) return null
const file = this.get(infoHash)?.files.find(file => file.path === filePath)
if (!file) return null
response.setHeader('Access-Control-Allow-Origin', '*')
response.setHeader('Content-Type', mime.getType(file.name) || 'application/octet-stream')
response.setHeader('Accept-Ranges', 'bytes')
let range = rangeParser(file.length, request.headers.range || '')
if (Array.isArray(range)) {
response.statusCode = 206
range = range[0]
response.setHeader(
'Content-Range',
`bytes ${range.start}-${range.end}/${file.length}`
)
response.setHeader('Content-Length', range.end - range.start + 1)
} else {
response.statusCode = 200
range = null
response.setHeader('Content-Length', file.length)
}
if (response.method === 'HEAD') {
return response.end()
}
let stream = file.createReadStream(range)
if (stream && !this.parsed) {
if (file.name.endsWith('.mkv')) {
this.parserInstance = new SubtitleStream(this.parserInstance)
this.handleSubtitleParser(this.parserInstance, true)
stream = pump(stream, this.parserInstance)
}
}
pump(stream, response)
})
this.server.on('error', console.warn)
this.server = this.createServer(undefined, 'node')
this.server.listen(0)
}
@ -91,10 +38,10 @@ class TorrentClient extends WebTorrent {
return {
infoHash: torrent.infoHash,
name: file.name,
type: file._getMimeType(),
type: file.type,
size: file.size,
path: file.path,
url: encodeURI(`http://localhost:${this.server.address().port}/webtorrent/${torrent.infoHash}/${file.path}`)
url: 'http://localhost:' + this.server.address().port + file.streamURL
}
})
this.dispatch('files', files)
@ -102,7 +49,7 @@ class TorrentClient extends WebTorrent {
this.dispatch('torrent', Array.from(torrent.torrentFile))
}
handleMessage ({ data }) {
async handleMessage ({ data }) {
switch (data.type) {
case 'current': {
this.current?.removeListener('done', this.boundParse)
@ -112,11 +59,18 @@ class TorrentClient extends WebTorrent {
this.current = null
this.parsed = false
if (data.data) {
this.current = this?.get(data.data.infoHash)?.files.find(file => file.path === data.data.path)
this.current = (await this.get(data.data.infoHash))?.files.find(file => file.path === data.data.path)
if (this.current?.name.endsWith('.mkv')) {
// if (this.current.done) this.parseSubtitles()
// this.current.once('done', this.boundParse)
this.parseFonts(this.current)
this.current.on('iterator', ({ iterator, req }, cb) => {
if (!this.parsed) {
this.stream = new SubtitleStream(this.stream, iterator)
this.handleSubtitleParser(this.stream, true)
cb(this.stream)
}
})
}
// TODO: findSubtitleFiles(current)
}
@ -124,7 +78,7 @@ class TorrentClient extends WebTorrent {
}
case 'torrent': {
const id = typeof data.data !== 'string' ? Buffer.from(data.data) : data.data
const existing = this.get(id)
const existing = await this.get(id)
if (existing) {
if (existing.ready) return this.handleTorrent(existing)
existing.once('ready', this.handleTorrent.bind(this))
@ -179,21 +133,17 @@ class TorrentClient extends WebTorrent {
}
parseFonts (file) {
const stream = new SubtitleParser()
const stream = new SubtitleParser(file)
this.handleSubtitleParser(stream)
stream.once('tracks', tracks => {
if (!tracks.length) {
this.parsed = true
stream.destroy()
fileStreamStream.destroy()
}
})
stream.once('subtitle', () => {
fileStreamStream.destroy()
stream.destroy()
})
const fileStreamStream = file.createReadStream()
fileStreamStream.pipe(stream)
}
handleSubtitleParser (parser, skipFile) {

261
src/background/matroska.js Normal file
View file

@ -0,0 +1,261 @@
import { EbmlIteratorDecoder, EbmlTagId } from 'ebml-iterator'
import { EventEmitter } from 'events'
import join from 'join-async-iterator'
import { inflate } from 'pako'
const SSA_TYPES = new Set(['ssa', 'ass'])
const SSA_KEYS = ['readOrder', 'layer', 'style', 'name', 'marginL', 'marginR', 'marginV', 'effect', 'text']
function getChild (chunk, tag) {
return chunk?.Children.find(({ id }) => id === tag)
}
function getData (chunk, tag) {
return getChild(chunk, tag)?.data
}
export class SubtitleParserBase extends EventEmitter {
constructor () {
super()
this.subtitleTracks = new Map()
this.timecodeScale = 1
this._currentClusterTimecode = null
this.destroyed = false
this._tagMap = {
// Segment Information
[EbmlTagId.TimecodeScale]: ({ data }) => {
this.timecodeScale = data / 1000000
},
// Assumption: This is a Cluster `Timecode`
[EbmlTagId.Timecode]: ({ data }) => {
this._currentClusterTimecode = data
},
// Parse attached files, mainly to allow extracting subtitle font files.
[EbmlTagId.AttachedFile]: chunk => {
this.emit('file', {
filename: getData(chunk, EbmlTagId.FileName),
mimetype: getData(chunk, EbmlTagId.FileMimeType),
data: getData(chunk, EbmlTagId.FileData)
})
},
// Duration for chapters which don't specify an end position
[EbmlTagId.Duration]: ({ data }) => {
if (this.chapters) {
this.chapters[this.chapters.length - 1].end = data
this.emit('chapters', this.chapters)
} else {
this.duration = data
}
},
[EbmlTagId.Tracks]: this.handleTracks.bind(this),
[EbmlTagId.BlockGroup]: this.handleBlockGroup.bind(this),
[EbmlTagId.Chapters]: this.handleChapters.bind(this)
}
}
async * [Symbol.asyncIterator] (stream) {
const decoder = new EbmlIteratorDecoder({
bufferTagIds: [
EbmlTagId.TimecodeScale,
EbmlTagId.Tracks,
EbmlTagId.BlockGroup,
EbmlTagId.AttachedFile,
EbmlTagId.Chapters,
EbmlTagId.Duration
],
stream
})
for await (const chunk of stream) {
if (this.destroyed) return null
const tags = decoder.parseTags(chunk)
for (const tag of tags) {
this._tagMap[tag.id]?.(tag)
if (tag.id === EbmlTagId.Tracks) {
if (!tag.Children.some(({ id }) => id === EbmlTagId.TrackEntry)) return this.destroy()
}
}
yield chunk
}
}
handleTracks (chunk) {
for (const entry of chunk.Children.filter(c => c.id === EbmlTagId.TrackEntry)) {
// Skip non subtitle tracks
if (getData(entry, EbmlTagId.TrackType) !== 0x11) continue
const codecID = getData(entry, EbmlTagId.CodecID) || ''
if (codecID.startsWith('S_TEXT')) {
const track = {
number: getData(entry, EbmlTagId.TrackNumber),
language: getData(entry, EbmlTagId.Language),
type: codecID.substring(7).toLowerCase()
}
const name = getData(entry, EbmlTagId.Name)
if (name) track.name = name
const header = getData(entry, EbmlTagId.CodecPrivate)
if (header) track.header = header.toString()
// TODO: Assume zlib deflate compression
const compressed = entry.Children.find(c =>
c.id === EbmlTagId.ContentEncodings &&
c.Children.find(cc =>
cc.id === EbmlTagId.ContentEncoding &&
getChild(cc, EbmlTagId.ContentCompression)
)
)
if (compressed) track._compressed = true
this.subtitleTracks.set(track.number, track)
}
}
this.emit('tracks', [...this.subtitleTracks.values()])
}
handleBlockGroup (chunk) {
const block = getChild(chunk, EbmlTagId.Block)
if (block && this.subtitleTracks.has(block.track)) {
const blockDuration = getData(chunk, EbmlTagId.BlockDuration)
const track = this.subtitleTracks.get(block.track)
const payload = track._compressed
? inflate(Buffer.from(block.payload), { to: 'string' })
: block.payload
const subtitle = {
text: payload.toString('utf8'),
time: (block.value + this._currentClusterTimecode) * this.timecodeScale,
duration: blockDuration * this.timecodeScale
}
if (SSA_TYPES.has(track.type)) {
// extract SSA/ASS keys
const values = subtitle.text.split(',')
// ignore read-order, and skip layer if ssa
for (let i = track.type === 'ssa' ? 2 : 1; i < 8; i++) {
subtitle[SSA_KEYS[i]] = values[i]
}
subtitle.text = values.slice(8).join(',')
}
this.emit('subtitle', subtitle, block.track)
}
}
handleChapters ({ Children }) {
const editions = Children.filter(c => c.id === EbmlTagId.EditionEntry)
// https://www.matroska.org/technical/chapters.html#default-edition
// finds first default edition, or first entry
const defaultEdition = editions.find(c => {
return c.Children.some(cc => {
return cc.id === EbmlTagId.EditionFlagDefault && Boolean(cc.data)
})
}) || editions[0]
// exclude hidden atoms
const atoms = defaultEdition.Children.filter(c => c.id === EbmlTagId.ChapterAtom && !getData(c, EbmlTagId.ChapterFlagHidden))
const chapters = []
for (let i = atoms.length - 1; i >= 0; --i) {
const start = getData(atoms[i], EbmlTagId.ChapterTimeStart) / this.timecodeScale / 1000000
const end = (getData(atoms[i], EbmlTagId.ChapterTimeEnd) / this.timecodeScale / 1000000)
const disp = getChild(atoms[i], EbmlTagId.ChapterDisplay)
chapters[i] = {
start,
end,
text: getData(disp, EbmlTagId.ChapString),
language: getData(disp, EbmlTagId.ChapLanguage)
}
}
chapters.sort((a, b) => a.start - b.start)
for (let i = chapters.length - 1; i >= 0; --i) {
chapters[i].end ||= chapters[i + 1]?.start || this.duration
}
if (this.duration) {
this.emit('chapters', chapters)
} else {
this.chapters = chapters
}
}
destroy () {
this.destroyed = true
this.emit('finish')
}
}
export class SubtitleParser extends SubtitleParserBase {
constructor (stream) {
super()
;(async () => {
try {
// eslint-disable-next-line no-unused-vars
for await (const _ of super[Symbol.asyncIterator](stream)) {
if (this.destroyed) break
}
} catch (e) {}
this.emit('finish')
})()
}
}
export class SubtitleStream extends SubtitleParserBase {
constructor (prevInstance, stream) {
super()
this._stream = stream
if (prevInstance instanceof SubtitleParserBase) {
// copy previous metadata
this.subtitleTracks = prevInstance.subtitleTracks
this.timecodeScale = prevInstance.timecodeScale
// may not be at ebml tag offset
this.unstable = true
}
}
async * [Symbol.asyncIterator] (stream = this._stream) {
while (true) {
if (this.destroyed) return
if (this.unstable) {
const iterator = stream[Symbol.asyncIterator]()
const { value: chunk } = await iterator.next()
if (!chunk) return
// the ebml decoder expects to see a tag, so we won't use it until we find a cluster
for (let i = 0; i < chunk.length - 12; i++) {
// cluster id 0x1F43B675
// https://matroska.org/technical/elements.html#LevelCluster
if (chunk[i] === 0x1f && chunk[i + 1] === 0x43 && chunk[i + 2] === 0xb6 && chunk[i + 3] === 0x75) {
// length of cluster size tag
const len = 8 - Math.floor(Math.log2(chunk[i + 4]))
// first tag in cluster is a valid EbmlTag
if (EbmlTagId[chunk[i + 4 + len]]) {
// okay this is probably a cluster
this.unstable = false
yield chunk.slice(0, i)
yield * super[Symbol.asyncIterator](join([[chunk.slice(i)], iterator]))
return
}
}
}
yield chunk
} else {
yield * super[Symbol.asyncIterator](stream)
return
}
}
}
}

View file

@ -116,6 +116,7 @@ ipcMain.on('doh', (event, dns) => {
})
function createWindow () {
const development = process.env.NODE_ENV?.trim() === 'development'
// Create the browser window.
webtorrentWindow = new BrowserWindow({
show: false,
@ -140,19 +141,19 @@ function createWindow () {
webPreferences: {
enableBlinkFeatures: 'FontAccess, AudioVideoTracks',
backgroundThrottling: false,
preload: path.join(__dirname, '/preload.js')
preload: development ? path.join(__dirname, '/preload.js') : path.join(__dirname, '/preload.js')
},
icon: path.join(__dirname, '/renderer/public/logo.ico'),
icon: path.join(__dirname, '/logo.ico'),
show: false
})
mainWindow.setMenuBarVisibility(false)
protocol.registerHttpProtocol('miru', (req, cb) => {
const token = req.url.slice(7)
if (process.env.NODE_ENV !== 'development ') {
mainWindow.loadURL(path.join(__dirname, '/renderer/dist/index.html' + token))
if (development) {
mainWindow.loadURL(path.join(__dirname, '/index.html' + token))
} else {
mainWindow.loadURL('http://localhost:5173/' + token)
mainWindow.loadURL('http://localhost:5000/' + token)
}
})
@ -168,16 +169,16 @@ function createWindow () {
let torrentLoad = null
if (process.env.NODE_ENV !== 'development ') {
if (process.env.NODE_ENV?.trim() !== 'development') {
// Load production build
torrentLoad = webtorrentWindow.loadFile(path.join(__dirname, '/renderer/dist/webtorrent.html'))
mainWindow.loadFile(path.join(__dirname, '/renderer/dist/index.html'))
torrentLoad = webtorrentWindow.loadFile(path.join(__dirname, '/background.html'))
mainWindow.loadFile(path.join(__dirname, '/index.html'))
} else {
// Load vite dev server page
console.log('Development mode')
torrentLoad = webtorrentWindow.loadURL('http://localhost:5173/webtorrent.html')
torrentLoad = webtorrentWindow.loadURL('http://localhost:5000/background.html')
webtorrentWindow.webContents.openDevTools()
mainWindow.loadURL('http://localhost:5173/')
mainWindow.loadURL('http://localhost:5000/index.html')
mainWindow.webContents.openDevTools()
}
@ -250,31 +251,7 @@ let status = null
const discord = new Client({
transport: 'ipc'
})
function setDiscordRPC (data) {
if (!data) {
data = {
activity: {
timestamps: {
start: Date.now()
},
details: 'Stream anime torrents, real-time.',
state: 'Watching anime',
assets: {
small_image: 'logo',
small_text: 'https://github.com/ThaUnknown/miru'
},
buttons: [
{
label: 'Download app',
url: 'https://github.com/ThaUnknown/miru/releases/latest'
}
],
instance: true,
type: 3
}
}
}
function setDiscordRPC (event, data) {
status = data
if (discord?.user && status) {
status.pid = process.pid
@ -282,39 +259,9 @@ function setDiscordRPC (data) {
}
}
let allowDiscordDetails = false
let requestedDiscordDetails = false
let rpcStarted = false
let cachedPresence = null
ipcMain.on('discord_status', (event, data) => {
requestedDiscordDetails = data;
if (!rpcStarted) {
handleRPC()
setInterval(handleRPC, 5000) //According to Discord documentation, clients can only update their presence 5 times per 20 seconds. We will add an extra second to be safe.
rpcStarted = true
}
})
function handleRPC() {
if (allowDiscordDetails === requestedDiscordDetails) return
allowDiscordDetails = requestedDiscordDetails
if (!allowDiscordDetails) {
setDiscordRPC(null)
} else if (cachedPresence) {
setDiscordRPC(cachedPresence)
}
}
ipcMain.on('discord', (event, data) => {
cachedPresence = data
if (allowDiscordDetails) {
setDiscordRPC(data)
}
})
ipcMain.on('discord', setDiscordRPC)
discord.on('ready', async () => {
setDiscordRPC(status)
setDiscordRPC(null, status)
discord.subscribe('ACTIVITY_JOIN_REQUEST')
discord.subscribe('ACTIVITY_JOIN')
discord.subscribe('ACTIVITY_SPECTATE')

View file

@ -1,5 +1,5 @@
/* eslint n/no-callback-literal: 0 */
const { contextBridge, ipcRenderer } = require('electron')
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('IPC', {
emit: (event, data) => {

View file

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta name="theme-color" content="#191c20">
<title>Miru</title>
<link rel='icon' href='/logo.ico'>
<link href="/lib/Material-Icons.css" rel="stylesheet">
<script defer type="module" src="./src/main.js" async></script>
</head>
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars">
</body>
</html>

View file

@ -7,7 +7,8 @@
import { page } from '@/App.svelte'
import 'browser-event-target-emitter'
import P2PT from 'https://esm.sh/p2pt?bundle'
// import P2PT from 'https://esm.sh/p2pt?bundle'
const P2PT = {}
export const w2gEmitter = new EventTarget()

View file

@ -1,7 +1,6 @@
import { add } from './torrent.js'
import { DOMPARSER, PromiseBatch } from './util.js'
import { alRequest, alSearch } from './anilist.js'
import 'anitomyscript/dist/anitomyscript.wasm?url'
import anitomyscript from 'anitomyscript'
import { media } from '@/lib/Player/MediaHandler.svelte'
import { addToast } from '@/lib/Toasts.svelte'

View file

@ -1,7 +1,4 @@
import JASSUB from 'jassub'
import workerUrl from 'jassub/dist/jassub-worker.js?url'
import modernWasmUrl from 'jassub/dist/jassub-worker-modern.wasm?url'
import wasmUrl from 'jassub/dist/jassub-worker.wasm?url'
import { toTS, videoRx, subRx } from './util.js'
import { set } from '@/lib/Settings.svelte'
@ -163,9 +160,10 @@ export default class Subtitles {
availableFonts: {
'roboto medium': './Roboto.ttf'
},
workerUrl,
wasmUrl,
modernWasmUrl,
workerUrl: new URL('jassub/dist/jassub-worker.js', import.meta.url).toString(),
wasmUrl: new URL('jassub/dist/jassub-worker.wasm', import.meta.url).toString(),
legacyWasmUrl: new URL('jassub/dist/jassub-worker.wasm.js', import.meta.url).toString(),
modernWasmUrl: new URL('jassub/dist/jassub-worker-modern.wasm', import.meta.url).toString(),
useLocalFonts: set.missingFont,
dropAllBlur: set.disableSubtitleBlur
}

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta name="theme-color" content="#191c20">
<title>WebTorrent Hidden Window</title>
<script defer src="./lib/webtorrent.js" async></script>
</head>
<body></body>
</html>

View file

@ -1,35 +0,0 @@
import path from 'path'
import process from 'process'
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import commonjs from 'vite-plugin-commonjs'
const root = path.resolve(process.cwd(), 'src/renderer')
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
return {
resolve: {
alias: {
'@': path.resolve('src/renderer/src')
}
},
plugins: [mode !== 'development' && commonjs(), svelte()],
root,
server: {
hmr: false
},
base: './',
build: {
sourcemap: true,
rollupOptions: {
output: {
assetFileNames: '[name].[ext]'
},
input: {
index: root + '/index.html',
torrent: root + '/webtorrent.html'
}
}
}
}
})

146
webpack.config.cjs Normal file
View file

@ -0,0 +1,146 @@
const { join, resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const mode = process.env.NODE_ENV?.trim() || 'development'
const isDev = mode === 'development'
module.exports = [
{
entry: join(__dirname, 'src', 'background', 'background.js'),
output: {
path: join(__dirname, 'build'),
filename: 'background.js'
},
mode,
externals: {
'utp-native': 'require("utp-native")'
},
resolve: {
aliasFields: [],
mainFields: ['module', 'main', 'node'],
alias: {
'node-fetch': false
}
},
plugins: [new HtmlWebpackPlugin({ filename: 'background.html' })],
target: 'electron20.0-renderer',
devServer: {
devMiddleware: {
writeToDisk: true
},
hot: true,
client: {
overlay: { errors: true, warnings: false, runtimeErrors: false }
},
port: 5000
}
},
{
entry: join(__dirname, 'src', 'renderer', 'src', 'main.js'),
output: {
path: join(__dirname, 'build'),
filename: 'renderer.js'
},
mode,
module: {
rules: [
{
test: /\.svelte$/,
use: {
loader: 'svelte-loader',
options: {
compilerOptions: {
dev: isDev
},
emitCss: !isDev,
hotReload: isDev
}
}
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
// required to prevent errors from Svelte on Webpack 5+
test: /node_modules\/svelte\/.*\.mjs$/,
resolve: {
fullySpecified: false
}
}
]
},
resolve: {
aliasFields: [],
alias: {
'@': resolve('src/renderer/src'),
module: false,
url: false
},
extensions: ['.mjs', '.js', '.svelte']
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
}),
new CopyWebpackPlugin({
patterns: [
{ from: 'src/renderer/public' }
]
}),
new HtmlWebpackPlugin({
filename: 'index.html',
inject: false,
templateContent: ({ htmlWebpackPlugin }) => /* html */`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta name="theme-color" content="#191c20">
<title>Miru</title>
<link rel='icon' href='/logo.ico'>
<link href="./lib/Material-Icons.css" rel="stylesheet">
${htmlWebpackPlugin.tags.headTags}
</head>
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars">
${htmlWebpackPlugin.tags.bodyTags}
</body>
</html> `
})],
target: 'web'
},
{
entry: join(__dirname, 'src', 'preload', 'preload.js'),
output: {
path: join(__dirname, 'build'),
filename: 'preload.js'
},
resolve: {
aliasFields: []
},
mode,
target: 'electron20.0-preload'
},
{
entry: join(__dirname, 'src', 'main', 'main.js'),
output: {
path: join(__dirname, 'build'),
filename: 'main.js'
},
resolve: {
aliasFields: []
},
mode,
target: 'electron20.0-main'
}
]