22 Commits

Author SHA1 Message Date
75309d0945 fix build
Some checks failed
Publish documentation / docs (push) Failing after 2s
Check Semantic Commit / Validate PR Title (pull_request) Failing after 2s
Test / Test (22.12.x) (pull_request) Has been cancelled
2025-12-12 17:11:16 +01:00
6f0968cdce Merge branch 'main' into esm-asar-entrypoints 2025-12-12 14:42:02 +01:00
dependabot[bot]
bf1269fe21 build(deps): bump glob from 10.4.5 to 10.5.0 (#159)
Some checks failed
Release / test (push) Failing after 1s
Release / Release (push) Has been skipped
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 13:54:26 -08:00
dependabot[bot]
ca53e14488 build(deps): bump actions/checkout from 5.0.0 to 6.0.0 (#158)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...1af3b93b68)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:59:33 -08:00
dependabot[bot]
1b9f5eb340 build(deps): bump tar from 7.5.1 to 7.5.2 (#157)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.1 to 7.5.2.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.2)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 16:07:54 -08:00
Keeley Hammond
53c72d8c47 ci: use npm trusted publishing instead of CFA (#156) 2025-11-13 15:33:47 -08:00
Erick Zhao
b61638598d docs: add API docs and clean up README (#155) 2025-11-12 13:09:18 -08:00
dependabot[bot]
0a0b41d115 build(deps): bump vite from 6.3.6 to 6.4.1 (#154)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 17:51:24 -08:00
dependabot[bot]
ed0459457f build(deps): bump actions/setup-node from 5.0.0 to 6.0.0 (#153)
* build(deps): bump actions/setup-node from 5.0.0 to 6.0.0

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](a0853c2454...2028fbc5c2)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci: use yarn cache

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2025-11-03 15:24:25 -08:00
Fedor Indutny
0939980564 fix: fully respect singleArchFiles option (#152)
Some checks failed
Publish documentation / docs (push) Failing after 13s
Files listed under `singleArchFiles` are allowed to be unique for
different platforms so `dupedFiles` should not return them.

Fix: #151
2025-10-29 12:52:59 -04:00
dependabot[bot]
2e087ef6c8 build(deps): bump brace-expansion from 1.1.11 to 1.1.12 (#150)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 10:16:37 -07:00
Erick Zhao
355fd7c6e8 build: upgrade to Yarn v4 (#149) 2025-10-17 01:51:49 -07:00
dependabot[bot]
7a73b7793e build(deps): bump azure/cli from 2.1.0 to 2.2.0 (#147)
Bumps [azure/cli](https://github.com/azure/cli) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/azure/cli/releases)
- [Changelog](https://github.com/Azure/cli/blob/master/ReleaseProcess.md)
- [Commits](089eac9d8c...9f7ce6f37c)

---
updated-dependencies:
- dependency-name: azure/cli
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 14:26:10 -07:00
dependabot[bot]
436b2abeff build(deps): bump actions/setup-node from 4.4.0 to 5.0.0 (#146)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 5.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](49933ea528...a0853c2454)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:25:03 -07:00
dependabot[bot]
01eec61200 build(deps): bump vite from 6.3.5 to 6.3.6 (#145)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 19:53:39 -07:00
dependabot[bot]
9a2c19c940 build(deps): bump amannn/action-semantic-pull-request (#142)
Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 5.5.3 to 6.1.1.
- [Release notes](https://github.com/amannn/action-semantic-pull-request/releases)
- [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md)
- [Commits](0723387faa...48f256284b)

---
updated-dependencies:
- dependency-name: amannn/action-semantic-pull-request
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Samuel Attard <sam@electronjs.org>
2025-09-01 23:55:52 -07:00
dependabot[bot]
4eb37fab7b build(deps): bump actions/checkout from 4.2.2 to 5.0.0 (#143)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](11bd71901b...08c6903cd8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-01 23:41:51 -07:00
Erick Zhao
b3059564b7 Merge branch 'main' into esm-asar-entrypoints 2024-06-21 16:12:39 -07:00
Erick Zhao
f7d15b8d34 Merge remote-tracking branch 'origin' into esm-asar-entrypoints 2024-06-17 15:18:33 -07:00
Erick Zhao
1c55526cdb Update package.json
Co-authored-by: Erik Moura <erikian@erikian.dev>
2024-06-16 21:04:57 -07:00
Erick Zhao
e9a5812213 add no-asar 2024-06-12 20:50:35 -07:00
Erick Zhao
ed1efe60a0 beep boop 2024-06-12 20:34:54 -07:00
26 changed files with 4274 additions and 1744 deletions

View File

@@ -14,12 +14,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: docs-publish environment: docs-publish
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies - name: Install dependencies
run: yarn --frozen-lockfile run: yarn --immutable
- name: Build API documentation - name: Build API documentation
run: yarn build:docs run: yarn build:docs
- name: Azure login - name: Azure login
@@ -29,7 +30,7 @@ jobs:
tenant-id: ${{ secrets.AZURE_OIDC_TENANT_ID }} tenant-id: ${{ secrets.AZURE_OIDC_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_OIDC_SUBSCRIPTION_ID }} subscription-id: ${{ secrets.AZURE_OIDC_SUBSCRIPTION_ID }}
- name: Upload to Azure Blob Storage - name: Upload to Azure Blob Storage
uses: azure/cli@089eac9d8cc39f5d003e94f8b65efc51076c9cbd # v2.1.0 uses: azure/cli@9f7ce6f37c31b777ec6c6b6d1dfe7db79f497956 # v2.2.0
with: with:
inlineScript: | inlineScript: |
az storage blob upload-batch --account-name ${{ secrets.AZURE_ECOSYSTEM_PACKAGES_STORAGE_ACCOUNT_NAME }} -d '$web/${{ github.event.repository.name }}/${{ github.ref_name }}' -s ./docs --overwrite --auth-mode login az storage blob upload-batch --account-name ${{ secrets.AZURE_ECOSYSTEM_PACKAGES_STORAGE_ACCOUNT_NAME }} -d '$web/${{ github.event.repository.name }}/${{ github.ref_name }}' -s ./docs --overwrite --auth-mode login

View File

@@ -13,24 +13,25 @@ jobs:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: test needs: test
environment: npm environment: npm-trusted-publisher
permissions: permissions:
id-token: write # for CFA and npm provenance id-token: write # for publishing releases
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- name: Install - name: Install
run: yarn install --frozen-lockfile run: yarn install --immutable
- uses: continuousauth/action@4e8a2573eeb706f6d7300d6a9f3ca6322740b72d # v1.0.5 - name: Get GitHub App Token
timeout-minutes: 60 id: secret-service
uses: electron/secret-service-action@3476425e8b30555aac15b1b7096938e254b0e155 # v1.0.0
- name: Run Semantic Release
uses: electron/semantic-trusted-release@5eceb399ac8de8863205cf6e34109bce473ba566 # v1.0.1
with: with:
project-id: ${{ secrets.CFA_PROJECT_ID }} github-token: ${{ fromJSON(steps.secret-service.outputs.secrets).GITHUB_TOKEN }}
secret: ${{ secrets.CFA_SECRET }}
npm-token: ${{ secrets.NPM_TOKEN }}

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: semantic-pull-request - name: semantic-pull-request
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

View File

@@ -22,14 +22,14 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version: "${{ matrix.node-version }}" node-version: "${{ matrix.node-version }}"
cache: 'yarn' cache: 'yarn'
- name: Install - name: Install
run: yarn install --frozen-lockfile run: yarn install --immutable
- name: Build - name: Build
run: yarn build run: yarn build
- name: Lint - name: Lint

7
.gitignore vendored
View File

@@ -1,9 +1,12 @@
node_modules node_modules
dist dist
entry-asar/*.js* entry-asar/cjs/*.js*
entry-asar/*.ts entry-asar/cjs/*.d.ts
entry-asar/esm/*.?js*
entry-asar/esm/*.d.?ts
*.app *.app
test/fixtures/apps test/fixtures/apps
coverage coverage
docs docs
.vscode .vscode
.yarn/install-state.gz

5
.npmignore Normal file
View File

@@ -0,0 +1,5 @@
# npmignore overrides .gitignore for yarn pack
# Only exclude source files, not built files
entry-asar/**/*.mts
entry-asar/**/*.ts
entry-asar/**/tsconfig.json

View File

@@ -2,7 +2,7 @@
"plugins": [ "plugins": [
"@semantic-release/commit-analyzer", "@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
"@continuous-auth/semantic-release-npm", "@semantic-release/npm",
"@semantic-release/github" "@semantic-release/github"
], ],
"branches": [ "main" ] "branches": [ "main" ]

942
.yarn/releases/yarn-4.10.3.cjs vendored Executable file

File diff suppressed because one or more lines are too long

10
.yarnrc.yml Normal file
View File

@@ -0,0 +1,10 @@
enableScripts: false
nodeLinker: node-modules
npmMinimalAgeGate: 10080
npmPreapprovedPackages:
- "@electron/*"
yarnPath: .yarn/releases/yarn-4.10.3.cjs

View File

@@ -4,6 +4,7 @@
[![Test](https://github.com/electron/universal/actions/workflows/test.yml/badge.svg)](https://github.com/electron/universal/actions/workflows/test.yml) [![Test](https://github.com/electron/universal/actions/workflows/test.yml/badge.svg)](https://github.com/electron/universal/actions/workflows/test.yml)
[![NPM package](https://img.shields.io/npm/v/@electron/universal)](https://npm.im/@electron/universal) [![NPM package](https://img.shields.io/npm/v/@electron/universal)](https://npm.im/@electron/universal)
[![API docs](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.npmjs.org%2F%40electron%2Funiversal%2Flatest&query=%24.version&logo=typescript&logoColor=white&label=API%20Docs)](https://packages.electronjs.org/universal)
## Usage ## Usage
@@ -22,6 +23,8 @@ await makeUniversalApp({
}); });
``` ```
For full API usage, see the [API documentation](https://packages.electronjs.org/universal).
## Advanced configuration ## Advanced configuration
The basic usage patterns will work for most apps out of the box. Additional configuration The basic usage patterns will work for most apps out of the box. Additional configuration
@@ -118,8 +121,4 @@ Note that if you are using `mergeASARs`, you may need to add architecture-specif
binary resources to the `singleArchFiles` pattern. binary resources to the `singleArchFiles` pattern.
See [Merging ASARs usage](#merging-asar-archives-to-reduce-app-size) for an example. See [Merging ASARs usage](#merging-asar-archives-to-reduce-app-size) for an example.
#### How do I build my app for Apple silicon in the first place?
Check out the [Electron Apple silicon blog post](https://www.electronjs.org/blog/apple-silicon).
[`minimatch`]: https://github.com/isaacs/minimatch?tab=readme-ov-file#features [`minimatch`]: https://github.com/isaacs/minimatch?tab=readme-ov-file#features

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": ".",
},
"include": [
".",
"../ambient.d.ts"
],
"exclude": []
}

View File

@@ -0,0 +1,28 @@
import { app } from 'electron';
import { createRequire } from 'node:module';
import path from 'node:path';
if (process.arch === 'arm64') {
await setPaths('arm64');
} else {
await setPaths('x64');
}
async function setPaths(platform: string) {
// This should return the full path, ending in something like
// Notion.app/Contents/Resources/app.asar
const appPath = app.getAppPath();
const asarFile = `app-${platform}.asar`;
// Maybe we'll handle this in Electron one day
if (path.basename(appPath) === 'app.asar') {
const platformAppPath = path.join(path.dirname(appPath), asarFile);
// This is an undocumented API. It exists.
app.setAppPath(platformAppPath);
}
const require = createRequire(import.meta.url);
process._archPath = require.resolve(`../${asarFile}`);
await import(process._archPath);
}

View File

@@ -0,0 +1,29 @@
import { app } from 'electron';
import { createRequire } from 'node:module';
import path from 'node:path';
if (process.arch === 'arm64') {
await setPaths('arm64');
} else {
await setPaths('x64');
}
async function setPaths(platform: string) {
// This should return the full path, ending in something like
// Notion.app/Contents/Resources/app
const appPath = app.getAppPath();
const appFolder = `app-${platform}`;
// Maybe we'll handle this in Electron one day
if (path.basename(appPath) === 'app') {
const platformAppPath = path.join(path.dirname(appPath), appFolder);
// This is an undocumented private API. It exists.
app.setAppPath(platformAppPath);
}
const require = createRequire(import.meta.url);
process._archPath = require.resolve(`../${appFolder}`);
await import(process._archPath);
}

View File

@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"target":"ESNext",
"outDir": ".",
"moduleResolution": "bundler"
},
"include": [
".",
"../ambient.d.ts"
],
"exclude": []
}

View File

@@ -19,8 +19,12 @@
}, },
"files": [ "files": [
"dist/*", "dist/*",
"entry-asar/*", "entry-asar/**/*",
"!entry-asar/**/*.ts", "!entry-asar/**/has-asar.ts",
"!entry-asar/**/no-asar.ts",
"!entry-asar/**/has-asar.mts",
"!entry-asar/**/no-asar.mts",
"!entry-asar/**/tsconfig.json",
"README.md" "README.md"
], ],
"author": "Samuel Attard", "author": "Samuel Attard",
@@ -28,12 +32,11 @@
"provenance": true "provenance": true
}, },
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json && tsc -p tsconfig.entry-asar.json", "build": "tsc -p tsconfig.json && tsc -p entry-asar/esm/tsconfig.json && tsc -p entry-asar/cjs/tsconfig.json",
"build:docs": "npx typedoc", "build:docs": "npx typedoc",
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"", "lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"", "prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
"prepublishOnly": "npm run build", "prepack": "npm run build",
"pretest": "npm run build",
"test": "vitest run", "test": "vitest run",
"prepare": "husky" "prepare": "husky"
}, },
@@ -65,5 +68,6 @@
"*.ts": [ "*.ts": [
"prettier --write" "prettier --write"
] ]
} },
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f"
} }

View File

@@ -3,15 +3,19 @@ import path from 'node:path';
import { promises as stream } from 'node:stream'; import { promises as stream } from 'node:stream';
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise'; import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
import { minimatch } from 'minimatch';
const MACHO_PREFIX = 'Mach-O '; const MACHO_PREFIX = 'Mach-O ';
const UNPACKED_ASAR_PATH = path.join('Contents', 'Resources', 'app.asar.unpacked');
export enum AppFileType { export enum AppFileType {
MACHO, MACHO,
PLAIN, PLAIN,
INFO_PLIST, INFO_PLIST,
SNAPSHOT, SNAPSHOT,
APP_CODE, APP_CODE,
SINGLE_ARCH,
} }
export type AppFile = { export type AppFile = {
@@ -19,11 +23,37 @@ export type AppFile = {
type: AppFileType; type: AppFileType;
}; };
export type GetAllAppFilesOpts = {
singleArchFiles?: string;
};
const isSingleArchFile = (relativePath: string, opts: GetAllAppFilesOpts): boolean => {
if (opts.singleArchFiles === undefined) {
return false;
}
const unpackedPath = path.relative(UNPACKED_ASAR_PATH, relativePath);
// Outside of app.asar.unpacked
if (unpackedPath.startsWith('..')) {
return false;
}
return minimatch(unpackedPath, opts.singleArchFiles, {
matchBase: true,
});
};
/** /**
* *
* @param appPath Path to the application * @param appPath Path to the application
*/ */
export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => { export const getAllAppFiles = async (
appPath: string,
opts: GetAllAppFilesOpts,
): Promise<AppFile[]> => {
const unpackedPath = path.join('Contents', 'Resources', 'app.asar.unpacked');
const files: AppFile[] = []; const files: AppFile[] = [];
const visited = new Set<string>(); const visited = new Set<string>();
@@ -35,6 +65,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
const info = await fs.promises.stat(p); const info = await fs.promises.stat(p);
if (info.isSymbolicLink()) return; if (info.isSymbolicLink()) return;
if (info.isFile()) { if (info.isFile()) {
const relativePath = path.relative(appPath, p);
let fileType = AppFileType.PLAIN; let fileType = AppFileType.PLAIN;
var fileOutput = ''; var fileOutput = '';
@@ -49,6 +81,8 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
} }
if (p.endsWith('.asar')) { if (p.endsWith('.asar')) {
fileType = AppFileType.APP_CODE; fileType = AppFileType.APP_CODE;
} else if (isSingleArchFile(relativePath, opts)) {
fileType = AppFileType.SINGLE_ARCH;
} else if (fileOutput.startsWith(MACHO_PREFIX)) { } else if (fileOutput.startsWith(MACHO_PREFIX)) {
fileType = AppFileType.MACHO; fileType = AppFileType.MACHO;
} else if (p.endsWith('.bin')) { } else if (p.endsWith('.bin')) {
@@ -58,7 +92,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
} }
files.push({ files.push({
relativePath: path.relative(appPath, p), relativePath,
type: fileType, type: fileType,
}); });
} }

View File

@@ -75,7 +75,12 @@ export type MakeUniversalOpts = {
}; };
const dupedFiles = (files: AppFile[]) => const dupedFiles = (files: AppFile[]) =>
files.filter((f) => f.type !== AppFileType.SNAPSHOT && f.type !== AppFileType.APP_CODE); files.filter(
(f) =>
f.type !== AppFileType.SNAPSHOT &&
f.type !== AppFileType.APP_CODE &&
f.type !== AppFileType.SINGLE_ARCH,
);
export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => { export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> => {
d('making a universal app with options', opts); d('making a universal app with options', opts);
@@ -121,8 +126,8 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const uniqueToX64: string[] = []; const uniqueToX64: string[] = [];
const uniqueToArm64: string[] = []; const uniqueToArm64: string[] = [];
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp)); const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp), opts);
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath)); const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath), opts);
for (const file of dupedFiles(x64Files)) { for (const file of dupedFiles(x64Files)) {
if (!arm64Files.some((f) => f.relativePath === file.relativePath)) if (!arm64Files.some((f) => f.relativePath === file.relativePath))
@@ -143,7 +148,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
); );
} }
for (const file of x64Files.filter((f) => f.type === AppFileType.PLAIN)) { // Single Arch files are copied as is without processing.
const multiArchFiles = x64Files.filter((f) => f.type !== AppFileType.SINGLE_ARCH);
for (const file of multiArchFiles.filter((f) => f.type === AppFileType.PLAIN)) {
const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath)); const x64Sha = await sha(path.resolve(opts.x64AppPath, file.relativePath));
const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath)); const arm64Sha = await sha(path.resolve(opts.arm64AppPath, file.relativePath));
if (x64Sha !== arm64Sha) { if (x64Sha !== arm64Sha) {
@@ -159,7 +166,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
} }
} }
const knownMergedMachOFiles = new Set(); const knownMergedMachOFiles = new Set();
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) { for (const machOFile of multiArchFiles.filter((f) => f.type === AppFileType.MACHO)) {
const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath)); const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
const second = await fs.promises.realpath( const second = await fs.promises.realpath(
path.resolve(opts.arm64AppPath, machOFile.relativePath), path.resolve(opts.arm64AppPath, machOFile.relativePath),
@@ -247,17 +254,30 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const entryAsar = path.resolve(tmpDir, 'entry-asar'); const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.promises.mkdir(entryAsar, { recursive: true }); await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'no-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
let pj = JSON.parse( let pj = JSON.parse(
await fs.promises.readFile( await fs.promises.readFile(
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'), path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
'utf8', 'utf8',
), ),
); );
pj.main = 'index.js';
// Load a shim that redirects to the correct folder for the architecture.
// This needs to be a different file depending on if the app entrypoint is CommonJS or ESM.
if (pj.type === 'module' || pj.main.endsWith('.mjs')) {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'esm', 'no-asar.mjs'),
path.resolve(entryAsar, 'index.mjs'),
);
pj.main = 'index.mjs';
} else {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'cjs', 'no-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
pj.main = 'index.js';
}
await fs.promises.writeFile( await fs.promises.writeFile(
path.resolve(entryAsar, 'package.json'), path.resolve(entryAsar, 'package.json'),
JSON.stringify(pj) + '\n', JSON.stringify(pj) + '\n',
@@ -330,10 +350,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const entryAsar = path.resolve(tmpDir, 'entry-asar'); const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.promises.mkdir(entryAsar, { recursive: true }); await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'has-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
let pj = JSON.parse( let pj = JSON.parse(
( (
await asar.extractFile( await asar.extractFile(
@@ -342,7 +358,23 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
) )
).toString('utf8'), ).toString('utf8'),
); );
pj.main = 'index.js';
// Load a shim that redirects to the correct `app.asar` for the architecture.
// This needs to be a different file depending on if the app entrypoint is CommonJS or ESM.
if (pj.type === 'module' || pj.main.endsWith('.mjs')) {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'esm', 'has-asar.mjs'),
path.resolve(entryAsar, 'index.mjs'),
);
pj.main = 'index.mjs';
} else {
await fs.promises.cp(
path.resolve(import.meta.dirname, '..', 'entry-asar', 'cjs', 'has-asar.js'),
path.resolve(entryAsar, 'index.js'),
);
pj.main = 'index.js';
}
await fs.promises.writeFile( await fs.promises.writeFile(
path.resolve(entryAsar, 'package.json'), path.resolve(entryAsar, 'package.json'),
JSON.stringify(pj) + '\n', JSON.stringify(pj) + '\n',
@@ -355,9 +387,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
} }
} }
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents')); const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'), opts);
const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST); const plistFiles = multiArchFiles.filter((f) => f.type === AppFileType.INFO_PLIST);
for (const plistFile of plistFiles) { for (const plistFile of plistFiles) {
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath); const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath); const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);

