gemercheung 3 years ago
commit
1ebf4e9d39
70 changed files with 8261 additions and 0 deletions
  1. 18 0
      .editorconfig
  2. 23 0
      .electron-builder.config.js
  3. 53 0
      .eslintrc.json
  4. 3 0
      .gitattributes
  5. 4 0
      .github/FUNDING.yml
  6. 27 0
      .github/ISSUE_TEMPLATE/bug_report.md
  7. 5 0
      .github/ISSUE_TEMPLATE/config.yml
  8. 20 0
      .github/ISSUE_TEMPLATE/feature_request.md
  9. 23 0
      .github/actions/release-notes/action.yml
  10. 346 0
      .github/actions/release-notes/main.js
  11. 40 0
      .github/renovate.json
  12. 29 0
      .github/workflows/ci.yml
  13. 23 0
      .github/workflows/lint.yml
  14. 127 0
      .github/workflows/release.yml
  15. 44 0
      .github/workflows/tests.yml
  16. 24 0
      .github/workflows/typechecking.yml
  17. 58 0
      .gitignore
  18. 16 0
      .idea/codeStyles/Project.xml
  19. 5 0
      .idea/codeStyles/codeStyleConfig.xml
  20. 28 0
      .idea/deployment.xml
  21. 6 0
      .idea/inspectionProfiles/Project_Default.xml
  22. 6 0
      .idea/jsLibraryMappings.xml
  23. 7 0
      .idea/jsLinters/eslint.xml
  24. 8 0
      .idea/modules.xml
  25. 6 0
      .idea/vcs.xml
  26. 17 0
      .idea/vite-electron-builder.iml
  27. 14 0
      .idea/webResources.xml
  28. 21 0
      .nano-staged.mjs
  29. 3 0
      .simple-git-hooks.json
  30. 21 0
      LICENSE
  31. 233 0
      README.md
  32. 0 0
      buildResources/.gitkeep
  33. BIN
      buildResources/icon.icns
  34. BIN
      buildResources/icon.png
  35. 27 0
      contributing.md
  36. 68 0
      package.json
  37. 68 0
      packages/main/src/index.ts
  38. 85 0
      packages/main/src/mainWindow.ts
  39. 81 0
      packages/main/src/muti-client.mjs
  40. 127 0
      packages/main/src/security-restrictions.ts
  41. 69 0
      packages/main/tests/unit.spec.ts
  42. 28 0
      packages/main/tsconfig.json
  43. 41 0
      packages/main/vite.config.js
  44. 16 0
      packages/preload/src/index.ts
  45. 7 0
      packages/preload/src/nodeCrypto.ts
  46. 1 0
      packages/preload/src/versions.ts
  47. 20 0
      packages/preload/tests/unit.spec.ts
  48. 23 0
      packages/preload/tsconfig.json
  49. 38 0
      packages/preload/vite.config.js
  50. 15 0
      packages/renderer/.eslintrc.json
  51. 15 0
      packages/renderer/assets/logo.svg
  52. 13 0
      packages/renderer/index.html
  53. 78 0
      packages/renderer/src/App.vue
  54. 153 0
      packages/renderer/src/app.css
  55. 21 0
      packages/renderer/src/components/ElectronVersions.vue
  56. 13 0
      packages/renderer/src/components/ReactiveCounter.vue
  57. 31 0
      packages/renderer/src/components/ReactiveHash.vue
  58. 5 0
      packages/renderer/src/index.ts
  59. 21 0
      packages/renderer/tests/ElectronVersions.spec.ts
  60. 14 0
      packages/renderer/tests/ReactiveCounter.spec.ts
  61. 23 0
      packages/renderer/tests/ReactiveHash.spec.ts
  62. 36 0
      packages/renderer/tsconfig.json
  63. 6 0
      packages/renderer/types/shims-vue.d.ts
  64. 50 0
      packages/renderer/vite.config.js
  65. 15 0
      scripts/update-electron-vendors.js
  66. 149 0
      scripts/watch.js
  67. 77 0
      tests/e2e.spec.ts
  68. 23 0
      types/env.d.ts
  69. 23 0
      vitest.config.js
  70. 5523 0
      yarn.lock

+ 18 - 0
.editorconfig

@@ -0,0 +1,18 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# https://github.com/jokeyrhyme/standard-editorconfig
+
+# top-most EditorConfig file
+root = true
+
+# defaults
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_size = 2
+indent_style = space
+
+[*.md]
+trim_trailing_whitespace = false

+ 23 - 0
.electron-builder.config.js

@@ -0,0 +1,23 @@
+if (process.env.VITE_APP_VERSION === undefined) {
+  const now = new Date;
+  process.env.VITE_APP_VERSION = `${now.getUTCFullYear() - 2000}.${now.getUTCMonth() + 1}.${now.getUTCDate()}-${now.getUTCHours() * 60 + now.getUTCMinutes()}`;
+}
+
+/**
+ * @type {import('electron-builder').Configuration}
+ * @see https://www.electron.build/configuration/configuration
+ */
+const config = {
+  directories: {
+    output: 'dist',
+    buildResources: 'buildResources',
+  },
+  files: [
+    'packages/**/dist/**',
+  ],
+  extraMetadata: {
+    version: process.env.VITE_APP_VERSION,
+  },
+};
+
+module.exports = config;

+ 53 - 0
.eslintrc.json

@@ -0,0 +1,53 @@
+{
+  "root": true,
+  "env": {
+    "es2021": true,
+    "node": true,
+    "browser": false
+  },
+  "extends": [
+    "eslint:recommended",
+    /** @see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#recommended-configs */
+    "plugin:@typescript-eslint/recommended"
+  ],
+  "parser": "@typescript-eslint/parser",
+  "parserOptions": {
+    "ecmaVersion": 12,
+    "sourceType": "module"
+  },
+  "plugins": [
+    "@typescript-eslint"
+  ],
+  "ignorePatterns": [
+    "node_modules/**",
+    "**/dist/**"
+  ],
+  "rules": {
+    "@typescript-eslint/no-unused-vars": "error",
+    "@typescript-eslint/no-var-requires": "off",
+    "@typescript-eslint/consistent-type-imports": "error",
+
+  /**
+   * Having a semicolon helps the optimizer interpret your code correctly.
+   * This avoids rare errors in optimized code.
+   * @see https://twitter.com/alex_kozack/status/1364210394328408066
+   */
+    "semi": [
+      "error",
+      "always"
+    ],
+    /**
+     * This will make the history of changes in the hit a little cleaner
+     */
+    "comma-dangle": [
+      "warn",
+      "always-multiline"
+    ],
+    /**
+     * Just for beauty
+     */
+    "quotes": [
+      "warn", "single"
+    ]
+  }
+}

+ 3 - 0
.gitattributes

