Skip to content

Instantly share code, notes, and snippets.

@davidrios
Created July 28, 2024 23:42
Show Gist options
  • Save davidrios/17de4718823271f8a4741a940c933b5e to your computer and use it in GitHub Desktop.
Save davidrios/17de4718823271f8a4741a940c933b5e to your computer and use it in GitHub Desktop.
wowexport.patch
diff --git a/.eslintrc.json b/.eslintrc.json
index d171a236..614cf829 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -12,7 +12,8 @@
"chrome": true,
"crash": true,
"getErrorDump": true,
- "BigInt": true
+ "BigInt": true,
+ "mainWindow": true
},
"env": {
"browser": true,
diff --git a/build.conf b/build.conf
index 9b982702..87f09e37 100644
--- a/build.conf
+++ b/build.conf
@@ -7,6 +7,7 @@
"manifest": {
"name": "wow.export",
"main": "./src/index.html",
+ "domain": "wow.export",
"chromium-args": "--disable-devtools --disable-raf-throttling --mixed-context --enable-node-worker --disable-logging",
"product_string": "wow.export",
"user-agent": "wow.export (%ver); %osinfo",
@@ -43,6 +44,26 @@
},
"manifestInherit": ["name", "description", "license", "version", "contributors"],
"builds": [
+ {
+ "name": "win-x64-debug-hmr",
+ "bundle": "nwjs-sdk-v%s-win-x64.zip",
+ "bundleType": "ZIP",
+ "sourceMethod": "LINK",
+ "sourceTarget": "./src",
+ "manifestTarget": "./package.json",
+ "filter": {
+ "blacklist": [
+ "locales\/[^.]+.pak(.info|)$",
+ "notification_helper.exe"
+ ],
+ "whitelist": [
+ "locales\/en-US.pak$"
+ ]
+ },
+ "manifest": {
+ "main": "./src/hmr-main.html"
+ }
+ },
{
"name": "win-x64-debug",
"bundle": "nwjs-sdk-v%s-win-x64.zip",
@@ -68,7 +89,7 @@
"sourceMethod": "BUNDLE",
"sourceTarget": "./src",
"bundleConfig": {
- "filterExt": [".js", ".scss", ".css"],
+ "filterExt": [".mjs", ".js", ".scss", ".css"],
"sassEntry": "app.scss",
"sassOut": "app.css",
"jsEntry": "app.js"
@@ -125,7 +146,7 @@
"sourceMethod": "BUNDLE",
"sourceTarget": "./src",
"bundleConfig": {
- "filterExt": [".js", ".scss", ".css"],
+ "filterExt": [".mjs", ".js", ".scss", ".css"],
"sassEntry": "app.scss",
"sassOut": "app.css",
"jsEntry": "app.js"
@@ -165,7 +186,7 @@
"sourceMethod": "BUNDLE",
"sourceTarget": "./src",
"bundleConfig": {
- "filterExt": [".js", ".scss", ".css"],
+ "filterExt": [".mjs", ".js", ".scss", ".css"],
"sassEntry": "app.scss",
"sassOut": "app.css",
"jsEntry": "app.js"
diff --git a/build.js b/build.js
index 00cfa72d..18289011 100644
--- a/build.js
+++ b/build.js
@@ -22,6 +22,8 @@ const uuid = require('uuid/v4');
const crypto = require('crypto');
const argv = process.argv.splice(2);
const pkg = require('pkg');
+const fse = require('fs-extra');
+const { rollup } = require('rollup');
const CONFIG_FILE = './build.conf';
const MANIFEST_FILE = './package.json';
@@ -199,6 +201,25 @@ const collectFiles = async (dir, out = []) => {
return out;
};
+async function removeFilesByExtension(directoryPath, targetExtension) {
+ try {
+ const entries = await fsp.readdir(directoryPath, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(directoryPath, entry.name);
+ if (entry.isDirectory()) {
+ await removeFilesByExtension(fullPath, targetExtension);
+ } else {
+ const ext = path.extname(entry.name);
+ if (ext.toLowerCase() === targetExtension.toLowerCase())
+ await fsp.unlink(fullPath);
+ }
+ }
+ } catch (err) {
+ console.error(`Error processing directory: ${err}`);
+ }
+}
+
/**
* Check if an AST node matches a structure.
* Returns AST_NO_MATCH or an object containing exports.
@@ -544,7 +565,20 @@ const deflateBuffer = util.promisify(zlib.deflate);
} else if (isBundle) {
// Bundle everything together, packaged for production release.
const bundleConfig = build.bundleConfig;
- const jsEntry = path.join(sourceDirectory, bundleConfig.jsEntry);
+
+ const preBuildDir = path.join(outDir, '_prebuild');
+ await fsp.rm(preBuildDir, { recursive: true, force: true });
+ await createDirectory(preBuildDir);
+ await fse.copy(sourceDirectory, preBuildDir, { overwrite: true });
+ const rollupBundle = await rollup({input: path.join(sourceDirectory, bundleConfig.jsEntry.replace('.js', '.mjs'))});
+ await rollupBundle.write({
+ file: path.join(preBuildDir, bundleConfig.jsEntry),
+ format: 'cjs',
+ inlineDynamicImports: true,
+ });
+ removeFilesByExtension(preBuildDir, '.mjs');
+
+ const jsEntry = path.join(preBuildDir, bundleConfig.jsEntry);
log.info('Bundling sources (entry: *%s*)...', jsEntry);
// Make sure the source directory exists.
@@ -608,6 +642,10 @@ const deflateBuffer = util.promisify(zlib.deflate);
await fsp.writeFile(path.join(sourceTarget, bundleConfig.jsEntry), minified.code, 'utf8');
log.success('*%d* sources bundled *%s* -> *%s* (*%d%*)', moduleTree.length, filesize(rawSize), filesize(minified.code.length), 100 - Math.round((minified.code.length / rawSize) * 100));
+ const initModule = await fsp.readFile(path.join(sourceDirectory, 'init.js'), 'utf8');
+ const minifiedInit = await terser.minify(initModule, config.terserConfig);
+ await fsp.writeFile(path.join(sourceTarget, 'init.js'), minifiedInit.code, 'utf8');
+
// Compile SCSS files into a single minified CSS output.
const sassEntry = path.join(sourceDirectory, bundleConfig.sassEntry);
log.info('Compiling stylesheet (entry: *%s*)...', sassEntry);
@@ -679,6 +717,8 @@ const deflateBuffer = util.promisify(zlib.deflate);
// Apply manifest properties defined in the config.
Object.assign(manifest, config.manifest);
+ Object.assign(manifest, build.manifest || {});
+
// Apply build specific meta data to the manifest.
Object.assign(manifest, { flavour: build.name, guid: buildGUID });
diff --git a/debug.js b/debug.js
index 22ba971e..e01f3838 100644
--- a/debug.js
+++ b/debug.js
@@ -2,11 +2,136 @@ const fs = require('fs');
const path = require('path');
const sass = require('sass');
const childProcess = require('child_process');
+const net = require('net');
+const waitOn = require('wait-on');
+const vite = require('vite');
+const recast = require('recast');
+const parser = require('recast/parsers/babel');
-const nwPath = './bin/win-x64-debug/nw.exe';
+const argv = process.argv.splice(2);
+const isHmr = argv[0] === 'hmr';
+const vitePort = isHmr ? argv[1] ?? 4175 : null;
+
+const nwPath = `./bin/win-x64-debug${isHmr ? '-hmr' : ''}/nw.exe`;
const srcDir = './src/';
const appScss = './src/app.scss';
+function convertModuleImport(moduleImport, relativeBase) {
+ if (moduleImport.startsWith('.'))
+ return path.join(path.dirname(relativeBase), moduleImport).substring(1).replace(/\\/g, '/');
+ else if (moduleImport.startsWith('/'))
+ return path.join('src', moduleImport.substring(1)).replace(/\\/g, '/');
+}
+
+function adjustRequireSrc(ast, id) {
+ recast.types.visit(ast, {
+ visitCallExpression(sourcePath) {
+ const node = sourcePath.node;
+ if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
+ const [arg] = node.arguments;
+ if (arg.type === 'StringLiteral') {
+ const converted = convertModuleImport(arg.value, id);
+ if (converted != null)
+ arg.value = converted;
+ }
+ }
+ this.traverse(sourcePath);
+ }
+ });
+}
+
+function getVueComponent(ast) {
+ let retDecl = null;
+
+ recast.types.visit(ast, {
+ visitObjectExpression(sourcePath) {
+ const decl = sourcePath.value;
+
+ // detect vue component by default export with `template` and (`setup` or `data`) properties
+ let hasTemplate = false;
+ let hasSetupData = false;
+ for (const prop of decl.properties) {
+ if (prop.key.type !== 'Identifier')
+ continue;
+
+ hasTemplate = hasTemplate || prop.key.name === 'template';
+ hasSetupData = hasSetupData || prop.key.name === 'setup' || prop.key.name === 'data';
+ }
+
+ if (hasTemplate && hasSetupData) {
+ retDecl = decl;
+ return false;
+ }
+
+ this.traverse(sourcePath);
+ }
+ });
+
+ return retDecl;
+}
+
+function addVueHmr(ast, id) {
+ let components = new Set();
+ const b = recast.types.builders;
+
+ const variableDeclarations = Object.fromEntries(
+ ast.program.body
+ .filter(node => node.type === 'VariableDeclaration')
+ .map(node => [node.declarations[0].id.name, node])
+ );
+
+ for (let i = 0; i < ast.program.body.length; i++) {
+ const node = ast.program.body[i];
+ if (!node.type.startsWith('Export'))
+ continue;
+
+ try {
+ let vueComponent = getVueComponent(node);
+ if (vueComponent == null) {
+ const variableName = node.declaration.name ?? (node.declaration.declarations ?? '')[0]?.init?.name;
+ const variable = variableDeclarations[variableName]
+ if (variableName == null || variable == null)
+ continue;
+
+ vueComponent = getVueComponent(variable);
+ if (vueComponent == null)
+ continue
+ }
+
+ const name = node.type === 'ExportDefaultDeclaration'
+ ? 'default'
+ : node.declaration.declarations[0].id.name;
+
+ const componentId = `${id}:${name}`;
+ components.add([componentId, name]);
+
+ vueComponent.properties.push(b.objectProperty(
+ b.identifier('__hmrId'),
+ b.stringLiteral(componentId)));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ if (components.size > 0) {
+ const astHot = recast.parse(`
+if (import.meta.hot) {
+ import.meta.hot.accept((newModule) => {
+ if (newModule == null)
+ return;
+
+ ${Array.from(components.values())
+ .map(([componentId, name]) => `__VUE_HMR_RUNTIME__.reload(${JSON.stringify(componentId)}, newModule.${name})`)
+ .join(';')};
+ });
+}`, { parser });
+
+ ast.program.body.push(...astHot.program.body);
+
+ return true;
+ }
+}
+
(async () => {
// Check if nw.exe exists
try {
@@ -40,8 +165,60 @@ const appScss = './src/app.scss';
});
});
+ const vueHmr = {
+ name: 'vue-hmr',
+ async transformIndexHtml(html) {
+ return html.replace(
+ '<script defer type="text/javascript" src="app.js"></script>',
+ '<script type="module" src="app-loader.js"></script>'
+ );
+ },
+ async transform(code, id) {
+ const relativeId = id.substring(path.resolve(__dirname).length);
+ const isModule = relativeId.endsWith('.mjs');
+
+ if (!(relativeId.startsWith('/src') && (isModule || relativeId === '/src/init.js')))
+ return;
+
+ const ast = recast.parse(code, { sourceFileName: id, parser });
+ adjustRequireSrc(ast, relativeId);
+
+ if (isModule && !code.includes('import.meta.hot')) {
+ if (addVueHmr(ast, relativeId))
+ console.log('vue-hmr:', relativeId);
+ }
+
+ return recast.print(ast, { sourceMapName: id });
+ },
+ }
+
+ if (isHmr) {
+ const viteServer = await vite.createServer({
+ configFile: false,
+ root: path.join(__dirname, srcDir),
+ server: { port: vitePort },
+ plugins: [vueHmr],
+ sourcemap: true,
+ })
+ viteServer.listen();
+
+ await waitOn({
+ resources: [`http-get://localhost:${vitePort}`],
+ headers: { 'accept': 'text/html' },
+ });
+ }
+
+ const debugSocketPath = path.join('\\\\?\\pipe', nwPath, 'debug-window');
+ let debugSocket;
+ const debugServer = net.createServer().listen(debugSocketPath);
+ debugServer.on('connection', async (socket) => { debugSocket = socket; });
+ process.on('SIGINT', async function () {
+ debugSocket?.write('please_exit');
+ setTimeout(process.exit, 500);
+ });
+
// Launch nw.exe
- const nwProcess = childProcess.spawn(nwPath, { stdio: 'inherit' });
+ const nwProcess = childProcess.spawn(nwPath, { stdio: 'inherit', env: { ...process.env, DEBUG_SOCKET: debugSocketPath, VITE_PORT: vitePort } });
// When the spawned process is closed, exit the Node.js process as well
nwProcess.on('close', code => {
diff --git a/package.json b/package.json
index 9b698f10..9371c151 100644
--- a/package.json
+++ b/package.json
@@ -28,12 +28,18 @@
"request": "^2.88.0",
"sass": "^1.52.1",
"ssh2-sftp-client": "^9.0.4",
- "terser": "^5.14.2",
"tar": "^5.0.10",
+ "terser": "^5.14.2",
"uuid": "^3.3.3"
},
"devDependencies": {
+ "@msgpack/msgpack": "^2.8.0",
"eslint": "^6.8.0",
- "eslint-plugin-vue": "^6.1.2"
+ "eslint-plugin-vue": "^6.1.2",
+ "fs-extra": "^11.2.0",
+ "recast": "^0.23.9",
+ "rollup": "^4.19.1",
+ "vite": "^5.3.5",
+ "wait-on": "^7.2.0"
}
}
diff --git a/src/app-loader.js b/src/app-loader.js
new file mode 100644
index 00000000..20a737ab
--- /dev/null
+++ b/src/app-loader.js
@@ -0,0 +1,4 @@
+(async function () {
+ await mainWindow.isReady;
+ await import('./app.mjs');
+})();
\ No newline at end of file
diff --git a/src/app.js b/src/app.mjs
similarity index 92%
rename from src/app.js
rename to src/app.mjs
index 934d5a76..60f89e94 100644
--- a/src/app.js
+++ b/src/app.mjs
@@ -4,12 +4,6 @@
License: MIT
*/
-// BUILD_RELEASE will be set globally by Terser during bundling allowing us
-// to discern a production build. However, for debugging builds it will throw
-// a ReferenceError without the following check. Any code that only runs when
-// BUILD_RELEASE is set to false will be removed as dead-code during compile.
-BUILD_RELEASE = typeof BUILD_RELEASE !== 'undefined';
-
/**
* crash() is used to inform the user that the application has exploded.
* It is purposely global and primitive as we have no idea what state
@@ -18,7 +12,7 @@ BUILD_RELEASE = typeof BUILD_RELEASE !== 'undefined';
* @param {string} errorText
*/
let isCrashed = false;
-crash = (errorCode, errorText) => {
+window.crash = (errorCode, errorText) => {
// Prevent a never-ending cycle of depression.
if (isCrashed)
return;
@@ -69,11 +63,6 @@ if (!BUILD_RELEASE) {
process.on('unhandledRejection', e => crash('ERR_UNHANDLED_REJECTION', e.message));
process.on('uncaughtException', e => crash('ERR_UNHANDLED_EXCEPTION', e.message));
-const win = nw.Window.get();
-// Launch DevTools for debug builds.
-if (!BUILD_RELEASE)
- win.showDevTools();
-
// Imports
const os = require('os');
const path = require('path');
@@ -92,7 +81,7 @@ const ExportHelper = require('./js/casc/export-helper');
const ExternalLinks = require('./js/external-links');
const textureRibbon = require('./js/ui/texture-ribbon');
-const Listbox = require('./js/components/listbox');
+import Listbox from './js/components/listbox.mjs';
const Listboxb = require('./js/components/listboxb');
const Itemlistbox = require('./js/components/itemlistbox');
const Checkboxlist = require('./js/components/checkboxlist');
@@ -102,7 +91,7 @@ const ComboBox = require('./js/components/combobox');
const Slider = require('./js/components/slider');
const ModelViewer = require('./js/components/model-viewer');
const MapViewer = require('./js/components/map-viewer');
-const DataTable = require('./js/components/data-table');
+import DataTable from './js/components/data-table.mjs';
const ResizeLayer = require('./js/components/resize-layer');
const ContextMenu = require('./js/components/context-menu');
@@ -115,15 +104,14 @@ require('./js/ui/tab-text.js');
require('./js/ui/tab-models');
require('./js/ui/tab-maps');
require('./js/ui/tab-items');
-const TabData = require('./js/ui/tab-data');
+import TabData from './js/ui/tab-data.mjs';
require('./js/ui/tab-raw');
require('./js/ui/tab-install');
require('./js/ui/tab-characters');
const RCPServer = require('./js/rcp/rcp-server');
-win.setProgressBar(-1); // Reset taskbar progress in-case it's stuck.
-win.on('close', () => process.exit()); // Ensure we exit when window is closed.
+mainWindow.setProgressBar(-1); // Reset taskbar progress in-case it's stuck.
// Prevent files from being dropped onto the window. These are over-written
// later but we disable here to prevent them working if init fails.
@@ -560,7 +548,7 @@ document.addEventListener('click', function(e) {
* @param {float} val
*/
loadPct: function(val) {
- win.setProgressBar(val);
+ mainWindow.setProgressBar(val);
},
/**
@@ -573,6 +561,7 @@ document.addEventListener('click', function(e) {
});
// Interlink error handling for Vue.
+ if (BUILD_RELEASE)
app.config.errorHandler = err => crash('ERR_VUE', err.message);
app.component('Listbox', Listbox);
diff --git a/src/index.html b/src/index.html
index cf6b3003..6553c03c 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,8 +1,10 @@
<!DOCTYPE html>
+<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script defer type="text/javascript" src="lib/vue.js"></script>
<script defer type="text/javascript" src="lib/three.js"></script>
+ <script defer type="text/javascript" src="init.js"></script>
<script defer type="text/javascript" src="app.js"></script>
<link rel="stylesheet" type="text/css" data-href="app.css" href="app.css"/>
<title>wow.export</title>
@@ -817,6 +819,7 @@
</div>
</template>
</div>
+
<div id="source-remote" :class="{ disabled: !!availableRemoteBuilds }" @click="click('source-remote', $event)">
<template v-if="availableRemoteBuilds">
<div class="source-builds">
@@ -832,7 +835,7 @@
<ul id="source-cdn" class="ui-multi-button">
<li v-for="region in cdnRegions" :class="{ selected: selectedCDNRegion === region }" @click.stop="setSelectedCDN(region)">
{{ region.tag.toUpperCase() }}
- <span v-if="region.delay !== null">{{ region.delay < 0 ? 'N/A' : region.delay + 'ms' }}</span>
+ <span v-if="region.delay !== null">{{ region.delay &lt; 0 ? 'N/A' : region.delay + 'ms' }}</span>
</li>
</ul>
</div>
diff --git a/src/init.js b/src/init.js
new file mode 100644
index 00000000..b2ffa635
--- /dev/null
+++ b/src/init.js
@@ -0,0 +1,28 @@
+/*!
+ wow.export (https://github.com/Kruithne/wow.export)
+ Authors: Kruithne <kruithne@gmail.com>
+ License: MIT
+ */
+
+// BUILD_RELEASE will be set globally by Terser during bundling allowing us
+// to discern a production build. However, for debugging builds it will throw
+// a ReferenceError without the following check. Any code that only runs when
+// BUILD_RELEASE is set to false will be removed as dead-code during compile.
+BUILD_RELEASE = typeof BUILD_RELEASE !== 'undefined';
+
+if (!BUILD_RELEASE && typeof chrome.runtime === 'undefined') {
+ require('./js/init-hmr');
+} else {
+ const win = nw.Window.get();
+ win.on('close', () => process.exit()); // Ensure we exit when window is closed.
+
+ if (!BUILD_RELEASE)
+ win.showDevTools();
+
+ mainWindow = {
+ setProgressBar(value) {
+ nw.Window.get().setProgressBar(value);
+ },
+ isReady: new Promise((resolve) => resolve())
+ }
+}
diff --git a/src/js/casc/build-cache.js b/src/js/casc/build-cache.js
index 34eb7686..df4ea8bd 100644
--- a/src/js/casc/build-cache.js
+++ b/src/js/casc/build-cache.js
@@ -131,6 +131,7 @@ class BuildCache {
cacheIntegrity[filePath] = hash;
await fsp.writeFile(filePath, data.raw);
+ if (core.view != null)
core.view.cacheSize += data.byteLength;
await this.saveCacheIntegrity();
diff --git a/src/js/components/data-table.js b/src/js/components/data-table.mjs
similarity index 99%
rename from src/js/components/data-table.js
rename to src/js/components/data-table.mjs
index d6655725..625623e6 100644
--- a/src/js/components/data-table.js
+++ b/src/js/components/data-table.mjs
@@ -3,7 +3,7 @@
Authors: Kruithne <kruithne@gmail.com>, Marlamin <marlamin@marlamin.com>
License: MIT
*/
-module.exports = {
+export default {
/**
* selectedOption: An array of strings denoting options shown in the menu.
*/
diff --git a/src/js/components/listbox.js b/src/js/components/listbox.mjs
similarity index 96%
rename from src/js/components/listbox.js
rename to src/js/components/listbox.mjs
index cf4f4a59..01eb1a87 100644
--- a/src/js/components/listbox.js
+++ b/src/js/components/listbox.mjs
@@ -16,7 +16,7 @@ const fid_filter = (e) => {
return e;
};
-module.exports = {
+export default {
/**
* items: Item entries displayed in the list.
* filter: Optional reactive filter for items.
diff --git a/src/js/ui/tab-data.js b/src/js/ui/tab-data.mjs
similarity index 95%
rename from src/js/ui/tab-data.js
rename to src/js/ui/tab-data.mjs
index 1615d29f..52976925 100644
--- a/src/js/ui/tab-data.js
+++ b/src/js/ui/tab-data.mjs
@@ -3,15 +3,16 @@
Authors: Kruithne <kruithne@gmail.com>, Marlamin <marlamin@marlamin.com>
License: MIT
*/
-const log = require('../log');
-const generics = require('../generics');
-const listfile = require('../casc/listfile');
-const WDCReader = require('../db/WDCReader');
+
+const log = require('/js/log');
+const generics = require('/js/generics');
+const listfile = require('/js/casc/listfile');
+const WDCReader = require('/js/db/WDCReader');
const path = require('path');
const { inject, ref } = Vue;
-module.exports = {
+export default {
setup() {
const view = inject('view');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment