diff --git a/build/scripts/build.mjs b/build/scripts/build.mjs new file mode 100644 index 00000000..c56d16d1 --- /dev/null +++ b/build/scripts/build.mjs @@ -0,0 +1,79 @@ +import vue from '@vitejs/plugin-vue'; +import esbuild from 'esbuild'; +import { $ } from 'execa'; +import fs from 'fs-extra'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import * as vite from 'vite'; +import { getMainProcessCommonConfig } from './helpers.mjs'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(dirname, '..', '..'); + +const $$ = $({ stdio: 'inherit' }); +await $$`rm -rf ${path.join(root, 'dist_electron', 'build')}`; + +await buildMainProcessSource(); +await buildRendererProcessSource(); + +async function buildMainProcessSource() { + const commonConfig = getMainProcessCommonConfig(root); + const result = await esbuild.build({ + ...commonConfig, + outfile: path.join(root, 'dist_electron', 'build', 'main.js'), + }); + + if (result.errors.length) { + console.error('app build failed due to main process source build'); + result.errors.forEach((err) => console.error(err)); + process.exit(1); + } +} + +async function buildRendererProcessSource() { + const base = 'app://'; + const outDir = path.join(root, 'dist_electron', 'build', 'src'); + await vite.build({ + base: `/${base}`, + root: path.join(root, 'src'), + build: { outDir }, + plugins: [vue()], + resolve: { + alias: { + vue: 'vue/dist/vue.esm-bundler.js', + fyo: path.join(root, 'fyo'), + src: path.join(root, 'src'), + schemas: path.join(root, 'schemas'), + backend: path.join(root, 'backend'), + models: path.join(root, 'models'), + utils: path.join(root, 'utils'), + regional: path.join(root, 'regional'), + reports: path.join(root, 'reports'), + dummy: path.join(root, 'dummy'), + fixtures: path.join(root, 'fixtures'), + }, + }, + }); + removeBaseLeadingSlash(outDir, base); +} + +/** + * Removes leading slash from all renderer files + * electron uses a custom registered protocol to load the + * files: "app://" + * + * @param {string} dir + * @param {string} base + */ +function removeBaseLeadingSlash(dir, base) { + for (const file of fs.readdirSync(dir)) { + const filePath = path.join(dir, file); + if (fs.lstatSync(filePath).isDirectory()) { + removeBaseLeadingSlash(filePath, base); + continue; + } + + const contents = fs.readFileSync(filePath).toString('utf-8'); + fs.writeFileSync(filePath, contents.replaceAll('/' + base, base)); + } +} diff --git a/build/scripts/dev.mjs b/build/scripts/dev.mjs index 3f7c17e9..61085614 100644 --- a/build/scripts/dev.mjs +++ b/build/scripts/dev.mjs @@ -3,7 +3,7 @@ import esbuild from 'esbuild'; import { $ } from 'execa'; import path from 'path'; import { fileURLToPath } from 'url'; -import { excludeVendorFromSourceMap } from './plugins.mjs'; +import { getMainProcessCommonConfig } from './helpers.mjs'; process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; process.env['NODE_ENV'] = 'development'; @@ -45,16 +45,8 @@ if (!process.argv.includes('--no-renderer')) { * to [re]build the main process code */ const ctx = await esbuild.context({ - entryPoints: [path.join(root, 'main.ts')], - bundle: true, - sourcemap: true, - sourcesContent: false, - platform: 'node', - target: 'node16', - outfile: path.join(root, 'dist', 'dev', 'main.js'), - external: ['knex', 'electron', 'better-sqlite3'], - plugins: [excludeVendorFromSourceMap], - write: true, + ...getMainProcessCommonConfig(root), + outfile: path.join(root, 'dist_electron', 'dev', 'main.js'), }); /** @@ -135,7 +127,7 @@ async function handleResult(result) { function runElectron() { const electronProcess = $$`npx electron --inspect=5858 ${path.join( root, - 'dist', + 'dist_electron', 'dev', 'main.js' )}`; diff --git a/build/scripts/helpers.mjs b/build/scripts/helpers.mjs new file mode 100644 index 00000000..70ae2ef8 --- /dev/null +++ b/build/scripts/helpers.mjs @@ -0,0 +1,51 @@ +import fs from 'fs'; +import path from 'path'; + +/** + * Common ESBuild config used for building main process source + * code for both dev and production. + * + * @param {string} root + * @returns {import('esbuild').BuildOptions} + */ +export function getMainProcessCommonConfig(root) { + return { + entryPoints: [path.join(root, 'main.ts')], + bundle: true, + sourcemap: true, + sourcesContent: false, + platform: 'node', + target: 'node16', + external: ['knex', 'electron', 'better-sqlite3'], + plugins: [excludeVendorFromSourceMap], + write: true, + }; +} + +/** + * ESBuild plugin used to prevent source maps from being generated for + * packages inside node_modules, only first-party code source maps + * are to be included. + * + * Note, this is used only for the main process source code. + * + * source: https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409 + * @type {import('esbuild').Plugin} + */ +export const excludeVendorFromSourceMap = { + name: 'excludeVendorFromSourceMap', + setup(build) { + build.onLoad({ filter: /node_modules/ }, (args) => { + if (args.path.endsWith('.json')) { + return; + } + + return { + contents: + fs.readFileSync(args.path, 'utf8') + + '\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==', + loader: 'default', + }; + }); + }, +}; diff --git a/build/scripts/plugins.mjs b/build/scripts/plugins.mjs deleted file mode 100644 index 29b70d3e..00000000 --- a/build/scripts/plugins.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import fs from 'fs'; - -/** - * source: https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409 - * @type {import('esbuild').Plugin} - */ -export const excludeVendorFromSourceMap = { - name: 'excludeVendorFromSourceMap', - setup(build) { - build.onLoad({ filter: /node_modules/ }, (args) => { - if (args.path.endsWith('.json')) { - return; - } - - return { - contents: - fs.readFileSync(args.path, 'utf8') + - '\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==', - loader: 'default', - }; - }); - }, -}; diff --git a/main.ts b/main.ts index fed2c76b..ba716bd1 100644 --- a/main.ts +++ b/main.ts @@ -8,9 +8,12 @@ import { BrowserWindow, BrowserWindowConstructorOptions, protocol, + ProtocolRequest, + ProtocolResponse, } from 'electron'; import Store from 'electron-store'; import { autoUpdater } from 'electron-updater'; +import fs from 'fs'; import path from 'path'; import registerAppLifecycleListeners from './main/registerAppLifecycleListeners'; import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners'; @@ -122,15 +125,20 @@ export class Main { this.mainWindow = new BrowserWindow(options); if (this.isDevelopment) { - this.loadDevServerURL(); + this.setViteServerURL(); } else { - this.loadAppUrl(); + this.registerAppProtocol(); + } + + this.mainWindow!.loadURL(this.winURL); + if (this.isDevelopment && !this.isTest) { + this.mainWindow!.webContents.openDevTools(); } this.setMainWindowListeners(); } - loadDevServerURL() { + setViteServerURL() { let port = 6969; let host = '0.0.0.0'; @@ -141,18 +149,13 @@ export class Main { // Load the url of the dev server if in development mode this.winURL = `http://${host}:${port}/`; - this.mainWindow!.loadURL(this.winURL); - - if (this.isDevelopment && !this.isTest) { - this.mainWindow!.webContents.openDevTools(); - } } - loadAppUrl() { - // createProtocol('app'); - // Load the index.html when not in development - // this.winURL = 'app://./index.html'; - // this.mainWindow!.loadURL(this.winURL); + registerAppProtocol() { + protocol.registerBufferProtocol('app', bufferProtocolCallback); + + // Use the registered protocol url to load the files. + this.winURL = 'app://./index.html'; } setMainWindowListeners() { @@ -170,4 +173,36 @@ export class Main { } } +/** + * Callback used to register the custom app protocol, + * during prod, files are read and served by using this + * protocol. + */ +function bufferProtocolCallback( + request: ProtocolRequest, + callback: (response: ProtocolResponse) => void +) { + const { pathname, host } = new URL(request.url); + const filePath = path.join( + __dirname, + 'src', + decodeURI(host), + decodeURI(pathname) + ); + + fs.readFile(filePath, (_, data) => { + const extension = path.extname(filePath).toLowerCase(); + const mimeType = + { + '.js': 'text/javascript', + '.css': 'text/css', + '.html': 'text/html', + '.svg': 'image/svg+xml', + '.json': 'application/json', + }[extension] ?? ''; + + callback({ mimeType, data }); + }); +} + export default new Main(); diff --git a/main/getPrintTemplates.ts b/main/getPrintTemplates.ts index d0aa48df..062fec1a 100644 --- a/main/getPrintTemplates.ts +++ b/main/getPrintTemplates.ts @@ -29,7 +29,7 @@ async function getPrintTemplatePaths(): Promise<{ const files = await fs.readdir(root); return { files, root }; } catch { - root = path.join(__dirname, `../templates`); + root = path.join(__dirname, '..', '..', `templates`); } try { diff --git a/package.json b/package.json index 2eae09d3..e773123d 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,13 @@ }, "scripts": { "dev": "node build/scripts/dev.mjs", + "build": "node build/scripts/build.mjs", "dev:main": "node build/scripts/dev.mjs --no-renderer", "dev:renderer": "yarn vite", - "build": "vue-cli-service build", "lint": "vue-cli-service lint", "release": "scripts/publish-mac-arm.sh", "postinstall": "electron-rebuild", "postuninstall": "electron-rebuild", - "electron:build": "vue-cli-service electron:build", - "electron:serve": "vue-cli-service electron:serve", "script:translate": "scripts/runner.sh scripts/generateTranslations.ts", "script:profile": "scripts/profile.sh", "test": "scripts/test.sh" @@ -73,6 +71,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^7.0.0", "execa": "^7.1.1", + "fs-extra": "^11.1.1", "lint-staged": "^11.2.6", "postcss": "^8", "prettier": "^2.4.1", diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 0ca6eee1..00000000 --- a/public/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - Frappe Books - - - - - - diff --git a/vite.config.ts b/vite.config.ts index f97b9ed9..96598b65 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,11 +2,15 @@ import vue from '@vitejs/plugin-vue'; import path from 'path'; import { defineConfig } from 'vite'; -/** - * This is a work in progress vite config. Currently only dev works. - */ -// https://vitejs.dev/config/ +/** + * This vite config file is used only for dev mode, i.e. + * to create a serve build modules of the source code + * which will be rendered by electron. + * + * For building the project, vite is used programmatically + * see build/scripts/build.mjs for this. + */ export default () => { let port = 6969; let host = '0.0.0.0'; diff --git a/yarn.lock b/yarn.lock index 21a5bc38..6536b24b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6534,6 +6534,15 @@ fs-extra@^10.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"