@@ -0,0 +1,3 @@
+.github/actions/**/*.js linguist-detectable=false
+scripts/*.js linguist-detectable=false
+*.config.js linguist-detectable=false

+ 4 - 0
.github/FUNDING.yml

@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+patreon: Kozack
+open_collective: vite-electron-builder

+ 27 - 0
.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,27 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: cawa-93
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Additional context**
+Add any other context about the problem here.

+ 5 - 0
.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: Questions & Discussions
+    url: https://github.com/cawa-93/vite-electron-builder/discussions/categories/q-a
+    about: Use GitHub discussions for message-board style questions and discussions.

+ 20 - 0
.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: cawa-93
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.

+ 23 - 0
.github/actions/release-notes/action.yml

@@ -0,0 +1,23 @@
+name: 'Release Notes'
+description: 'Return release notes based on Git Commits'
+inputs:
+  from:
+    description: 'Commit from which start log'
+    required: true
+  to:
+    description: 'Commit to which end log'
+    required: true
+  include-commit-body:
+    description: 'Should the commit body be in notes'
+    required: false
+    default: 'false'
+  include-abbreviated-commit:
+    description: 'Should the commit sha be in notes'
+    required: false
+    default: 'true'
+outputs:
+  release-note: # id of output
+    description: 'Release notes'
+runs:
+  using: 'node12'
+  main: 'main.js'

+ 346 - 0
.github/actions/release-notes/main.js

@@ -0,0 +1,346 @@
+// TODO: Refactor this action
+
+const {execSync} = require('child_process');
+
+/**
+ * Gets the value of an input.  The value is also trimmed.
+ *
+ * @param     name     name of the input to get
+ * @param     options  optional. See InputOptions.
+ * @returns   string
+ */
+function getInput(name, options) {
+  const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
+  if (options && options.required && !val) {
+    throw new Error(`Input required and not supplied: ${name}`);
+  }
+
+  return val.trim();
+}
+
+const START_FROM = getInput('from');
+const END_TO = getInput('to');
+const INCLUDE_COMMIT_BODY = getInput('include-commit-body') === 'true';
+const INCLUDE_ABBREVIATED_COMMIT = getInput('include-abbreviated-commit') === 'true';
+
+/**
+ * @typedef {Object} ICommit
+ * @property {string | undefined} abbreviated_commit
+ * @property {string | undefined} subject
+ * @property {string | undefined} body
+ */
+
+/**
+ * @typedef {ICommit & {type: string | undefined, scope: string | undefined}} ICommitExtended
+ */
+
+
+/**
+ * Any unique string that is guaranteed not to be used in committee text.
+ * Used to split data in the commit line
+ * @type {string}
+ */
+const commitInnerSeparator = '~~~~';
+
+
+/**
+ * Any unique string that is guaranteed not to be used in committee text.
+ * Used to split each commit line
+ * @type {string}
+ */
+const commitOuterSeparator = '₴₴₴₴';
+
+
+/**
+ * Commit data to be obtained.
+ * @type {Map<string, string>}
+ *
+ * @see https://git-scm.com/docs/git-log#Documentation/git-log.txt-emnem
+ */
+const commitDataMap = new Map([
+  ['subject', '%s'], // Required
+]);
+
+if (INCLUDE_COMMIT_BODY) {
+  commitDataMap.set('body', '%b');
+}
+
+if (INCLUDE_ABBREVIATED_COMMIT) {
+  commitDataMap.set('abbreviated_commit', '%h');
+}
+
+/**
+ * The type used to group commits that do not comply with the convention
+ * @type {string}
+ */
+const fallbackType = 'other';
+
+
+/**
+ * List of all desired commit groups and in what order to display them.
+ * @type {string[]}
+ */
+const supportedTypes = [
+  'feat',
+  'fix',
+  'perf',
+  'refactor',
+  'style',
+  'docs',
+  'test',
+  'build',
+  'ci',
+  'chore',
+  'revert',
+  'deps',
+  fallbackType,
+];
+
+/**
+ * @param {string} commitString
+ * @returns {ICommit}
+ */
+function parseCommit(commitString) {
+  /** @type {ICommit} */
+  const commitDataObj = {};
+  const commitDataArray =
+    commitString
+      .split(commitInnerSeparator)
+      .map(s => s.trim());
+
+  for (const [key] of commitDataMap) {
+    commitDataObj[key] = commitDataArray.shift();
+  }
+
+  return commitDataObj;
+}
+
+/**
+ * Returns an array of commits since the last git tag
+ * @return {ICommit[]}
+ */
+function getCommits() {
+
+  const format = Array.from(commitDataMap.values()).join(commitInnerSeparator) + commitOuterSeparator;
+
+  const logs = String(execSync(`git --no-pager log ${START_FROM}..${END_TO} --pretty=format:"${format}" --reverse`));
+
+  return logs
+    .trim()
+    .split(commitOuterSeparator)
+    .filter(r => !!r.trim()) // Skip empty lines
+    .map(parseCommit);
+}
+
+
+/**
+ *
+ * @param {ICommit} commit
+ * @return {ICommitExtended}
+ */
+function setCommitTypeAndScope(commit) {
+
+  const matchRE = new RegExp(`^(?:(${supportedTypes.join('|')})(?:\\((\\S+)\\))?:)?(.*)`, 'i');
+
+  let [, type, scope, clearSubject] = commit.subject.match(matchRE);
+
+  /**
+   * Additional rules for checking committees that do not comply with the convention, but for which it is possible to determine the type.
+   */
+  // Commits like `revert something`
+  if (type === undefined && commit.subject.startsWith('revert')) {
+    type = 'revert';
+  }
+
+  return {
+    ...commit,
+    type: (type || fallbackType).toLowerCase().trim(),
+    scope: (scope || '').toLowerCase().trim(),
+    subject: (clearSubject || commit.subject).trim(),
+  };
+}
+
+class CommitGroup {
+  constructor() {
+    this.scopes = new Map;
+    this.commits = [];
+  }
+
+  /**
+   *
+   * @param {ICommitExtended[]} array
+   * @param {ICommitExtended} commit
+   */
+  static _pushOrMerge(array, commit) {
+    const similarCommit = array.find(c => c.subject === commit.subject);
+    if (similarCommit) {
+      if (commit.abbreviated_commit !== undefined) {
+        similarCommit.abbreviated_commit += `, ${commit.abbreviated_commit}`;
+      }
+    } else {
+      array.push(commit);
+    }
+  }
+
+  /**
+   * @param {ICommitExtended} commit
+   */
+  push(commit) {
+    if (!commit.scope) {
+      CommitGroup._pushOrMerge(this.commits, commit);
+      return;
+    }
+
+    const scope = this.scopes.get(commit.scope) || {commits: []};
+    CommitGroup._pushOrMerge(scope.commits, commit);
+    this.scopes.set(commit.scope, scope);
+  }
+
+  get isEmpty() {
+    return this.commits.length === 0 && this.scopes.size === 0;
+  }
+}
+
+
+/**
+ * Groups all commits by type and scopes
+ * @param {ICommit[]} commits
+ * @returns {Map<string, CommitGroup>}
+ */
+function getGroupedCommits(commits) {
+  const parsedCommits = commits.map(setCommitTypeAndScope);
+
+  const types = new Map(
+    supportedTypes.map(id => ([id, new CommitGroup()])),
+  );
+
+  for (const parsedCommit of parsedCommits) {
+    const typeId = parsedCommit.type;
+    const type = types.get(typeId);
+    type.push(parsedCommit);
+  }
+
+  return types;
+}
+
+/**
+ * Return markdown list with commits
+ * @param {ICommitExtended[]} commits
+ * @param {string} pad
+ * @returns {string}
+ */
+function getCommitsList(commits, pad = '') {
+  let changelog = '';
+  for (const commit of commits) {
+    changelog += `${pad}- ${commit.subject}.`;
+
+    if (commit.abbreviated_commit !== undefined) {
+      changelog += ` (${commit.abbreviated_commit})`;
+    }
+
+    changelog += '\r\n';
+
+    if (commit.body === undefined) {
+      continue;
+    }
+
+    const body = commit.body.replace('[skip ci]', '').trim();
+    if (body !== '') {
+      changelog += `${
+        body
+          .split(/\r*\n+/)
+          .filter(s => !!s.trim())
+          .map(s => `${pad}  ${s}`)
+          .join('\r\n')
+      }${'\r\n'}`;
+    }
+  }
+
+  return changelog;
+}
+
+
+function replaceHeader(str) {
+  switch (str) {
+    case 'feat':
+      return 'New Features';
+    case 'fix':
+      return 'Bug Fixes';
+    case 'docs':
+      return 'Documentation Changes';
+    case 'build':
+      return 'Build System';
+    case 'chore':
+      return 'Chores';
+    case 'ci':
+      return 'Continuous Integration';
+    case 'refactor':
+      return 'Refactors';
+    case 'style':
+      return 'Code Style Changes';
+    case 'test':
+      return 'Tests';
+    case 'perf':
+      return 'Performance improvements';
+    case 'revert':
+      return 'Reverts';
+    case 'deps':
+      return 'Dependency updates';
+    case 'other':
+      return 'Other Changes';
+    default:
+      return str;
+  }
+}
+
+
+/**
+ * Return markdown string with changelog
+ * @param {Map<string, CommitGroup>} groups
+ */
+function getChangeLog(groups) {
+
+  let changelog = '';
+
+  for (const [typeId, group] of groups) {
+    if (group.isEmpty) {
+      continue;
+    }
+
+    changelog += `### ${replaceHeader(typeId)}${'\r\n'}`;
+
+    for (const [scopeId, scope] of group.scopes) {
+      if (scope.commits.length) {
+        changelog += `- #### ${replaceHeader(scopeId)}${'\r\n'}`;
+        changelog += getCommitsList(scope.commits, '  ');
+      }
+    }
+
+    if (group.commits.length) {
+      changelog += getCommitsList(group.commits);
+    }
+
+    changelog += ('\r\n' + '\r\n');
+  }
+
+  return changelog.trim();
+}
+
+
+function escapeData(s) {
+  return String(s)
+    .replace(/%/g, '%25')
+    .replace(/\r/g, '%0D')
+    .replace(/\n/g, '%0A');
+}
+
+try {
+  const commits = getCommits();
+  const grouped = getGroupedCommits(commits);
+  const changelog = getChangeLog(grouped);
+  process.stdout.write('::set-output name=release-note::' + escapeData(changelog) + '\r\n');
+// require('fs').writeFileSync('../CHANGELOG.md', changelog, {encoding: 'utf-8'})
+} catch (e) {
+  console.error(e);
+  process.exit(1);
+}

+ 40 - 0
.github/renovate.json

@@ -0,0 +1,40 @@
+{
+  "extends": [
+    "config:base",
+    "group:allNonMajor",
+    ":semanticCommits",
+    ":automergeTypes",
+    ":disableDependencyDashboard",
+    ":pinVersions",
+    ":semanticCommitType(deps)",
+    ":onlyNpm",
+    ":label(dependencies)",
+    "schedule:weekly"
+  ],
+  "baseBranches": [
+    "main"
+  ],
+  "bumpVersion": "patch",
+
+  "packageRules": [
+    {
+      "packageNames": [
+        "node",
+        "npm"
+      ],
+      "enabled": false
+    },
+    {
+      "depTypeList": [
+        "devDependencies"
+      ],
+      "semanticCommitType": "build"
+    },
+    {
+      "matchPackageNames": [
+        "electron"
+      ],
+      "separateMajorMinor": false
+    }
+  ]
+}

+ 29 - 0
.github/workflows/ci.yml

@@ -0,0 +1,29 @@
+# This workflow is the entry point for all CI processes.
+# It is from here that all other workflows are launched.
+on:
+  push:
+    branches:
+      - main
+      - master
+    paths-ignore:
+      - '**.md'
+      - .editorconfig
+      - .gitignore
+  pull_request:
+    paths-ignore:
+      - '**.md'
+      - .editorconfig
+      - .gitignore
+
+jobs:
+  lint:
+    uses: ./.github/workflows/lint.yml
+  typechecking:
+    uses: ./.github/workflows/typechecking.yml
+  tests:
+    uses: ./.github/workflows/tests.yml
+  draft_release:
+    if: github.event_name == 'push' && github.ref_name == 'main'
+    needs: [ typechecking, tests ]
+    uses: ./.github/workflows/release.yml
+

+ 23 - 0
.github/workflows/lint.yml

@@ -0,0 +1,23 @@
+name: Linters
+on: [workflow_call]
+
+defaults:
+  run:
+    shell: 'bash'
+
+jobs:
+  eslint:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 16 # Need for npm >=7.7
+          cache: 'npm'
+
+      # TODO: Install not all dependencies, but only those required for this workflow
+      - name: Install dependencies
+        run: npm ci
+
+      - run: npm run lint --if-present

+ 127 - 0
.github/workflows/release.yml

