Repository: excalidraw/excalidraw-desktop Branch: master Commit: d58887254548 Files: 25 Total size: 24.0 KB Directory structure: gitextract_o3swcrh1/ ├── .eslintrc.json ├── .github/ │ └── workflows/ │ ├── build.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── build/ │ └── icon.icns ├── electron-builder.json ├── package.json ├── src/ │ ├── constants.ts │ ├── main.ts │ ├── menu.ts │ ├── pages/ │ │ └── about.html │ ├── preload.ts │ ├── renderer.ts │ ├── types.ts │ └── util/ │ ├── checkVersion.ts │ └── metadata.ts ├── tasks/ │ ├── dist.js │ └── download.js ├── test/ │ └── main.spec.js ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "env": { "browser": true, "es6": true, "node": true }, "extends": ["prettier"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2019, "project": "./tsconfig.json", "sourceType": "module" }, "plugins": ["@typescript-eslint", "prettier"], "rules": { "curly": "error", "no-else-return": "error", "prettier/prettier": "error" }, "ignorePatterns": ["dist", "*.json"] } ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: - master pull_request: jobs: build: strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{matrix.platform}} steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: version: 12 - run: | yarn yarn build ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: - master pull_request: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: version: 12 - name: Cache node modules uses: actions/cache@v1 env: cache-name: cache-node-modules with: path: node_modules key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock') }} - run: yarn install --frozen-lockfile - run: yarn lint test-linux: needs: lint runs-on: ubuntu-latest steps: - run: lsb_release -a - run: uname -a - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: version: 12 - name: Cache node modules uses: actions/cache@v1 env: cache-name: cache-node-modules with: path: node_modules key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock') }} - run: yarn install --frozen-lockfile - run: yarn build:app - run: sudo apt-get install xvfb - run: | export DISPLAY=:99.0 xvfb-run --auto-servernum yarn test:spec # NOTE: # Xvfb is an X server that can run on linux machines with no display hardware # and no physical input devices. It emulates a dumb framebuffer using # virtual memory. test-macos: needs: lint runs-on: macos-latest steps: - run: uname -a - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: version: 12 - name: Cache node modules uses: actions/cache@v1 env: cache-name: cache-node-modules with: path: node_modules key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock') }} - run: yarn install --frozen-lockfile - run: yarn build:app - run: export {no_proxy,NO_PROXY}="127.0.0.1,localhost" # ChromeDriver timesout if NO_PROXY env variable is not set. - run: yarn test:spec # !FIXME: Spectron tests fail on Windows, probably due to a configuration issue. # test-windows: # needs: lint # runs-on: windows-latest # steps: # - run: systeminfo # - run: git config --global core.autocrlf false # - run: git config --global core.eol lf # - uses: actions/checkout@v1 # - uses: actions/setup-node@v1 # with: # version: 12 # - name: Cache node modules # uses: actions/cache@v1 # env: # cache-name: cache-node-modules # with: # path: node_modules # key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock') }} # - run: yarn install --frozen-lockfile --ignore-scripts # - run: yarn build:app # - run: SET no_proxy=localhost,127.0.0.1 # - run: SET NO_PROXY=localhost,127.0.0.1 # - run: yarn test:spec ================================================ FILE: .gitignore ================================================ .DS_Store dist excalidraw.asar node_modules logs *.log ================================================ FILE: .prettierignore ================================================ dist ================================================ FILE: .prettierrc.json ================================================ { "bracketSpacing": false, "trailingComma": "all" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Excalidraw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Excalidraw Desktop (deprecated) > Read more on why we deprecated this repo: [Deprecating Excalidraw Electron in favor of the Web version](https://blog.excalidraw.com/deprecating-excalidraw-electron/) [![Excalidraw Desktop](https://pbs.twimg.com/media/EPafpoLWoAAcFhc?format=jpg&name=large)](https://excalidraw.com/?id=5698913638023168) ## Develop ### Install dependencies ``` yarn ``` ### Commands | Command | Description | | --------------- | ------------------------------------------ | | `yarn start` | Start development application | | `yarn fix` | Reformat all files with Prettier | | `yarn test` | Run linting and tests | | `yarn download` | Fetch latest excalidraw bundle | | `yarn build` | Build artifacts for windows, mac and linux | ## Contributing Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw-desktop/issues/new) first to discuss what you would like to change. ================================================ FILE: electron-builder.json ================================================ { "appId": "com.excalidraw.Excalidraw", "productName": "Excalidraw", "directories": { "output": "dist" }, "files": [ "package.json", "dist/main.js", "dist/renderer.js", "dist/preload.js", "dist/pages/*.html", "dist/client", "build/icon.*" ], "win": { "target": [ { "target": "zip", "arch": ["x64", "ia32"] } ] }, "linux": { "category": "Development", "target": [ { "target": "tar.gz", "arch": ["x64", "ia32"] } ] }, "mac": { "hardenedRuntime": true, "target": "dmg" }, "fileAssociations": [ { "ext": "excalidraw", "name": "Excalidraw", "description": "Excalidraw file", "role": "Editor", "mimeType": "application/json" } ] } ================================================ FILE: package.json ================================================ { "author": "Excalidraw Team", "description": "Excalidraw Desktop", "devDependencies": { "@types/minimist": "1.2.0", "@types/node": "14", "@types/node-fetch": "^2.5.7", "@typescript-eslint/eslint-plugin": "4.0.0", "@typescript-eslint/parser": "3.10.1", "asar": "3.0.3", "electron": "8.5.2", "electron-builder": "22.8.0", "eslint": "7.13.0", "eslint-config-prettier": "6.11.0", "eslint-plugin-prettier": "3.1.4", "execa": "4.0.3", "husky": "4.3.0", "lint-staged": "10.5.1", "minimist": "1.2.5", "mocha": "8.1.3", "mri": "1.1.6", "node-fetch": "2.6.1", "prettier": "2.1.2", "spectron": "10.0.1", "ts-loader": "8.0.3", "typescript": "4.0.5", "webpack": "4.44.2", "webpack-cli": "3.3.12" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,ts,tsx}": [ "eslint --fix" ], "*.{json,md,yml}": [ "prettier --write" ] }, "main": "dist/main.js", "name": "excalidraw-desktop", "scripts": { "build": "yarn download && yarn build:app && yarn build:dist", "build:app": "webpack", "build:dist": "node ./tasks/dist.js", "download": "node ./tasks/download.js", "fix": "yarn fix:other && yarn fix:code", "fix:code": "yarn lint:code --fix", "fix:other": "yarn prettier --write", "postinstall": "yarn download", "preinstall": "npx mkdirp dist", "prettier": "prettier \"**/*.{json,md,yml}\"", "start": "yarn build:app && electron ./dist/main.js --devtools", "test": "yarn lint && yarn test:spec", "test:spec": "mocha --exit", "lint": "yarn lint:code && yarn lint:other", "lint:code": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", "lint:other": "yarn prettier --ignore-path .gitignore --list-different", "watch": "webpack -w" }, "version": "0.0.1", "dependencies": { "electron-store": "^5.2.0", "html-webpack-plugin": "^4.3.0" } } ================================================ FILE: src/constants.ts ================================================ export const APP_NAME = "Excalidraw"; export const EXCALIDRAW_API = "https://excalidraw.com/version.json"; export const EXCALIDRAW_ASAR_SOURCE = "https://excalidraw.com/excalidraw.asar"; export const EXCALIDRAW_GITHUB_PACKAGE_JSON_URL = "https://raw.githubusercontent.com/excalidraw/excalidraw-desktop/master/package.json"; ================================================ FILE: src/main.ts ================================================ import {app, BrowserWindow, shell, globalShortcut} from "electron"; import * as minimist from "minimist"; import * as path from "path"; import * as url from "url"; import {setupMenu} from "./menu"; import checkVersion from "./util/checkVersion"; import {setMetadata, setAppName} from "./util/metadata"; import {APP_NAME} from "./constants"; let mainWindow: Electron.BrowserWindow; const argv = minimist(process.argv.slice(1)); const EXCALIDRAW_BUNDLE = path.join(__dirname, "client", "index.html"); const APP_ICON_PATH = path.join(__dirname, "client", "logo-180x180.png"); function createWindow() { mainWindow = new BrowserWindow({ show: false, height: 600, width: 800, webPreferences: { contextIsolation: true, // protect against prototype pollution preload: `${__dirname}/preload.js`, }, }); if (argv.devtools) { mainWindow.webContents.openDevTools({mode: "detach"}); } mainWindow.webContents.on("will-navigate", openExternalURLs); mainWindow.webContents.on("new-window", openExternalURLs); mainWindow.loadURL( url.format({ pathname: EXCALIDRAW_BUNDLE, protocol: "file", slashes: true, }), ); mainWindow.on("closed", () => { mainWindow = null; }); // Enable Cmd+Q on mac to quit the application if (process.platform === "darwin") { globalShortcut.register("Command+Q", () => { app.quit(); }); } // calling.show after this event, ensure there's no visual flash mainWindow.once("ready-to-show", async () => { const versions = await checkVersion(); console.info("Current version", versions.local); console.info("Needs update", versions.needsUpdate); setAppName(APP_NAME); setMetadata("versions", versions); setMetadata("appIconPath", APP_ICON_PATH); setupMenu(mainWindow); mainWindow.show(); }); } // Open external links in user's default browser instead of webview function openExternalURLs(event: Electron.Event, url: string) { if (url.startsWith("http")) { event.preventDefault(); shell.openExternal(url); } } app.on("ready", createWindow); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } }); app.on("activate", () => { if (mainWindow === null) { createWindow(); } }); ================================================ FILE: src/menu.ts ================================================ import {BrowserWindow, Menu, MenuItem} from "electron"; import * as path from "path"; import * as url from "url"; import {APP_NAME} from "./constants"; import {getAppVersions, getAppName, getMetadata} from "./util/metadata"; const ABOUT_PAGE_PATH = path.resolve(__dirname, "pages", "about.html"); const openAboutWindow = () => { let aboutWindow = new BrowserWindow({ height: 320, width: 320, modal: true, backgroundColor: "white", show: false, webPreferences: { contextIsolation: true, preload: `${__dirname}/preload.js`, }, }); aboutWindow.loadURL( url.format({ pathname: ABOUT_PAGE_PATH, protocol: "file", slashes: true, }), ); aboutWindow.setMenuBarVisibility(false); aboutWindow.center(); aboutWindow.on("ready-to-show", () => aboutWindow.show()); aboutWindow.on("show", () => { const aboutContent = { appName: getAppName(), iconPath: getMetadata("appIconPath"), versions: getAppVersions(), }; aboutWindow.webContents.send("show-about-contents", aboutContent); }); }; export const setupMenu = (activeWindow: BrowserWindow, options = {}) => { const isDarwin = process.platform === "darwin"; const defaultMenuItems: MenuItem[] = Menu.getApplicationMenu().items; const menuTemplate = []; if (isDarwin) { defaultMenuItems.shift(); menuTemplate.push({ label: APP_NAME, submenu: [ { label: `About ${APP_NAME}`, enabled: true, click: () => openAboutWindow(), }, ], }); menuTemplate.push(...defaultMenuItems); } else { defaultMenuItems.pop(); menuTemplate.push(...defaultMenuItems); menuTemplate.push({ label: "Help", submenu: [ { label: `About ${APP_NAME}`, enabled: true, click: () => openAboutWindow(), }, ], }); } // TODO: Remove default menu items that aren't relevant const appMenu = Menu.buildFromTemplate(menuTemplate); Menu.setApplicationMenu(appMenu); }; ================================================ FILE: src/pages/about.html ================================================ About This App App icon

