diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e2bf21..a8ed7e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,14 +23,11 @@ jobs: yarn yarn devInstall yarn compile + yarn build - - name: Build/release Electron app - uses: samuelmeuli/action-electron-builder@v1 + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') with: - # GitHub token, automatically provided to the action - # (No need to define this secret in the repo settings) - github_token: ${{ secrets.github_token }} - - # If the commit is tagged with a version (e.g. "v1.0.0"), - # release the app after building - release: ${{ startsWith(github.ref, 'refs/tags/v') }} \ No newline at end of file + files: + - 'builds/*' \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 59f6584..0000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -language: node_js -node_js: "12" -before_install: - - npm i - -jobs: - include: - - stage: Linux & Mac Build - os: osx - osx_image: xcode10.2 - env: - - ELECTRON_CACHE=$HOME/.cache/electron - - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder - before_cache: - - rm -rf $ELECTRON_BUILDER_CACHE/wine - script: - - npm run devInstall - - npm run build -# - stage: Windows Build -# os: windows -# script: -# - export NPM_CONFIG_PREFIX=C:\\npm_prefix -# - export PATH="/c/npm_prefix:$PATH" -# - npm i -g npm@latest -# - npm run devInstall -# - npm run build -# - stage: GitHub Release -# script: -# - export TRAVIS_TAG=${TRAVIS_TAG:-$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)} -# - git tag $TRAVIS_TAG -# deploy: -# provider: releases -# prerelease: true -# api_key: "$GH_TOKEN" -# cleanup: false -# file: -# - builds/lightcord-win32-ia32.zip -# - builds/lightcord-win32.exe -# - builds/lightcord-linux-x64.zip -# - builds/lightcord-darwin.zip -# on: -# tags: true diff --git a/modules/discord_voice/index.js b/modules/discord_voice/index.js index 87e90d0..724b9a2 100644 --- a/modules/discord_voice/index.js +++ b/modules/discord_voice/index.js @@ -80,271 +80,270 @@ if (process.arch === 'arm64') { module.exports = { consoleLog = (...args) => {}, } - return; -} +} else { + features.declareSupported('voice_panning'); + features.declareSupported('voice_multiple_connections'); + features.declareSupported('media_devices'); + features.declareSupported('media_video'); + features.declareSupported('debug_logging'); + features.declareSupported('set_audio_device_by_id'); + features.declareSupported('set_video_device_by_id'); + features.declareSupported('loopback'); + features.declareSupported('experiment_config'); + features.declareSupported('remote_locus_network_control'); + features.declareSupported('connection_replay'); + features.declareSupported('simulcast'); + features.declareSupported('direct_video'); -features.declareSupported('voice_panning'); -features.declareSupported('voice_multiple_connections'); -features.declareSupported('media_devices'); -features.declareSupported('media_video'); -features.declareSupported('debug_logging'); -features.declareSupported('set_audio_device_by_id'); -features.declareSupported('set_video_device_by_id'); -features.declareSupported('loopback'); -features.declareSupported('experiment_config'); -features.declareSupported('remote_locus_network_control'); -features.declareSupported('connection_replay'); -features.declareSupported('simulcast'); -features.declareSupported('direct_video'); - -if (process.platform === 'win32') { - features.declareSupported('voice_legacy_subsystem'); - features.declareSupported('soundshare'); - features.declareSupported('wumpus_video'); - features.declareSupported('hybrid_video'); - features.declareSupported('elevated_hook'); - features.declareSupported('soundshare_loopback'); - features.declareSupported('screen_previews'); - features.declareSupported('window_previews'); - features.declareSupported('audio_debug_state'); - features.declareSupported('video_effects'); - // NOTE(jvass): currently there's no experimental encoders! Add this back if you - // add one and want to re-enable the UI for them. - // features.declareSupported('experimental_encoders'); -} - -function bindConnectionInstance(instance) { - return { - destroy: () => instance.destroy(), - - setTransportOptions: (options) => instance.setTransportOptions(options), - setSelfMute: (mute) => instance.setSelfMute(mute), - setSelfDeafen: (deaf) => instance.setSelfDeafen(deaf), - - mergeUsers: (users) => instance.mergeUsers(users), - destroyUser: (userId) => instance.destroyUser(userId), - - setLocalVolume: (userId, volume) => instance.setLocalVolume(userId, volume), - setLocalMute: (userId, mute) => instance.setLocalMute(userId, mute), - setLocalPan: (userId, left, right) => instance.setLocalPan(userId, left, right), - setDisableLocalVideo: (userId, disabled) => instance.setDisableLocalVideo(userId, disabled), - - setMinimumOutputDelay: (delay) => instance.setMinimumOutputDelay(delay), - getEncryptionModes: (callback) => instance.getEncryptionModes(callback), - configureConnectionRetries: (baseDelay, maxDelay, maxAttempts) => - instance.configureConnectionRetries(baseDelay, maxDelay, maxAttempts), - setOnSpeakingCallback: (callback) => instance.setOnSpeakingCallback(callback), - setOnSpeakingWhileMutedCallback: (callback) => instance.setOnSpeakingWhileMutedCallback(callback), - setPingInterval: (interval) => instance.setPingInterval(interval), - setPingCallback: (callback) => instance.setPingCallback(callback), - setPingTimeoutCallback: (callback) => instance.setPingTimeoutCallback(callback), - setRemoteUserSpeakingStatus: (userId, speaking) => instance.setRemoteUserSpeakingStatus(userId, speaking), - setRemoteUserCanHavePriority: (userId, canHavePriority) => - instance.setRemoteUserCanHavePriority(userId, canHavePriority), - - setOnVideoCallback: (callback) => instance.setOnVideoCallback(callback), - setVideoBroadcast: (broadcasting) => instance.setVideoBroadcast(broadcasting), - setDesktopSource: (id, videoHook, type) => instance.setDesktopSource(id, videoHook, type), - setDesktopSourceStatusCallback: (callback) => instance.setDesktopSourceStatusCallback(callback), - setOnDesktopSourceEnded: (callback) => instance.setOnDesktopSourceEnded(callback), - setOnSoundshare: (callback) => instance.setOnSoundshare(callback), - setOnSoundshareEnded: (callback) => instance.setOnSoundshareEnded(callback), - setOnSoundshareFailed: (callback) => instance.setOnSoundshareFailed(callback), - setPTTActive: (active, priority) => instance.setPTTActive(active, priority), - getStats: (callback) => instance.getStats(callback), - getFilteredStats: (filter, callback) => instance.getFilteredStats(filter, callback), - startReplay: () => instance.startReplay(), - }; -} - -VoiceEngine.createTransport = VoiceEngine._createTransport; - -if (isElectronRenderer) { - VoiceEngine.setImageDataAllocator((width, height) => new window.ImageData(width, height)); -} - -VoiceEngine.createVoiceConnection = function (audioSSRC, userId, address, port, onConnectCallback, experiments, rids) { - let instance = null; - if (rids != null) { - instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments, rids); - } else if (experiments != null) { - instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments); - } else { - instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback); - } - return bindConnectionInstance(instance); -}; -VoiceEngine.createOwnStreamConnection = VoiceEngine.createVoiceConnection; - -VoiceEngine.createReplayConnection = function (audioEngineId, callback, replayLog) { - if (replayLog == null) { - return null; + if (process.platform === 'win32') { + features.declareSupported('voice_legacy_subsystem'); + features.declareSupported('soundshare'); + features.declareSupported('wumpus_video'); + features.declareSupported('hybrid_video'); + features.declareSupported('elevated_hook'); + features.declareSupported('soundshare_loopback'); + features.declareSupported('screen_previews'); + features.declareSupported('window_previews'); + features.declareSupported('audio_debug_state'); + features.declareSupported('video_effects'); + // NOTE(jvass): currently there's no experimental encoders! Add this back if you + // add one and want to re-enable the UI for them. + // features.declareSupported('experimental_encoders'); } - return bindConnectionInstance(new VoiceEngine.VoiceReplayConnection(replayLog, audioEngineId, callback)); -}; + function bindConnectionInstance(instance) { + return { + destroy: () => instance.destroy(), -VoiceEngine.setAudioSubsystem = function (subsystem) { - if (appSettings == null) { - console.warn('Unable to access app settings.'); - return; + setTransportOptions: (options) => instance.setTransportOptions(options), + setSelfMute: (mute) => instance.setSelfMute(mute), + setSelfDeafen: (deaf) => instance.setSelfDeafen(deaf), + + mergeUsers: (users) => instance.mergeUsers(users), + destroyUser: (userId) => instance.destroyUser(userId), + + setLocalVolume: (userId, volume) => instance.setLocalVolume(userId, volume), + setLocalMute: (userId, mute) => instance.setLocalMute(userId, mute), + setLocalPan: (userId, left, right) => instance.setLocalPan(userId, left, right), + setDisableLocalVideo: (userId, disabled) => instance.setDisableLocalVideo(userId, disabled), + + setMinimumOutputDelay: (delay) => instance.setMinimumOutputDelay(delay), + getEncryptionModes: (callback) => instance.getEncryptionModes(callback), + configureConnectionRetries: (baseDelay, maxDelay, maxAttempts) => + instance.configureConnectionRetries(baseDelay, maxDelay, maxAttempts), + setOnSpeakingCallback: (callback) => instance.setOnSpeakingCallback(callback), + setOnSpeakingWhileMutedCallback: (callback) => instance.setOnSpeakingWhileMutedCallback(callback), + setPingInterval: (interval) => instance.setPingInterval(interval), + setPingCallback: (callback) => instance.setPingCallback(callback), + setPingTimeoutCallback: (callback) => instance.setPingTimeoutCallback(callback), + setRemoteUserSpeakingStatus: (userId, speaking) => instance.setRemoteUserSpeakingStatus(userId, speaking), + setRemoteUserCanHavePriority: (userId, canHavePriority) => + instance.setRemoteUserCanHavePriority(userId, canHavePriority), + + setOnVideoCallback: (callback) => instance.setOnVideoCallback(callback), + setVideoBroadcast: (broadcasting) => instance.setVideoBroadcast(broadcasting), + setDesktopSource: (id, videoHook, type) => instance.setDesktopSource(id, videoHook, type), + setDesktopSourceStatusCallback: (callback) => instance.setDesktopSourceStatusCallback(callback), + setOnDesktopSourceEnded: (callback) => instance.setOnDesktopSourceEnded(callback), + setOnSoundshare: (callback) => instance.setOnSoundshare(callback), + setOnSoundshareEnded: (callback) => instance.setOnSoundshareEnded(callback), + setOnSoundshareFailed: (callback) => instance.setOnSoundshareFailed(callback), + setPTTActive: (active, priority) => instance.setPTTActive(active, priority), + getStats: (callback) => instance.getStats(callback), + getFilteredStats: (filter, callback) => instance.getFilteredStats(filter, callback), + startReplay: () => instance.startReplay(), + }; } - // TODO: With experiment controlling ADM selection, this may be incorrect since - // audioSubsystem is read from settings (or default if does not exists) - // and not the actual ADM used. - if (subsystem === audioSubsystem) { - return; - } - - appSettings.set('audioSubsystem', subsystem); - appSettings.set('useLegacyAudioDevice', false); + VoiceEngine.createTransport = VoiceEngine._createTransport; if (isElectronRenderer) { - window.DiscordNative.app.relaunch(); - } -}; - -VoiceEngine.setDebugLogging = function (enable) { - if (appSettings == null) { - console.warn('Unable to access app settings.'); - return; + VoiceEngine.setImageDataAllocator((width, height) => new window.ImageData(width, height)); } - if (debugLogging === enable) { - return; - } - - appSettings.set('debugLogging', enable); - - if (isElectronRenderer) { - window.DiscordNative.app.relaunch(); - } -}; - -VoiceEngine.getDebugLogging = function () { - return debugLogging; -}; - -const videoStreams = {}; - -const ensureCanvasContext = function (sinkId) { - let canvas = document.getElementById(sinkId); - if (canvas == null) { - for (const popout of window.popouts.values()) { - const element = popout.document != null && popout.document.getElementById(sinkId); - if (element != null) { - canvas = element; - break; - } + VoiceEngine.createVoiceConnection = function (audioSSRC, userId, address, port, onConnectCallback, experiments, rids) { + let instance = null; + if (rids != null) { + instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments, rids); + } else if (experiments != null) { + instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments); + } else { + instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback); } + return bindConnectionInstance(instance); + }; + VoiceEngine.createOwnStreamConnection = VoiceEngine.createVoiceConnection; - if (canvas == null) { + VoiceEngine.createReplayConnection = function (audioEngineId, callback, replayLog) { + if (replayLog == null) { return null; } - } - const context = canvas.getContext('2d'); - if (context == null) { - console.log(`Failed to initialize context for sinkId ${sinkId}`); - return null; - } + return bindConnectionInstance(new VoiceEngine.VoiceReplayConnection(replayLog, audioEngineId, callback)); + }; - return context; -}; + VoiceEngine.setAudioSubsystem = function (subsystem) { + if (appSettings == null) { + console.warn('Unable to access app settings.'); + return; + } -// [adill] NB: with context isolation it has become extremely costly (both memory & performance) to provide the image -// data directly to clients at any reasonably fast interval so we've replaced setVideoOutputSink with a direct canvas -// renderer via addVideoOutputSink -const setVideoOutputSink = VoiceEngine.setVideoOutputSink; -const clearVideoOutputSink = (streamId) => { - // [adill] NB: if you don't pass a frame callback setVideoOutputSink clears the sink - setVideoOutputSink(streamId); -}; -const signalVideoOutputSinkReady = VoiceEngine.signalVideoOutputSinkReady; -delete VoiceEngine.setVideoOutputSink; -delete VoiceEngine.signalVideoOutputSinkReady; + // TODO: With experiment controlling ADM selection, this may be incorrect since + // audioSubsystem is read from settings (or default if does not exists) + // and not the actual ADM used. + if (subsystem === audioSubsystem) { + return; + } -function addVideoOutputSinkInternal(sinkId, streamId, frameCallback) { - let sinks = videoStreams[streamId]; - if (sinks == null) { - sinks = videoStreams[streamId] = new Map(); - } + appSettings.set('audioSubsystem', subsystem); + appSettings.set('useLegacyAudioDevice', false); - if (sinks.size === 0) { - console.log(`Subscribing to frames for streamId ${streamId}`); - const onFrame = (imageData) => { - const sinks = videoStreams[streamId]; - if (sinks != null) { - for (const callback of sinks.values()) { - if (callback != null) { - callback(imageData); - } + if (isElectronRenderer) { + window.DiscordNative.app.relaunch(); + } + }; + + VoiceEngine.setDebugLogging = function (enable) { + if (appSettings == null) { + console.warn('Unable to access app settings.'); + return; + } + + if (debugLogging === enable) { + return; + } + + appSettings.set('debugLogging', enable); + + if (isElectronRenderer) { + window.DiscordNative.app.relaunch(); + } + }; + + VoiceEngine.getDebugLogging = function () { + return debugLogging; + }; + + const videoStreams = {}; + + const ensureCanvasContext = function (sinkId) { + let canvas = document.getElementById(sinkId); + if (canvas == null) { + for (const popout of window.popouts.values()) { + const element = popout.document != null && popout.document.getElementById(sinkId); + if (element != null) { + canvas = element; + break; } } - signalVideoOutputSinkReady(streamId); - }; - setVideoOutputSink(streamId, onFrame, true); - } - sinks.set(sinkId, frameCallback); -} - -VoiceEngine.addVideoOutputSink = function (sinkId, streamId, frameCallback) { - let canvasContext = null; - addVideoOutputSinkInternal(sinkId, streamId, (imageData) => { - if (canvasContext == null) { - canvasContext = ensureCanvasContext(sinkId); - if (canvasContext == null) { - return; + if (canvas == null) { + return null; } } - if (frameCallback != null) { - frameCallback(imageData.width, imageData.height); - } - // [adill] NB: Electron 9+ on macOS would show massive leaks in the the GPU helper process when a non-Discord - // window completely occludes the Discord window. Adding this tiny readback ameliorates the issue. We tried WebGL - // rendering which did not exhibit the issue, however, the context limit of 16 was too small to be a real - // alternative. - const leak = canvasContext.getImageData(0, 0, 1, 1); - canvasContext.putImageData(imageData, 0, 0); - }); -}; -VoiceEngine.removeVideoOutputSink = function (sinkId, streamId) { - const sinks = videoStreams[streamId]; - if (sinks != null) { - sinks.delete(sinkId); + const context = canvas.getContext('2d'); + if (context == null) { + console.log(`Failed to initialize context for sinkId ${sinkId}`); + return null; + } + + return context; + }; + + // [adill] NB: with context isolation it has become extremely costly (both memory & performance) to provide the image + // data directly to clients at any reasonably fast interval so we've replaced setVideoOutputSink with a direct canvas + // renderer via addVideoOutputSink + const setVideoOutputSink = VoiceEngine.setVideoOutputSink; + const clearVideoOutputSink = (streamId) => { + // [adill] NB: if you don't pass a frame callback setVideoOutputSink clears the sink + setVideoOutputSink(streamId); + }; + const signalVideoOutputSinkReady = VoiceEngine.signalVideoOutputSinkReady; + delete VoiceEngine.setVideoOutputSink; + delete VoiceEngine.signalVideoOutputSinkReady; + + function addVideoOutputSinkInternal(sinkId, streamId, frameCallback) { + let sinks = videoStreams[streamId]; + if (sinks == null) { + sinks = videoStreams[streamId] = new Map(); + } + if (sinks.size === 0) { - delete videoStreams[streamId]; - console.log(`Unsubscribing from frames for streamId ${streamId}`); - clearVideoOutputSink(streamId); + console.log(`Subscribing to frames for streamId ${streamId}`); + const onFrame = (imageData) => { + const sinks = videoStreams[streamId]; + if (sinks != null) { + for (const callback of sinks.values()) { + if (callback != null) { + callback(imageData); + } + } + } + signalVideoOutputSinkReady(streamId); + }; + setVideoOutputSink(streamId, onFrame, true); } + + sinks.set(sinkId, frameCallback); } -}; -let sinkId = 0; -VoiceEngine.getNextVideoOutputFrame = function (streamId) { - const nextVideoFrameSinkId = `getNextVideoFrame_${++sinkId}`; + VoiceEngine.addVideoOutputSink = function (sinkId, streamId, frameCallback) { + let canvasContext = null; + addVideoOutputSinkInternal(sinkId, streamId, (imageData) => { + if (canvasContext == null) { + canvasContext = ensureCanvasContext(sinkId); + if (canvasContext == null) { + return; + } + } + if (frameCallback != null) { + frameCallback(imageData.width, imageData.height); + } + // [adill] NB: Electron 9+ on macOS would show massive leaks in the the GPU helper process when a non-Discord + // window completely occludes the Discord window. Adding this tiny readback ameliorates the issue. We tried WebGL + // rendering which did not exhibit the issue, however, the context limit of 16 was too small to be a real + // alternative. + const leak = canvasContext.getImageData(0, 0, 1, 1); + canvasContext.putImageData(imageData, 0, 0); + }); + }; - return new Promise((resolve, reject) => { - setTimeout(() => { - VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId); - reject(new Error('getNextVideoOutputFrame timeout')); - }, 5000); + VoiceEngine.removeVideoOutputSink = function (sinkId, streamId) { + const sinks = videoStreams[streamId]; + if (sinks != null) { + sinks.delete(sinkId); + if (sinks.size === 0) { + delete videoStreams[streamId]; + console.log(`Unsubscribing from frames for streamId ${streamId}`); + clearVideoOutputSink(streamId); + } + } + }; - addVideoOutputSinkInternal(nextVideoFrameSinkId, streamId, (imageData) => { - VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId); - resolve({ - width: imageData.width, - height: imageData.height, - data: new Uint8ClampedArray(imageData.data.buffer), + let sinkId = 0; + VoiceEngine.getNextVideoOutputFrame = function (streamId) { + const nextVideoFrameSinkId = `getNextVideoFrame_${++sinkId}`; + + return new Promise((resolve, reject) => { + setTimeout(() => { + VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId); + reject(new Error('getNextVideoOutputFrame timeout')); + }, 5000); + + addVideoOutputSinkInternal(nextVideoFrameSinkId, streamId, (imageData) => { + VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId); + resolve({ + width: imageData.width, + height: imageData.height, + data: new Uint8ClampedArray(imageData.data.buffer), + }); }); }); - }); -}; + }; -console.log(`Initializing voice engine with audio subsystem: ${audioSubsystem}`); -VoiceEngine.initialize({audioSubsystem, logLevel, dataDirectory}); + console.log(`Initializing voice engine with audio subsystem: ${audioSubsystem}`); + VoiceEngine.initialize({audioSubsystem, logLevel, dataDirectory}); -module.exports = VoiceEngine; + module.exports = VoiceEngine; +} \ No newline at end of file diff --git a/scripts/build.js b/scripts/build.js index 65fedb6..b74bc14 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,286 +1,505 @@ -const child_process = require("child_process") -const path = require("path") -const terser = require("terser") -const util = require("util") +const child_process = require("child_process"); +const path = require("path"); +const terser = require("terser"); +const util = require("util"); -const production = true -const includeSourcesMaps = true +const production = true; +const includeSourcesMaps = true; -let fs = require("fs") +let fs = require("fs"); -exports.default = async function beforeBuild(context){ - await main() - return true -} +exports.default = async function beforeBuild(context) { + await main(); + return true; +}; const PROJECT_DIR = path.resolve(__dirname, ".."); console.log = (...args) => { - process.stdout.write(Buffer.from(util.formatWithOptions({colors: true}, ...args)+"\n", "binary").toString("utf8")) -} + process.stdout.write( + Buffer.from( + util.formatWithOptions({ colors: true }, ...args) + "\n", + "binary" + ).toString("utf8") + ); +}; console.info = (...args) => { - console.log(`\x1b[34m[INFO]\x1b[0m`, ...args) -} -let commit = child_process.execSync("git rev-parse HEAD").toString().split("\n")[0].trim() -console.info(`Obtained commit ${commit} for the build`) - -async function processNextDir(folder, folders, predicate, compile, ignoreModules){ - if(typeof ignoreModules === "undefined")ignoreModules = false - let files = fs.readdirSync(folder, {withFileTypes: true}) - for(let file of files){ - if(file.isFile()){ - let isMinified = file.name.endsWith(".min.js") || file.name.endsWith(".min.css") - let filepath = path.join(folder, file.name) - let type = file.name.split(".").pop().toLowerCase() - if(type === file.name)type = "" - if([ - "ts", - "md", - "gitignore", - "map" - ].includes(type)){ - console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because of type ${type}\x1b[0m`) - continue - } - if([ - "tsconfig.json", - "webpack.config.js" - ].includes(file.name)){ - console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because of name ${file.name}\x1b[0m`) - continue - } - if(folders.exclude && folders.exclude.test(filepath)){ - console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because regex\x1b[0m`) - continue - } - let hasMinifiedVersion = (type === "js" || type === "css") && !isMinified && files.find(f => { - return f.name === file.name.split(".").slice(0, -1).join(".")+".min."+type - }) - if(hasMinifiedVersion){ - console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because it has a minified version.\x1b[0m`) - continue - } - if(!isMinified && predicate(filepath) && filepath.split(/[\\/]+/).reverse()[1] !== "js"){ - await compile(filepath, path.join(filepath.replace(folders.startDir, folders.newDir)), "..") - }else{ - if(["js", "css"].includes(type)){ - if(!includeSourcesMaps){ - console.log(`We don't include sourcemap for this build. Skipping ${file.name}.`) - return await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) - } - let fileContent = (await fs.promises.readFile(filepath, "utf8")) - let sourceMap = fileContent.split(/[\n\r]+/g).pop() - if(!sourceMap || !sourceMap.startsWith("//# sourceMappingURL=")){ - console.log(`This file doesn't have sourcemap. ${file.name}.`) - await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) - continue - } - let sourceMapContent - if(sourceMap.slice(21).startsWith("data:")){ - console.log(`Extracting sourcemap from data uri. From file ${file.name}.`) - sourceMapContent = Buffer.from(sourceMap.split("=").slice(1).join("="), "base64").toString("utf-8") - }else{ - console.log(`Extracting sourcemap from file ${file.name}.map.`) - await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) - sourceMapContent = await fs.promises.readFile(path.join(folder, sourceMap.slice(21)), "utf8") - } - sourceMapContent = JSON.parse(sourceMapContent) - sourceMapContent.sourcesContent = [] - let sourceMapPath = filepath + ".map" - fileContent = fileContent - // source map - .replace(sourceMap, "//# sourceMappingURL="+filepath.split(/[\\\/]+/g).pop()+".map") - await fs.promises.writeFile(filepath.replace(folders.startDir, folders.newDir), fileContent) - await fs.promises.writeFile(filepath.replace(folders.startDir, folders.newDir)+".map", JSON.stringify(sourceMapContent)) - }else{ - await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) - } - } - }else if(file.isDirectory()){ - if(ignoreModules && file.name === "node_modules")continue - if(folders.exclude && folders.exclude.test(path.join(folder, file.name)))continue - await fs.promises.mkdir(path.join(folder, file.name).replace(folders.startDir, folders.newDir), {recursive: true}) - await processNextDir(path.join(folder, file.name), ...Array.from(arguments).slice(1)) + console.log(`\x1b[34m[INFO]\x1b[0m`, ...args); +}; +let commit = child_process + .execSync("git rev-parse HEAD") + .toString() + .split("\n")[0] + .trim(); +console.info(`Obtained commit ${commit} for the build`); + +async function processNextDir( + folder, + folders, + predicate, + compile, + ignoreModules +) { + if (typeof ignoreModules === "undefined") ignoreModules = false; + let files = fs.readdirSync(folder, { withFileTypes: true }); + for (let file of files) { + if (file.isFile()) { + let isMinified = + file.name.endsWith(".min.js") || file.name.endsWith(".min.css"); + let filepath = path.join(folder, file.name); + let type = file.name.split(".").pop().toLowerCase(); + if (type === file.name) type = ""; + if (["ts", "md", "gitignore", "map"].includes(type)) { + console.warn( + `\x1b[33mIgnored file ${path.relative( + folders.startDir, + filepath + )} because of type ${type}\x1b[0m` + ); + continue; + } + if (["tsconfig.json", "webpack.config.js"].includes(file.name)) { + console.warn( + `\x1b[33mIgnored file ${path.relative( + folders.startDir, + filepath + )} because of name ${file.name}\x1b[0m` + ); + continue; + } + if (folders.exclude && folders.exclude.test(filepath)) { + console.warn( + `\x1b[33mIgnored file ${path.relative( + folders.startDir, + filepath + )} because regex\x1b[0m` + ); + continue; + } + let hasMinifiedVersion = + (type === "js" || type === "css") && + !isMinified && + files.find((f) => { + return ( + f.name === + file.name.split(".").slice(0, -1).join(".") + ".min." + type + ); + }); + if (hasMinifiedVersion) { + console.warn( + `\x1b[33mIgnored file ${path.relative( + folders.startDir, + filepath + )} because it has a minified version.\x1b[0m` + ); + continue; + } + if ( + !isMinified && + predicate(filepath) && + filepath.split(/[\\/]+/).reverse()[1] !== "js" + ) { + await compile( + filepath, + path.join(filepath.replace(folders.startDir, folders.newDir)), + ".." + ); + } else { + if (["js", "css"].includes(type)) { + if (!includeSourcesMaps) { + console.log( + `We don't include sourcemap for this build. Skipping ${file.name}.` + ); + return await fs.promises.copyFile( + filepath, + filepath.replace(folders.startDir, folders.newDir) + ); + } + let fileContent = await fs.promises.readFile(filepath, "utf8"); + let sourceMap = fileContent.split(/[\n\r]+/g).pop(); + if (!sourceMap || !sourceMap.startsWith("//# sourceMappingURL=")) { + console.log(`This file doesn't have sourcemap. ${file.name}.`); + await fs.promises.copyFile( + filepath, + filepath.replace(folders.startDir, folders.newDir) + ); + continue; + } + let sourceMapContent; + if (sourceMap.slice(21).startsWith("data:")) { + console.log( + `Extracting sourcemap from data uri. From file ${file.name}.` + ); + sourceMapContent = Buffer.from( + sourceMap.split("=").slice(1).join("="), + "base64" + ).toString("utf-8"); + } else { + console.log(`Extracting sourcemap from file ${file.name}.map.`); + await fs.promises.copyFile( + filepath, + filepath.replace(folders.startDir, folders.newDir) + ); + sourceMapContent = await fs.promises.readFile( + path.join(folder, sourceMap.slice(21)), + "utf8" + ); + } + sourceMapContent = JSON.parse(sourceMapContent); + sourceMapContent.sourcesContent = []; + let sourceMapPath = filepath + ".map"; + fileContent = fileContent + // source map + .replace( + sourceMap, + "//# sourceMappingURL=" + + filepath.split(/[\\\/]+/g).pop() + + ".map" + ); + await fs.promises.writeFile( + filepath.replace(folders.startDir, folders.newDir), + fileContent + ); + await fs.promises.writeFile( + filepath.replace(folders.startDir, folders.newDir) + ".map", + JSON.stringify(sourceMapContent) + ); + } else { + await fs.promises.copyFile( + filepath, + filepath.replace(folders.startDir, folders.newDir) + ); } + } + } else if (file.isDirectory()) { + if (ignoreModules && file.name === "node_modules") continue; + if (folders.exclude && folders.exclude.test(path.join(folder, file.name))) + continue; + await fs.promises.mkdir( + path.join(folder, file.name).replace(folders.startDir, folders.newDir), + { recursive: true } + ); + await processNextDir( + path.join(folder, file.name), + ...Array.from(arguments).slice(1) + ); } + } } -async function main(){ - let startTimestamp = Date.now() - console.info("Starting build") - - console.info("Reseting existent directory...") - try{ - await fs.promises.rm("./distApp", {"recursive": true}) - } catch (error) { - console.error(error); - } +async function main() { + let startTimestamp = Date.now(); + console.info("Starting build"); - await fs.promises.mkdir(PROJECT_DIR+"/distApp/dist", {"recursive": true}) - - console.info("Executing command `yarn compile`") - child_process.execSync("yarn compile", { - encoding: "binary", - stdio: "inherit" - }) - - let startDir = path.join(PROJECT_DIR, "./dist") - let newDir = path.join(PROJECT_DIR, "./distApp/dist") - console.info("No error detected. Copying files from "+startDir+".") - await fs.promises.mkdir(startDir, {recursive: true}) + console.info("Reseting existent directory..."); + try { + await fs.promises.rm("./distApp", { recursive: true }); + } catch (error) { + console.error(error); + } - await processNextDir(startDir, { - startDir, - newDir - }, ((filepath) => filepath.endsWith(".js")), async (filepath, newpath) => { - console.info(`Minifying ${filepath} to ${newpath}`) + await fs.promises.mkdir(PROJECT_DIR + "/distApp/dist", { recursive: true }); - if(filepath.endsWith("git.js")){ - await fs.promises.writeFile(newpath, terser.minify(fs.readFileSync(filepath, "utf8").replace(/"{commit}"/g, `"${commit}"`)).code, "utf8") - }else{ - await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") + console.info("Executing command `yarn compile`"); + child_process.execSync("yarn compile", { + encoding: "binary", + stdio: "inherit", + }); + + let startDir = path.join(PROJECT_DIR, "./dist"); + let newDir = path.join(PROJECT_DIR, "./distApp/dist"); + console.info("No error detected. Copying files from " + startDir + "."); + await fs.promises.mkdir(startDir, { recursive: true }); + + await processNextDir( + startDir, + { + startDir, + newDir, + }, + (filepath) => filepath.endsWith(".js"), + async (filepath, newpath) => { + console.info(`Minifying ${filepath} to ${newpath}`); + + if (filepath.endsWith("git.js")) { + await fs.promises.writeFile( + newpath, + ( + await terser.minify( + fs + .readFileSync(filepath, "utf8") + .replace(/"{commit}"/g, `"${commit}"`) + ) + ).code, + "utf8" + ); + } else { + await fs.promises.writeFile( + newpath, + ( + await terser.minify(await fs.promises.readFile(filepath, "utf8")) + ).code, + "utf8" + ); + } + }, + true + ).then(() => { + console.info(`Copied files and minified them from ${startDir}.`); + }); + + await processNextDir( + path.join(PROJECT_DIR, "modules"), + { + startDir: path.join(PROJECT_DIR, "modules"), + newDir: path.join(PROJECT_DIR, "distApp", "modules"), + exclude: /discord_spellcheck/g, + }, + (filepath) => filepath.endsWith(".js"), + async (filepath, newpath) => { + console.info(`Minifying ${filepath} to ${newpath}`); + await fs.promises.writeFile( + newpath, + (await terser.minify(await fs.promises.readFile(filepath, "utf8"))).code, + "utf8" + ); + }, + true + ).then(() => { + console.info( + `Copied files and minified them from ${path.join( + PROJECT_DIR, + "modules" + )}.` + ); + }); + + await Promise.all( + ( + await fs.promises.readdir(path.join(PROJECT_DIR, "distApp", "modules")) + ).map(async (mdl) => { + let dir = path.join(PROJECT_DIR, "distApp", "modules", mdl); + + if (!fs.existsSync(path.join(dir, "package.json"))) { + if (mdl === "discord_desktop_core") { + dir = path.join(dir, "core"); + } else { + return; } - }, true).then(() => { - console.info(`Copied files and minified them from ${startDir}.`) - }) - - await processNextDir(path.join(PROJECT_DIR, "modules"), { - startDir: path.join(PROJECT_DIR, "modules"), - newDir: path.join(PROJECT_DIR, "distApp", "modules"), - exclude: /discord_spellcheck/g - }, ((filepath) => filepath.endsWith(".js")), async (filepath, newpath) => { - console.info(`Minifying ${filepath} to ${newpath}`) - await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") - }, true).then(() => { - console.info(`Copied files and minified them from ${path.join(PROJECT_DIR, "modules")}.`) - }) + } - await Promise.all((await fs.promises.readdir(path.join(PROJECT_DIR, "distApp", "modules"))).map(async mdl => { - let dir = path.join(PROJECT_DIR, "distApp", "modules", mdl) - - if(!fs.existsSync(path.join(dir, "package.json"))){ - if(mdl === "discord_desktop_core"){ - dir = path.join(dir, "core") - }else{ - return - } - } - - console.info(`Installing modules for ${mdl}`) - child_process.execSync("yarn --production", { - encoding: "binary", - cwd: dir, - stdio: "inherit" - }) - })) - - await fs.promises.mkdir(path.join(PROJECT_DIR, "distApp", "modules", "discord_spellcheck"), {recursive: true}) - await processNextDir(path.join(PROJECT_DIR, "modules", "discord_spellcheck"), { - startDir: path.join(PROJECT_DIR, "modules", "discord_spellcheck"), - newDir: path.join(PROJECT_DIR, "distApp", "modules", "discord_spellcheck") - }, ((filepath) => filepath.endsWith(".js")), async (filepath, newpath) => { - console.info(`Minifying ${filepath} to ${newpath}`) - await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") - }, false).then(() => { - console.info(`Copied files and minified them from ${path.join(PROJECT_DIR, "modules")}.`) - }) - - await processNextDir(path.join(PROJECT_DIR, "LightcordApi"), { - startDir: path.join(PROJECT_DIR, "LightcordApi"), - newDir: path.join(PROJECT_DIR, "distApp", "LightcordApi"), - exclude: /(src|webpack\.config\.js|tsconfig\.json|dist|docs)/g - }, ((filepath) => filepath.endsWith(".js") && (!production ? !filepath.includes("node_modules") : true)), async (filepath, newpath) => { - await fs.promises.copyFile(filepath, newpath) - }, true).then(() => { - console.info(`Copied files and minified them from ${path.join(PROJECT_DIR, "LightcordApi")}.`) - }) - - child_process.execSync("yarn --production", { + console.info(`Installing modules for ${mdl}`); + child_process.execSync("yarn --production", { encoding: "binary", - cwd: path.join(PROJECT_DIR, "distApp", "LightcordApi"), - stdio: "inherit" + cwd: dir, + stdio: "inherit", + }); }) + ); - function processDJS(dir){ - fs.mkdirSync(path.join(PROJECT_DIR, "distApp", "DiscordJS", dir), {recursive: true}) - return processNextDir(path.join(PROJECT_DIR, "DiscordJS", dir), { - startDir: path.join(PROJECT_DIR, "DiscordJS", dir), - newDir: path.join(PROJECT_DIR, "distApp", "DiscordJS", dir), - exclude: /node_modules/g - }, ((filepath) => filepath.endsWith(".js")), async (filepath, newpath) => { - console.info(`Minifying ${filepath} to ${newpath}`) - await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") - }).then(() => { - console.info(`Copied files and minified them from ${path.join(PROJECT_DIR, "DiscordJS", dir)}.`) - }) - } - async function copyFileDJS(file){ - await fs.promises.writeFile(path.join(PROJECT_DIR, "distApp", "DiscordJS", file), await fs.promises.readFile(path.join(PROJECT_DIR, "DiscordJS", file))) + await fs.promises.mkdir( + path.join(PROJECT_DIR, "distApp", "modules", "discord_spellcheck"), + { recursive: true } + ); + await processNextDir( + path.join(PROJECT_DIR, "modules", "discord_spellcheck"), + { + startDir: path.join(PROJECT_DIR, "modules", "discord_spellcheck"), + newDir: path.join( + PROJECT_DIR, + "distApp", + "modules", + "discord_spellcheck" + ), + }, + (filepath) => filepath.endsWith(".js"), + async (filepath, newpath) => { + console.info(`Minifying ${filepath} to ${newpath}`); + await fs.promises.writeFile( + newpath, + (await terser.minify(await fs.promises.readFile(filepath, "utf8"))).code, + "utf8" + ); + }, + false + ).then(() => { + console.info( + `Copied files and minified them from ${path.join( + PROJECT_DIR, + "modules" + )}.` + ); + }); + + await processNextDir( + path.join(PROJECT_DIR, "LightcordApi"), + { + startDir: path.join(PROJECT_DIR, "LightcordApi"), + newDir: path.join(PROJECT_DIR, "distApp", "LightcordApi"), + exclude: /(src|webpack\.config\.js|tsconfig\.json|dist|docs)/g, + }, + (filepath) => + filepath.endsWith(".js") && + (!production ? !filepath.includes("node_modules") : true), + async (filepath, newpath) => { + await fs.promises.copyFile(filepath, newpath); + }, + true + ).then(() => { + console.info( + `Copied files and minified them from ${path.join( + PROJECT_DIR, + "LightcordApi" + )}.` + ); + }); + + child_process.execSync("yarn --production", { + encoding: "binary", + cwd: path.join(PROJECT_DIR, "distApp", "LightcordApi"), + stdio: "inherit", + }); + + function processDJS(dir) { + fs.mkdirSync(path.join(PROJECT_DIR, "distApp", "DiscordJS", dir), { + recursive: true, + }); + return processNextDir( + path.join(PROJECT_DIR, "DiscordJS", dir), + { + startDir: path.join(PROJECT_DIR, "DiscordJS", dir), + newDir: path.join(PROJECT_DIR, "distApp", "DiscordJS", dir), + exclude: /node_modules/g, + }, + (filepath) => filepath.endsWith(".js"), + async (filepath, newpath) => { + console.info(`Minifying ${filepath} to ${newpath}`); + await fs.promises.writeFile( + newpath, + (await terser.minify(await fs.promises.readFile(filepath, "utf8"))).code, + "utf8" + ); + } + ).then(() => { + console.info( + `Copied files and minified them from ${path.join( + PROJECT_DIR, + "DiscordJS", + dir + )}.` + ); + }); + } + async function copyFileDJS(file) { + await fs.promises.writeFile( + path.join(PROJECT_DIR, "distApp", "DiscordJS", file), + await fs.promises.readFile(path.join(PROJECT_DIR, "DiscordJS", file)) + ); + } + + await processDJS("dist"); + await copyFileDJS("package.json"); + + child_process.execSync("yarn --production", { + encoding: "binary", + cwd: path.join(PROJECT_DIR, "distApp", "DiscordJS"), + stdio: "inherit", + }); + + fs.mkdirSync(path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist"), { + recursive: true, + }); + const BDPackageJSON = require("../BetterDiscordApp/package.json"); + fs.writeFileSync( + path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "package.json"), + JSON.stringify(BDPackageJSON), + "utf8" + ); + const files = ["index.min.js", "style.min.css"]; + files.forEach((e) => { + files.push(e + ".map"); + }); + files.forEach((e) => { + const pth = path.join(PROJECT_DIR, "BetterDiscordApp", "dist", e); + if (!fs.existsSync(pth)) + return console.error( + `\x1b[31mFile ${pth} from betterdiscord does not exist.\x1b[0m` + ); + if (e.endsWith(".map")) { + const data = JSON.parse(fs.readFileSync(pth, "utf8")); + data.sourcesContent = []; + fs.writeFileSync( + path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist", e), + JSON.stringify(data) + ); + } else { + fs.copyFileSync( + pth, + path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist", e) + ); } + }); - await processDJS("dist") - await copyFileDJS("package.json") - - child_process.execSync("yarn --production", { - encoding: "binary", - cwd: path.join(PROJECT_DIR, "distApp", "DiscordJS"), - stdio: "inherit" - }) - - fs.mkdirSync(path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist"), {recursive: true}) - const BDPackageJSON = require("../BetterDiscordApp/package.json") - fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "package.json"), JSON.stringify(BDPackageJSON), "utf8") - const files = [ - "index.min.js", - "style.min.css" - ] - files.forEach(e => { - files.push(e + ".map") - }) - files.forEach(e => { - const pth = path.join(PROJECT_DIR, "BetterDiscordApp", "dist", e) - if(!fs.existsSync(pth))return console.error(`\x1b[31mFile ${pth} from betterdiscord does not exist.\x1b[0m`) - if(e.endsWith(".map")){ - const data = JSON.parse(fs.readFileSync(pth, "utf8")) - data.sourcesContent = [] - fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist", e), JSON.stringify(data)) - }else{ - fs.copyFileSync(pth, path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist", e)) - } - }) - - await fs.promises.mkdir(path.join(PROJECT_DIR, "distApp", "splash", "videos"), {recursive: true}) - await processNextDir(path.join(PROJECT_DIR, "splash"), { - startDir: path.join(PROJECT_DIR, "splash"), - newDir: path.join(PROJECT_DIR, "distApp", "splash"), - exclude: /node_modules/g - }, (filepath) => { - if(filepath.endsWith(".js"))return true - return false - }, async (filepath, newpath) => { - console.info(`Minifying ${filepath} to ${newpath}`) - await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") - }).then(() => { - console.info(`Copied files and minified them from ${path.join(PROJECT_DIR, "splash")}.`) - }) - fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "LICENSE"), fs.readFileSync(path.join(PROJECT_DIR, "LICENSE"))) - - let packageJSON = require("../package.json") - packageJSON.scripts["build:electron_linux"] = packageJSON.scripts["build:electron_linux"].replace("./distApp", ".") - packageJSON.scripts["build:electron_win"] = packageJSON.scripts["build:electron_win"].replace("./distApp", ".") - packageJSON.scripts["build:electron_mac"] = packageJSON.scripts["build:electron_mac"].replace("./distApp", ".") - - fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "package.json"), JSON.stringify(packageJSON), "utf8") - - console.info(`Installing ${Object.keys(packageJSON.dependencies).length} packages...`) - child_process.execSync("yarn --production", { - encoding: "binary", - cwd: path.join(PROJECT_DIR, "distApp"), - stdio: "inherit" - }) - console.info("Build took "+(Date.now() - startTimestamp) +"ms.") + await fs.promises.mkdir( + path.join(PROJECT_DIR, "distApp", "splash", "videos"), + { recursive: true } + ); + await processNextDir( + path.join(PROJECT_DIR, "splash"), + { + startDir: path.join(PROJECT_DIR, "splash"), + newDir: path.join(PROJECT_DIR, "distApp", "splash"), + exclude: /node_modules/g, + }, + (filepath) => { + if (filepath.endsWith(".js")) return true; + return false; + }, + async (filepath, newpath) => { + console.info(`Minifying ${filepath} to ${newpath}`); + await fs.promises.writeFile( + newpath, + (await terser.minify(await fs.promises.readFile(filepath, "utf8"))).code, + "utf8" + ); + } + ).then(() => { + console.info( + `Copied files and minified them from ${path.join(PROJECT_DIR, "splash")}.` + ); + }); + fs.writeFileSync( + path.join(PROJECT_DIR, "distApp", "LICENSE"), + fs.readFileSync(path.join(PROJECT_DIR, "LICENSE")) + ); + + let packageJSON = require("../package.json"); + packageJSON.scripts["build:electron_linux"] = packageJSON.scripts[ + "build:electron_linux" + ].replace("./distApp", "."); + packageJSON.scripts["build:electron_win"] = packageJSON.scripts[ + "build:electron_win" + ].replace("./distApp", "."); + packageJSON.scripts["build:electron_mac"] = packageJSON.scripts[ + "build:electron_mac" + ].replace("./distApp", "."); + + fs.writeFileSync( + path.join(PROJECT_DIR, "distApp", "package.json"), + JSON.stringify(packageJSON), + "utf8" + ); + + console.info( + `Installing ${Object.keys(packageJSON.dependencies).length} packages...` + ); + child_process.execSync("yarn --production", { + encoding: "binary", + cwd: path.join(PROJECT_DIR, "distApp"), + stdio: "inherit", + }); + console.info("Build took " + (Date.now() - startTimestamp) + "ms."); } -main() -.catch(err => { - console.error(err) - process.exit(1) -}) +main().catch((err) => { + console.error(err); + process.exit(1); +});