@@ -0,0 +1,127 @@
+name: Release
+on: [workflow_call, workflow_dispatch]
+
+concurrency:
+  group: release-${{ github.ref }}
+  cancel-in-progress: true
+
+
+defaults:
+  run:
+    shell: 'bash'
+
+
+jobs:
+
+  draft:
+    runs-on: ubuntu-latest
+    outputs:
+      release-note: ${{ steps.release-note.outputs.release-note }}
+      version: ${{ steps.version.outputs.build-version }}
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 14
+
+      - name: Get last git tag
+        id: tag
+        run: echo "::set-output name=last-tag::$(git describe --tags --abbrev=0 || git rev-list --max-parents=0 ${{github.ref}})"
+
+      - name: Generate release notes
+        uses: ./.github/actions/release-notes
+        id: release-note
+        with:
+          from: ${{ steps.tag.outputs.last-tag }}
+          to: ${{ github.ref }}
+          include-commit-body: true
+          include-abbreviated-commit: true
+
+      - name: Get version from current date
+        id: version
+        run: echo "::set-output name=build-version::$(node -e "try{console.log(require('./.electron-builder.config.js').extraMetadata.version)}catch(e){console.error(e);process.exit(1)}")"
+
+      - name: Delete outdated drafts
+        uses: hugo19941994/delete-draft-releases@v1.0.0
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Create Release Draft
+        uses: softprops/action-gh-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.github_token }}
+        with:
+          prerelease: true
+          draft: true
+          tag_name: v${{ steps.version.outputs.build-version }}
+          name: v${{ steps.version.outputs.build-version }}
+          body: ${{ steps.release-note.outputs.release-note }}
+
+  upload_artifacts:
+    needs: [ draft ]
+
+    strategy:
+      matrix:
+        os: [ windows-latest ]
+    #    To compile the application for different platforms, use:
+    #    os: [ macos-latest, ubuntu-latest, windows-latest ]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 16 # Need for npm >=7.7
+          cache: 'npm'
+
+      - name: Install dependencies
+        run: npm ci
+
+      # The easiest way to transfer release notes to a compiled application is create `release-notes.md` in the build resources.
+      # See https://github.com/electron-userland/electron-builder/issues/1511#issuecomment-310160119
+      - name: Prepare release notes
+        env:
+          RELEASE_NOTE: ${{ needs.draft.outputs.release-note }}
+        run: echo "$RELEASE_NOTE" >> ./buildResources/release-notes.md
+
+      # Compile app and upload artifacts
+      - name: Compile & release Electron app
+        uses: samuelmeuli/action-electron-builder@v1
+        env:
+          VITE_APP_VERSION: ${{ needs.draft.outputs.version }}
+        with:
+          build_script_name: build
+          args: --config .electron-builder.config.js
+
+          # GitHub token, automatically provided to the action
+          # (No need to define this secret in the repo settings)
+          github_token: ${{ secrets.github_token }}
+
+          # If the commit is tagged with a version (e.g. "v1.0.0"),
+          # release the app after building
+          release: true
+
+          # Sometimes the build may fail due to a connection problem with Apple, GitHub, etc. servers.
+          # This option will restart the build as many attempts as possible
+          max_attempts: 3
+
+
+          # Code Signing params
+
+          # Base64-encoded code signing certificate for Windows
+          # windows_certs: ''
+
+          # Password for decrypting `windows_certs`
+          # windows_certs_password: ''
+
+          # Base64-encoded code signing certificate for macOS
+          # mac_certs: ''
+
+          # Password for decrypting `mac_certs`
+          # mac_certs_password: ''

+ 44 - 0
.github/workflows/tests.yml

@@ -0,0 +1,44 @@
+name: Tests
+on: [workflow_call]
+
+
+defaults:
+  run:
+    shell: 'bash'
+
+jobs:
+
+  unit:
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ windows-latest, ubuntu-latest, macos-latest ]
+        package: [ main, preload, renderer ]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 16
+          cache: 'npm'
+      - run: npm ci
+      - run: npm run test:${{ matrix.package }} --if-present
+
+  e2e:
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ windows-latest, ubuntu-latest, macos-latest ]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 16
+          cache: 'npm'
+      - run: npm ci
+      - run: npx playwright install --with-deps
+      - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e --if-present
+        if: matrix.os == 'ubuntu-latest'
+      - run: npm run test:e2e --if-present
+        if: matrix.os != 'ubuntu-latest'

+ 24 - 0
.github/workflows/typechecking.yml

@@ -0,0 +1,24 @@
+name: Typechecking
+on: [workflow_call]
+
+
+defaults:
+  run:
+    shell: 'bash'
+
+jobs:
+  typescript:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 16 # Need for npm >=7.7
+          cache: 'npm'
+
+      # TODO: Install not all dependencies, but only those required for this workflow
+      - name: Install dependencies
+        run: npm ci
+
+      - run: npm run typecheck --if-present

+ 58 - 0
.gitignore

@@ -0,0 +1,58 @@
+node_modules
+.DS_Store
+dist
+*.local
+thumbs.db
+
+.eslintcache
+.browserslistrc
+.electron-vendors.cache.json
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+ .idea/artifacts
+ .idea/compiler.xml
+ .idea/jarRepositories.xml
+ .idea/modules.xml
+ .idea/*.iml
+ .idea/modules
+ *.iml
+ *.ipr
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# Editor-based Rest Client
+.idea/httpRequests
+/.idea/csv-plugin.xml

+ 16 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,16 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <JSCodeStyleSettings version="0">
+      <option name="FORCE_SEMICOLON_STYLE" value="true" />
+      <option name="USE_DOUBLE_QUOTES" value="false" />
+      <option name="FORCE_QUOTE_STYlE" value="true" />
+      <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
+    </JSCodeStyleSettings>
+    <TypeScriptCodeStyleSettings version="0">
+      <option name="FORCE_SEMICOLON_STYLE" value="true" />
+      <option name="USE_DOUBLE_QUOTES" value="false" />
+      <option name="FORCE_QUOTE_STYlE" value="true" />
+      <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
+    </TypeScriptCodeStyleSettings>
+  </code_scheme>
+</component>

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>

+ 28 - 0
.idea/deployment.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
+    <serverData>
+      <paths name="ihappymama-aliexpress">
+        <serverdata>
+          <mappings>
+            <mapping local="$PROJECT_DIR$" web="/" />
+          </mappings>
+        </serverdata>
+      </paths>
+      <paths name="iosico.com">
+        <serverdata>
+          <mappings>
+            <mapping local="$PROJECT_DIR$" web="/" />
+          </mappings>
+        </serverdata>
+      </paths>
+      <paths name="somespeed.com">
+        <serverdata>
+          <mappings>
+            <mapping local="$PROJECT_DIR$" web="/" />
+          </mappings>
+        </serverdata>
+      </paths>
+    </serverData>
+  </component>
+</project>

+ 6 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+  </profile>
+</component>

+ 6 - 0
.idea/jsLibraryMappings.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="JavaScriptLibraryMappings">
+    <includedPredefinedLibrary name="Node.js Core" />
+  </component>
+</project>

+ 7 - 0
.idea/jsLinters/eslint.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="EslintConfiguration">
+    <work-dir-patterns value="src/**/*.{ts,vue} {bin,config}/**/*.js" />
+    <option name="fix-on-save" value="true" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/vite-electron-builder.iml" filepath="$PROJECT_DIR$/.idea/vite-electron-builder.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 17 - 0
.idea/vite-electron-builder.iml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/packages/main/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/packages/preload/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/packages/renderer/src" isTestSource="false" />
+      <excludeFolder url="file://$MODULE_DIR$/packages/renderer/dist" />
+      <excludeFolder url="file://$MODULE_DIR$/dist" />
+      <excludeFolder url="file://$MODULE_DIR$/packages/main/dist" />
+      <excludeFolder url="file://$MODULE_DIR$/packages/preload/dist" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 14 - 0
.idea/webResources.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="WebResourcesPaths">
+    <contentEntries>
+      <entry url="file://$PROJECT_DIR$">
+        <entryData>
+          <resourceRoots>
+            <path value="file://$PROJECT_DIR$/packages/renderer/assets" />
+          </resourceRoots>
+        </entryData>
+      </entry>
+    </contentEntries>
+  </component>
+</project>

+ 21 - 0
.nano-staged.mjs

@@ -0,0 +1,21 @@
+import {resolve, sep} from 'path';
+
+export default {
+  '*.{js,ts,vue}': 'eslint --cache --fix',
+
+  /**
+   * Run typechecking if any type-sensitive files was staged
+   * @param {string[]} filenames
+   * @return {string[]}
+   */
+  'packages/**/{*.ts,*.vue,tsconfig.json}': ({filenames}) => {
+    const pathToPackages = resolve(process.cwd(), 'packages') + sep;
+    return Array.from(
+      filenames.reduce((set, filename) => {
+        const pack = filename.replace(pathToPackages, '').split(sep)[0];
+        set.add(`npm run typecheck:${pack} --if-present`);
+        return set;
+      }, new Set),
+    );
+  },
+};

+ 3 - 0
.simple-git-hooks.json