View File

@@ -17,13 +17,20 @@ export interface AsarIntegrity {
[key: string]: HeaderHash; [key: string]: HeaderHash;
} }
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> { export type ComputeIntegrityDataOpts = {
singleArchFiles?: string;
};
export async function computeIntegrityData(
contentsPath: string,
opts: ComputeIntegrityDataOpts,
): Promise<AsarIntegrity> {
const root = await fs.promises.realpath(contentsPath); const root = await fs.promises.realpath(contentsPath);
const resourcesRelativePath = 'Resources'; const resourcesRelativePath = 'Resources';
const resourcesPath = path.resolve(root, resourcesRelativePath); const resourcesPath = path.resolve(root, resourcesRelativePath);
const resources = await getAllAppFiles(resourcesPath); const resources = await getAllAppFiles(resourcesPath, opts);
const resourceAsars = resources const resourceAsars = resources
.filter((file) => file.type === AppFileType.APP_CODE) .filter((file) => file.type === AppFileType.APP_CODE)
.reduce<IntegrityMap>( .reduce<IntegrityMap>(

View File

@@ -339,6 +339,86 @@ exports[`makeUniversalApp > asar mode > should merge two different asars when \`
} }
`; `;
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 1`] = `
{
"files": {
"hello-world-arm64": "<stripped>",
"hello-world-x64": "<stripped>",
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
},
},
},
},
},
"var": {
"link": "private/var",
},
},
}
`;
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 2`] = `[]`;
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 3`] = `
[
"hello-world-arm64",
"hello-world-x64",
]
`;
exports[`makeUniversalApp > asar mode > should merge two different asars with native files when \`mergeASARs\` is enabled 4`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "<stripped>",
},
},
}
`;
exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = ` exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = `
{ {
"files": { "files": {

View File

@@ -12,8 +12,8 @@ describe('file-utils', () => {
let noAsarFiles: AppFile[]; let noAsarFiles: AppFile[];
beforeAll(async () => { beforeAll(async () => {
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app')); asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64Asar.app'), {});
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app')); noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'Arm64NoAsar.app'), {});
}); });
it('should correctly identify plist files', async () => { it('should correctly identify plist files', async () => {

View File

@@ -142,6 +142,65 @@ describe('makeUniversalApp', () => {
}, },
); );
it(
'should merge two different asars with native files when `mergeASARs` is enabled',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-x64.app',
arch: 'x64',
createAsar: true,
singleArchBindings: true,
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-arm64.app',
arch: 'arm64',
createAsar: true,
singleArchBindings: true,
});
const out = path.resolve(appsOutPath, 'SingleArchFiles.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath: out,
mergeASARs: true,
singleArchFiles: 'hello-world-*',
});
await verifyApp(out, true);
},
);
it(
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique native file',
{ timeout: VERIFY_APP_TIMEOUT },
async () => {
const x64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-2-x64.app',
arch: 'x64',
createAsar: true,
singleArchBindings: true,
});
const arm64AppPath = await generateNativeApp({
appNameWithExtension: 'SingleArchFiles-2-arm64.app',
arch: 'arm64',
createAsar: true,
singleArchBindings: true,
});
const out = path.resolve(appsOutPath, 'SingleArchFiles-2.app');
await expect(
makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath: out,
mergeASARs: true,
singleArchFiles: 'bad-rule',
}),
).rejects.toThrow(
/the number of mach-o files is not the same between the arm64 and x64 builds/,
);
},
);
it( it(
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`', 'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
{ timeout: VERIFY_APP_TIMEOUT }, { timeout: VERIFY_APP_TIMEOUT },

View File

@@ -33,7 +33,12 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
// verify header // verify header
const asarFs = getRawHeader(path.resolve(resourcesDir, asar)); const asarFs = getRawHeader(path.resolve(resourcesDir, asar));
expect( expect(
removeUnstableProperties(asarFs.header, containsRuntimeGeneratedMacho ? ['hello-world'] : []), removeUnstableProperties(
asarFs.header,
containsRuntimeGeneratedMacho
? ['hello-world', 'hello-world-arm64', 'hello-world-x64']
: [],
),
).toMatchSnapshot(); ).toMatchSnapshot();
} }
@@ -53,7 +58,7 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
await verifyFileTree(path.resolve(resourcesDir, dir)); await verifyFileTree(path.resolve(resourcesDir, dir));
} }
const allFiles = await fileUtils.getAllAppFiles(appPath); const allFiles = await fileUtils.getAllAppFiles(appPath, {});
const infoPlists = allFiles const infoPlists = allFiles
.filter( .filter(
(appFile) => (appFile) =>
@@ -89,7 +94,7 @@ const extractAsarIntegrity = async (infoPlist: string) => {
export const verifyFileTree = async (dirPath: string) => { export const verifyFileTree = async (dirPath: string) => {
const { expect } = await import('vitest'); const { expect } = await import('vitest');
const dirFiles = await fileUtils.getAllAppFiles(dirPath); const dirFiles = await fileUtils.getAllAppFiles(dirPath, {});
const files = dirFiles.map((file) => { const files = dirFiles.map((file) => {
const it = path.join(dirPath, file.relativePath); const it = path.join(dirPath, file.relativePath);
const name = toSystemIndependentPath(file.relativePath); const name = toSystemIndependentPath(file.relativePath);
@@ -229,6 +234,7 @@ export const generateNativeApp = async (options: {
createAsar: boolean; createAsar: boolean;
nativeModuleArch?: string; nativeModuleArch?: string;
additionalFiles?: Record<string, string>; additionalFiles?: Record<string, string>;
singleArchBindings?: boolean;
}) => { }) => {
const { const {
appNameWithExtension, appNameWithExtension,
@@ -236,6 +242,7 @@ export const generateNativeApp = async (options: {
createAsar, createAsar,
nativeModuleArch = arch, nativeModuleArch = arch,
additionalFiles, additionalFiles,
singleArchBindings,
} = options; } = options;
const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => { const appPath = await templateApp(appNameWithExtension, arch, async (appPath) => {
const resources = path.join(appPath, 'Contents', 'Resources'); const resources = path.join(appPath, 'Contents', 'Resources');
@@ -247,14 +254,19 @@ export const generateNativeApp = async (options: {
path.basename(appNameWithExtension, '.app'), path.basename(appNameWithExtension, '.app'),
additionalFiles, additionalFiles,
); );
await fs.promises.cp( let targetBinding: string;
path.join(appsDir, `hello-world-${nativeModuleArch}`), if (singleArchBindings) {
path.join(testPath, 'hello-world'), targetBinding = path.join(testPath, `hello-world-${nativeModuleArch}`);
{ recursive: true, verbatimSymlinks: true }, } else {
); targetBinding = path.join(testPath, 'hello-world');
}
await fs.promises.cp(path.join(appsDir, `hello-world-${nativeModuleArch}`), targetBinding, {
recursive: true,
verbatimSymlinks: true,
});
if (createAsar) { if (createAsar) {
await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), { await createPackageWithOptions(testPath, path.resolve(resources, 'app.asar'), {
unpack: '**/hello-world', unpack: '**/hello-world*',
}); });
} else { } else {
await fs.promises.cp(testPath, resourcesApp, { recursive: true, verbatimSymlinks: true }); await fs.promises.cp(testPath, resourcesApp, { recursive: true, verbatimSymlinks: true });

View File

@@ -1,23 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"lib": [
"es2017"
],
"sourceMap": true,
"strict": true,
"outDir": "entry-asar",
"types": [
"node",
],
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": false
},
"include": [
"entry-asar"
],
"exclude": []
}

4596
yarn.lock

File diff suppressed because it is too large Load Diff