diff --git a/.gitignore b/.gitignore index 48bef8880..bf0c17f46 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,8 @@ src/package-lock.json *.asar _* miniSrc/ +tmp/ +AGENTS.md +.venv/ -*.crswap # crostini tmp files \ No newline at end of file +*.crswap # crostini tmp files diff --git a/README.md b/README.md index 139af6698..6f976f5d6 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,9 @@ ## [Install Guide](https://github.com/GooseMod/OpenAsar/wiki/Install-Guide) +## Local Build +See [docs/build.md](docs/build.md) for local build instructions, including local test builds with `--disable-autoupdate`. + ## Config + You can configure OpenAsar by clicking the "OpenAsar..." version info in the bottom of your settings sidebar, which will open the config window. diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 000000000..e4777c9da --- /dev/null +++ b/docs/build.md @@ -0,0 +1,53 @@ +# Local Build + +The GitHub nightly workflow builds OpenAsar by: + +- stamping a `nightly-` version into `src/index.js` +- stripping the `src/` tree with `node scripts/strip.js` +- packing the final archive with `asar pack` + +For local builds, use `scripts/pack.js`, which follows that same flow without modifying your working tree in place. + +## Requirements +- `node` +- `asar` + +Example install for `asar`: + +```bash +npm i -g asar +``` + +## Build With Normal Auto-Update Behavior +This keeps the default OpenAsar self-update behavior enabled. + +```bash +node scripts/pack.js --version nightly-$(git rev-parse --short HEAD) --output tmp/app.asar +``` + +This build updates from the default upstream release repo: + +```text +GooseMod/OpenAsar +``` + +## Build With A Custom Update Repo +Use this when you want a build to self-update from your own fork releases instead of upstream. + +```bash +node scripts/pack.js --update-repo owner/repo --version nightly-$(git rev-parse --short HEAD) --output tmp/app.asar +``` + +## Build With Auto-Update Disabled +Use this for local testing when you do not want the built `app.asar` to replace itself with the upstream nightly release on launch. + +```bash +node scripts/pack.js --disable-autoupdate --version nightly-$(git rev-parse --short HEAD)-localtest --output tmp/app.asar +``` + +## Output +All commands above produce: + +```text +tmp/app.asar +``` diff --git a/scripts/pack.js b/scripts/pack.js new file mode 100644 index 000000000..7fb9b83b5 --- /dev/null +++ b/scripts/pack.js @@ -0,0 +1,127 @@ +const { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } = require('fs'); +const { join, resolve } = require('path'); +const { spawnSync } = require('child_process'); + +const root = resolve(__dirname, '..'); +const tmpRoot = join(root, 'tmp', 'pack-build'); + +const args = process.argv.slice(2); +let disableAutoUpdate = false; +let updateRepo = 'GooseMod/OpenAsar'; +let version; +let output = join(root, 'tmp', 'openasar-build', 'app.asar'); + +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '--disable-autoupdate') { + disableAutoUpdate = true; + continue; + } + + if (arg === '--version') { + version = args[++i]; + continue; + } + + if (arg === '--update-repo') { + updateRepo = args[++i]; + continue; + } + + if (arg === '--output') { + output = resolve(args[++i]); + continue; + } + + if (arg === '--help') { + console.log('Usage: node scripts/pack.js [--disable-autoupdate] [--update-repo ] [--version ] [--output ]'); + process.exit(0); + } + + throw new Error(`Unknown argument: ${arg}`); +} + +if (!/^[^/\s]+\/[^/\s]+$/.test(updateRepo)) throw new Error(`Invalid --update-repo value: ${updateRepo}`); + +if (!version) { + const git = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { + cwd: root, + encoding: 'utf8' + }); + + const shortSha = git.status === 0 ? git.stdout.trim() : 'local'; + version = `nightly-${shortSha}`; +} + +const stripCode = code => code + .replace(/(^| )\/\/.*$/gm, '') + .replaceAll('const ', 'const~') + .replaceAll('let ', 'let~') + .replaceAll('var ', 'var~') + .replaceAll('class ', 'class~') + .replace(/get [^=}]/g, _ => _.replaceAll(' ', '~')) + .replaceAll('delete ', 'delete~') + .replaceAll(' extends ', '~extends~') + .replaceAll('typeof ', 'typeof~') + .replaceAll(' of ', '~of~') + .replaceAll(' in ', '~in~') + .replaceAll('case ', 'case~') + .replaceAll('await ', 'await~') + .replaceAll('new ', 'new~') + .replaceAll('return ', 'return~') + .replaceAll('function ', 'function~') + .replaceAll('void ', 'void~') + .replaceAll('throw ', 'throw~') + .replaceAll('async ', 'async~') + .replaceAll('else ', 'else~') + .replace('/([0-9]+) files/', '/([0-9]+)~files/') + .replace(/((['"`])[\s\S]*?\2)|[ \n]/g, (_, g1) => g1 || '') + .replaceAll('~', ' ') + .replaceAll('? ?', '??'); + +const fixHtml = code => code + .replaceAll(' loop', '~loop') + .replaceAll(' autoplay', '~autoplay') + .replaceAll(' src', '~src') + .replaceAll(' id', '~id'); + +const stripJs = path => writeFileSync(path, stripCode(readFileSync(path, 'utf8'))); +const stripHtml = path => writeFileSync(path, stripCode(fixHtml(readFileSync(path, 'utf8')))); +const stripJson = path => { + const data = JSON.parse(readFileSync(path, 'utf8')); + if (data.description) delete data.description; + writeFileSync(path, JSON.stringify(data)); +}; + +const stripTree = dirPath => readdirSync(dirPath, { withFileTypes: true }).forEach(entry => { + const path = join(dirPath, entry.name); + + if (entry.isDirectory()) return stripTree(path); + if (entry.name.endsWith('.js')) return stripJs(path); + if (entry.name.endsWith('.json')) return stripJson(path); + if (entry.name.endsWith('.html')) return stripHtml(path); +}); + +rmSync(tmpRoot, { recursive: true, force: true }); +mkdirSync(tmpRoot, { recursive: true }); +cpSync(join(root, 'src'), join(tmpRoot, 'src'), { recursive: true }); + +const indexPath = join(tmpRoot, 'src', 'index.js'); +let indexCode = readFileSync(indexPath, 'utf8'); +indexCode = indexCode.replace("global.oaVersion = 'nightly';", `global.oaVersion = '${version}';`); +indexCode = indexCode.replace('', disableAutoUpdate ? 'true' : 'false'); +indexCode = indexCode.replaceAll('', updateRepo); +writeFileSync(indexPath, indexCode); + +stripTree(join(tmpRoot, 'src')); + +mkdirSync(resolve(output, '..'), { recursive: true }); +const asar = spawnSync('asar', ['pack', join(tmpRoot, 'src'), output], { + cwd: root, + stdio: 'inherit' +}); + +if (asar.status !== 0) process.exit(asar.status ?? 1); + +if (existsSync(output)) console.log(output); diff --git a/src/asarUpdate.js b/src/asarUpdate.js index fd4113281..32da6c4fc 100644 --- a/src/asarUpdate.js +++ b/src/asarUpdate.js @@ -11,10 +11,14 @@ const redirs = url => new Promise(res => get(url, r => { // Minimal wrapper arou })); module.exports = async () => { // (Try) update asar + if (global.oaDisableAutoUpdate) return log('AsarUpdate', 'Skipping build-configured auto-update disable'); if (!oaVersion.includes('-')) return; + const releaseChannel = oaVersion.split('-')[0]; + const updateRepo = global.oaUpdateRepo || 'GooseMod/OpenAsar'; + log('AsarUpdate', 'Updating...'); - const res = (await redirs(`https://github.com/GooseMod/OpenAsar/releases/download/${oaVersion.split('-')[0]}/app.asar`)); + const res = (await redirs(`https://github.com/${updateRepo}/releases/download/${releaseChannel}/app.asar`)); let data = []; res.on('data', d => { diff --git a/src/index.js b/src/index.js index 41fe9bde5..904652622 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,11 @@ const { join } = require('path'); global.log = (area, ...args) => console.log(`[\x1b[38;2;88;101;242mOpenAsar\x1b[0m > ${area}]`, ...args); // Make log global for easy usage everywhere +const defaultUpdateRepo = 'GooseMod/OpenAsar'; +const stampedUpdateRepo = ''; global.oaVersion = 'nightly'; +global.oaDisableAutoUpdate = '' === 'true'; +global.oaUpdateRepo = stampedUpdateRepo.startsWith('<') ? defaultUpdateRepo : stampedUpdateRepo; log('Init', 'OpenAsar', oaVersion); @@ -37,4 +41,4 @@ if (process.argv.includes('--overlay-host')) { // If overlay require('discord_overlay2/standalone_host.js'); // Start overlay } else { require('./bootstrap')(); // Start bootstrap -} \ No newline at end of file +} diff --git a/src/mainWindow.js b/src/mainWindow.js index 440bacbf4..dc81fe45c 100644 --- a/src/mainWindow.js +++ b/src/mainWindow.js @@ -27,28 +27,55 @@ const themesync = async () => { // Settings injection setInterval(() => { - const versionInfo = document.querySelector('[class*="sidebar"] [class*="compactInfo"]'); - if (!versionInfo || document.getElementById('openasar-ver')) return; - - const oaVersionInfo = versionInfo.cloneNode(true); - const oaVersion = oaVersionInfo.children[0]; - oaVersion.id = 'openasar-ver'; - oaVersion.textContent = 'OpenAsar ()'; - oaVersion.onclick = () => DiscordNative.ipc.send('DISCORD_UPDATED_QUOTES', 'o'); - - oaVersionInfo.textContent = ''; - oaVersionInfo.appendChild(oaVersion); - versionInfo.parentElement.parentElement.lastElementChild.insertAdjacentElement('beforebegin', oaVersionInfo); + const openSettings = () => DiscordNative.ipc.send('DISCORD_UPDATED_QUOTES', 'o'); + + const versionInfo = + document.querySelector('.bd-version-info > div:nth-child(2)') ?? + document.querySelector('.bd-version-info') ?? + document.querySelector('[class*="sidebar"] [class*="compactInfo"]') ?? + [...document.querySelectorAll('[class*="sidebar"] [class*="info"] [class*="line"]')].find(x => x.textContent?.startsWith('Host ')); + + if (versionInfo && !document.getElementById('openasar-ver')) { + const oaVersionInfo = versionInfo.cloneNode(true); + const oaVersion = oaVersionInfo.children?.[0] ?? oaVersionInfo; + oaVersion.id = 'openasar-ver'; + oaVersion.textContent = 'OpenAsar ()'; + oaVersion.onclick = openSettings; + + if (oaVersionInfo !== oaVersion) { + oaVersionInfo.textContent = ''; + oaVersionInfo.appendChild(oaVersion); + } + + const versionTarget = versionInfo.parentElement?.parentElement?.lastElementChild; + if (versionTarget) versionTarget.insertAdjacentElement('beforebegin', oaVersionInfo); + else versionInfo.insertAdjacentElement('afterend', oaVersionInfo); + } if (document.getElementById('openasar-item')) return; + const sidebar = document.querySelector('[data-list-id="settings-sidebar"]') ?? document.querySelector('[class*="sidebar"] [class*="nav"]'); + const appSection = sidebar && ( + sidebar.querySelector('ul[aria-label="App Settings"]') ?? + [...sidebar.querySelectorAll('ul, [class*="section"]')].find(x => x.getAttribute?.('aria-label') === 'App Settings') ?? + [...sidebar.querySelectorAll('ul, [class*="section"]')].find(section => [...section.querySelectorAll('h1, h2, h3, [data-text-variant]')].some(x => x.textContent?.trim() === 'App Settings')) + ); let advanced = document.querySelector('[data-list-item-id="settings-sidebar___advanced_sidebar_item"]'); + if (appSection) { + const appItems = [ + ...appSection.querySelectorAll('[role="listitem"]'), + ...appSection.querySelectorAll('[data-list-item-id^="settings-sidebar___"]') + ]; + + advanced = appItems[appItems.length - 1] ?? advanced; + } if (!advanced) advanced = document.querySelector('[class*="sidebar"] [class*="nav"] > [class*="section"]:nth-child(3) > :last-child'); if (!advanced) advanced = [...document.querySelectorAll('[class*="item"]')].find(x => x.textContent === 'Advanced'); + if (!advanced) return; const oaSetting = advanced.cloneNode(true); oaSetting.querySelector('[class*="text"]').textContent = 'OpenAsar'; oaSetting.id = 'openasar-item'; - oaSetting.onclick = oaVersion.onclick; + oaSetting.onclick = openSettings; advanced.insertAdjacentElement('afterend', oaSetting); }, 800);