@@ -0,0 +1,3 @@
+{
+  "pre-commit": "npx nano-staged"
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Alex Kozack
+
+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.

+ 233 - 0
README.md

@@ -0,0 +1,233 @@
+# Vite Electron Builder Boilerplate
+
+----
+
+[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua)
+----
+
+[![GitHub issues by-label](https://img.shields.io/github/issues/cawa-93/vite-electron-builder/help%20wanted?label=issues%20need%20help&logo=github)](https://github.com/cawa-93/vite-electron-builder/issues?q=label%3A%22help+wanted%22+is%3Aopen+is%3Aissue)
+[![Required Node.JS >= v16.13](https://img.shields.io/static/v1?label=node&message=%3E=16.13&logo=node.js&color)](https://nodejs.org/about/releases/)
+[![Required npm >= v8.1](https://img.shields.io/static/v1?label=npm&message=%3E=8.1&logo=npm&color)](https://github.com/npm/cli/releases)
+
+> Vite+Electron = 🔥
+
+This is template for secure electron applications. Written following the latest safety requirements, recommendations and best practices.
+
+Under the hood is used [Vite] — superfast, nextgen bundler, and [electron-builder] for compilation.
+
+
+## Get started
+
+Follow these steps to get started with this template:
+
+1. Click the **[Use this template](https://github.com/cawa-93/vite-electron-builder/generate)** button (you must be logged in) or just clone this repo.
+2. If you want to use another package manager don't forget to edit [`.github/workflows`](/.github/workflows) -- it uses `npm` by default.
+
+That's all you need. 😉
+
+**Note**: This template uses npm v7 feature — [**Installing Peer Dependencies Automatically**](https://github.com/npm/rfcs/blob/latest/implemented/0025-install-peer-deps.md). If you are using a different package manager, you may need to install some peerDependencies manually.
+
+
+**Note**: Find more useful forks [here](https://github.com/cawa-93/vite-electron-builder/discussions/categories/forks).
+
+
+## Features
+
+### Electron [![Electron version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/electron?label=%20)][electron]
+- This template uses the latest electron version with all the latest security patches.
+- The architecture of the application is built according to the security [guides](https://www.electronjs.org/docs/tutorial/security) and best practices.
+- The latest version of the [electron-builder] is used to compile the application.
+
+
+### Vite [![Vite version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vite?label=%20)][vite]
+- [Vite] is used to bundle all source codes. This is an extremely fast packer that has a bunch of great features. You can learn more about how it is arranged in [this](https://youtu.be/xXrhg26VCSc) video.
+- Vite [supports](https://vitejs.dev/guide/env-and-mode.html) reading `.env` files. You can also specify types of your environment variables in [`types/env.d.ts`](types/env.d.ts).
+- Hot reloads for `Main` and `Renderer` processes.
+
+Vite provides many useful features, such as: `TypeScript`, `TSX/JSX`, `CSS/JSON Importing`, `CSS Modules`, `Web Assembly` and much more.
+
+[See all Vite features](https://vitejs.dev/guide/features.html).
+
+
+### TypeScript [![TypeScript version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/typescript?label=%20)][typescript] (optional)
+- The latest version of TypeScript is used for all the source code.
+- **Vite** supports TypeScript out of the box. However, it does not support type checking.
+- Code formatting rules follow the latest TypeScript recommendations and best practices thanks to [@typescript-eslint/eslint-plugin](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin).
+
+**[See this discussion](https://github.com/cawa-93/vite-electron-builder/discussions/339)** if you want completely remove TypeScript.
+
+
+### Vue [![Vue version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/vue?label=%20&)][vue] (optional)
+- By default, web pages are built using [Vue]. However, you can easily change that. Or not use additional frameworks at all.
+- Code formatting rules follow the latest Vue recommendations and best practices thanks to [eslint-plugin-vue].
+- Installed [Vue.js devtools beta](https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg) with Vue 3 support.
+
+See [examples of web pages for different frameworks](https://github.com/vitejs/vite/tree/main/packages/create-vite).
+
+### Continuous Integration
+- The configured workflow will check the types for each push and PR.
+- The configured workflow will check the code style for each push and PR.
+- **Automatic tests** used [Vitest ![Vitest version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vitest?label=%20&color=yellow)][vitest] -- A blazing fast test framework powered by Vite.
+  - Unit tests are placed within each package and run separately.
+  - End-to-end tests are placed in the root [`tests`](tests) directory and use [playwright].
+
+
+
+### Continuous delivery
+- Each time you push changes to the `main` branch, the [`release`](.github/workflows/release.yml) workflow starts, which creates a release draft.
+  - The version is automatically set based on the current date in the format `yy.mm.dd-minutes`.
+  - Notes are automatically generated and added to the release draft.
+  - Code signing supported. See [`compile` job in the `release` workflow](.github/workflows/release.yml).
+- **Auto-update is supported**. After the release is published, all client applications will download the new version and install updates silently.
+
+## How it works
+The template requires a minimum amount [dependencies](package.json). Only **Vite** is used for building, nothing more.
+
+### Project Structure
+
+The structure of this template is very similar to the structure of a monorepo.
+
+```mermaid
+flowchart TB;
+
+packages/preload <-. IPC Messages .-> packages/main
+
+subgraph packages/main
+M[index.ts] --> EM[Electron Main Process Modules]
+M --> N2[Node.js API]
+end
+
+
+subgraph packages/preload
+P[index.ts] --> N[Node.js API]
+P --> ED[External dependencies]
+P --> ER[Electron Renderer Process Modules]
+end
+
+
+subgraph packages/renderer
+R[index.html] --> W[Web API]
+R --> BD[Bundled dependencies]
+R --> F[Web Frameforks]
+end
+
+packages/renderer -- Call Exposed API --> P
+```
+
+The entire source code of the program is divided into three modules (packages) that are each bundled independently:
+- [`packages/renderer`](packages/renderer). Responsible for the contents of the application window. In fact, it is a regular web application. In developer mode, you can even open it in a browser. The development and build process is the same as for classic web applications. Access to low-level API electrons or Node.js is through the _preload_ layer.
+- [`packages/preload`](packages/preload). Acts as an intermediate link between the _renderer_ layer and the low-level API electrons or Node.js. Runs in an _isolated browser context_, but has direct access to Node.js api. See [Checklist: Security Recommendations](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content).
+- [`packages/main`](packages/main)
+  Electron [**main script**](https://www.electronjs.org/docs/tutorial/quick-start#create-the-main-script-file).
+
+
+### Build web resources
+
+The `main` and `preload` packages are built in [library mode](https://vitejs.dev/guide/build.html#library-mode) as it is simple javascript.
+The `renderer` package builds as a regular web app.
+
+
+### Compile App
+The next step is to package and compile a ready to distribute Electron app for macOS, Windows and Linux with "auto update" support out of the box.
+
+To do this using the [electron-builder]:
+- Using the npm script `compile`: This script is configured to compile the application as quickly as possible. It is not ready for distribution, it is compiled only for the current platform and is used for debugging.
+- Using GitHub Actions: The application is compiled for any platform and ready-to-distribute files are automatically added as a draft to the GitHub releases page.
+
+### Working with dependencies
+Because the `renderer` works and builds like a _regular web application_, you can only use dependencies that support the browser or compile to a browser-friendly state.
+
+This means that in the `renderer` you are free to use dependencies such as Vue, React, lodash, axios and so on. But you can't use, say, systeminformation or  pg because these dependencies need access to a node api to work, which is not available in the `renderer` context.
+
+All dependencies that require node.js api can be used in the [`preload` script](https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts).
+
+Here is an example. Let's say you need to read some data from the file system or database in the renderer.
+
+In preload context create a method that reads and return data. To make the method announced in the preload available in the render, you usually need to call the [`electron.contextBridge.exposeInMainWorld`](https://www.electronjs.org/ru/docs/latest/api/context-bridge). However, this template uses the [unplugin-auto-expose](https://github.com/cawa-93/unplugin-auto-expose) plugin, so you just need to export the method from the preload. The `exposeInMainWorld` will be called automatically.
+```ts
+// preload/index.ts
+import {writeFile} from 'fs'
+
+// Everything you exported from preload/index.ts may be called in renderer
+export function getData() {
+  return /* ... */
+}
+```
+Now you can import and call the method in renderer
+```ts
+// renderer/somewere.component.ts
+import {getData} from '#preload'
+const dataFromFS = getData()
+```
+
+[Read more about Security Considerations](https://www.electronjs.org/docs/tutorial/context-isolation#security-considerations).
+
+### Working with Electron API
+Although the preload has access to Node.js API, it **still runs in the BrowserWindow context**, so a limited electron modules are available in it. Check the [electron docs](https://www.electronjs.org/ru/docs/latest/api/clipboard) for full list of available methods.
+
+All other electron methods can be invoked in the `main`.
+
+As a result, the architecture of interaction between all modules is as follows:
+
+```mermaid
+sequenceDiagram
+renderer->>+preload: Read data from file system
+preload->>-renderer: Data
+renderer->>preload: Maximize window
+activate preload
+preload-->>main: Invoke IPC command
+activate main
+main-->>preload: IPC response
+deactivate main
+preload->>renderer: Window maximized
+deactivate preload
+```
+[Read more aboud Inter-Process Communication](https://www.electronjs.org/docs/latest/tutorial/ipc)
+
+
+### Modes and Environment Variables
+All environment variables set as part of the `import.meta`, so you can access them as follows: `import.meta.env`.
+
+If you are using TypeScript and want to get code completion you must add all the environment variables to the [`ImportMetaEnv` in `types/env.d.ts`](types/env.d.ts).
+
+The mode option is used to specify the value of `import.meta.env.MODE` and the corresponding environment variables files that need to be loaded.
+
+By default, there are two modes:
+- `production` is used by default
+- `development` is used by `npm run watch` script
+
+When running the build script, the environment variables are loaded from the following files in your project root:
+
+```
+.env                # loaded in all cases
+.env.local          # loaded in all cases, ignored by git
+.env.[mode]         # only loaded in specified env mode
+.env.[mode].local   # only loaded in specified env mode, ignored by git
+```
+
+To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code. e.g. the following file:
+
+```
+DB_PASSWORD=foobar
+VITE_SOME_KEY=123
+```
+Only `VITE_SOME_KEY` will be exposed as `import.meta.env.VITE_SOME_KEY` to your client source code, but `DB_PASSWORD` will not.
+
+
+## Contribution
+
+See [Contributing Guide](contributing.md).
+
+
+[vite]: https://github.com/vitejs/vite/
+[electron]: https://github.com/electron/electron
+[electron-builder]: https://github.com/electron-userland/electron-builder
+[vue]: https://github.com/vuejs/vue-next
+[vue-router]: https://github.com/vuejs/vue-router-next/
+[typescript]: https://github.com/microsoft/TypeScript/
+[playwright]: https://playwright.dev
+[vitest]: https://vitest.dev
+[vue-tsc]: https://github.com/johnsoncodehk/vue-tsc
+[eslint-plugin-vue]: https://github.com/vuejs/eslint-plugin-vue
+[cawa-93-github]: https://github.com/cawa-93/
+[cawa-93-sponsor]: https://www.patreon.com/Kozack/

+ 0 - 0
buildResources/.gitkeep


BIN
buildResources/icon.icns


BIN
buildResources/icon.png


+ 27 - 0
contributing.md

@@ -0,0 +1,27 @@
+# Contributing
+
+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.
+
+## Issues
+
+Do not create issues about bumping dependencies unless a bug has been identified, and you can demonstrate that it effects this library.
+
+**Help us to help you**
+
+Remember that we’re here to help, but not to make guesses about what you need help with:
+
+- Whatever bug or issue you're experiencing, assume that it will not be as obvious to the maintainers as it is to you.
+- 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.
+
+_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._
+
+
+## Repo Setup
+The package manager used to install and link dependencies must be npm v7 or later.
+
+1. Clone repo
+1. `npm run watch` start electron app in watch mode.
+1. `npm run compile` build app but for local debugging only.
+1. `npm run lint` lint your code.
+1. `npm run typecheck` Run typescript check.
+1. `npm run test` Run app test.

+ 68 - 0
package.json

@@ -0,0 +1,68 @@
+{
+  "name": "vite-electron-builder",
+  "description": "Secure boilerplate for Electron app based on Vite",
+  "private": true,
+  "engines": {
+    "node": ">=v16.13",
+    "npm": ">=8.1"
+  },
+  "author": {
+    "email": "kozackunisoft@gmail.com",
+    "name": "Alex Kozack",
+    "url": "https://kozack.me"
+  },
+  "main": "packages/main/dist/index.cjs",
+  "scripts": {
+    "build": "npm run build:main && npm run build:preload && npm run build:renderer",
+    "build:main": "cd ./packages/main && vite build",
+    "build:preload": "cd ./packages/preload && vite build",
+    "build:renderer": "cd ./packages/renderer && vite build",
+    "compile": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.js --dir --config.asar=false",
+    "test": "npm run test:main && npm run test:preload && npm run test:renderer && npm run test:e2e",
+    "test:e2e": "vitest run",
+    "test:main": "vitest run -r packages/main --passWithNoTests",
+    "test:preload": "vitest run -r packages/preload --passWithNoTests",
+    "test:renderer": "vitest run -r packages/renderer --passWithNoTests",
+    "watch": "node scripts/watch.js",
+    "lint": "eslint . --ext js,ts,vue --fix",
+    "typecheck:main": "tsc --noEmit -p packages/main/tsconfig.json",
+    "typecheck:preload": "tsc --noEmit -p packages/preload/tsconfig.json",
+    "typecheck:renderer": "vue-tsc --noEmit -p packages/renderer/tsconfig.json",
+    "typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer",
+    "rebuild-node-pty": "electron-rebuild -f -o node-pty",
+    "postinstall": "cross-env ELECTRON_RUN_AS_NODE=1 npx --no-install electron ./scripts/update-electron-vendors.js && yarn run rebuild-node-pty"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "5.30.5",
+    "@typescript-eslint/parser": "^5.30.7",
+    "@vitejs/plugin-vue": "3.0.0-beta.1",
+    "@vue/test-utils": "2.0.2",
+    "autoprefixer": "^10.4.7",
+    "cpy-cli": "^3.1.1",
+    "cross-env": "7.0.3",
+    "electron": "19.0.8",
+    "electron-builder": "23.1.0",
+    "electron-devtools-installer": "3.2.0",
+    "eslint": "8.19.0",
+    "eslint-plugin-vue": "9.2.0",
+    "happy-dom": "6.0.2",
+    "nano-staged": "0.8.0",
+    "playwright": "1.23.2",
+    "postcss": "^8.4.14",
+    "simple-git-hooks": "2.8.0",
+    "typescript": "4.7.4",
+    "unplugin-auto-expose": "0.0.1",
+    "vite": "3.0.0",
+    "vitest": "0.18.0",
+    "vue-tsc": "0.38.4"
+  },
+  "dependencies": {
+    "electron-rebuild": "^3.2.8",
+    "electron-updater": "5.0.5",
+    "node-pty": "^0.10.1",
+    "socket.io-client": "^4.5.1",
+    "socket.io-msgpack-parser": "^3.0.1",
+    "vue": "3.2.37",
+    "xterm": "^4.19.0"
+  }
+}

+ 68 - 0
packages/main/src/index.ts

@@ -0,0 +1,68 @@
+import {app} from 'electron';
+import './security-restrictions';
+import {restoreOrCreateWindow} from '/@/mainWindow';
+
+
+/**
+ * Prevent multiple instances
+ */
+const isSingleInstance = app.requestSingleInstanceLock();
+if (!isSingleInstance) {
+  app.quit();
+  process.exit(0);
+}
+app.on('second-instance', restoreOrCreateWindow);
+
+
+/**
+ * Disable Hardware Acceleration for more power-save
+ */
+app.disableHardwareAcceleration();
+
+/**
+ * Shout down background process if all windows was closed
+ */
+app.on('window-all-closed', () => {
+  if (process.platform !== 'darwin') {
+    app.quit();
+  }
+});
+
+/**
+ * @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate'
+ */
+app.on('activate', restoreOrCreateWindow);
+
+
+/**
+ * Create app window when background process will be ready
+ */
+app.whenReady()
+  .then(restoreOrCreateWindow)
+  .catch((e) => console.error('Failed create window:', e));
+
+
+/**
+ * Install Vue.js or some other devtools in development mode only
+ */
+if (import.meta.env.DEV) {
+  app.whenReady()
+    .then(() => import('electron-devtools-installer'))
+    .then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, {
+      loadExtensionOptions: {
+        allowFileAccess: true,
+      },
+    }))
+    .catch(e => console.error('Failed install extension:', e));
+}
+
+/**
+ * Check new app version in production mode only
+ */
+if (import.meta.env.PROD) {
+  app.whenReady()
+    .then(() => import('electron-updater'))
+    .then(({autoUpdater}) => autoUpdater.checkForUpdatesAndNotify())
+    .catch((e) => console.error('Failed check updates:', e));
+}
+

+ 85 - 0
packages/main/src/mainWindow.ts

@@ -0,0 +1,85 @@
+import { BrowserWindow, ipcMain } from 'electron';
+import { join } from 'path';
+import { URL } from 'url';
+import * as pty from 'node-pty';
+import * as os from 'os';
+
+const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
+
+interface ProcessEnv { [key: string]: string; }
+
+async function createWindow() {
+  const browserWindow = new BrowserWindow({
+    show: false, // Use 'ready-to-show' event to show window
+    webPreferences: {
+      webviewTag: false, // The webview tag is not recommended. Consider alternatives like iframe or Electron's BrowserView. https://www.electronjs.org/docs/latest/api/webview-tag#warning
+      preload: join(__dirname, '../../preload/dist/index.cjs'),
+    },
+  });
+
+  /**
+   * If you install `show: true` then it can cause issues when trying to close the window.
+   * Use `show: false` and listener events `ready-to-show` to fix these issues.
+   *
+   * @see https://github.com/electron/electron/issues/25012
+   */
+  browserWindow.on('ready-to-show', () => {
+    browserWindow?.show();
+
+    if (import.meta.env.DEV) {
+      browserWindow?.webContents.openDevTools();
+    }
+    ipcMain.on('start-Benmark-test', () => {
+      console.log('hey');
+      // console.log('pty', pty);
+      const ptyProcess = pty.spawn(shell, [`node ${join(__dirname, '../../main/src/muti-client.mjs')}`], {
+        name: 'xterm-color',
+        cols: 80,
+        rows: 30,
+        cwd: process.env.HOME,
+        env: process.env as unknown as ProcessEnv,
+      });
+      ptyProcess.on('data', function (data) {
+        // console.log('data', data);
+        browserWindow.webContents.send('terminal.incomingData', data);
+      });
+
+      // ptyProcess.write('ls\r');
+      // ptyProcess.resize(100, 40);
+      // ptyProcess.write('ls\r');
+    });
+  });
+
+  /**
+   * URL for main window.
+   * Vite dev server for development.
+   * `file://../renderer/index.html` for production and test
+   */
+  const pageUrl = import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined
+    ? import.meta.env.VITE_DEV_SERVER_URL
+    : new URL('../renderer/dist/index.html', 'file://' + __dirname).toString();
+
+
+  await browserWindow.loadURL(pageUrl);
+
+
+
+  return browserWindow;
+}
+
+/**
+ * Restore existing BrowserWindow or Create new BrowserWindow
+ */
+export async function restoreOrCreateWindow() {
+  let window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed());
+
+  if (window === undefined) {
+    window = await createWindow();
+  }
+
+  if (window.isMinimized()) {
+    window.restore();
+  }
+
+  window.focus();
+}

+ 81 - 0
packages/main/src/muti-client.mjs

@@ -0,0 +1,81 @@
+import { io } from "socket.io-client";
+import customParser from "socket.io-msgpack-parser";
+const URL = process.env.URL || "wss://test-socket.4dkankan.com";
+// http://zhang9394@zhangyupeng:face3d.4dage.com:7005/zhangyupeng/chatIM.git
+const MAX_CLIENTS = 500;
+const POLLING_PERCENTAGE = 0.05;
+const CLIENT_CREATION_INTERVAL_IN_MS = 10;
+const EMIT_INTERVAL_IN_MS = 1000;
+// wws://test-socket.4dkankan.com/watch
+let clientCount = 0;
+let lastReport = new Date().getTime();
+let packetsSinceLastReport = 0;
+let testSceneNum = "t-test";
+let roomId = "00001";
+let userLimitNum = 2000;
+let agentId = 0;
+
+const createAgent = () => {
+  agentId += 1;
+  const nickName = `test_name_${agentId}`;
+  const userId = `6666666${agentId}`;
+  const role = agentId === 1 ? "leader" : "customer";
+  createClient({ userId, nickName, from: "0", role: role });
+  createClient({ userId, nickName, from: "1", role: role });
+  createClient({ userId, nickName, from: "2", role: role });
+};
+
+const createClient = ({ userId, nickName, from, role }) => {
+  // for demonstration purposes, some clients stay stuck in HTTP long-polling
+
+  const socket = io(URL, {
+    path: "/fsl-node",
+    transport: ["websocket"],
+    parser: customParser,
+    query: {
+      userId: userId,
+      from: from || 2,
+      sceneNum: testSceneNum,
+      role: role,
+      nickName: nickName,
+      roomId: roomId,
+      voiceStatus: 0,
+      enableTalk: true,
+      isAuthMic: 0,
+      isAllowMic: 0,
+      userLimitNum,
+      myHeadUrl: "http://downza.img.zz314.com/edu/pc/wlgj-1008/2016-06-23/64ec0888b15773e3ba5b5f744b9df16c.jpg",
+    },
+  });
+
+  setInterval(() => {
+    socket.emit("client to server event");
+  }, EMIT_INTERVAL_IN_MS);
+
+  socket.on("server to client event", () => {
+    packetsSinceLastReport++;
+  });
+
+  socket.on("disconnect", (reason) => {
+    console.log(`disconnect due to ${reason}`);
+  });
+
+  if (++clientCount < MAX_CLIENTS) {
+    setTimeout(createAgent, CLIENT_CREATION_INTERVAL_IN_MS);
+  }
+};
+
+createAgent();
+
+const printReport = () => {
+  const now = new Date().getTime();
+  const durationSinceLastReport = (now - lastReport) / 1000;
+  const packetsPerSeconds = (packetsSinceLastReport / durationSinceLastReport).toFixed(2);
+
+  console.log(`client count: ${clientCount} ; average packets received per second: ${packetsPerSeconds}`);
+
+  packetsSinceLastReport = 0;
+  lastReport = now;
+};
+
+setInterval(printReport, 5000);

+ 127 - 0
packages/main/src/security-restrictions.ts

@@ -0,0 +1,127 @@
+import {app, shell} from 'electron';
+import {URL} from 'url';
+
+/**
+ * List of origins that you allow open INSIDE the application and permissions for each of them.
+ *
+ * In development mode you need allow open `VITE_DEV_SERVER_URL`
+ */
+const ALLOWED_ORIGINS_AND_PERMISSIONS = new Map<string, Set<'clipboard-read' | 'media' | 'display-capture' | 'mediaKeySystem' | 'geolocation' | 'notifications' | 'midi' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' | 'unknown'>>(
+  import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL
+    ? [[new URL(import.meta.env.VITE_DEV_SERVER_URL).origin, new Set]]
+    : [],
+);
+
+/**
+ * List of origins that you allow open IN BROWSER.
+ * Navigation to origins below is possible only if the link opens in a new window
+ *
+ * @example
+ * <a
+ *   target="_blank"
+ *   href="https://github.com/"
+ * >
+ */
+const ALLOWED_EXTERNAL_ORIGINS = new Set<`https://${string}`>([
+  'https://github.com',
+]);
+
+
+app.on('web-contents-created', (_, contents) => {
+
+  /**
+   * Block navigation to origins not on the allowlist.
+   *
+   * Navigation is a common attack vector. If an attacker can convince the app to navigate away
+   * from its current page, they can possibly force the app to open web sites on the Internet.
+   *
+   * @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
+   */
+  contents.on('will-navigate', (event, url) => {
+    const {origin} = new URL(url);
+    if (ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
+      return;
+    }
+
+    // Prevent navigation
+    event.preventDefault();
+
+    if (import.meta.env.DEV) {
+      console.warn('Blocked navigating to an unallowed origin:', origin);
+    }
+  });
+
+
+  /**
+   * Block requested unallowed permissions.
+   * By default, Electron will automatically approve all permission requests.
+   *
+   * @see https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
+   */
+  contents.session.setPermissionRequestHandler((webContents, permission, callback) => {
+    const {origin} = new URL(webContents.getURL());
+
+    const permissionGranted = !!ALLOWED_ORIGINS_AND_PERMISSIONS.get(origin)?.has(permission);
+    callback(permissionGranted);
+
+    if (!permissionGranted && import.meta.env.DEV) {
+      console.warn(`${origin} requested permission for '${permission}', but was blocked.`);
+    }
+  });
+
+
+  /**
+   * Hyperlinks to allowed sites open in the default browser.
+   *
+   * The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
+   * frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
+   * You should deny any unexpected window creation.
+   *
+   * @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
+   * @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
+   */
+  contents.setWindowOpenHandler(({url}) => {
+    const {origin} = new URL(url);
+
+    // @ts-expect-error Type checking is performed in runtime
+    if (ALLOWED_EXTERNAL_ORIGINS.has(origin)) {
+      // Open default browser
+      shell.openExternal(url).catch(console.error);
+
+    } else if (import.meta.env.DEV) {
+      console.warn('Blocked the opening of an unallowed origin:', origin);
+    }
+
+    // Prevent creating new window in application
+    return {action: 'deny'};
+  });
+
+
+  /**
+   * Verify webview options before creation
+   *
+   * Strip away preload scripts, disable Node.js integration, and ensure origins are on the allowlist.
+   *
+   * @see https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
+   */
+  contents.on('will-attach-webview', (event, webPreferences, params) => {
+    const {origin} = new URL(params.src);
+    if (!ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
+
+      if (import.meta.env.DEV) {
+        console.warn(`A webview tried to attach ${params.src}, but was blocked.`);
+      }
+
+      event.preventDefault();
+      return;
+    }
+
+    // Strip away preload scripts if unused or verify their location is legitimate
+    delete webPreferences.preload;
+    // @ts-expect-error `preloadURL` exists - see https://www.electronjs.org/docs/latest/api/web-contents#event-will-attach-webview
+    delete webPreferences.preloadURL;
+
+    // Disable Node.js integration
+    webPreferences.nodeIntegration = false;
+  });
+});

+ 69 - 0
packages/main/tests/unit.spec.ts

@@ -0,0 +1,69 @@
+import type {MaybeMocked} from 'vitest';
+import {beforeEach, expect, test, vi} from 'vitest';
+import {restoreOrCreateWindow} from '../src/mainWindow';
+
+import {BrowserWindow} from 'electron';
+
+/**
+ * Mock real electron BrowserWindow API
+ */
+vi.mock('electron', () => {
+
+  const bw = vi.fn() as MaybeMocked<typeof BrowserWindow>;
+  // @ts-expect-error It's work in runtime, but I Haven't idea how to fix this type error
+  bw.getAllWindows = vi.fn(() => bw.mock.instances);
+  bw.prototype.loadURL = vi.fn();
+  bw.prototype.on = vi.fn();
+  bw.prototype.destroy = vi.fn();
+  bw.prototype.isDestroyed = vi.fn();
+  bw.prototype.isMinimized = vi.fn();
+  bw.prototype.focus = vi.fn();
+  bw.prototype.restore = vi.fn();
+
+  return {BrowserWindow: bw};
+});
+
+
+beforeEach(() => {
+  vi.clearAllMocks();
+});
+
+
+test('Should create new window', async () => {
+  const {mock} = vi.mocked(BrowserWindow);
+  expect(mock.instances).toHaveLength(0);
+
+  await restoreOrCreateWindow();
+  expect(mock.instances).toHaveLength(1);
+  expect(mock.instances[0].loadURL).toHaveBeenCalledOnce();
+  expect(mock.instances[0].loadURL).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/));
+});
+
+
+test('Should restore existing window', async () => {
+  const {mock} = vi.mocked(BrowserWindow);
+
+  // Create Window and minimize it
+  await restoreOrCreateWindow();
+  expect(mock.instances).toHaveLength(1);
+  const appWindow = vi.mocked(mock.instances[0]);
+  appWindow.isMinimized.mockReturnValueOnce(true);
+
+  await restoreOrCreateWindow();
+  expect(mock.instances).toHaveLength(1);
+  expect(appWindow.restore).toHaveBeenCalledOnce();
+});
+
+
+test('Should create new window if previous was destroyed', async () => {
+  const {mock} = vi.mocked(BrowserWindow);
+
+  // Create Window and destroy it
+  await restoreOrCreateWindow();
+  expect(mock.instances).toHaveLength(1);
+  const appWindow = vi.mocked(mock.instances[0]);
+  appWindow.isDestroyed.mockReturnValueOnce(true);
+
+  await restoreOrCreateWindow();
+  expect(mock.instances).toHaveLength(2);
+});

+ 28 - 0
packages/main/tsconfig.json

@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "module": "esnext",
+    "target": "esnext",
+    "sourceMap": false,
+    "moduleResolution": "Node",
+    "skipLibCheck": true,
+    "strict": true,
+    "isolatedModules": true,
+
+    "types" : ["node"],
+
+    "baseUrl": ".",
+    "paths": {
+      "/@/*": [
+        "./src/*"
+      ]
+    },
+  },
+  "include": [
+    "src/**/*.ts",
+    "../../types/**/*.d.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts"
+  ]
+}

+ 41 - 0
packages/main/vite.config.js

@@ -0,0 +1,41 @@
+import {node} from '../../.electron-vendors.cache.json';
+import {join} from 'path';
+
+const PACKAGE_ROOT = __dirname;
+
+
+/**
+ * @type {import('vite').UserConfig}
+ * @see https://vitejs.dev/config/
+ */
+const config = {
+  mode: process.env.MODE,
+  root: PACKAGE_ROOT,
+  envDir: process.cwd(),
+  resolve: {
+    alias: {
+      '/@/': join(PACKAGE_ROOT, 'src') + '/',
+    },
+  },
+  build: {
+    ssr: true,
+    sourcemap: 'inline',
+    target: `node${node}`,
+    outDir: 'dist',
+    assetsDir: '.',
+    minify: process.env.MODE !== 'development',
+    lib: {
+      entry: 'src/index.ts',
+      formats: ['cjs'],
+    },
+    rollupOptions: {
+      output: {
+        entryFileNames: '[name].cjs',
+      },
+    },
+    emptyOutDir: true,
+    brotliSize: false,
+  },
+};
+
+export default config;

+ 16 - 0
packages/preload/src/index.ts

@@ -0,0 +1,16 @@
+/**
+ * @module preload
+ */
+
+export { sha256sum } from './nodeCrypto';
+export { versions } from './versions';
+
+
+import { contextBridge, ipcRenderer } from 'electron';
+
+contextBridge.exposeInMainWorld('ipc', {
+    send: (channel: string, data: unknown) => ipcRenderer.send(channel, data),
+    on: (channel: string, fun: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => ipcRenderer.on(channel, fun),
+});
+
+

+ 7 - 0
packages/preload/src/nodeCrypto.ts

@@ -0,0 +1,7 @@
+import {type BinaryLike, createHash} from 'crypto';
+
+export function sha256sum(data: BinaryLike) {
+  return createHash('sha256')
+    .update(data)
+    .digest('hex');
+}

+ 1 - 0
packages/preload/src/versions.ts

@@ -0,0 +1 @@
+export const versions = process.versions;

+ 20 - 0
packages/preload/tests/unit.spec.ts

@@ -0,0 +1,20 @@
+import {createHash} from 'crypto';
+import {expect, test} from 'vitest';
+import {versions, sha256sum} from '../src';
+
+test('versions', async () => {
+  expect(versions).toBe(process.versions);
+});
+
+
+test('nodeCrypto', async () => {
+  /**
+   * Random string to test hashing
+   */
+  const testString = Math.random().toString(36).slice(2, 7);
+  const expectedHash = createHash('sha256')
+    .update(testString)
+    .digest('hex');
+
+  expect(sha256sum(testString)).toBe(expectedHash);
+});

+ 23 - 0
packages/preload/tsconfig.json

@@ -0,0 +1,23 @@
+{
+  "compilerOptions": {
+    "module": "esnext",
+    "target": "esnext",
+    "sourceMap": false,
+    "moduleResolution": "Node",
+    "skipLibCheck": true,
+    "strict": true,
+    "isolatedModules": true,
+
+    "types" : ["node"],
+
+    "baseUrl": ".",
+  },
+  "include": [
+    "src/**/*.ts",
+    "../../types/**/*.d.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts"
+  ]
+}

+ 38 - 0
packages/preload/vite.config.js

@@ -0,0 +1,38 @@
+import {chrome} from '../../.electron-vendors.cache.json';
+import {preload} from 'unplugin-auto-expose';
+
+const PACKAGE_ROOT = __dirname;
+
+/**
+ * @type {import('vite').UserConfig}
+ * @see https://vitejs.dev/config/
+ */
+const config = {
+  mode: process.env.MODE,
+  root: PACKAGE_ROOT,
+  envDir: process.cwd(),
+  build: {
+    ssr: true,
+    sourcemap: 'inline',
+    target: `chrome${chrome}`,
+    outDir: 'dist',
+    assetsDir: '.',
+    minify: process.env.MODE !== 'development',
+    lib: {
+      entry: 'src/index.ts',
+      formats: ['cjs'],
+    },
+    rollupOptions: {
+      output: {
+        entryFileNames: '[name].cjs',
+      },
+    },
+    emptyOutDir: true,
+    brotliSize: false,
+  },
+  plugins: [
+    preload.vite(),
+  ],
+};
+
+export default config;

+ 15 - 0
packages/renderer/.eslintrc.json

@@ -0,0 +1,15 @@
+{
+  "env": {
+    "browser": true,
+    "node": false
+  },
+  "extends": [
+    /** @see https://eslint.vuejs.org/rules/ */
+    "plugin:vue/vue3-recommended"
+  ],
+  "parserOptions": {
+    "parser": "@typescript-eslint/parser",
+    "ecmaVersion": 12,
+    "sourceType": "module"
+  }
+}

File diff suppressed because it is too large
+ 15 - 0
packages/renderer/assets/logo.svg


+ 13 - 0
packages/renderer/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="Content-Security-Policy" content="script-src 'self' blob:">
+  <meta content="width=device-width, initial-scale=1.0" name="viewport">
+  <title>Vite App</title>
+</head>
+<body>
+<div id="app"></div>
+<script src="./src/index.ts" type="module"></script>
+</body>
+</html>

+ 78 - 0
packages/renderer/src/App.vue

@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import './app.css';
+// import { ipcRenderer } from "#preload";
+import { Terminal } from 'xterm';
+import { onMounted } from 'vue';
+
+//
+onMounted(() => {
+  const terminal = new Terminal();
+  const terminalContainer: HTMLElement | null =
+    document.getElementById('terminal');
+  if (terminalContainer) {
+    terminal.open(terminalContainer);
+    // console.log('ipcRenderer', window.ipc);
+    // window.ipc.on('terminal.incomingData', (event, data) => {
+    //   console.log('1111', data);
+    //   terminal.write(data);
+    // });
+  }
+});
+
+// var shell = os.platform() === "win32" ? "powershell.exe" : "bash";
+
+function start() {
+  // window.ipc.send('start-Benmark-test');
+}
+</script>
+
+<template>
+  <h2>Socket Benmark test</h2>
+  <fieldset>
+    <legend>配置信息</legend>
+    <figure><span class="label">socket地址URL:</span> <input></figure>
+    <figure><span class="label">测试人数:</span> <input></figure>
+  </fieldset>
+
+  <div class="btns">
+    <button
+      class="start"
+      @click="start"
+    >
+      开始测试
+    </button>
+    <button class="end">
+      停止测试
+    </button>
+  </div>
+
+  <p>
+    Edit
+    <code>packages/renderer/src/App.vue</code> to test hot module replacement.
+  </p>
+  <div id="terminal" />
+</template>
+
+<style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-align: center;
+  color: #2c3e50;
+  margin: 60px auto;
+  max-width: 700px;
+}
+fieldset {
+  margin: 2rem;
+  padding: 1rem;
+}
+fieldset .label {
+  min-width: 120px;
+  display: inline-block;
+}
+.btns button {
+  padding: 5px 10px;
+  margin: 0 5px;
+}
+</style>

+ 153 - 0
packages/renderer/src/app.css

@@ -0,0 +1,153 @@
+.xterm {
+    cursor: text;
+    position: relative;
+    user-select: none;
+    -ms-user-select: none;
+    -webkit-user-select: none;
+}
+
+.xterm.focus,
+.xterm:focus {
+    outline: none;
+}
+
+.xterm .xterm-helpers {
+    position: absolute;
+    top: 0;
+    /**
+     * The z-index of the helpers must be higher than the canvases in order for
+     * IMEs to appear on top.
+     */
+    z-index: 5;
+}
+
+.xterm .xterm-helper-textarea {
+    padding: 0;
+    border: 0;
+    margin: 0;
+    /* Move textarea out of the screen to the far left, so that the cursor is not visible */
+    position: absolute;
+    opacity: 0;
+    left: -9999em;
+    top: 0;
+    width: 0;
+    height: 0;
+    z-index: -5;
+    /** Prevent wrapping so the IME appears against the textarea at the correct position */
+    white-space: nowrap;
+    overflow: hidden;
+    resize: none;
+}
+
+.xterm .composition-view {
+    /* TODO: Composition position got messed up somewhere */
+    background: #000;
+    color: #FFF;
+    display: none;
+    position: absolute;
+    white-space: nowrap;
+    z-index: 1;
+}
+
+.xterm .composition-view.active {
+    display: block;
+}
+
+.xterm .xterm-viewport {
+    /* On OS X this is required in order for the scroll bar to appear fully opaque */
+    background-color: #000;
+    overflow-y: scroll;
+    cursor: default;
+    position: absolute;
+    right: 0;
+    left: 0;
+    top: 0;
+    bottom: 0;
+}
+
+.xterm .xterm-screen {
+    position: relative;
+}
+
+.xterm .xterm-screen canvas {
+    position: absolute;
+    left: 0;
+    top: 0;
+}
+
+.xterm .xterm-scroll-area {
+    visibility: hidden;
+}
+
+.xterm-char-measure-element {
+    display: inline-block;
+    visibility: hidden;
+    position: absolute;
+    top: 0;
+    left: -9999em;
+    line-height: normal;
+}
+
+.xterm.enable-mouse-events {
+    /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
+    cursor: default;
+}
+
+.xterm.xterm-cursor-pointer,
+.xterm .xterm-cursor-pointer {
+    cursor: pointer;
+}
+
+.xterm.column-select.focus {
+    /* Column selection mode */
+    cursor: crosshair;
+}
+
+.xterm .xterm-accessibility,
+.xterm .xterm-message {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    z-index: 10;
+    color: transparent;
+}
+
+.xterm .live-region {
+    position: absolute;
+    left: -9999px;
+    width: 1px;
+    height: 1px;
+    overflow: hidden;
+}
+
+.xterm-dim {
+    opacity: 0.5;
+}
+
+.xterm-underline {
+    text-decoration: underline;
+}
+
+.xterm-strikethrough {
+    text-decoration: line-through;
+}
+
+.xterm-screen .xterm-decoration-container .xterm-decoration {
+	z-index: 6;
+	position: absolute;
+}
+
+.xterm-decoration-overview-ruler {
+    z-index: 7;
+    position: absolute;
+    top: 0;
+    right: 0;
+    pointer-events: none;
+}
+
+.xterm-decoration-top {
+    z-index: 2;
+    position: relative;
+}

+ 21 - 0
packages/renderer/src/components/ElectronVersions.vue

@@ -0,0 +1,21 @@
+<script lang="ts" setup>
+import {versions} from '#preload';
+</script>
+
+<template>
+  <ul id="process-versions">
+    <li
+      v-for="(version, lib) in versions"
+      :key="lib"
+    >
+      <strong>{{ lib }}</strong>: v{{ version }}
+    </li>
+  </ul>
+  <code>packages/renderer/src/components/ElectronVersions.vue</code>
+</template>
+
+<style scoped>
+ul {
+  list-style: none;
+}
+</style>

+ 13 - 0
packages/renderer/src/components/ReactiveCounter.vue

@@ -0,0 +1,13 @@
+<script lang="ts" setup>
+import {ref} from 'vue';
+
+const count = ref(0);
+</script>
+
+<template>
+  <button @click="count++">
+    count is: {{ count }}
+  </button>
+  <br><br>
+  <code>packages/renderer/src/components/ReactiveCounter.vue</code>
+</template>

+ 31 - 0
packages/renderer/src/components/ReactiveHash.vue

@@ -0,0 +1,31 @@
+<script lang="ts" setup>
+import {computed, ref} from 'vue';
+import {sha256sum} from '#preload';
+
+const rawString = ref('Hello World');
+/**
+ * window.nodeCrypto was exposed from {@link module:preload}
+ */
+const hashedString = computed(() => sha256sum(rawString.value));
+</script>
+
+<template>
+  <label>
+    Raw value
+    <input
+      v-model="rawString"
+      type="text"
+    >
+  </label>
+  <br>
+  <label>
+    Hashed by node:crypto
+    <input
+      v-model="hashedString"
+      type="text"
+      readonly
+    >
+  </label>
+  <br><br>
+  <code>packages/renderer/src/components/ReactiveHash.vue</code>
+</template>

+ 5 - 0
packages/renderer/src/index.ts

@@ -0,0 +1,5 @@
+import {createApp} from 'vue';
+import App from '/@/App.vue';
+
+createApp(App).mount('#app');
+

+ 21 - 0
packages/renderer/tests/ElectronVersions.spec.ts

@@ -0,0 +1,21 @@
+import {mount} from '@vue/test-utils';
+import {expect, test, vi} from 'vitest';
+import ElectronVersions from '../src/components/ElectronVersions.vue';
+
+vi.mock('#preload', () => {
+  return {
+    versions: {lib1: 1, lib2: 2},
+  };
+});
+
+
+test('ElectronVersions component', async () => {
+  expect(ElectronVersions).toBeTruthy();
+  const wrapper = mount(ElectronVersions);
+
+  const lis = wrapper.findAll<HTMLElement>('li');
+  expect(lis.length).toBe(2);
+  expect(lis[0].text()).toBe('lib1: v1');
+  expect(lis[1].text()).toBe('lib2: v2');
+
+});

+ 14 - 0
packages/renderer/tests/ReactiveCounter.spec.ts

@@ -0,0 +1,14 @@
+import {mount} from '@vue/test-utils';
+import {expect, test} from 'vitest';
+import ReactiveCounter from '../src/components/ReactiveCounter.vue';
+
+test('ReactiveHash component', async () => {
+  expect(ReactiveCounter).toBeTruthy();
+  const wrapper = mount(ReactiveCounter);
+
+  const button = wrapper.get('button');
+
+  expect(button.text()).toBe('count is: 0');
+  await button.trigger('click');
+  expect(button.text()).toBe('count is: 1');
+});

+ 23 - 0
packages/renderer/tests/ReactiveHash.spec.ts

@@ -0,0 +1,23 @@
+import {mount} from '@vue/test-utils';
+import {expect, test, vi} from 'vitest';
+import ReactiveHash from '../src/components/ReactiveHash.vue';
+
+
+vi.mock('#preload', () => {
+  return {
+    sha256sum: vi.fn((s: string) => `${s}:HASHED`),
+  };
+});
+
+
+test('ReactiveHash component', async () => {
+  expect(ReactiveHash).toBeTruthy();
+  const wrapper = mount(ReactiveHash);
+
+  const dataInput = wrapper.get<HTMLInputElement>('input:not([readonly])');
+  const hashInput = wrapper.get<HTMLInputElement>('input[readonly]');
+
+  const dataToHashed = Math.random().toString(36).slice(2, 7);
+  await dataInput.setValue(dataToHashed);
+  expect(hashInput.element.value).toBe(`${dataToHashed}:HASHED`);
+});

+ 36 - 0
packages/renderer/tsconfig.json

@@ -0,0 +1,36 @@
+{
+  "compilerOptions": {
+    "module": "esnext",
+    "target": "esnext",
+    "sourceMap": false,
+    "moduleResolution": "Node",
+    "skipLibCheck": true,
+    "strict": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+
+    "types" : ["node"],
+    "baseUrl": ".",
+    "paths": {
+      "#preload": [
+        "../preload/src/index"
+      ],
+      "/@/*": [
+        "./src/*"
+      ]
+    },
+    "lib": ["ESNext", "dom", "dom.iterable"]
+  },
+
+  "include": [
+    "src/**/*.vue",
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "types/**/*.d.ts",
+    "../../types/**/*.d.ts",
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts",
+  ]
+}

+ 6 - 0
packages/renderer/types/shims-vue.d.ts

@@ -0,0 +1,6 @@
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue';
+  // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
+  const component: DefineComponent<{}, {}, any>;
+  export default component;
+}

+ 50 - 0
packages/renderer/vite.config.js

@@ -0,0 +1,50 @@
+/* eslint-env node */
+
+import {chrome} from '../../.electron-vendors.cache.json';
+import {join} from 'path';
+import vue from '@vitejs/plugin-vue';
+import {renderer} from 'unplugin-auto-expose';
+
+const PACKAGE_ROOT = __dirname;
+
+/**
+ * @type {import('vite').UserConfig}
+ * @see https://vitejs.dev/config/
+ */
+const config = {
+  mode: process.env.MODE,
+  root: PACKAGE_ROOT,
+  resolve: {
+    alias: {
+      '/@/': join(PACKAGE_ROOT, 'src') + '/',
+    },
+  },
+  base: '',
+  server: {
+    fs: {
+      strict: true,
+    },
+  },
+  build: {
+    sourcemap: true,
+    target: `chrome${chrome}`,
+    outDir: 'dist',
+    assetsDir: '.',
+    rollupOptions: {
+      input: join(PACKAGE_ROOT, 'index.html'),
+    },
+    emptyOutDir: true,
+    brotliSize: false,
+  },
+  test: {
+    environment: 'happy-dom',
+  },
+  plugins: [
+    vue(),
+    renderer.vite({
+      preloadEntry: join(PACKAGE_ROOT, '../preload/src/index.ts'),
+    }),
+  ],
+};
+
+export default config;

+ 15 - 0
scripts/update-electron-vendors.js

@@ -0,0 +1,15 @@
+const {writeFileSync} = require('fs');
+const path = require('path');
+
+const electronRelease = process.versions;
+
+const node = electronRelease.node.split('.')[0];
+const chrome = electronRelease.v8.split('.').splice(0, 2).join('');
+
+const browserslistrcPath = path.resolve(process.cwd(), '.browserslistrc');
+
+writeFileSync('./.electron-vendors.cache.json',
+  JSON.stringify({chrome, node}),
+);
+
+writeFileSync(browserslistrcPath, `Chrome ${chrome}`, 'utf8');

+ 149 - 0
scripts/watch.js

@@ -0,0 +1,149 @@
+#!/usr/bin/env node
+
+const {createServer, build, createLogger} = require('vite');
+const electronPath = require('electron');
+const {spawn} = require('child_process');
+
+
+/** @type 'production' | 'development'' */
+const mode = process.env.MODE = process.env.MODE || 'development';
+
+
+/** @type {import('vite').LogLevel} */
+const logLevel = 'info';
+
+
+/** Messages on stderr that match any of the contained patterns will be stripped from output */
+const stderrFilterPatterns = [
+  /**
+   * warning about devtools extension
+   * @see https://github.com/cawa-93/vite-electron-builder/issues/492
+   * @see https://github.com/MarshallOfSound/electron-devtools-installer/issues/143
+   */
+  /ExtensionLoadWarning/,
+];
+
+
+/**
+ * Setup watcher for `main` package
+ * On file changed it totally re-launch electron app.
+ * @param {import('vite').ViteDevServer} watchServer Renderer watch server instance.
+ * Needs to set up `VITE_DEV_SERVER_URL` environment variable from {@link import('vite').ViteDevServer.resolvedUrls}
+ */
+const setupMainPackageWatcher = ({resolvedUrls}) => {
+  process.env.VITE_DEV_SERVER_URL = resolvedUrls.local[0];
+
+  const logger = createLogger(logLevel, {
+    prefix: '[main]',
+  });
+
+  /** @type {ChildProcessWithoutNullStreams | null} */
+  let spawnProcess = null;
+
+  return build({
+    mode,
+    logLevel,
+    build: {
+      /**
+       * Set to {} to enable rollup watcher
+       * @see https://vitejs.dev/config/build-options.html#build-watch
+       */
+      watch: {},
+    },
+    configFile: 'packages/main/vite.config.js',
+    plugins: [{
+      name: 'reload-app-on-main-package-change',
+      writeBundle() {
+        /** Kill electron ff process already exist */
+        if (spawnProcess !== null) {
+          spawnProcess.off('exit', process.exit);
+          spawnProcess.kill('SIGINT');
+          spawnProcess = null;
+        }
+
+        /** Spawn new electron process */
+        spawnProcess = spawn(String(electronPath), ['.']);
+
+        /** Proxy all logs */
+        spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: true}));
+
+        /** Proxy error logs but stripe some noisy messages. See {@link stderrFilterPatterns} */
+        spawnProcess.stderr.on('data', d => {
+          const data = d.toString().trim();
+          if (!data) return;
+          const mayIgnore = stderrFilterPatterns.some((r) => r.test(data));
+          if (mayIgnore) return;
+          logger.error(data, {timestamp: true});
+        });
+
+        /** Stops the watch script when the application has been quit */
+        spawnProcess.on('exit', process.exit);
+      },
+    }],
+  });
+
+};
+
+
+/**
+ * Setup watcher for `preload` package
+ * On file changed it reload web page.
+ * @param {import('vite').ViteDevServer} watchServer Renderer watch server instance.
+ * Required to access the web socket of the page. By sending the `full-reload` command to the socket, it reloads the web page.
+ */
+const setupPreloadPackageWatcher = ({ws}) =>
+  build({
+    mode,
+    logLevel,
+    build: {
+      /**
+       * Set to {} to enable rollup watcher
+       * @see https://vitejs.dev/config/build-options.html#build-watch
+       */
+      watch: {},
+    },
+    configFile: 'packages/preload/vite.config.js',
+    plugins: [{
+      name: 'reload-page-on-preload-package-change',
+      writeBundle() {
+        ws.send({
+          type: 'full-reload',
+        });
+      },
+    }],
+  });
+
+
+(async () => {
+  try {
+
+    /**
+     * Renderer watcher
+     * This must be the first,
+     * because the {@link setupMainPackageWatcher} and {@link setupPreloadPackageWatcher} depend on the renderer params
+     */
+    const rendererWatchServer = await createServer({
+      mode,
+      logLevel,
+      configFile: 'packages/renderer/vite.config.js',
+    });
+
+    /**
+     * Should launch watch server before create other watchers
+     */
+    await rendererWatchServer.listen();
+
+    /**
+     * See {@link setupPreloadPackageWatcher} JSDoc
+     */
+    await setupPreloadPackageWatcher(rendererWatchServer);
+
+    /**
+     * See {@link setupMainPackageWatcher} JSDoc
+     */
+    await setupMainPackageWatcher(rendererWatchServer);
+  } catch (e) {
+    console.error(e);
+    process.exit(1);
+  }
+})();

+ 77 - 0
tests/e2e.spec.ts

@@ -0,0 +1,77 @@
+import type {ElectronApplication} from 'playwright';
+import {_electron as electron} from 'playwright';
+import {afterAll, beforeAll, expect, test} from 'vitest';
+import {createHash} from 'crypto';
+
+
+let electronApp: ElectronApplication;
+
+
+beforeAll(async () => {
+  electronApp = await electron.launch({args: ['.'], timeout: 60000 * 5});
+});
+
+
+afterAll(async () => {
+  await electronApp.close();
+});
+
+
+test('Main window state', async () => {
+  const windowState: { isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean }
+    = await electronApp.evaluate(({BrowserWindow}) => {
+    const mainWindow = BrowserWindow.getAllWindows()[0];
+
+    const getState = () => ({
+      isVisible: mainWindow.isVisible(),
+      isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
+      isCrashed: mainWindow.webContents.isCrashed(),
+    });
+
+    return new Promise((resolve) => {
+      if (mainWindow.isVisible()) {
+        resolve(getState());
+      } else
+        mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0));
+    });
+  });
+
+  expect(windowState.isCrashed, 'App was crashed').toBeFalsy();
+  expect(windowState.isVisible, 'Main window was not visible').toBeTruthy();
+  expect(windowState.isDevToolsOpened, 'DevTools was opened').toBeFalsy();
+});
+
+
+test('Main window web content', async () => {
+  const page = await electronApp.firstWindow();
+  const element = await page.$('#app', {strict: true});
+  expect(element, 'Can\'t find root element').toBeDefined();
+  expect((await element.innerHTML()).trim(), 'Window content was empty').not.equal('');
+});
+
+
+test('Preload versions', async () => {
+  const page = await electronApp.firstWindow();
+  const renderedVersions = await page.locator('#process-versions').innerText();
+
+  const expectedVersions = await electronApp.evaluate(() => process.versions);
+
+  for (const expectedVersionsKey in expectedVersions) {
+    expect(renderedVersions).include(`${expectedVersionsKey}: v${expectedVersions[expectedVersionsKey]}`);
+  }
+});
+
+
+test('Preload nodeCrypto', async () => {
+  const page = await electronApp.firstWindow();
+
+  /**
+   * Random string to test hashing
+   */
+  const testString = Math.random().toString(36).slice(2, 7);
+
+  await page.fill('input', testString);
+  const renderedHash = await page.inputValue('input[readonly]');
+  const expectedHash = createHash('sha256').update(testString).digest('hex');
+  expect(renderedHash).toEqual(expectedHash);
+});

+ 23 - 0
types/env.d.ts

@@ -0,0 +1,23 @@
+/// <reference types="vite/client" />
+
+/**
+ * Describes all existing environment variables and their types.
+ * Required for Code completion and type checking
+ *
+ * Note: To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code
+ *
+ * @see https://github.com/vitejs/vite/blob/cab55b32de62e0de7d7789e8c2a1f04a8eae3a3f/packages/vite/types/importMeta.d.ts#L62-L69 Base Interface
+ * @see https://vitejs.dev/guide/env-and-mode.html#env-files Vite Env Variables Doc
+ */
+interface ImportMetaEnv {
+
+  /**
+   * URL where `renderer` web page located.
+   * Variable initialized in scripts/watch.ts
+   */
+  readonly VITE_DEV_SERVER_URL: undefined | string;
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 23 - 0
vitest.config.js

@@ -0,0 +1,23 @@
+/**
+ * Config for global end-to-end tests
+ * placed in project root tests folder
+ * @type {import('vite').UserConfig}
+ * @see https://vitest.dev/config/
+ */
+const config = {
+  test: {
+    /**
+     * By default, vitest search test files in all packages.
+     * For e2e tests have sense search only is project root tests folder
+     */
+    include: ['./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+
+    /**
+     * A default timeout of 5000ms is sometimes not enough for playwright.
+     */
+    testTimeout: 120000 * 5,
+    hookTimeout: 120000 * 5,
+  },
+};
+
+export default config;

File diff suppressed because it is too large
+ 5523 - 0
yarn.lock