├── .editorconfig ├── .env.development ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── actions │ └── release-notes │ │ ├── action.yml │ │ └── main.js ├── renovate.json └── workflows │ ├── lint.yml │ ├── release.yml │ ├── tests.yml │ ├── typechecking.yml │ └── update-electron-vendors.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── deployment.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLibraryMappings.xml ├── jsLinters │ └── eslint.xml ├── modules.xml ├── vcs.xml ├── vite-electron-builder.iml └── webResources.xml ├── LICENSE ├── README.md ├── buildResources ├── .gitkeep ├── icon.icns └── icon.png ├── contributing.md ├── doc ├── inkdrop-logo.png └── screenshot.png ├── electron-builder.config.js ├── electron-vendors.config.json ├── package-lock.json ├── package.json ├── packages ├── main │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.js ├── preload │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── types │ │ └── electron-api.d.ts │ └── vite.config.js └── renderer │ ├── assets │ └── logo.svg │ ├── index.html │ ├── src │ ├── app.css │ ├── app.tsx │ ├── editor.css │ ├── editor.tsx │ ├── index.css │ ├── index.tsx │ ├── preview.css │ ├── preview.tsx │ ├── remark-code.tsx │ ├── runmode.ts │ ├── shim.js │ └── use-codemirror.tsx │ ├── tsconfig.json │ └── vite.config.js ├── prettier.config.js ├── scripts ├── build.js ├── buildEnvTypes.js ├── loadAndSetEnv.mjs ├── update-electron-vendors.js └── watch.js ├── tests └── app.spec.js ├── tsconfig.json ├── types └── .gitkeep └── vetur.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # https://github.com/jokeyrhyme/standard-editorconfig 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # defaults 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # This is a stub. 2 | # It is needed as a data sample for TypeScript & Typechecking. 3 | # The real value of the variable is set in scripts/watch.js and depend on packages/main/vite.config.js 4 | VITE_DEV_SERVER_URL=http://localhost:3000/ 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es2021": true, 5 | "node": true, 6 | "browser": false 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint" 20 | ], 21 | "ignorePatterns": [ 22 | "types/env.d.ts", 23 | "node_modules/**", 24 | "**/dist/**" 25 | ], 26 | "rules": { 27 | "@typescript-eslint/no-unused-vars": "error", 28 | "@typescript-eslint/no-var-requires": "off", 29 | "@typescript-eslint/consistent-type-imports": "error" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: cawa-93 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions & Discussions 4 | url: https://github.com/cawa-93/vite-electron-builder/discussions/categories/q-a 5 | about: Use GitHub discussions for message-board style questions and discussions. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: cawa-93 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/release-notes/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Notes' 2 | description: 'Return release notes based on Git Commits' 3 | inputs: 4 | from: 5 | description: 'Commit from which start log' 6 | required: true 7 | to: 8 | description: 'Commit to which end log' 9 | required: true 10 | include-commit-body: 11 | description: 'Should the commit body be in notes' 12 | required: false 13 | default: 'false' 14 | include-abbreviated-commit: 15 | description: 'Should the commit sha be in notes' 16 | required: false 17 | default: 'true' 18 | outputs: 19 | release-note: # id of output 20 | description: 'Release notes' 21 | runs: 22 | using: 'node12' 23 | main: 'main.js' 24 | -------------------------------------------------------------------------------- /.github/actions/release-notes/main.js: -------------------------------------------------------------------------------- 1 | // TODO: Refactor this action 2 | 3 | const {execSync} = require('child_process'); 4 | 5 | /** 6 | * Gets the value of an input. The value is also trimmed. 7 | * 8 | * @param name name of the input to get 9 | * @param options optional. See InputOptions. 10 | * @returns string 11 | */ 12 | function getInput(name, options) { 13 | const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 14 | if (options && options.required && !val) { 15 | throw new Error(`Input required and not supplied: ${name}`); 16 | } 17 | 18 | return val.trim(); 19 | } 20 | 21 | const START_FROM = getInput('from'); 22 | const END_TO = getInput('to'); 23 | const INCLUDE_COMMIT_BODY = getInput('include-commit-body') === 'true'; 24 | const INCLUDE_ABBREVIATED_COMMIT = getInput('include-abbreviated-commit') === 'true'; 25 | 26 | /** 27 | * @typedef {Object} ICommit 28 | * @property {string | undefined} abbreviated_commit 29 | * @property {string | undefined} subject 30 | * @property {string | undefined} body 31 | */ 32 | 33 | /** 34 | * @typedef {ICommit & {type: string | undefined, scope: string | undefined}} ICommitExtended 35 | */ 36 | 37 | 38 | /** 39 | * Any unique string that is guaranteed not to be used in committee text. 40 | * Used to split data in the commit line 41 | * @type {string} 42 | */ 43 | const commitInnerSeparator = '~~~~'; 44 | 45 | 46 | /** 47 | * Any unique string that is guaranteed not to be used in committee text. 48 | * Used to split each commit line 49 | * @type {string} 50 | */ 51 | const commitOuterSeparator = '₴₴₴₴'; 52 | 53 | 54 | /** 55 | * Commit data to be obtained. 56 | * @type {Map} 57 | * 58 | * @see https://git-scm.com/docs/git-log#Documentation/git-log.txt-emnem 59 | */ 60 | const commitDataMap = new Map([ 61 | ['subject', '%s'], // Required 62 | ]); 63 | 64 | if (INCLUDE_COMMIT_BODY) { 65 | commitDataMap.set('body', '%b'); 66 | } 67 | 68 | if (INCLUDE_ABBREVIATED_COMMIT) { 69 | commitDataMap.set('abbreviated_commit', '%h'); 70 | } 71 | 72 | /** 73 | * The type used to group commits that do not comply with the convention 74 | * @type {string} 75 | */ 76 | const fallbackType = 'other'; 77 | 78 | 79 | /** 80 | * List of all desired commit groups and in what order to display them. 81 | * @type {string[]} 82 | */ 83 | const supportedTypes = [ 84 | 'feat', 85 | 'fix', 86 | 'perf', 87 | 'refactor', 88 | 'style', 89 | 'docs', 90 | 'test', 91 | 'build', 92 | 'ci', 93 | 'chore', 94 | 'revert', 95 | 'deps', 96 | fallbackType, 97 | ]; 98 | 99 | /** 100 | * @param {string} commitString 101 | * @returns {ICommit} 102 | */ 103 | function parseCommit(commitString) { 104 | /** @type {ICommit} */ 105 | const commitDataObj = {}; 106 | const commitDataArray = 107 | commitString 108 | .split(commitInnerSeparator) 109 | .map(s => s.trim()); 110 | 111 | for (const [key] of commitDataMap) { 112 | commitDataObj[key] = commitDataArray.shift(); 113 | } 114 | 115 | return commitDataObj; 116 | } 117 | 118 | /** 119 | * Returns an array of commits since the last git tag 120 | * @return {ICommit[]} 121 | */ 122 | function getCommits() { 123 | 124 | const format = Array.from(commitDataMap.values()).join(commitInnerSeparator) + commitOuterSeparator; 125 | 126 | const logs = String(execSync(`git --no-pager log ${START_FROM}..${END_TO} --pretty=format:"${format}" --reverse`)); 127 | 128 | return logs 129 | .trim() 130 | .split(commitOuterSeparator) 131 | .filter(r => !!r.trim()) // Skip empty lines 132 | .map(parseCommit); 133 | } 134 | 135 | 136 | /** 137 | * 138 | * @param {ICommit} commit 139 | * @return {ICommitExtended} 140 | */ 141 | function setCommitTypeAndScope(commit) { 142 | 143 | const matchRE = new RegExp(`^(?:(${supportedTypes.join('|')})(?:\\((\\S+)\\))?:)?(.*)`, 'i'); 144 | 145 | let [, type, scope, clearSubject] = commit.subject.match(matchRE); 146 | 147 | /** 148 | * Additional rules for checking committees that do not comply with the convention, but for which it is possible to determine the type. 149 | */ 150 | // Commits like `revert something` 151 | if (type === undefined && commit.subject.startsWith('revert')) { 152 | type = 'revert'; 153 | } 154 | 155 | return { 156 | ...commit, 157 | type: (type || fallbackType).toLowerCase().trim(), 158 | scope: (scope || '').toLowerCase().trim(), 159 | subject: (clearSubject || commit.subject).trim(), 160 | }; 161 | } 162 | 163 | class CommitGroup { 164 | constructor() { 165 | this.scopes = new Map; 166 | this.commits = []; 167 | } 168 | 169 | /** 170 | * 171 | * @param {ICommitExtended[]} array 172 | * @param {ICommitExtended} commit 173 | */ 174 | static _pushOrMerge(array, commit) { 175 | const similarCommit = array.find(c => c.subject === commit.subject); 176 | if (similarCommit) { 177 | if (commit.abbreviated_commit !== undefined) { 178 | similarCommit.abbreviated_commit += `, ${commit.abbreviated_commit}`; 179 | } 180 | } else { 181 | array.push(commit); 182 | } 183 | } 184 | 185 | /** 186 | * @param {ICommitExtended} commit 187 | */ 188 | push(commit) { 189 | if (!commit.scope) { 190 | CommitGroup._pushOrMerge(this.commits, commit); 191 | return; 192 | } 193 | 194 | const scope = this.scopes.get(commit.scope) || {commits: []}; 195 | CommitGroup._pushOrMerge(scope.commits, commit); 196 | this.scopes.set(commit.scope, scope); 197 | } 198 | 199 | get isEmpty() { 200 | return this.commits.length === 0 && this.scopes.size === 0; 201 | } 202 | } 203 | 204 | 205 | /** 206 | * Groups all commits by type and scopes 207 | * @param {ICommit[]} commits 208 | * @returns {Map} 209 | */ 210 | function getGroupedCommits(commits) { 211 | const parsedCommits = commits.map(setCommitTypeAndScope); 212 | 213 | const types = new Map( 214 | supportedTypes.map(id => ([id, new CommitGroup()])), 215 | ); 216 | 217 | for (const parsedCommit of parsedCommits) { 218 | const typeId = parsedCommit.type; 219 | const type = types.get(typeId); 220 | type.push(parsedCommit); 221 | } 222 | 223 | return types; 224 | } 225 | 226 | /** 227 | * Return markdown list with commits 228 | * @param {ICommitExtended[]} commits 229 | * @param {string} pad 230 | * @returns {string} 231 | */ 232 | function getCommitsList(commits, pad = '') { 233 | let changelog = ''; 234 | for (const commit of commits) { 235 | changelog += `${pad}- ${commit.subject}.`; 236 | 237 | if (commit.abbreviated_commit !== undefined) { 238 | changelog += ` (${commit.abbreviated_commit})`; 239 | } 240 | 241 | changelog += '\r\n'; 242 | 243 | if (commit.body === undefined) { 244 | continue; 245 | } 246 | 247 | const body = commit.body.replace('[skip ci]', '').trim(); 248 | if (body !== '') { 249 | changelog += `${ 250 | body 251 | .split(/\r*\n+/) 252 | .filter(s => !!s.trim()) 253 | .map(s => `${pad} ${s}`) 254 | .join('\r\n') 255 | }${'\r\n'}`; 256 | } 257 | } 258 | 259 | return changelog; 260 | } 261 | 262 | 263 | function replaceHeader(str) { 264 | switch (str) { 265 | case 'feat': 266 | return 'New Features'; 267 | case 'fix': 268 | return 'Bug Fixes'; 269 | case 'docs': 270 | return 'Documentation Changes'; 271 | case 'build': 272 | return 'Build System'; 273 | case 'chore': 274 | return 'Chores'; 275 | case 'ci': 276 | return 'Continuous Integration'; 277 | case 'refactor': 278 | return 'Refactors'; 279 | case 'style': 280 | return 'Code Style Changes'; 281 | case 'test': 282 | return 'Tests'; 283 | case 'perf': 284 | return 'Performance improvements'; 285 | case 'revert': 286 | return 'Reverts'; 287 | case 'deps': 288 | return 'Dependency updates'; 289 | case 'other': 290 | return 'Other Changes'; 291 | default: 292 | return str; 293 | } 294 | } 295 | 296 | 297 | /** 298 | * Return markdown string with changelog 299 | * @param {Map} groups 300 | */ 301 | function getChangeLog(groups) { 302 | 303 | let changelog = ''; 304 | 305 | for (const [typeId, group] of groups) { 306 | if (group.isEmpty) { 307 | continue; 308 | } 309 | 310 | changelog += `### ${replaceHeader(typeId)}${'\r\n'}`; 311 | 312 | for (const [scopeId, scope] of group.scopes) { 313 | if (scope.commits.length) { 314 | changelog += `- #### ${replaceHeader(scopeId)}${'\r\n'}`; 315 | changelog += getCommitsList(scope.commits, ' '); 316 | } 317 | } 318 | 319 | if (group.commits.length) { 320 | changelog += getCommitsList(group.commits); 321 | } 322 | 323 | changelog += ('\r\n' + '\r\n'); 324 | } 325 | 326 | return changelog.trim(); 327 | } 328 | 329 | 330 | function escapeData(s) { 331 | return String(s) 332 | .replace(/%/g, '%25') 333 | .replace(/\r/g, '%0D') 334 | .replace(/\n/g, '%0A'); 335 | } 336 | 337 | try { 338 | const commits = getCommits(); 339 | const grouped = getGroupedCommits(commits); 340 | const changelog = getChangeLog(grouped); 341 | process.stdout.write('::set-output name=release-note::' + escapeData(changelog) + '\r\n'); 342 | // require('fs').writeFileSync('../CHANGELOG.md', changelog, {encoding: 'utf-8'}) 343 | } catch (e) { 344 | console.error(e); 345 | process.exit(1); 346 | } 347 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":semanticCommits", 5 | ":automergeTypes" 6 | ], 7 | "labels": [ 8 | "dependencies" 9 | ], 10 | "baseBranches": [ 11 | "main" 12 | ], 13 | "bumpVersion": "patch", 14 | "patch": { 15 | "automerge": true 16 | }, 17 | "minor": { 18 | "automerge": true 19 | }, 20 | "packageRules": [ 21 | { 22 | "packageNames": [ 23 | "node", 24 | "npm" 25 | ], 26 | "enabled": false 27 | }, 28 | { 29 | "depTypeList": [ 30 | "devDependencies" 31 | ], 32 | "semanticCommitType": "build" 33 | }, 34 | { 35 | "matchSourceUrlPrefixes": [ 36 | "https://github.com/vitejs/vite/" 37 | ], 38 | "groupName": "Vite monorepo packages", 39 | "automerge": false 40 | }, 41 | { 42 | "matchPackagePatterns": [ 43 | "^@typescript-eslint", 44 | "^eslint" 45 | ], 46 | "automerge": true, 47 | "groupName": "eslint" 48 | }, 49 | { 50 | "matchPackageNames": [ 51 | "electron", 52 | "electron-releases", 53 | "spectron" 54 | ], 55 | "separateMajorMinor": false, 56 | "groupName": "electron" 57 | } 58 | ], 59 | "rangeStrategy": "bump" 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - '**.js' 8 | - '**.ts' 9 | - '**.vue' 10 | - 'package-lock.json' 11 | - '.github/workflows/lint.yml' 12 | pull_request: 13 | paths: 14 | - '**.js' 15 | - '**.ts' 16 | - '**.vue' 17 | - 'package-lock.json' 18 | - '.github/workflows/lint.yml' 19 | 20 | 21 | defaults: 22 | run: 23 | shell: 'bash' 24 | 25 | jobs: 26 | eslint: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions/setup-node@v2 32 | with: 33 | node-version: 16 # Need for npm >=7.7 34 | cache: 'npm' 35 | 36 | # TODO: Install not all dependencies, but only those required for this workflow 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - run: npm run lint 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - '**.md' 8 | - '**.spec.js' 9 | - '.idea' 10 | - '.gitignore' 11 | - '.github/**' 12 | - '!.github/workflows/release.yml' 13 | 14 | concurrency: 15 | group: release-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | 19 | defaults: 20 | run: 21 | shell: 'bash' 22 | 23 | 24 | jobs: 25 | 26 | draft: 27 | runs-on: ubuntu-latest 28 | outputs: 29 | release-note: ${{ steps.release-note.outputs.release-note }} 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | with: 34 | fetch-depth: 0 35 | 36 | - uses: actions/setup-node@v2 37 | with: 38 | node-version: 14 39 | 40 | - name: Get last git tag 41 | id: tag 42 | run: echo "::set-output name=last-tag::$(git describe --tags --abbrev=0 || git rev-list --max-parents=0 ${{github.ref}})" 43 | 44 | - name: Generate release notes 45 | uses: ./.github/actions/release-notes 46 | id: release-note 47 | with: 48 | from: ${{ steps.tag.outputs.last-tag }} 49 | to: ${{ github.ref }} 50 | include-commit-body: true 51 | include-abbreviated-commit: true 52 | 53 | - name: Get version from current date 54 | id: version 55 | run: echo "::set-output name=current-version::$(node -e "try{console.log(require('./electron-builder.config.js').extraMetadata.version)}catch(e){console.error(e);process.exit(1)}")" 56 | 57 | 58 | - name: Waiting on All checks 59 | uses: lewagon/wait-on-check-action@v0.2 60 | with: 61 | ref: ${{ github.ref }} 62 | repo-token: ${{ secrets.GITHUB_TOKEN }} 63 | running-workflow-name: 'draft' 64 | 65 | - name: Delete outdated drafts 66 | uses: hugo19941994/delete-draft-releases@v0.1.0 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: Create Release Draft 71 | uses: softprops/action-gh-release@v1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.github_token }} 74 | with: 75 | prerelease: true 76 | draft: true 77 | tag_name: v${{ steps.version.outputs.current-version }} 78 | name: v${{ steps.version.outputs.current-version }} 79 | body: ${{ steps.release-note.outputs.release-note }} 80 | 81 | upload_artifacts: 82 | needs: [ draft ] 83 | 84 | strategy: 85 | matrix: 86 | os: [ windows-latest ] 87 | # To compile the application for different platforms, use: 88 | # os: [ macos-latest, ubuntu-latest, windows-latest ] 89 | 90 | runs-on: ${{ matrix.os }} 91 | 92 | steps: 93 | - uses: actions/checkout@v2 94 | 95 | - uses: actions/setup-node@v2 96 | with: 97 | node-version: 16 # Need for npm >=7.7 98 | cache: 'npm' 99 | 100 | - name: Install dependencies 101 | run: npm ci 102 | 103 | # The easiest way to transfer release notes to a compiled application is create `release-notes.md` in the build resources. 104 | # See https://github.com/electron-userland/electron-builder/issues/1511#issuecomment-310160119 105 | - name: Prepare release notes 106 | env: 107 | RELEASE_NOTE: ${{ needs.draft.outputs.release-note }} 108 | run: echo "$RELEASE_NOTE" >> ./buildResources/release-notes.md 109 | 110 | # Compile app and upload artifacts 111 | - name: Compile & release Electron app 112 | uses: samuelmeuli/action-electron-builder@v1 113 | with: 114 | build_script_name: build 115 | args: --config electron-builder.config.js 116 | 117 | # GitHub token, automatically provided to the action 118 | # (No need to define this secret in the repo settings) 119 | github_token: ${{ secrets.github_token }} 120 | 121 | # If the commit is tagged with a version (e.g. "v1.0.0"), 122 | # release the app after building 123 | release: true 124 | 125 | # Sometimes the build may fail due to a connection problem with Apple, GitHub, etc. servers. 126 | # This option will restart the build as many attempts as possible 127 | max_attempts: 3 128 | 129 | 130 | # Code Signing params 131 | 132 | # Base64-encoded code signing certificate for Windows 133 | # windows_certs: '' 134 | 135 | # Password for decrypting `windows_certs` 136 | # windows_certs_password: '' 137 | 138 | # Base64-encoded code signing certificate for macOS 139 | # mac_certs: '' 140 | 141 | # Password for decrypting `mac_certs` 142 | # mac_certs_password: '' 143 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'packages/**' 8 | - 'tests/**' 9 | - 'package-lock.json' 10 | - '.github/workflows/tests.yml' 11 | pull_request: 12 | paths: 13 | - 'packages/**' 14 | - 'tests/**' 15 | - 'package-lock.json' 16 | - '.github/workflows/tests.yml' 17 | 18 | defaults: 19 | run: 20 | shell: 'bash' 21 | 22 | jobs: 23 | e2e: 24 | strategy: 25 | matrix: 26 | os: [ windows-latest ] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-node@v2 33 | with: 34 | node-version: 16 # Need for npm >=7.7 35 | cache: 'npm' 36 | 37 | # TODO: Install not all dependencies, but only those required for this workflow 38 | - name: Install dependencies 39 | run: npm ci 40 | 41 | - run: npm test 42 | -------------------------------------------------------------------------------- /.github/workflows/typechecking.yml: -------------------------------------------------------------------------------- 1 | name: Typechecking 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - '**.ts' 8 | - '**.vue' 9 | - '**/tsconfig.json' 10 | - 'package-lock.json' 11 | - '.github/workflows/typechecking.yml' 12 | pull_request: 13 | paths: 14 | - '**.ts' 15 | - '**.vue' 16 | - '**/tsconfig.json' 17 | - 'package-lock.json' 18 | - '.github/workflows/typechecking.yml' 19 | 20 | defaults: 21 | run: 22 | shell: 'bash' 23 | 24 | jobs: 25 | typescript: 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: actions/setup-node@v2 31 | with: 32 | node-version: 16 # Need for npm >=7.7 33 | cache: 'npm' 34 | 35 | # TODO: Install not all dependencies, but only those required for this workflow 36 | - name: Install dependencies 37 | run: npm ci 38 | 39 | - run: npm run buildEnvTypes 40 | 41 | # Type checking is divided into three separate commands for more convenient logs 42 | - run: npm run typecheck-main 43 | - run: npm run typecheck-preload 44 | - run: npm run typecheck-renderer 45 | -------------------------------------------------------------------------------- /.github/workflows/update-electron-vendors.yml: -------------------------------------------------------------------------------- 1 | name: Update Electon vendors versions 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'package-lock.json' 8 | 9 | 10 | concurrency: 11 | group: update-electron-vendors-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | 15 | defaults: 16 | run: 17 | shell: 'bash' 18 | 19 | 20 | jobs: 21 | node-chrome: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v2 27 | with: 28 | node-version: 16 # Need for npm >=7.7 29 | cache: 'npm' 30 | 31 | # TODO: Install not all dependencies, but only those required for this workflow 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | - run: node ./scripts/update-electron-vendors.js 36 | 37 | - name: Create Pull Request 38 | uses: peter-evans/create-pull-request@v3 39 | with: 40 | delete-branch: true 41 | commit-message: Update electron vendors 42 | branch: autoupdates/electron-vendors 43 | title: Update electron vendors 44 | body: Updated versions of electron vendors in `electron-vendors.config.json` and `.browserslistrc` files 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | thumbs.db 6 | types/env.d.ts 7 | 8 | .eslintcache 9 | 10 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 11 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 12 | 13 | # User-specific stuff 14 | .idea/**/workspace.xml 15 | .idea/**/tasks.xml 16 | .idea/**/usage.statistics.xml 17 | .idea/**/dictionaries 18 | .idea/**/shelf 19 | 20 | # Generated files 21 | .idea/**/contentModel.xml 22 | 23 | # Sensitive or high-churn files 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | .idea/**/dbnavigator.xml 31 | 32 | # Gradle 33 | .idea/**/gradle.xml 34 | .idea/**/libraries 35 | 36 | # Gradle and Maven with auto-import 37 | # When using Gradle or Maven with auto-import, you should exclude module files, 38 | # since they will be recreated, and may cause churn. Uncomment if using 39 | # auto-import. 40 | .idea/artifacts 41 | .idea/compiler.xml 42 | .idea/jarRepositories.xml 43 | .idea/modules.xml 44 | .idea/*.iml 45 | .idea/modules 46 | *.iml 47 | *.ipr 48 | 49 | # Mongo Explorer plugin 50 | .idea/**/mongoSettings.xml 51 | 52 | # File-based project format 53 | *.iws 54 | 55 | # Editor-based Rest Client 56 | .idea/httpRequests 57 | /.idea/csv-plugin.xml 58 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/deployment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vite-electron-builder.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Takuya Matsuyama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Editor Tutorial 2 | 3 | ![screenshot](./doc/screenshot.png) 4 | 5 | A tutorial for building a beautiful Markdown editor 6 | 7 | ## Sponsor 8 | 9 | [![Inkdrop](./doc/inkdrop-logo.png)](https://www.inkdrop.app/) 10 | A cross-platform Markdown note-taking app 11 | 12 | ## Stack 13 | 14 | - [Electron](https://www.electronjs.org/) - A framework for building cross-platform desktop apps using HTML, JS, and CSS 15 | - [Vite](https://vitejs.dev/) - A fast build tool 16 | - React - A library for building UI 17 | - TypeScript - A typed JavaScript 18 | - [CodeMirror 6](https://codemirror.net/6/) - An extensible code editor for the web 19 | - [Remark](https://remark.js.org/) - An extensible Markdown processor 20 | 21 | ## Get started 22 | 23 | ```sh 24 | npm i 25 | npm run watch 26 | ``` 27 | 28 | ## Project Structure 29 | 30 | The structure of this project is very similar to the structure of a monorepo. 31 | 32 | The entire source code of the program is divided into three modules (packages) that are bundled each independently: 33 | - [`packages/main`](packages/main) 34 | Electron [**main script**](https://www.electronjs.org/docs/tutorial/quick-start#create-the-main-script-file). 35 | - [`packages/preload`](packages/preload) 36 | Used in `BrowserWindow.webPreferences.preload`. See [Checklist: Security Recommendations](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content). 37 | - [`packages/renderer`](packages/renderer) 38 | Electron [**web page**](https://www.electronjs.org/docs/tutorial/quick-start#create-a-web-page). 39 | 40 | ### Build web resources 41 | 42 | Packages `main` and `preload` are built in [library mode](https://vitejs.dev/guide/build.html#library-mode) as it is a simple javascript. 43 | `renderer` package build as regular web app. 44 | 45 | The build of web resources is performed in the [`scripts/build.js`](scripts/build.js). Its analogue is a sequential call to `vite build` for each package. 46 | 47 | ### Compile App 48 | Next step is run packaging and compilation a ready for distribution Electron app for macOS, Windows and Linux with "auto update" support out of the box. 49 | 50 | To do this, using the [electron-builder]: 51 | - In npm script `compile`: This script is configured to compile the application as quickly as possible. It is not ready for distribution, is compiled only for the current platform and is used for debugging. 52 | - In GitHub Action: The application is compiled for any platform and ready-to-distribute files are automatically added to the draft GitHub release. 53 | 54 | 55 | ### Using Node.js API in renderer 56 | According to [Electron's security guidelines](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content), Node.js integration is disabled for remote content. This means that **you cannot call any Node.js api in the `packages/renderer` directly**. To do this, you **must** describe the interface in the `packages/preload` where Node.js api is allowed: 57 | ```ts 58 | // packages/preload/src/index.ts 59 | import {readFile} from 'fs/promises' 60 | 61 | const api = { 62 | readConfig: () => readFile('/path/to/config.json', {encoding: 'utf-8'}), 63 | } 64 | 65 | contextBridge.exposeInMainWorld('electron', api) 66 | ``` 67 | 68 | ```ts 69 | // packages/renderer/src/App.vue 70 | import {useElectron} from '/@/use/electron' 71 | 72 | const {readConfig} = useElectron() 73 | ``` 74 | 75 | [Read more about Security Considerations](https://www.electronjs.org/docs/tutorial/context-isolation#security-considerations). 76 | 77 | **Note**: Context isolation disabled for `test` environment. See [#693](https://github.com/electron-userland/spectron/issues/693#issuecomment-747872160). 78 | 79 | 80 | 81 | ### Modes and Environment Variables 82 | All environment variables set as part of the `import.meta`, so you can access them as follows: `import.meta.env`. 83 | 84 | You can also build type definitions of your variables by running `scripts/buildEnvTypes.js`. This command will create `types/env.d.ts` file with describing all environment variables for all modes. 85 | 86 | The mode option is used to specify the value of `import.meta.env.MODE` and the corresponding environment variables files that needs to be loaded. 87 | 88 | By default, there are two modes: 89 | - `production` is used by default 90 | - `development` is used by `npm run watch` script 91 | - `test` is used by `npm test` script 92 | 93 | When running building, environment variables are loaded from the following files in your project root: 94 | 95 | ``` 96 | .env # loaded in all cases 97 | .env.local # loaded in all cases, ignored by git 98 | .env.[mode] # only loaded in specified env mode 99 | .env.[mode].local # only loaded in specified env mode, ignored by git 100 | ``` 101 | 102 | **Note:** only variables prefixed with `VITE_` are exposed to your code (e.g. `VITE_SOME_KEY=123`) and `SOME_KEY=123` will not. you can access `VITE_SOME_KEY` using `import.meta.env.VITE_SOME_KEY`. This is because the `.env` files may be used by some users for server-side or build scripts and may contain sensitive information that should not be exposed in code shipped to browsers. 103 | 104 | ## Author 105 | 106 | Takuya Matsuyama ([@craftzdog](https://github.com/craftzdog)) 107 | 108 | 109 | [vite]: https://github.com/vitejs/vite/ 110 | [electron]: https://github.com/electron/electron 111 | [electron-builder]: https://github.com/electron-userland/electron-builder 112 | [vue]: https://github.com/vuejs/vue-next 113 | [vue-router]: https://github.com/vuejs/vue-router-next/ 114 | [typescript]: https://github.com/microsoft/TypeScript/ 115 | [spectron]: https://github.com/electron-userland/spectron 116 | [vue-tsc]: https://github.com/johnsoncodehk/vue-tsc 117 | [eslint-plugin-vue]: https://github.com/vuejs/eslint-plugin-vue 118 | [cawa-93-github]: https://github.com/cawa-93/ 119 | [cawa-93-sponsor]: https://www.patreon.com/Kozack/ 120 | -------------------------------------------------------------------------------- /buildResources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftzdog/electron-markdown-editor-tutorial/289bed4f105ef58471d9397ec37f235123d3832a/buildResources/.gitkeep -------------------------------------------------------------------------------- /buildResources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftzdog/electron-markdown-editor-tutorial/289bed4f105ef58471d9397ec37f235123d3832a/buildResources/icon.icns -------------------------------------------------------------------------------- /buildResources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftzdog/electron-markdown-editor-tutorial/289bed4f105ef58471d9397ec37f235123d3832a/buildResources/icon.png -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First and foremost, thank you! We appreciate that you want to contribute to vite-electron-builder, your time is valuable, and your contributions mean a lot to us. 4 | 5 | ## Issues 6 | 7 | Do not create issues about bumping dependencies unless a bug has been identified, and you can demonstrate that it effects this library. 8 | 9 | **Help us to help you** 10 | 11 | Remember that we’re here to help, but not to make guesses about what you need help with: 12 | 13 | - Whatever bug or issue you're experiencing, assume that it will not be as obvious to the maintainers as it is to you. 14 | - Spell it out completely. Keep in mind that maintainers need to think about _all potential use cases_ of a library. It's important that you explain how you're using a library so that maintainers can make that connection and solve the issue. 15 | 16 | _It can't be understated how frustrating and draining it can be to maintainers to have to ask clarifying questions on the most basic things, before it's even possible to start debugging. Please try to make the best use of everyone's time involved, including yourself, by providing this information up front._ 17 | 18 | 19 | ## Repo Setup 20 | The package manager used to install and link dependencies must be npm v7 or later. 21 | 22 | 1. Clone repo 23 | 1. `npm run watch` start electron app in watch mode. 24 | 1. `npm run compile` build app but for local debugging only. 25 | 1. `npm run lint` lint your code. 26 | 1. `npm run typecheck` Run typescript check. 27 | 1. `npm run test` Run app test. 28 | -------------------------------------------------------------------------------- /doc/inkdrop-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftzdog/electron-markdown-editor-tutorial/289bed4f105ef58471d9397ec37f235123d3832a/doc/inkdrop-logo.png -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftzdog/electron-markdown-editor-tutorial/289bed4f105ef58471d9397ec37f235123d3832a/doc/screenshot.png -------------------------------------------------------------------------------- /electron-builder.config.js: -------------------------------------------------------------------------------- 1 | const now = new Date; 2 | const buildVersion = `${now.getFullYear() - 2000}.${now.getMonth() + 1}.${now.getDate()}`; 3 | 4 | /** 5 | * @type {import('electron-builder').Configuration} 6 | * @see https://www.electron.build/configuration/configuration 7 | */ 8 | const config = { 9 | directories: { 10 | output: 'dist', 11 | buildResources: 'buildResources', 12 | }, 13 | files: [ 14 | 'packages/**/dist/**', 15 | ], 16 | extraMetadata: { 17 | version: buildVersion, 18 | }, 19 | }; 20 | 21 | module.exports = config; 22 | -------------------------------------------------------------------------------- /electron-vendors.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrome": "91", 3 | "node": "14" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-markdown-editor-tutorial", 3 | "private": true, 4 | "repository": { 5 | "type": "git", 6 | "url": "git+ssh://git@github.com/craftzdog/electron-markdown-editor-tutorial.git" 7 | }, 8 | "keywords": [ 9 | "Markdown", 10 | "remark", 11 | "CodeMirror", 12 | "Electron", 13 | "Vite", 14 | "TypeScript" 15 | ], 16 | "author": "Takuya Matsuyama", 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">=v14.16", 20 | "npm": ">=7.7" 21 | }, 22 | "main": "packages/main/dist/index.cjs", 23 | "scripts": { 24 | "buildEnvTypes": "node scripts/buildEnvTypes.js", 25 | "build": "node scripts/build.js", 26 | "precompile": "cross-env MODE=production npm run build", 27 | "compile": "electron-builder build --config electron-builder.config.js --dir --config.asar=false", 28 | "pretest": "cross-env MODE=test npm run build", 29 | "test": "node tests/app.spec.js", 30 | "watch": "node scripts/watch.js", 31 | "lint": "eslint . --ext js,ts", 32 | "pretypecheck": "npm run buildEnvTypes", 33 | "typecheck-main": "tsc --noEmit -p packages/main/tsconfig.json", 34 | "typecheck-preload": "tsc --noEmit -p packages/preload/tsconfig.json", 35 | "typecheck-renderer": "tsc --noEmit -p packages/renderer/tsconfig.json", 36 | "typecheck": "npm run typecheck-main && npm run typecheck-preload && npm run typecheck-renderer" 37 | }, 38 | "browserslist": [ 39 | "Chrome 91" 40 | ], 41 | "simple-git-hooks": { 42 | "pre-commit": "npx lint-staged", 43 | "pre-push": "npm run typecheck" 44 | }, 45 | "lint-staged": { 46 | "*.{js,ts}": "eslint --cache --fix" 47 | }, 48 | "devDependencies": { 49 | "@types/electron-devtools-installer": "^2.2.0", 50 | "@types/react": "^17.0.17", 51 | "@types/react-dom": "^17.0.9", 52 | "@typescript-eslint/eslint-plugin": "^4.29.1", 53 | "@typescript-eslint/parser": "^4.29.1", 54 | "cross-env": "^7.0.3", 55 | "electron": "^13.1.9", 56 | "electron-builder": "^22.11.7", 57 | "electron-devtools-installer": "^3.2.0", 58 | "eslint": "^7.32.0", 59 | "eslint-config-prettier": "^8.3.0", 60 | "lint-staged": "^11.1.2", 61 | "simple-git-hooks": "^2.5.1", 62 | "spectron": "^15.0.0", 63 | "typescript": "^4.3.5", 64 | "vite": "^2.4.4" 65 | }, 66 | "dependencies": { 67 | "@codemirror/commands": "^0.19.1", 68 | "@codemirror/gutter": "^0.19.0", 69 | "@codemirror/highlight": "^0.19.1", 70 | "@codemirror/history": "^0.19.0", 71 | "@codemirror/lang-javascript": "^0.19.1", 72 | "@codemirror/lang-markdown": "^0.19.1", 73 | "@codemirror/language": "^0.19.2", 74 | "@codemirror/language-data": "^0.19.0", 75 | "@codemirror/matchbrackets": "^0.19.1", 76 | "@codemirror/state": "^0.19.0", 77 | "@codemirror/theme-one-dark": "^0.19.0", 78 | "assert": "^2.0.0", 79 | "electron-updater": "^4.4.4", 80 | "github-markdown-css": "^4.0.0", 81 | "process": "^0.11.10", 82 | "react": "^17.0.2", 83 | "react-dom": "^17.0.2", 84 | "remark-gfm": "^2.0.0", 85 | "remark-parse": "^10.0.0", 86 | "remark-react": "^9.0.0", 87 | "unified": "^10.1.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/main/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import { join } from 'path'; 3 | import { URL } from 'url'; 4 | 5 | 6 | const isSingleInstance = app.requestSingleInstanceLock(); 7 | 8 | if (!isSingleInstance) { 9 | app.quit(); 10 | process.exit(0); 11 | } 12 | 13 | app.disableHardwareAcceleration(); 14 | 15 | /** 16 | * Workaround for TypeScript bug 17 | * @see https://github.com/microsoft/TypeScript/issues/41468#issuecomment-727543400 18 | */ 19 | const env = import.meta.env; 20 | 21 | 22 | // Install "Vue.js devtools" 23 | if (env.MODE === 'development') { 24 | app.whenReady() 25 | .then(() => import('electron-devtools-installer')) 26 | .then(({ default: installExtension, VUEJS3_DEVTOOLS }) => installExtension(VUEJS3_DEVTOOLS, { 27 | loadExtensionOptions: { 28 | allowFileAccess: true, 29 | }, 30 | })) 31 | .catch(e => console.error('Failed install extension:', e)); 32 | } 33 | 34 | let mainWindow: BrowserWindow | null = null; 35 | 36 | const createWindow = async () => { 37 | mainWindow = new BrowserWindow({ 38 | show: false, // Use 'ready-to-show' event to show window 39 | vibrancy: 'under-window', 40 | visualEffectState: 'active', 41 | webPreferences: { 42 | preload: join(__dirname, '../../preload/dist/index.cjs'), 43 | contextIsolation: env.MODE !== 'test', // Spectron tests can't work with contextIsolation: true 44 | enableRemoteModule: env.MODE === 'test', // Spectron tests can't work with enableRemoteModule: false 45 | }, 46 | }); 47 | 48 | /** 49 | * If you install `show: true` then it can cause issues when trying to close the window. 50 | * Use `show: false` and listener events `ready-to-show` to fix these issues. 51 | * 52 | * @see https://github.com/electron/electron/issues/25012 53 | */ 54 | mainWindow.on('ready-to-show', () => { 55 | if (!mainWindow?.isVisible()) { 56 | mainWindow?.show(); 57 | } 58 | 59 | if (env.MODE === 'development') { 60 | mainWindow?.webContents.openDevTools(); 61 | } 62 | }); 63 | 64 | /** 65 | * URL for main window. 66 | * Vite dev server for development. 67 | * `file://../renderer/index.html` for production and test 68 | */ 69 | const pageUrl = env.MODE === 'development' 70 | ? env.VITE_DEV_SERVER_URL 71 | : new URL('../renderer/dist/index.html', 'file://' + __dirname).toString(); 72 | 73 | 74 | await mainWindow.loadURL(pageUrl); 75 | }; 76 | 77 | 78 | app.on('second-instance', () => { 79 | // Someone tried to run a second instance, we should focus our window. 80 | if (mainWindow) { 81 | if (mainWindow.isMinimized()) mainWindow.restore(); 82 | mainWindow.focus(); 83 | } 84 | }); 85 | 86 | 87 | app.on('window-all-closed', () => { 88 | if (process.platform !== 'darwin') { 89 | app.quit(); 90 | } 91 | }); 92 | 93 | 94 | app.whenReady() 95 | .then(createWindow) 96 | .catch((e) => console.error('Failed create window:', e)); 97 | 98 | 99 | // Auto-updates 100 | if (env.PROD) { 101 | app.whenReady() 102 | .then(() => import('electron-updater')) 103 | .then(({ autoUpdater }) => autoUpdater.checkForUpdatesAndNotify()) 104 | .catch((e) => console.error('Failed check updates:', e)); 105 | } 106 | 107 | -------------------------------------------------------------------------------- /packages/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "/@/*": [ 7 | "./src/*" 8 | ] 9 | }, 10 | }, 11 | "files": [ 12 | "src/index.ts" 13 | ], 14 | "include": [ 15 | "../../types/**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/main/vite.config.js: -------------------------------------------------------------------------------- 1 | import {node} from '../../electron-vendors.config.json'; 2 | import {join} from 'path'; 3 | import { builtinModules } from 'module'; 4 | 5 | import {defineConfig} from 'vite'; 6 | import {loadAndSetEnv} from '../../scripts/loadAndSetEnv.mjs'; 7 | 8 | const PACKAGE_ROOT = __dirname; 9 | 10 | /** 11 | * Vite looks for `.env.[mode]` files only in `PACKAGE_ROOT` directory. 12 | * Therefore, you must manually load and set the environment variables from the root directory above 13 | */ 14 | loadAndSetEnv(process.env.MODE, process.cwd()); 15 | 16 | /** 17 | * @see https://vitejs.dev/config/ 18 | */ 19 | export default defineConfig({ 20 | root: PACKAGE_ROOT, 21 | resolve: { 22 | alias: { 23 | '/@/': join(PACKAGE_ROOT, 'src') + '/', 24 | }, 25 | }, 26 | build: { 27 | sourcemap: 'inline', 28 | target: `node${node}`, 29 | outDir: 'dist', 30 | assetsDir: '.', 31 | minify: process.env.MODE === 'development' ? false : 'terser', 32 | terserOptions: { 33 | ecma: 2020, 34 | compress: { 35 | passes: 2, 36 | }, 37 | safari10: false, 38 | }, 39 | lib: { 40 | entry: 'src/index.ts', 41 | formats: ['cjs'], 42 | }, 43 | rollupOptions: { 44 | external: [ 45 | 'electron', 46 | 'electron-devtools-installer', 47 | ...builtinModules, 48 | ], 49 | output: { 50 | entryFileNames: '[name].cjs', 51 | }, 52 | }, 53 | emptyOutDir: true, 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /packages/preload/src/index.ts: -------------------------------------------------------------------------------- 1 | import {contextBridge} from 'electron'; 2 | 3 | const apiKey = 'electron'; 4 | /** 5 | * @see https://github.com/electron/electron/issues/21437#issuecomment-573522360 6 | */ 7 | const api: ElectronApi = { 8 | versions: process.versions, 9 | }; 10 | 11 | if (import.meta.env.MODE !== 'test') { 12 | /** 13 | * The "Main World" is the JavaScript context that your main renderer code runs in. 14 | * By default, the page you load in your renderer executes code in this world. 15 | * 16 | * @see https://www.electronjs.org/docs/api/context-bridge 17 | */ 18 | contextBridge.exposeInMainWorld(apiKey, api); 19 | } else { 20 | 21 | /** 22 | * Recursively Object.freeze() on objects and functions 23 | * @see https://github.com/substack/deep-freeze 24 | * @param obj Object on which to lock the attributes 25 | */ 26 | const deepFreeze = (obj: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any 27 | if (typeof obj === 'object' && obj !== null) { 28 | Object.keys(obj).forEach((prop) => { 29 | const val = obj[prop]; 30 | if ((typeof val === 'object' || typeof val === 'function') && !Object.isFrozen(val)) { 31 | deepFreeze(val); 32 | } 33 | }); 34 | } 35 | 36 | return Object.freeze(obj); 37 | }; 38 | 39 | deepFreeze(api); 40 | 41 | window[apiKey] = api; 42 | 43 | // Need for Spectron tests 44 | window.electronRequire = require; 45 | } 46 | -------------------------------------------------------------------------------- /packages/preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "/@/*": [ 7 | "./src/*" 8 | ] 9 | } 10 | }, 11 | "files": [ 12 | "types/electron-api.d.ts", 13 | "src/index.ts" 14 | ], 15 | "include": [ 16 | "../../types/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/preload/types/electron-api.d.ts: -------------------------------------------------------------------------------- 1 | 2 | interface ElectronApi { 3 | readonly versions: Readonly 4 | } 5 | 6 | declare interface Window { 7 | electron: Readonly 8 | electronRequire?: NodeRequire 9 | } 10 | -------------------------------------------------------------------------------- /packages/preload/vite.config.js: -------------------------------------------------------------------------------- 1 | import {chrome} from '../../electron-vendors.config.json'; 2 | import {join} from 'path'; 3 | import { builtinModules } from 'module'; 4 | import {defineConfig} from 'vite'; 5 | import {loadAndSetEnv} from '../../scripts/loadAndSetEnv.mjs'; 6 | 7 | const PACKAGE_ROOT = __dirname; 8 | 9 | /** 10 | * Vite looks for `.env.[mode]` files only in `PACKAGE_ROOT` directory. 11 | * Therefore, you must manually load and set the environment variables from the root directory above 12 | */ 13 | loadAndSetEnv(process.env.MODE, process.cwd()); 14 | 15 | /** 16 | * @see https://vitejs.dev/config/ 17 | */ 18 | export default defineConfig({ 19 | root: PACKAGE_ROOT, 20 | resolve: { 21 | alias: { 22 | '/@/': join(PACKAGE_ROOT, 'src') + '/', 23 | }, 24 | }, 25 | build: { 26 | sourcemap: 'inline', 27 | target: `chrome${chrome}`, 28 | outDir: 'dist', 29 | assetsDir: '.', 30 | minify: process.env.MODE === 'development' ? false : 'terser', 31 | terserOptions: { 32 | ecma: 2020, 33 | compress: { 34 | passes: 2, 35 | }, 36 | safari10: false, 37 | }, 38 | lib: { 39 | entry: 'src/index.ts', 40 | formats: ['cjs'], 41 | }, 42 | rollupOptions: { 43 | external: [ 44 | 'electron', 45 | ...builtinModules, 46 | ], 47 | output: { 48 | entryFileNames: '[name].cjs', 49 | }, 50 | }, 51 | emptyOutDir: true, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /packages/renderer/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/renderer/src/app.css: -------------------------------------------------------------------------------- 1 | .app { 2 | background-color: transparent; 3 | display: flex; 4 | flex-direction: row; 5 | height: 100%; 6 | } 7 | 8 | button { 9 | font-size: calc(10px + 2vmin); 10 | } 11 | -------------------------------------------------------------------------------- /packages/renderer/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | import Editor from './editor' 3 | import Preview from './preview' 4 | import './app.css' 5 | 6 | const App: React.FC = () => { 7 | const [doc, setDoc] = useState('# Hello, World!\n') 8 | 9 | const handleDocChange = useCallback(newDoc => { 10 | setDoc(newDoc) 11 | }, []) 12 | 13 | return ( 14 |
15 | 16 | 17 |
18 | ) 19 | } 20 | 21 | export default App 22 | -------------------------------------------------------------------------------- /packages/renderer/src/editor.css: -------------------------------------------------------------------------------- 1 | .editor-wrapper { 2 | height: 100%; 3 | flex: 0 0 50%; 4 | } 5 | -------------------------------------------------------------------------------- /packages/renderer/src/editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react' 2 | import useCodeMirror from './use-codemirror' 3 | import './editor.css' 4 | 5 | interface Props { 6 | initialDoc: string, 7 | onChange: (doc: string) => void 8 | } 9 | 10 | const Editor: React.FC = (props) => { 11 | const { onChange, initialDoc } = props 12 | const handleChange = useCallback( 13 | state => onChange(state.doc.toString()), 14 | [onChange] 15 | ) 16 | const [refContainer, editorView] = useCodeMirror({ 17 | initialDoc: initialDoc, 18 | onChange: handleChange 19 | }) 20 | 21 | useEffect(() => { 22 | if (editorView) { 23 | // Do nothing for now 24 | } 25 | }, [editorView]) 26 | 27 | return
28 | } 29 | 30 | export default Editor 31 | -------------------------------------------------------------------------------- /packages/renderer/src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: transparent; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Ubuntu", "Canterell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 15 | } 16 | 17 | #root { 18 | height: 100%; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /packages/renderer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './shim' 2 | import * as React from 'react' 3 | import * as ReactDOM from 'react-dom' 4 | import './index.css' 5 | import App from './app' 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') 10 | ) 11 | -------------------------------------------------------------------------------- /packages/renderer/src/preview.css: -------------------------------------------------------------------------------- 1 | .preview.markdown-body { 2 | flex: 0 0 50%; 3 | padding: 12px; 4 | box-sizing: border-box; 5 | overflow: auto; 6 | color: #abb2bf; 7 | } 8 | 9 | .preview.markdown-body pre { 10 | background-color: rgba(27, 31, 35, 0.45); 11 | } 12 | -------------------------------------------------------------------------------- /packages/renderer/src/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { unified } from 'unified' 3 | import remarkParse from 'remark-parse' 4 | import remarkGfm from 'remark-gfm' 5 | import remarkReact from 'remark-react' 6 | import RemarkCode from './remark-code' 7 | import { defaultSchema } from 'hast-util-sanitize' 8 | import './preview.css' 9 | import 'github-markdown-css/github-markdown.css' 10 | 11 | interface Props { 12 | doc: string 13 | } 14 | 15 | const schema = { 16 | ...defaultSchema, 17 | attributes: { 18 | ...defaultSchema.attributes, 19 | code: [...(defaultSchema.attributes?.code || []), 'className'] 20 | } 21 | } 22 | 23 | const Preview: React.FC = (props) => { 24 | const md = unified() 25 | .use(remarkParse) 26 | .use(remarkGfm) 27 | .use(remarkReact, { 28 | createElement: React.createElement, 29 | sanitize: schema, 30 | remarkReactComponents: { 31 | code: RemarkCode 32 | } 33 | }) 34 | .processSync(props.doc).result 35 | return
{md}
36 | } 37 | 38 | export default Preview 39 | -------------------------------------------------------------------------------- /packages/renderer/src/remark-code.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import runmode, { getLanguage } from './runmode' 3 | 4 | type Tokens = { 5 | text: string, 6 | style: string | null 7 | }[] 8 | 9 | const RemarkCode: React.FC< 10 | React.DetailedHTMLProps, HTMLElement> 11 | > = props => { 12 | const [spans, setSpans] = useState([]) 13 | const { className } = props 14 | const langName = (className || '').substr(9) 15 | 16 | useEffect(() => { 17 | getLanguage(langName).then(language => { 18 | if (language) { 19 | const body = props.children instanceof Array ? props.children[0] : null 20 | const tokens: Tokens = [] 21 | runmode( 22 | body as string, 23 | language, 24 | (text: string, style: string | null, _from: number, _to: number) => { 25 | tokens.push({ text, style }) 26 | } 27 | ) 28 | setSpans(tokens) 29 | } 30 | }) 31 | }, [props.children]) 32 | 33 | if (spans.length > 0) { 34 | return ( 35 | 36 | {spans.map((span, i) => ( 37 | 38 | {span.text} 39 | 40 | ))} 41 | 42 | ) 43 | } else { 44 | 45 | return {props.children} 46 | } 47 | } 48 | 49 | export default RemarkCode 50 | -------------------------------------------------------------------------------- /packages/renderer/src/runmode.ts: -------------------------------------------------------------------------------- 1 | import { highlightTree } from '@codemirror/highlight' 2 | import { languages } from '@codemirror/language-data' 3 | import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark' 4 | import type { Language, LanguageDescription } from '@codemirror/language' 5 | 6 | type RunModeCallback = ( 7 | text: string, 8 | style: string | null, 9 | from: number, 10 | to: number 11 | ) => void 12 | 13 | function runmode( 14 | textContent: string, 15 | language: Language, 16 | callback: RunModeCallback 17 | ): void { 18 | const tree = language.parser.parse(textContent) 19 | let pos = 0 20 | highlightTree(tree, oneDarkHighlightStyle.match, (from, to, classes) => { 21 | if (from > pos) { 22 | callback(textContent.slice(pos, from), null, pos, from) 23 | } 24 | callback(textContent.slice(from, to), classes, from, to) 25 | pos = to 26 | }) 27 | if (pos !== tree.length) { 28 | callback(textContent.slice(pos, tree.length), null, pos, tree.length) 29 | } 30 | } 31 | 32 | export function findLanguage(langName: string): LanguageDescription | null { 33 | const i = languages.findIndex((lang: LanguageDescription) => { 34 | if (lang.alias.indexOf(langName) >= 0) { 35 | return true 36 | } 37 | }) 38 | if (i >= 0) { 39 | return languages[i] 40 | } else { 41 | return null 42 | } 43 | } 44 | 45 | export async function getLanguage(langName: string): Promise { 46 | const desc = findLanguage(langName) 47 | if (desc) { 48 | const langSupport = await desc.load() 49 | return langSupport.language 50 | } else { 51 | return null 52 | } 53 | } 54 | 55 | export default runmode 56 | -------------------------------------------------------------------------------- /packages/renderer/src/shim.js: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | if (typeof global === 'undefined' || typeof global.process === 'undefined') { 4 | /* global window */ 5 | window.global = window 6 | window.process = process 7 | } 8 | -------------------------------------------------------------------------------- /packages/renderer/src/use-codemirror.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react' 2 | import { EditorState } from '@codemirror/state' 3 | import { EditorView, keymap, highlightActiveLine } from '@codemirror/view' 4 | import { defaultKeymap } from '@codemirror/commands' 5 | import { history, historyKeymap } from '@codemirror/history' 6 | import { indentOnInput } from '@codemirror/language' 7 | import { bracketMatching } from '@codemirror/matchbrackets' 8 | import { lineNumbers, highlightActiveLineGutter } from '@codemirror/gutter' 9 | import { defaultHighlightStyle, HighlightStyle, tags } from '@codemirror/highlight' 10 | import { markdown, markdownLanguage } from '@codemirror/lang-markdown' 11 | import { languages } from '@codemirror/language-data' 12 | import { oneDark } from '@codemirror/theme-one-dark' 13 | 14 | export const transparentTheme = EditorView.theme({ 15 | '&': { 16 | backgroundColor: 'transparent !important', 17 | height: '100%' 18 | } 19 | }) 20 | 21 | const syntaxHighlighting = HighlightStyle.define([ 22 | { 23 | tag: tags.heading1, 24 | fontSize: '1.6em', 25 | fontWeight: 'bold' 26 | }, 27 | { 28 | tag: tags.heading2, 29 | fontSize: '1.4em', 30 | fontWeight: 'bold' 31 | }, 32 | { 33 | tag: tags.heading3, 34 | fontSize: '1.2em', 35 | fontWeight: 'bold' 36 | } 37 | ]) 38 | 39 | import type React from 'react' 40 | 41 | interface Props { 42 | initialDoc: string, 43 | onChange?: (state: EditorState) => void 44 | } 45 | 46 | const useCodeMirror = ( 47 | props: Props 48 | ): [React.MutableRefObject, EditorView?] => { 49 | const refContainer = useRef(null) 50 | const [editorView, setEditorView] = useState() 51 | const { onChange } = props 52 | 53 | useEffect(() => { 54 | if (!refContainer.current) return 55 | 56 | const startState = EditorState.create({ 57 | doc: props.initialDoc, 58 | extensions: [ 59 | keymap.of([...defaultKeymap, ...historyKeymap]), 60 | lineNumbers(), 61 | highlightActiveLineGutter(), 62 | history(), 63 | indentOnInput(), 64 | bracketMatching(), 65 | defaultHighlightStyle.fallback, 66 | highlightActiveLine(), 67 | markdown({ 68 | base: markdownLanguage, 69 | codeLanguages: languages, 70 | addKeymap: true 71 | }), 72 | oneDark, 73 | transparentTheme, 74 | syntaxHighlighting, 75 | EditorView.lineWrapping, 76 | EditorView.updateListener.of(update => { 77 | if (update.changes) { 78 | onChange && onChange(update.state) 79 | } 80 | }) 81 | ] 82 | }) 83 | 84 | const view = new EditorView({ 85 | state: startState, 86 | parent: refContainer.current 87 | }) 88 | setEditorView(view) 89 | }, [refContainer]) 90 | 91 | return [refContainer, editorView] 92 | } 93 | 94 | export default useCodeMirror 95 | -------------------------------------------------------------------------------- /packages/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "jsx": "react", 6 | "allowSyntheticDefaultImports": true, 7 | "paths": { 8 | "/@/*": [ 9 | "./src/*" 10 | ] 11 | }, 12 | "lib": ["ESNext", "dom", "dom.iterable"] 13 | }, 14 | 15 | "include": [ 16 | "src/**/*.ts", 17 | "src/**/*.tsx", 18 | "types/**/*.d.ts", 19 | "../../types/**/*.d.ts", 20 | "../preload/types/electron-api.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/renderer/vite.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | import {chrome} from '../../electron-vendors.config.json'; 4 | import {join} from 'path'; 5 | import { builtinModules } from 'module'; 6 | import {defineConfig} from 'vite'; 7 | import {loadAndSetEnv} from '../../scripts/loadAndSetEnv.mjs'; 8 | 9 | 10 | const PACKAGE_ROOT = __dirname; 11 | 12 | /** 13 | * Vite looks for `.env.[mode]` files only in `PACKAGE_ROOT` directory. 14 | * Therefore, you must manually load and set the environment variables from the root directory above 15 | */ 16 | loadAndSetEnv(process.env.MODE, process.cwd()); 17 | 18 | 19 | /** 20 | * @see https://vitejs.dev/config/ 21 | */ 22 | export default defineConfig({ 23 | root: PACKAGE_ROOT, 24 | resolve: { 25 | alias: { 26 | '/@/': join(PACKAGE_ROOT, 'src') + '/', 27 | }, 28 | }, 29 | plugins: [], 30 | base: '', 31 | server: { 32 | fsServe: { 33 | root: join(PACKAGE_ROOT, '../../'), 34 | }, 35 | }, 36 | build: { 37 | sourcemap: true, 38 | target: `chrome${chrome}`, 39 | outDir: 'dist', 40 | assetsDir: '.', 41 | terserOptions: { 42 | ecma: 2020, 43 | compress: { 44 | passes: 2, 45 | }, 46 | safari10: false, 47 | }, 48 | rollupOptions: { 49 | external: [ 50 | ...builtinModules.filter(m => m !== 'process' && m !== 'assert'), 51 | ], 52 | }, 53 | emptyOutDir: true, 54 | }, 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | arrowParens: 'avoid', 3 | singleQuote: true, 4 | bracketSpacing: true, 5 | endOfLine: 'lf', 6 | semi: false, 7 | tabWidth: 2, 8 | trailingComma: 'none' 9 | } 10 | 11 | module.exports = options 12 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | const {build} = require('vite'); 3 | const {dirname} = require('path'); 4 | 5 | /** @type 'production' | 'development' | 'test' */ 6 | const mode = process.env.MODE = process.env.MODE || 'production'; 7 | 8 | const packagesConfigs = [ 9 | 'packages/main/vite.config.js', 10 | 'packages/preload/vite.config.js', 11 | 'packages/renderer/vite.config.js', 12 | ]; 13 | 14 | 15 | /** 16 | * Run `vite build` for config file 17 | */ 18 | const buildByConfig = (configFile) => build({configFile, mode}); 19 | (async () => { 20 | try { 21 | const totalTimeLabel = 'Total bundling time'; 22 | console.time(totalTimeLabel); 23 | 24 | for (const packageConfigPath of packagesConfigs) { 25 | 26 | const consoleGroupName = `${dirname(packageConfigPath)}/`; 27 | console.group(consoleGroupName); 28 | 29 | const timeLabel = 'Bundling time'; 30 | console.time(timeLabel); 31 | 32 | await buildByConfig(packageConfigPath); 33 | 34 | console.timeEnd(timeLabel); 35 | console.groupEnd(); 36 | console.log('\n'); // Just for pretty print 37 | } 38 | console.timeEnd(totalTimeLabel); 39 | } catch (e) { 40 | console.error(e); 41 | process.exit(1); 42 | } 43 | })(); 44 | -------------------------------------------------------------------------------- /scripts/buildEnvTypes.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {resolveConfig} = require('vite'); 4 | const {writeFileSync, mkdirSync, existsSync} = require('fs'); 5 | const {resolve, dirname} = require('path'); 6 | 7 | const MODES = ['production', 'development', 'test']; 8 | 9 | const typeDefinitionFile = resolve(process.cwd(), './types/env.d.ts'); 10 | 11 | /** 12 | * 13 | * @return {string} 14 | */ 15 | function getBaseInterface() { 16 | return 'interface IBaseEnv {[key: string]: string}'; 17 | } 18 | 19 | /** 20 | * 21 | * @param {string} mode 22 | * @return {Promise<{name: string, declaration: string}>} 23 | */ 24 | async function getInterfaceByMode(mode) { 25 | const interfaceName = `${mode}Env`; 26 | const {env: envForMode} = await resolveConfig({mode}, 'build'); 27 | return { 28 | name: interfaceName, 29 | declaration: `interface ${interfaceName} extends IBaseEnv ${JSON.stringify(envForMode)}`, 30 | }; 31 | } 32 | 33 | /** 34 | * @param {string[]} modes 35 | * @param {string} filePath 36 | */ 37 | async function buildMode(modes, filePath) { 38 | 39 | const IBaseEnvDeclaration = getBaseInterface(); 40 | 41 | const interfaces = await Promise.all(modes.map(getInterfaceByMode)); 42 | 43 | const allDeclarations = interfaces.map(i => i.declaration); 44 | const allNames = interfaces.map(i => i.name); 45 | 46 | const ImportMetaEnvDeclaration = `type ImportMetaEnv = Readonly<${allNames.join(' | ')}>`; 47 | 48 | const content = ` 49 | ${IBaseEnvDeclaration} 50 | ${allDeclarations.join('\n')} 51 | ${ImportMetaEnvDeclaration} 52 | `; 53 | 54 | const dir = dirname(filePath); 55 | if (!existsSync(dir)) { 56 | mkdirSync(dir); 57 | } 58 | 59 | 60 | writeFileSync(filePath, content, {encoding: 'utf-8', flag: 'w'}); 61 | } 62 | 63 | buildMode(MODES, typeDefinitionFile) 64 | .catch(err => { 65 | console.error(err); 66 | process.exit(1); 67 | }); 68 | -------------------------------------------------------------------------------- /scripts/loadAndSetEnv.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | const {loadEnv} = require('vite') 3 | 4 | /** 5 | * Load variables from `.env.[mode]` files in cwd 6 | * and set it to `process.env` 7 | * 8 | * @param {string} mode 9 | * @param {string} cwd 10 | * 11 | * @return {void} 12 | */ 13 | export function loadAndSetEnv(mode, cwd) { 14 | const env = loadEnv(mode || 'production', cwd) 15 | for (const envKey in env) { 16 | if (process.env[envKey] === undefined && env.hasOwnProperty(envKey)) { 17 | process.env[envKey] = env[envKey] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/update-electron-vendors.js: -------------------------------------------------------------------------------- 1 | const {writeFile, readFile} = require('fs/promises'); 2 | const {execSync} = require('child_process'); 3 | const electron = require('electron'); 4 | const path = require('path'); 5 | 6 | /** 7 | * Returns versions of electron vendors 8 | * The performance of this feature is very poor and can be improved 9 | * @see https://github.com/electron/electron/issues/28006 10 | * 11 | * @returns {NodeJS.ProcessVersions} 12 | */ 13 | function getVendors() { 14 | const output = execSync(`${electron} -p "JSON.stringify(process.versions)"`, { 15 | env: {'ELECTRON_RUN_AS_NODE': '1'}, 16 | encoding: 'utf-8', 17 | }); 18 | 19 | return JSON.parse(output); 20 | } 21 | 22 | 23 | function formattedJSON(obj) { 24 | return JSON.stringify(obj, null, 2) + '\n'; 25 | } 26 | 27 | function updateVendors() { 28 | const electronRelease = getVendors(); 29 | 30 | const nodeMajorVersion = electronRelease.node.split('.')[0]; 31 | const chromeMajorVersion = electronRelease.v8.split('.')[0] + electronRelease.v8.split('.')[1]; 32 | 33 | const packageJSONPath = path.resolve(process.cwd(), 'package.json'); 34 | 35 | return Promise.all([ 36 | writeFile('./electron-vendors.config.json', 37 | formattedJSON({ 38 | chrome: chromeMajorVersion, 39 | node: nodeMajorVersion, 40 | }), 41 | ), 42 | 43 | readFile(packageJSONPath).then(JSON.parse).then((packageJSON) => { 44 | if (!packageJSON || !Array.isArray(packageJSON.browserslist)) { 45 | throw new Error(`Can't find browserslist in ${packageJSONPath}`); 46 | } 47 | 48 | packageJSON.browserslist = [`Chrome ${chromeMajorVersion}`]; 49 | 50 | return writeFile(packageJSONPath, formattedJSON(packageJSON)); 51 | }), 52 | ]); 53 | } 54 | 55 | updateVendors().catch(err => { 56 | console.error(err); 57 | process.exit(1); 58 | }); 59 | -------------------------------------------------------------------------------- /scripts/watch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const {createServer, build, createLogger} = require('vite'); 4 | const electronPath = require('electron'); 5 | const {spawn} = require('child_process'); 6 | 7 | 8 | /** @type 'production' | 'development' | 'test' */ 9 | const mode = process.env.MODE = process.env.MODE || 'development'; 10 | 11 | 12 | /** @type {import('vite').LogLevel} */ 13 | const LOG_LEVEL = 'warn'; 14 | 15 | 16 | /** @type {import('vite').InlineConfig} */ 17 | const sharedConfig = { 18 | mode, 19 | build: { 20 | watch: {}, 21 | }, 22 | logLevel: LOG_LEVEL, 23 | }; 24 | 25 | 26 | /** 27 | * @param configFile 28 | * @param writeBundle 29 | * @param name 30 | * @returns {Promise | import('vite').RollupWatcher>} 31 | */ 32 | const getWatcher = ({name, configFile, writeBundle}) => { 33 | return build({ 34 | ...sharedConfig, 35 | configFile, 36 | plugins: [{name, writeBundle}], 37 | }); 38 | }; 39 | 40 | 41 | /** 42 | * Start or restart App when source files are changed 43 | * @param {import('vite').ViteDevServer} viteDevServer 44 | * @returns {Promise | import('vite').RollupWatcher>} 45 | */ 46 | const setupMainPackageWatcher = (viteDevServer) => { 47 | // Write a value to an environment variable to pass it to the main process. 48 | { 49 | const protocol = `http${viteDevServer.config.server.https ? 's' : ''}:`; 50 | const host = viteDevServer.config.server.host || 'localhost'; 51 | const port = viteDevServer.config.server.port; // Vite searches for and occupies the first free port: 3000, 3001, 3002 and so on 52 | const path = '/'; 53 | process.env.VITE_DEV_SERVER_URL = `${protocol}//${host}:${port}${path}`; 54 | } 55 | 56 | const logger = createLogger(LOG_LEVEL, { 57 | prefix: '[main]', 58 | }); 59 | 60 | /** @type {ChildProcessWithoutNullStreams | null} */ 61 | let spawnProcess = null; 62 | 63 | return getWatcher({ 64 | name: 'reload-app-on-main-package-change', 65 | configFile: 'packages/main/vite.config.js', 66 | writeBundle() { 67 | if (spawnProcess !== null) { 68 | spawnProcess.kill('SIGINT'); 69 | spawnProcess = null; 70 | } 71 | 72 | spawnProcess = spawn(String(electronPath), ['.']); 73 | 74 | spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: true})); 75 | spawnProcess.stderr.on('data', d => d.toString().trim() && logger.error(d.toString(), {timestamp: true})); 76 | }, 77 | }); 78 | }; 79 | 80 | 81 | /** 82 | * Start or restart App when source files are changed 83 | * @param {import('vite').ViteDevServer} viteDevServer 84 | * @returns {Promise | import('vite').RollupWatcher>} 85 | */ 86 | const setupPreloadPackageWatcher = (viteDevServer) => { 87 | return getWatcher({ 88 | name: 'reload-page-on-preload-package-change', 89 | configFile: 'packages/preload/vite.config.js', 90 | writeBundle() { 91 | viteDevServer.ws.send({ 92 | type: 'full-reload', 93 | }); 94 | }, 95 | }); 96 | }; 97 | 98 | (async () => { 99 | try { 100 | const viteDevServer = await createServer({ 101 | ...sharedConfig, 102 | configFile: 'packages/renderer/vite.config.js', 103 | }); 104 | 105 | await viteDevServer.listen(); 106 | 107 | await setupPreloadPackageWatcher(viteDevServer); 108 | await setupMainPackageWatcher(viteDevServer); 109 | } catch (e) { 110 | console.error(e); 111 | process.exit(1); 112 | } 113 | })(); 114 | -------------------------------------------------------------------------------- /tests/app.spec.js: -------------------------------------------------------------------------------- 1 | const { Application } = require('spectron') 2 | const { strict: assert } = require('assert') 3 | 4 | const app = new Application({ 5 | path: require('electron'), 6 | requireName: 'electronRequire', 7 | args: ['.'] 8 | }) 9 | 10 | app 11 | .start() 12 | .then(async () => { 13 | const isVisible = await app.browserWindow.isVisible() 14 | assert.ok(isVisible, 'Main window not visible') 15 | }) 16 | 17 | .then(async () => { 18 | const isDevtoolsOpen = await app.webContents.isDevToolsOpened() 19 | assert.ok(!isDevtoolsOpen, 'DevTools opened') 20 | }) 21 | 22 | .then(async function() { 23 | // Get the window content 24 | const content = await app.client.$('#root') 25 | assert.notStrictEqual( 26 | await content.getHTML(), 27 | '
', 28 | 'Window content is empty' 29 | ) 30 | }) 31 | 32 | .then(function() { 33 | if (app && app.isRunning()) { 34 | return app.stop() 35 | } 36 | }) 37 | 38 | .then(() => process.exit(0)) 39 | 40 | .catch(function(error) { 41 | console.error(error) 42 | if (app && app.isRunning()) { 43 | app.stop() 44 | } 45 | process.exit(1) 46 | }) 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": true, 6 | "moduleResolution": "Node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "isolatedModules": true, 10 | "types": [ 11 | "vite/client", 12 | "node" 13 | ], 14 | "typeRoots": [ 15 | "node_modules/@types", 16 | ], 17 | "lib": ["ESNext"] 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /types/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftzdog/electron-markdown-editor-tutorial/289bed4f105ef58471d9397ec37f235123d3832a/types/.gitkeep -------------------------------------------------------------------------------- /vetur.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('vls').VeturConfig} */ 2 | module.exports = { 3 | settings: { 4 | 'vetur.useWorkspaceDependencies': true, 5 | 'vetur.experimental.templateInterpolationService': true, 6 | }, 7 | projects: [ 8 | { 9 | root: './packages/renderer', 10 | tsconfig: './tsconfig.json', 11 | snippetFolder: './.vscode/vetur/snippets', 12 | globalComponents: [ 13 | './src/components/**/*.vue', 14 | ], 15 | }, 16 | { 17 | root: './packages/main', 18 | tsconfig: './tsconfig.json', 19 | }, 20 | { 21 | root: './packages/preload', 22 | tsconfig: './tsconfig.json', 23 | }, 24 | ], 25 | }; 26 | --------------------------------------------------------------------------------