WIP: Fix builds

This commit is contained in:
Aaron Dewes 2021-08-06 16:40:08 +01:00
parent 18586644e1
commit 5f8adf787d
4 changed files with 721 additions and 548 deletions

View File

@ -23,14 +23,11 @@ jobs:
yarn yarn
yarn devInstall yarn devInstall
yarn compile yarn compile
yarn build
- name: Build/release Electron app - name: Release
uses: samuelmeuli/action-electron-builder@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with: with:
# GitHub token, automatically provided to the action files:
# (No need to define this secret in the repo settings) - 'builds/*'
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') }}

View File

@ -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

View File

@ -80,271 +80,270 @@ if (process.arch === 'arm64') {
module.exports = { module.exports = {
consoleLog = (...args) => {}, 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'); if (process.platform === 'win32') {
features.declareSupported('voice_multiple_connections'); features.declareSupported('voice_legacy_subsystem');
features.declareSupported('media_devices'); features.declareSupported('soundshare');
features.declareSupported('media_video'); features.declareSupported('wumpus_video');
features.declareSupported('debug_logging'); features.declareSupported('hybrid_video');
features.declareSupported('set_audio_device_by_id'); features.declareSupported('elevated_hook');
features.declareSupported('set_video_device_by_id'); features.declareSupported('soundshare_loopback');
features.declareSupported('loopback'); features.declareSupported('screen_previews');
features.declareSupported('experiment_config'); features.declareSupported('window_previews');
features.declareSupported('remote_locus_network_control'); features.declareSupported('audio_debug_state');
features.declareSupported('connection_replay'); features.declareSupported('video_effects');
features.declareSupported('simulcast'); // NOTE(jvass): currently there's no experimental encoders! Add this back if you
features.declareSupported('direct_video'); // add one and want to re-enable the UI for them.
// features.declareSupported('experimental_encoders');
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;
} }
return bindConnectionInstance(new VoiceEngine.VoiceReplayConnection(replayLog, audioEngineId, callback)); function bindConnectionInstance(instance) {
}; return {
destroy: () => instance.destroy(),
VoiceEngine.setAudioSubsystem = function (subsystem) { setTransportOptions: (options) => instance.setTransportOptions(options),
if (appSettings == null) { setSelfMute: (mute) => instance.setSelfMute(mute),
console.warn('Unable to access app settings.'); setSelfDeafen: (deaf) => instance.setSelfDeafen(deaf),
return;
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 VoiceEngine.createTransport = VoiceEngine._createTransport;
// 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);
if (isElectronRenderer) { if (isElectronRenderer) {
window.DiscordNative.app.relaunch(); VoiceEngine.setImageDataAllocator((width, height) => new window.ImageData(width, height));
}
};
VoiceEngine.setDebugLogging = function (enable) {
if (appSettings == null) {
console.warn('Unable to access app settings.');
return;
} }
if (debugLogging === enable) { VoiceEngine.createVoiceConnection = function (audioSSRC, userId, address, port, onConnectCallback, experiments, rids) {
return; let instance = null;
} if (rids != null) {
instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments, rids);
appSettings.set('debugLogging', enable); } else if (experiments != null) {
instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments);
if (isElectronRenderer) { } else {
window.DiscordNative.app.relaunch(); instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback);
}
};
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;
}
} }
return bindConnectionInstance(instance);
};
VoiceEngine.createOwnStreamConnection = VoiceEngine.createVoiceConnection;
if (canvas == null) { VoiceEngine.createReplayConnection = function (audioEngineId, callback, replayLog) {
if (replayLog == null) {
return null; return null;
} }
}
const context = canvas.getContext('2d'); return bindConnectionInstance(new VoiceEngine.VoiceReplayConnection(replayLog, audioEngineId, callback));
if (context == null) { };
console.log(`Failed to initialize context for sinkId ${sinkId}`);
return null;
}
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 // TODO: With experiment controlling ADM selection, this may be incorrect since
// data directly to clients at any reasonably fast interval so we've replaced setVideoOutputSink with a direct canvas // audioSubsystem is read from settings (or default if does not exists)
// renderer via addVideoOutputSink // and not the actual ADM used.
const setVideoOutputSink = VoiceEngine.setVideoOutputSink; if (subsystem === audioSubsystem) {
const clearVideoOutputSink = (streamId) => { return;
// [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) { appSettings.set('audioSubsystem', subsystem);
let sinks = videoStreams[streamId]; appSettings.set('useLegacyAudioDevice', false);
if (sinks == null) {
sinks = videoStreams[streamId] = new Map();
}
if (sinks.size === 0) { if (isElectronRenderer) {
console.log(`Subscribing to frames for streamId ${streamId}`); window.DiscordNative.app.relaunch();
const onFrame = (imageData) => { }
const sinks = videoStreams[streamId]; };
if (sinks != null) {
for (const callback of sinks.values()) { VoiceEngine.setDebugLogging = function (enable) {
if (callback != null) { if (appSettings == null) {
callback(imageData); 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); if (canvas == null) {
} return null;
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);
});
};
VoiceEngine.removeVideoOutputSink = function (sinkId, streamId) { const context = canvas.getContext('2d');
const sinks = videoStreams[streamId]; if (context == null) {
if (sinks != null) { console.log(`Failed to initialize context for sinkId ${sinkId}`);
sinks.delete(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) { if (sinks.size === 0) {
delete videoStreams[streamId]; console.log(`Subscribing to frames for streamId ${streamId}`);
console.log(`Unsubscribing from frames for streamId ${streamId}`); const onFrame = (imageData) => {
clearVideoOutputSink(streamId); 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.addVideoOutputSink = function (sinkId, streamId, frameCallback) {
VoiceEngine.getNextVideoOutputFrame = function (streamId) { let canvasContext = null;
const nextVideoFrameSinkId = `getNextVideoFrame_${++sinkId}`; 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) => { VoiceEngine.removeVideoOutputSink = function (sinkId, streamId) {
setTimeout(() => { const sinks = videoStreams[streamId];
VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId); if (sinks != null) {
reject(new Error('getNextVideoOutputFrame timeout')); sinks.delete(sinkId);
}, 5000); if (sinks.size === 0) {
delete videoStreams[streamId];
console.log(`Unsubscribing from frames for streamId ${streamId}`);
clearVideoOutputSink(streamId);
}
}
};
addVideoOutputSinkInternal(nextVideoFrameSinkId, streamId, (imageData) => { let sinkId = 0;
VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId); VoiceEngine.getNextVideoOutputFrame = function (streamId) {
resolve({ const nextVideoFrameSinkId = `getNextVideoFrame_${++sinkId}`;
width: imageData.width,
height: imageData.height, return new Promise((resolve, reject) => {
data: new Uint8ClampedArray(imageData.data.buffer), 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}`); console.log(`Initializing voice engine with audio subsystem: ${audioSubsystem}`);
VoiceEngine.initialize({audioSubsystem, logLevel, dataDirectory}); VoiceEngine.initialize({audioSubsystem, logLevel, dataDirectory});
module.exports = VoiceEngine; module.exports = VoiceEngine;
}

View File

@ -1,286 +1,505 @@
const child_process = require("child_process") const child_process = require("child_process");
const path = require("path") const path = require("path");
const terser = require("terser") const terser = require("terser");
const util = require("util") const util = require("util");
const production = true const production = true;
const includeSourcesMaps = true const includeSourcesMaps = true;
let fs = require("fs") let fs = require("fs");
exports.default = async function beforeBuild(context){ exports.default = async function beforeBuild(context) {
await main() await main();
return true return true;
} };
const PROJECT_DIR = path.resolve(__dirname, ".."); const PROJECT_DIR = path.resolve(__dirname, "..");
console.log = (...args) => { 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.info = (...args) => {
console.log(`\x1b[34m[INFO]\x1b[0m`, ...args) console.log(`\x1b[34m[INFO]\x1b[0m`, ...args);
} };
let commit = child_process.execSync("git rev-parse HEAD").toString().split("\n")[0].trim() let commit = child_process
console.info(`Obtained commit ${commit} for the build`) .execSync("git rev-parse HEAD")
.toString()
async function processNextDir(folder, folders, predicate, compile, ignoreModules){ .split("\n")[0]
if(typeof ignoreModules === "undefined")ignoreModules = false .trim();
let files = fs.readdirSync(folder, {withFileTypes: true}) console.info(`Obtained commit ${commit} for the build`);
for(let file of files){
if(file.isFile()){ async function processNextDir(
let isMinified = file.name.endsWith(".min.js") || file.name.endsWith(".min.css") folder,
let filepath = path.join(folder, file.name) folders,
let type = file.name.split(".").pop().toLowerCase() predicate,
if(type === file.name)type = "" compile,
if([ ignoreModules
"ts", ) {
"md", if (typeof ignoreModules === "undefined") ignoreModules = false;
"gitignore", let files = fs.readdirSync(folder, { withFileTypes: true });
"map" for (let file of files) {
].includes(type)){ if (file.isFile()) {
console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because of type ${type}\x1b[0m`) let isMinified =
continue file.name.endsWith(".min.js") || file.name.endsWith(".min.css");
} let filepath = path.join(folder, file.name);
if([ let type = file.name.split(".").pop().toLowerCase();
"tsconfig.json", if (type === file.name) type = "";
"webpack.config.js" if (["ts", "md", "gitignore", "map"].includes(type)) {
].includes(file.name)){ console.warn(
console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because of name ${file.name}\x1b[0m`) `\x1b[33mIgnored file ${path.relative(
continue folders.startDir,
} filepath
if(folders.exclude && folders.exclude.test(filepath)){ )} because of type ${type}\x1b[0m`
console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because regex\x1b[0m`) );
continue continue;
} }
let hasMinifiedVersion = (type === "js" || type === "css") && !isMinified && files.find(f => { if (["tsconfig.json", "webpack.config.js"].includes(file.name)) {
return f.name === file.name.split(".").slice(0, -1).join(".")+".min."+type console.warn(
}) `\x1b[33mIgnored file ${path.relative(
if(hasMinifiedVersion){ folders.startDir,
console.warn(`\x1b[33mIgnored file ${path.relative(folders.startDir, filepath)} because it has a minified version.\x1b[0m`) filepath
continue )} because of name ${file.name}\x1b[0m`
} );
if(!isMinified && predicate(filepath) && filepath.split(/[\\/]+/).reverse()[1] !== "js"){ continue;
await compile(filepath, path.join(filepath.replace(folders.startDir, folders.newDir)), "..") }
}else{ if (folders.exclude && folders.exclude.test(filepath)) {
if(["js", "css"].includes(type)){ console.warn(
if(!includeSourcesMaps){ `\x1b[33mIgnored file ${path.relative(
console.log(`We don't include sourcemap for this build. Skipping ${file.name}.`) folders.startDir,
return await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) filepath
} )} because regex\x1b[0m`
let fileContent = (await fs.promises.readFile(filepath, "utf8")) );
let sourceMap = fileContent.split(/[\n\r]+/g).pop() continue;
if(!sourceMap || !sourceMap.startsWith("//# sourceMappingURL=")){ }
console.log(`This file doesn't have sourcemap. ${file.name}.`) let hasMinifiedVersion =
await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) (type === "js" || type === "css") &&
continue !isMinified &&
} files.find((f) => {
let sourceMapContent return (
if(sourceMap.slice(21).startsWith("data:")){ f.name ===
console.log(`Extracting sourcemap from data uri. From file ${file.name}.`) file.name.split(".").slice(0, -1).join(".") + ".min." + type
sourceMapContent = Buffer.from(sourceMap.split("=").slice(1).join("="), "base64").toString("utf-8") );
}else{ });
console.log(`Extracting sourcemap from file ${file.name}.map.`) if (hasMinifiedVersion) {
await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) console.warn(
sourceMapContent = await fs.promises.readFile(path.join(folder, sourceMap.slice(21)), "utf8") `\x1b[33mIgnored file ${path.relative(
} folders.startDir,
sourceMapContent = JSON.parse(sourceMapContent) filepath
sourceMapContent.sourcesContent = [] )} because it has a minified version.\x1b[0m`
let sourceMapPath = filepath + ".map" );
fileContent = fileContent continue;
// source map }
.replace(sourceMap, "//# sourceMappingURL="+filepath.split(/[\\\/]+/g).pop()+".map") if (
await fs.promises.writeFile(filepath.replace(folders.startDir, folders.newDir), fileContent) !isMinified &&
await fs.promises.writeFile(filepath.replace(folders.startDir, folders.newDir)+".map", JSON.stringify(sourceMapContent)) predicate(filepath) &&
}else{ filepath.split(/[\\/]+/).reverse()[1] !== "js"
await fs.promises.copyFile(filepath, filepath.replace(folders.startDir, folders.newDir)) ) {
} await compile(
} filepath,
}else if(file.isDirectory()){ path.join(filepath.replace(folders.startDir, folders.newDir)),
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}) } else {
await processNextDir(path.join(folder, file.name), ...Array.from(arguments).slice(1)) 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(){ async function main() {
let startTimestamp = Date.now() let startTimestamp = Date.now();
console.info("Starting build") console.info("Starting build");
console.info("Reseting existent directory...")
try{
await fs.promises.rm("./distApp", {"recursive": true})
} catch (error) {
console.error(error);
}
await fs.promises.mkdir(PROJECT_DIR+"/distApp/dist", {"recursive": true}) console.info("Reseting existent directory...");
try {
console.info("Executing command `yarn compile`") await fs.promises.rm("./distApp", { recursive: true });
child_process.execSync("yarn compile", { } catch (error) {
encoding: "binary", console.error(error);
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, { await fs.promises.mkdir(PROJECT_DIR + "/distApp/dist", { recursive: true });
startDir,
newDir
}, ((filepath) => filepath.endsWith(".js")), async (filepath, newpath) => {
console.info(`Minifying ${filepath} to ${newpath}`)
if(filepath.endsWith("git.js")){ console.info("Executing command `yarn compile`");
await fs.promises.writeFile(newpath, terser.minify(fs.readFileSync(filepath, "utf8").replace(/"{commit}"/g, `"${commit}"`)).code, "utf8") child_process.execSync("yarn compile", {
}else{ encoding: "binary",
await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") 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 => { console.info(`Installing modules for ${mdl}`);
let dir = path.join(PROJECT_DIR, "distApp", "modules", mdl) child_process.execSync("yarn --production", {
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", {
encoding: "binary", encoding: "binary",
cwd: path.join(PROJECT_DIR, "distApp", "LightcordApi"), cwd: dir,
stdio: "inherit" stdio: "inherit",
});
}) })
);
function processDJS(dir){ await fs.promises.mkdir(
fs.mkdirSync(path.join(PROJECT_DIR, "distApp", "DiscordJS", dir), {recursive: true}) path.join(PROJECT_DIR, "distApp", "modules", "discord_spellcheck"),
return processNextDir(path.join(PROJECT_DIR, "DiscordJS", dir), { { recursive: true }
startDir: path.join(PROJECT_DIR, "DiscordJS", dir), );
newDir: path.join(PROJECT_DIR, "distApp", "DiscordJS", dir), await processNextDir(
exclude: /node_modules/g path.join(PROJECT_DIR, "modules", "discord_spellcheck"),
}, ((filepath) => filepath.endsWith(".js")), async (filepath, newpath) => { {
console.info(`Minifying ${filepath} to ${newpath}`) startDir: path.join(PROJECT_DIR, "modules", "discord_spellcheck"),
await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") newDir: path.join(
}).then(() => { PROJECT_DIR,
console.info(`Copied files and minified them from ${path.join(PROJECT_DIR, "DiscordJS", dir)}.`) "distApp",
}) "modules",
} "discord_spellcheck"
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))) },
(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 fs.promises.mkdir(
await copyFileDJS("package.json") path.join(PROJECT_DIR, "distApp", "splash", "videos"),
{ recursive: true }
child_process.execSync("yarn --production", { );
encoding: "binary", await processNextDir(
cwd: path.join(PROJECT_DIR, "distApp", "DiscordJS"), path.join(PROJECT_DIR, "splash"),
stdio: "inherit" {
}) startDir: path.join(PROJECT_DIR, "splash"),
newDir: path.join(PROJECT_DIR, "distApp", "splash"),
fs.mkdirSync(path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist"), {recursive: true}) exclude: /node_modules/g,
const BDPackageJSON = require("../BetterDiscordApp/package.json") },
fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "package.json"), JSON.stringify(BDPackageJSON), "utf8") (filepath) => {
const files = [ if (filepath.endsWith(".js")) return true;
"index.min.js", return false;
"style.min.css" },
] async (filepath, newpath) => {
files.forEach(e => { console.info(`Minifying ${filepath} to ${newpath}`);
files.push(e + ".map") await fs.promises.writeFile(
}) newpath,
files.forEach(e => { (await terser.minify(await fs.promises.readFile(filepath, "utf8"))).code,
const pth = path.join(PROJECT_DIR, "BetterDiscordApp", "dist", e) "utf8"
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")) ).then(() => {
data.sourcesContent = [] console.info(
fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist", e), JSON.stringify(data)) `Copied files and minified them from ${path.join(PROJECT_DIR, "splash")}.`
}else{ );
fs.copyFileSync(pth, path.join(PROJECT_DIR, "distApp", "BetterDiscordApp", "dist", e)) });
} fs.writeFileSync(
}) path.join(PROJECT_DIR, "distApp", "LICENSE"),
fs.readFileSync(path.join(PROJECT_DIR, "LICENSE"))
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"), let packageJSON = require("../package.json");
newDir: path.join(PROJECT_DIR, "distApp", "splash"), packageJSON.scripts["build:electron_linux"] = packageJSON.scripts[
exclude: /node_modules/g "build:electron_linux"
}, (filepath) => { ].replace("./distApp", ".");
if(filepath.endsWith(".js"))return true packageJSON.scripts["build:electron_win"] = packageJSON.scripts[
return false "build:electron_win"
}, async (filepath, newpath) => { ].replace("./distApp", ".");
console.info(`Minifying ${filepath} to ${newpath}`) packageJSON.scripts["build:electron_mac"] = packageJSON.scripts[
await fs.promises.writeFile(newpath, terser.minify(await fs.promises.readFile(filepath, "utf8")).code, "utf8") "build:electron_mac"
}).then(() => { ].replace("./distApp", ".");
console.info(`Copied files and minified them from ${path.join(PROJECT_DIR, "splash")}.`)
}) fs.writeFileSync(
fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "LICENSE"), fs.readFileSync(path.join(PROJECT_DIR, "LICENSE"))) path.join(PROJECT_DIR, "distApp", "package.json"),
JSON.stringify(packageJSON),
let packageJSON = require("../package.json") "utf8"
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", ".") console.info(
`Installing ${Object.keys(packageJSON.dependencies).length} packages...`
fs.writeFileSync(path.join(PROJECT_DIR, "distApp", "package.json"), JSON.stringify(packageJSON), "utf8") );
child_process.execSync("yarn --production", {
console.info(`Installing ${Object.keys(packageJSON.dependencies).length} packages...`) encoding: "binary",
child_process.execSync("yarn --production", { cwd: path.join(PROJECT_DIR, "distApp"),
encoding: "binary", stdio: "inherit",
cwd: path.join(PROJECT_DIR, "distApp"), });
stdio: "inherit" console.info("Build took " + (Date.now() - startTimestamp) + "ms.");
})
console.info("Build took "+(Date.now() - startTimestamp) +"ms.")
} }
main() main().catch((err) => {
.catch(err => { console.error(err);
console.error(err) process.exit(1);
process.exit(1) });
})