================================================ FILE: src/preload.ts ================================================ import {ipcRenderer, contextBridge, remote} from "electron"; import {IpcListener} from "./types"; contextBridge.exposeInMainWorld("ipcRenderer", { send: (channel: string, data: any[]) => { ipcRenderer.send(channel, data); }, receive: (channel: string, func: IpcListener) => { ipcRenderer.on(channel, (event, ...args: any[]) => func(event, ...args)); }, }); contextBridge.exposeInMainWorld("remote", { getVersion: remote.app.getVersion, }); ================================================ FILE: src/renderer.ts ================================================ const rendererWindow: RendererWindow = window; rendererWindow.ipcRenderer.receive("show-about-contents", (_, options: any) => { const {appName, versions, iconPath} = options; document.title = `About ${appName}`; const $titleElement = document.querySelector(".title") as HTMLHeadingElement; $titleElement.textContent = appName; const $iconElement = document.getElementById("app-icon") as HTMLImageElement; $iconElement.src = iconPath; const $appVersionElement = document.getElementById( "app-version", ) as HTMLParagraphElement; $appVersionElement.textContent = `Version ${versions.app.remote}`; const $webVersionElement = document.getElementById( "web-version", ) as HTMLParagraphElement; $webVersionElement.textContent = `${appName} for Web Version ${versions.web.remote}`; const $copyRightElement = document.querySelector( ".copyright", ) as HTMLParagraphElement; $copyRightElement.textContent = `Copyright (c) ${new Date().getFullYear()} ${appName}`; }); ================================================ FILE: src/types.ts ================================================ import {IpcRendererEvent, IpcRenderer} from "electron"; export type IpcListener = (event: IpcRendererEvent, ...args: any[]) => void; export type CustomIpcSender = (channel: string, ...args: any[]) => null; export interface CustomIpcRenderer extends IpcRenderer { send: (channel: string, ...args: any[]) => null; receive: (channel: string, listener: IpcListener) => null; } declare global { interface RendererWindow extends Window { ipcRenderer?: CustomIpcRenderer; remote?: { getVersion: () => string; }; } } ================================================ FILE: src/util/checkVersion.ts ================================================ import * as fs from "fs"; import * as path from "path"; import fetch from "node-fetch"; import {EXCALIDRAW_API, EXCALIDRAW_GITHUB_PACKAGE_JSON_URL} from "../constants"; const LOCAL_VERSION_PATH = path.resolve(__dirname, "client", "version.json"); interface CheckResponse { local: string; remote: string; appVersion: string; needsUpdate: boolean; } const _getLocalVersion = (): string => { const raw = fs.readFileSync(LOCAL_VERSION_PATH).toString(); const contents = JSON.parse(raw); return contents.version; }; const _getRemoteVersion = async (): Promise => { const raw = await fetch(EXCALIDRAW_API); const contents = await raw.json(); return contents.version; }; const _getRemoteDesktopAppVersion = async (): Promise => { const raw = await fetch(EXCALIDRAW_GITHUB_PACKAGE_JSON_URL); const contents = await raw.json(); return contents.version; }; export default async function checkVersion(): Promise { const localVersion = _getLocalVersion(); const remoteVersion = await _getRemoteVersion(); const appVersion = await _getRemoteDesktopAppVersion(); return { local: localVersion, remote: remoteVersion, appVersion, needsUpdate: localVersion < remoteVersion, }; } ================================================ FILE: src/util/metadata.ts ================================================ import * as Store from "electron-store"; let metadataStore: Store; if (!metadataStore) { metadataStore = new Store({}); } export const getMetadata = (key: string): Store => { return metadataStore.get(`metadata.${key}`); }; export const setMetadata = (key: string, value: any) => { return metadataStore.set(`metadata.${key}`, value); }; export const setAppName = (value: string) => { return metadataStore.set(`metadata.appName`, value); }; export const getAppName = () => { return metadataStore.get(`metadata.appName`); }; export const getAppVersions = () => { const versions = metadataStore.get(`metadata.versions`); const { local: localVersion, remote: remoteVersion, needsUpdate, appVersion, } = versions; return { needsUpdate, app: { local: "", remote: appVersion, }, web: { local: localVersion, remote: remoteVersion, }, }; }; ================================================ FILE: tasks/dist.js ================================================ #!/usr/bin/env node const argv = require("mri")(process.argv); const exec = require("execa").sync; const pkg = require("../package"); const {publish, config} = argv; const artifactOptions = [ "-c.artifactName=${name}-${version}-${os}-${arch}.${ext}", "-c.dmg.artifactName=${name}-${version}-${os}.${ext}", "-c.nsis.artifactName=${name}-${version}-${os}-setup.${ext}", "-c.nsisWeb.artifactName=${name}-${version}-${os}-web-setup.${ext}", argv.compress === false && "-c.compression=store", ].filter((f) => f); // interpret shorthand target options // --win, --linux, --mac const platforms = [ argv.win ? "win" : null, argv.linux ? "linux" : null, argv.mac ? "mac" : null, ].filter((f) => f); const platformOptions = platforms.map((p) => `--${p}`); const publishOptions = typeof publish !== undefined ? [`--publish=${publish ? "always" : "never"}`].filter((f) => f) : []; const signingOptions = [`-c.forceCodeSigning=${false}`]; if (publish && (argv.ia32 || argv.x64)) { console.error("Do not override arch; is manually pinned"); process.exit(1); } const archOptions = ["x64", "ia32"].filter((a) => argv[a]).map((a) => `--${a}`); const args = [ ...[config && `-c=${config}`].filter((f) => f), ...archOptions, ...signingOptions, ...platformOptions, ...publishOptions, ...artifactOptions, ]; console.log(` Building ${pkg.name} distro --- version: ${pkg.version} platforms: [${(platforms.length && platforms) || "current"}] publish: ${publish || false} --- electron-builder ${args.join(" ")} `); exec("electron-builder", args, { stdio: "inherit", }); ================================================ FILE: tasks/download.js ================================================ #!/usr/bin/env node const https = require("https"); const fs = require("fs"); const asar = require("asar"); const DEST = "dist/excalidraw.asar"; const SOURCE = "https://excalidraw.com/excalidraw.asar"; const UNPACK = "dist/client"; const file = fs.createWriteStream(DEST); const request = https .get(SOURCE, (response) => { response.pipe(file); file.on("finish", async () => { console.info(`${DEST} is downloaded`); // unpack await asar.extractAll(DEST, UNPACK); file.close(); }); }) .on("error", (error) => { fs.unlink(DEST, () => {}); console.error(error); }); ================================================ FILE: test/main.spec.js ================================================ const Application = require("spectron").Application; const assert = require("assert"); const path = require("path"); const rootDir = path.resolve(__dirname, ".."); const isWindows = process.platform === "win32"; const electronBinary = isWindows ? "electron.cmd" : "electron"; const electronPath = path.join(rootDir, "node_modules", ".bin", electronBinary); describe("Application launch", function () { this.timeout(20000); beforeEach(function () { this.app = new Application({ path: electronPath, args: [rootDir], startTimeout: 10000, waitTimeout: 10e3, chromeDriverLogPath: "./chromedriver.log", }); return this.app.start(); }); afterEach(function () { if (this.app && this.app.isRunning()) { return this.app.stop(); } }); it("shows an initial window", function () { return this.app.client.getWindowCount().then(function (count) { assert.equal(count, 1); }); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "*": ["node_modules/*"] } }, "include": ["src/**/*", "tasks", "test/**/*", "webpack.config.js"] } ================================================ FILE: webpack.config.js ================================================ const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const commonConfiguration = { mode: "development", devtool: "sourcemap", module: { rules: [ { test: /\.ts$/, include: /src/, use: [{loader: "ts-loader"}], }, ], }, resolve: { extensions: [".ts", ".js", ".json"], }, }; module.exports = [ { ...commonConfiguration, target: "electron-main", entry: "./src/main.ts", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, node: { __dirname: false, }, }, { ...commonConfiguration, target: "electron-renderer", entry: "./src/preload.ts", node: {__dirname: false, global: true}, output: { path: path.resolve(__dirname, "dist"), filename: "preload.js", }, }, { ...commonConfiguration, target: "electron-renderer", entry: "./src/renderer.ts", output: { path: path.resolve(__dirname, "dist"), filename: "renderer.js", }, plugins: [ new HtmlWebpackPlugin({ filename: "pages/about.html", template: "./src/pages/about.html", }), ], }, ];