feat!: bump engines to Node.js >=22.12.0 (#139)
Some checks failed
Publish documentation / docs (push) Failing after 1m9s

BREAKING CHANGE: Requires Node.js v22.12.0 LTS or higher. ESM-only.
This commit is contained in:
David Sanders
2025-07-03 15:30:07 -07:00
committed by GitHub
parent 175672e430
commit 421713cf80
26 changed files with 1234 additions and 2566 deletions

35
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Publish documentation
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
permissions:
id-token: write
contents: read
jobs:
docs:
runs-on: ubuntu-latest
environment: docs-publish
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Build API documentation
run: yarn build:docs
- name: Azure login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_OIDC_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_OIDC_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_OIDC_SUBSCRIPTION_ID }}
- name: Upload to Azure Blob Storage
uses: azure/cli@089eac9d8cc39f5d003e94f8b65efc51076c9cbd # v2.1.0
with:
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

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version: 20.x node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- name: Install - name: Install
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile

View File

@@ -18,9 +18,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: node-version:
- '20.5' - 22.12.x
- '18.17'
- '16.20'
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -30,12 +28,8 @@ jobs:
with: with:
node-version: "${{ matrix.node-version }}" node-version: "${{ matrix.node-version }}"
cache: 'yarn' cache: 'yarn'
- name: Install (Node.js v18+) - name: Install
if : ${{ matrix.node-version != '16.20' }}
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Install (Node.js < v18)
if : ${{ matrix.node-version == '16.20' }}
run: yarn install --frozen-lockfile --ignore-engines
- name: Build - name: Build
run: yarn build run: yarn build
- name: Lint - name: Lint

View File

@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged yarn lint-staged

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
22.12

View File

@@ -1,16 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts?$': [
'ts-jest',
{
tsconfig: 'tsconfig.jest.json',
},
],
},
testMatch: ['<rootDir>/test/**/*.spec.ts'],
globalSetup: './jest.setup.ts',
testTimeout: 10000,
};

View File

@@ -2,8 +2,8 @@
"name": "@electron/universal", "name": "@electron/universal",
"version": "0.0.0-development", "version": "0.0.0-development",
"description": "Utility for creating Universal macOS applications from two x64 and arm64 Electron applications", "description": "Utility for creating Universal macOS applications from two x64 and arm64 Electron applications",
"main": "dist/cjs/index.js", "type": "module",
"module": "dist/esm/index.js", "exports": "./dist/index.js",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
"electron", "electron",
@@ -15,7 +15,7 @@
"url": "git+https://github.com/electron/universal.git" "url": "git+https://github.com/electron/universal.git"
}, },
"engines": { "engines": {
"node": ">=16.4" "node": ">=22.12.0"
}, },
"files": [ "files": [
"dist/*", "dist/*",
@@ -28,38 +28,36 @@
"provenance": true "provenance": true
}, },
"scripts": { "scripts": {
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json", "build": "tsc -p tsconfig.json && tsc -p tsconfig.entry-asar.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", "prepublishOnly": "npm run build",
"test": "jest", "pretest": "npm run build",
"prepare": "husky install" "test": "vitest run",
"prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@electron/get": "^3.0.0", "@electron/get": "^4.0.0",
"@tsconfig/node22": "^22.0.1",
"@types/cross-zip": "^4.0.1", "@types/cross-zip": "^4.0.1",
"@types/debug": "^4.1.10", "@types/debug": "^4.1.10",
"@types/fs-extra": "^11.0.3",
"@types/jest": "^29.5.7",
"@types/minimatch": "^5.1.2", "@types/minimatch": "^5.1.2",
"@types/node": "^20.8.10", "@types/node": "~22.10.7",
"@types/plist": "^3.0.4", "@types/plist": "^3.0.4",
"cross-zip": "^4.0.0", "cross-zip": "^4.0.0",
"husky": "^8.0.3", "husky": "^9.1.7",
"jest": "^29.7.0", "lint-staged": "^16.1.0",
"lint-staged": "^15.2.10", "prettier": "^3.5.3",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1",
"typedoc": "~0.25.13", "typedoc": "~0.25.13",
"typescript": "^5.2.2" "typescript": "^5.8.3",
"vitest": "^3.1.3"
}, },
"dependencies": { "dependencies": {
"@electron/asar": "^3.3.1", "@electron/asar": "^4.0.0",
"@malept/cross-spawn-promise": "^2.0.0", "@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.3.1", "debug": "^4.3.1",
"dir-compare": "^4.2.0", "dir-compare": "^4.2.0",
"fs-extra": "^11.1.1",
"minimatch": "^9.0.3", "minimatch": "^9.0.3",
"plist": "^3.1.0" "plist": "^3.1.0"
}, },
@@ -67,8 +65,5 @@
"*.ts": [ "*.ts": [
"prettier --write" "prettier --write"
] ]
},
"resolutions": {
"jackspeak": "2.1.1"
} }
} }

View File

@@ -1,11 +1,13 @@
import asar from '@electron/asar'; import { execFileSync } from 'node:child_process';
import { execFileSync } from 'child_process'; import crypto from 'node:crypto';
import crypto from 'crypto'; import fs from 'node:fs';
import fs from 'fs-extra'; import os from 'node:os';
import path from 'path'; import path from 'node:path';
import * as asar from '@electron/asar';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import os from 'os';
import { d } from './debug'; import { d } from './debug.js';
const LIPO = 'lipo'; const LIPO = 'lipo';
@@ -40,7 +42,7 @@ export const detectAsarMode = async (appPath: string) => {
d('checking asar mode of', appPath); d('checking asar mode of', appPath);
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar'); const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
if (!(await fs.pathExists(asarPath))) { if (!fs.existsSync(asarPath)) {
d('determined no asar'); d('determined no asar');
return AsarMode.NO_ASAR; return AsarMode.NO_ASAR;
} }
@@ -169,8 +171,8 @@ export const mergeASARs = async ({
// Extract both // Extract both
// //
const x64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'x64-')); const x64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'x64-'));
const arm64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'arm64-')); const arm64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'arm64-'));
try { try {
d(`extracting ${x64AsarPath} to ${x64Dir}`); d(`extracting ${x64AsarPath} to ${x64Dir}`);
@@ -185,18 +187,22 @@ export const mergeASARs = async ({
if (isDirectory(arm64AsarPath, file)) { if (isDirectory(arm64AsarPath, file)) {
d(`creating unique directory: ${file}`); d(`creating unique directory: ${file}`);
await fs.mkdirp(destination); await fs.promises.mkdir(destination, { recursive: true });
continue; continue;
} }
d(`xopying unique file: ${file}`); d(`copying unique file: ${file}`);
await fs.mkdirp(path.dirname(destination)); await fs.promises.mkdir(path.dirname(destination), { recursive: true });
await fs.copy(source, destination); await fs.promises.cp(source, destination, {
force: true,
recursive: true,
verbatimSymlinks: true,
});
} }
for (const binding of commonBindings) { for (const binding of commonBindings) {
const source = await fs.realpath(path.resolve(arm64Dir, binding)); const source = await fs.promises.realpath(path.resolve(arm64Dir, binding));
const destination = await fs.realpath(path.resolve(x64Dir, binding)); const destination = await fs.promises.realpath(path.resolve(x64Dir, binding));
d(`merging binding: ${binding}`); d(`merging binding: ${binding}`);
execFileSync(LIPO, [source, destination, '-create', '-output', destination]); execFileSync(LIPO, [source, destination, '-create', '-output', destination]);
@@ -219,7 +225,10 @@ export const mergeASARs = async ({
d('done merging'); d('done merging');
} finally { } finally {
await Promise.all([fs.remove(x64Dir), fs.remove(arm64Dir)]); await Promise.all([
fs.promises.rm(x64Dir, { recursive: true, force: true }),
fs.promises.rm(arm64Dir, { recursive: true, force: true }),
]);
} }
}; };

View File

@@ -1,8 +1,9 @@
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise'; import fs from 'node:fs';
import * as fs from 'fs-extra'; import path from 'node:path';
import * as path from 'path';
import { promises as stream } from 'node:stream'; import { promises as stream } from 'node:stream';
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
const MACHO_PREFIX = 'Mach-O '; const MACHO_PREFIX = 'Mach-O ';
export enum AppFileType { export enum AppFileType {
@@ -27,11 +28,11 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
const visited = new Set<string>(); const visited = new Set<string>();
const traverse = async (p: string) => { const traverse = async (p: string) => {
p = await fs.realpath(p); p = await fs.promises.realpath(p);
if (visited.has(p)) return; if (visited.has(p)) return;
visited.add(p); visited.add(p);
const info = await fs.stat(p); const info = await fs.promises.stat(p);
if (info.isSymbolicLink()) return; if (info.isSymbolicLink()) return;
if (info.isFile()) { if (info.isFile()) {
let fileType = AppFileType.PLAIN; let fileType = AppFileType.PLAIN;
@@ -63,7 +64,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
} }
if (info.isDirectory()) { if (info.isDirectory()) {
for (const child of await fs.readdir(p)) { for (const child of await fs.promises.readdir(p)) {
await traverse(path.resolve(p, child)); await traverse(path.resolve(p, child));
} }
} }
@@ -83,3 +84,21 @@ export const readMachOHeader = async (path: string) => {
}); });
return Buffer.concat(chunks); return Buffer.concat(chunks);
}; };
export const fsMove = async (oldPath: string, newPath: string) => {
try {
await fs.promises.rename(oldPath, newPath);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'EXDEV') {
// Cross-device link, fallback to copy and delete
await fs.promises.cp(oldPath, newPath, {
force: true,
recursive: true,
verbatimSymlinks: true,
});
await fs.promises.rm(oldPath, { force: true, recursive: true });
} else {
throw err;
}
}
};

View File

@@ -1,17 +1,18 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import * as asar from '@electron/asar'; import * as asar from '@electron/asar';
import { spawn } from '@malept/cross-spawn-promise'; import { spawn } from '@malept/cross-spawn-promise';
import * as dircompare from 'dir-compare'; import * as dircompare from 'dir-compare';
import * as fs from 'fs-extra';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import * as os from 'os';
import * as path from 'path';
import * as plist from 'plist'; import * as plist from 'plist';
import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils'; import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils.js';
import { AppFile, AppFileType, getAllAppFiles, readMachOHeader } from './file-utils'; import { AppFile, AppFileType, fsMove, getAllAppFiles, readMachOHeader } from './file-utils.js';
import { sha } from './sha'; import { sha } from './sha.js';
import { d } from './debug'; import { d } from './debug.js';
import { computeIntegrityData } from './integrity'; import { computeIntegrityData } from './integrity.js';
/** /**
* Options to pass into the {@link makeUniversalApp} function. * Options to pass into the {@link makeUniversalApp} function.
@@ -88,7 +89,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (!opts.outAppPath || !path.isAbsolute(opts.outAppPath)) if (!opts.outAppPath || !path.isAbsolute(opts.outAppPath))
throw new Error('Expected opts.outAppPath to be an absolute path but it was not'); throw new Error('Expected opts.outAppPath to be an absolute path but it was not');
if (await fs.pathExists(opts.outAppPath)) { if (fs.existsSync(opts.outAppPath)) {
d('output path exists already'); d('output path exists already');
if (!opts.force) { if (!opts.force) {
throw new Error( throw new Error(
@@ -96,7 +97,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
); );
} else { } else {
d('overwriting existing application because force == true'); d('overwriting existing application because force == true');
await fs.remove(opts.outAppPath); await fs.promises.rm(opts.outAppPath, { recursive: true, force: true });
} }
} }
@@ -110,7 +111,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)', 'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)',
); );
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-')); const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-universal-'));
d('building universal app in', tmpDir); d('building universal app in', tmpDir);
try { try {
@@ -120,8 +121,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.realpath(tmpApp)); const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp));
const arm64Files = await getAllAppFiles(await fs.realpath(opts.arm64AppPath)); const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath));
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))
@@ -159,8 +160,10 @@ 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 x64Files.filter((f) => f.type === AppFileType.MACHO)) {
const first = await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)); const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
const second = await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath)); const second = await fs.promises.realpath(
path.resolve(opts.arm64AppPath, machOFile.relativePath),
);
if ( if (
isUniversalMachO(await readMachOHeader(first)) && isUniversalMachO(await readMachOHeader(first)) &&
@@ -201,7 +204,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
second, second,
'-create', '-create',
'-output', '-output',
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)), await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath)),
]); ]);
knownMergedMachOFiles.add(machOFile.relativePath); knownMergedMachOFiles.add(machOFile.relativePath);
} }
@@ -232,26 +235,34 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (nonMergedDifferences.length > 0) { if (nonMergedDifferences.length > 0) {
d('x64 and arm64 app folders are different, creating dynamic entry ASAR'); d('x64 and arm64 app folders are different, creating dynamic entry ASAR');
await fs.move( await fsMove(
path.resolve(tmpApp, 'Contents', 'Resources', 'app'), path.resolve(tmpApp, 'Contents', 'Resources', 'app'),
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64'),
); );
await fs.copy( await fs.promises.cp(
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'), path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'),
{ force: true, recursive: true, verbatimSymlinks: true },
); );
const entryAsar = path.resolve(tmpDir, 'entry-asar'); const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.mkdir(entryAsar); await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.copy( await fs.promises.cp(
path.resolve(__dirname, '..', '..', 'entry-asar', 'no-asar.js'), path.resolve(import.meta.dirname, '..', 'entry-asar', 'no-asar.js'),
path.resolve(entryAsar, 'index.js'), path.resolve(entryAsar, 'index.js'),
); );
let pj = await fs.readJson( let pj = JSON.parse(
await fs.promises.readFile(
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'), path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
'utf8',
),
); );
pj.main = 'index.js'; pj.main = 'index.js';
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj); await fs.promises.writeFile(
path.resolve(entryAsar, 'package.json'),
JSON.stringify(pj) + '\n',
'utf8',
);
await asar.createPackage( await asar.createPackage(
entryAsar, entryAsar,
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
@@ -288,19 +299,20 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (x64AsarSha !== arm64AsarSha) { if (x64AsarSha !== arm64AsarSha) {
d('x64 and arm64 asars are different'); d('x64 and arm64 asars are different');
const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'); const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar');
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath); await fsMove(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath);
const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked'); const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked');
if (await fs.pathExists(x64Unpacked)) { if (fs.existsSync(x64Unpacked)) {
await fs.move( await fsMove(
x64Unpacked, x64Unpacked,
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'),
); );
} }
const arm64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar'); const arm64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar');
await fs.copy( await fs.promises.cp(
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'), path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'),
arm64AsarPath, arm64AsarPath,
{ force: true, recursive: true, verbatimSymlinks: true },
); );
const arm64Unpacked = path.resolve( const arm64Unpacked = path.resolve(
opts.arm64AppPath, opts.arm64AppPath,
@@ -308,17 +320,18 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
'Resources', 'Resources',
'app.asar.unpacked', 'app.asar.unpacked',
); );
if (await fs.pathExists(arm64Unpacked)) { if (fs.existsSync(arm64Unpacked)) {
await fs.copy( await fs.promises.cp(
arm64Unpacked, arm64Unpacked,
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'), path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'),
{ force: true, recursive: true, verbatimSymlinks: true },
); );
} }
const entryAsar = path.resolve(tmpDir, 'entry-asar'); const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.mkdir(entryAsar); await fs.promises.mkdir(entryAsar, { recursive: true });
await fs.copy( await fs.promises.cp(
path.resolve(__dirname, '..', '..', 'entry-asar', 'has-asar.js'), path.resolve(import.meta.dirname, '..', 'entry-asar', 'has-asar.js'),
path.resolve(entryAsar, 'index.js'), path.resolve(entryAsar, 'index.js'),
); );
let pj = JSON.parse( let pj = JSON.parse(
@@ -330,7 +343,11 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
).toString('utf8'), ).toString('utf8'),
); );
pj.main = 'index.js'; pj.main = 'index.js';
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj); await fs.promises.writeFile(
path.resolve(entryAsar, 'package.json'),
JSON.stringify(pj) + '\n',
'utf8',
);
const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'); const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar');
await asar.createPackage(entryAsar, asarPath); await asar.createPackage(entryAsar, asarPath);
} else { } else {
@@ -346,10 +363,10 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath); const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);
const { ElectronAsarIntegrity: x64Integrity, ...x64Plist } = plist.parse( const { ElectronAsarIntegrity: x64Integrity, ...x64Plist } = plist.parse(
await fs.readFile(x64PlistPath, 'utf8'), await fs.promises.readFile(x64PlistPath, 'utf8'),
) as any; ) as any;
const { ElectronAsarIntegrity: arm64Integrity, ...arm64Plist } = plist.parse( const { ElectronAsarIntegrity: arm64Integrity, ...arm64Plist } = plist.parse(
await fs.readFile(arm64PlistPath, 'utf8'), await fs.promises.readFile(arm64PlistPath, 'utf8'),
) as any; ) as any;
if (JSON.stringify(x64Plist) !== JSON.stringify(arm64Plist)) { if (JSON.stringify(x64Plist) !== JSON.stringify(arm64Plist)) {
throw new Error( throw new Error(
@@ -364,23 +381,26 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
? { ...x64Plist, ElectronAsarIntegrity: generatedIntegrity } ? { ...x64Plist, ElectronAsarIntegrity: generatedIntegrity }
: { ...x64Plist }; : { ...x64Plist };
await fs.writeFile(path.resolve(tmpApp, plistFile.relativePath), plist.build(mergedPlist)); await fs.promises.writeFile(
path.resolve(tmpApp, plistFile.relativePath),
plist.build(mergedPlist),
);
} }
for (const snapshotsFile of arm64Files.filter((f) => f.type === AppFileType.SNAPSHOT)) { for (const snapshotsFile of arm64Files.filter((f) => f.type === AppFileType.SNAPSHOT)) {
d('copying snapshot file', snapshotsFile.relativePath, 'to target application'); d('copying snapshot file', snapshotsFile.relativePath, 'to target application');
await fs.copy( await fs.promises.cp(
path.resolve(opts.arm64AppPath, snapshotsFile.relativePath), path.resolve(opts.arm64AppPath, snapshotsFile.relativePath),
path.resolve(tmpApp, snapshotsFile.relativePath), path.resolve(tmpApp, snapshotsFile.relativePath),
); );
} }
d('moving final universal app to target destination'); d('moving final universal app to target destination');
await fs.mkdirp(path.dirname(opts.outAppPath)); await fs.promises.mkdir(path.dirname(opts.outAppPath), { recursive: true });
await spawn('mv', [tmpApp, opts.outAppPath]); await spawn('mv', [tmpApp, opts.outAppPath]);
} catch (err) { } catch (err) {
throw err; throw err;
} finally { } finally {
await fs.remove(tmpDir); await fs.promises.rm(tmpDir, { recursive: true, force: true });
} }
}; };

View File

@@ -1,8 +1,8 @@
import * as fs from 'fs-extra'; import fs from 'node:fs';
import path from 'path'; import path from 'node:path';
import { AppFileType, getAllAppFiles } from './file-utils';
import { sha } from './sha'; import { AppFileType, getAllAppFiles } from './file-utils.js';
import { generateAsarIntegrity } from './asar-utils'; import { generateAsarIntegrity } from './asar-utils.js';
type IntegrityMap = { type IntegrityMap = {
[filepath: string]: string; [filepath: string]: string;
@@ -18,7 +18,7 @@ export interface AsarIntegrity {
} }
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> { export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
const root = await fs.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);

View File

@@ -1,8 +1,8 @@
import * as fs from 'fs-extra'; import fs from 'node:fs';
import * as crypto from 'crypto'; import crypto from 'node:crypto';
import { pipeline } from 'stream/promises'; import { pipeline } from 'node:stream/promises';
import { d } from './debug'; import { d } from './debug.js';
export const sha = async (filePath: string) => { export const sha = async (filePath: string) => {
d('hashing', filePath); d('hashing', filePath);

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`makeUniversalApp asar mode should correctly merge two identical asars 1`] = ` exports[`makeUniversalApp > asar mode > should correctly merge two identical asars 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -29,7 +29,7 @@ exports[`makeUniversalApp asar mode should correctly merge two identical asars 1
} }
`; `;
exports[`makeUniversalApp asar mode should correctly merge two identical asars 2`] = ` exports[`makeUniversalApp > asar mode > should correctly merge two identical asars 2`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -40,7 +40,7 @@ exports[`makeUniversalApp asar mode should correctly merge two identical asars 2
} }
`; `;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 1`] = ` exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 1`] = `
{ {
"files": { "files": {
"extra-file.txt": { "extra-file.txt": {
@@ -80,7 +80,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
} }
`; `;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 2`] = ` exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 2`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -109,7 +109,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
} }
`; `;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 3`] = ` exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 3`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -138,7 +138,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
} }
`; `;
exports[`makeUniversalApp asar mode should create a shim if asars are different between architectures 4`] = ` exports[`makeUniversalApp > asar mode > should create a shim if asars are different between architectures 4`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app-arm64.asar": { "Resources/app-arm64.asar": {
@@ -157,7 +157,7 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
} }
`; `;
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 1`] = ` exports[`makeUniversalApp > asar mode > should generate AsarIntegrity for all asars in the application 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -215,7 +215,7 @@ exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars
} }
`; `;
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 2`] = ` exports[`makeUniversalApp > asar mode > should generate AsarIntegrity for all asars in the application 2`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -273,7 +273,7 @@ exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars
} }
`; `;
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 3`] = ` exports[`makeUniversalApp > asar mode > should generate AsarIntegrity for all asars in the application 3`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -288,7 +288,7 @@ exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars
} }
`; `;
exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 1`] = ` exports[`makeUniversalApp > asar mode > should merge two different asars when \`mergeASARs\` is enabled 1`] = `
{ {
"files": { "files": {
"extra-file.txt": { "extra-file.txt": {
@@ -328,7 +328,7 @@ exports[`makeUniversalApp asar mode should merge two different asars when \`merg
} }
`; `;
exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 2`] = ` exports[`makeUniversalApp > asar mode > should merge two different asars when \`mergeASARs\` is enabled 2`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -339,7 +339,7 @@ exports[`makeUniversalApp asar mode should merge two different asars when \`merg
} }
`; `;
exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = ` exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -397,14 +397,14 @@ exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into
} }
`; `;
exports[`makeUniversalApp asar mode should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 2`] = ` exports[`makeUniversalApp > asar mode > should not inject ElectronAsarIntegrity into \`infoPlistsToIgnore\` 2`] = `
{ {
"Contents/Info.plist": undefined, "Contents/Info.plist": undefined,
"Contents/Resources/SubApp-1.app/Contents/Info.plist": undefined, "Contents/Resources/SubApp-1.app/Contents/Info.plist": undefined,
} }
`; `;
exports[`makeUniversalApp force packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 1`] = ` exports[`makeUniversalApp > force > packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -433,7 +433,7 @@ exports[`makeUniversalApp force packages successfully if \`out\` bundle already
} }
`; `;
exports[`makeUniversalApp force packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 2`] = ` exports[`makeUniversalApp > force > packages successfully if \`out\` bundle already exists and \`force\` is \`true\` 2`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -444,7 +444,7 @@ exports[`makeUniversalApp force packages successfully if \`out\` bundle already
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 1`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -473,13 +473,13 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 2`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 2`] = `
[ [
"private/var/i-aint-got-no-rhythm.bin", "private/var/i-aint-got-no-rhythm.bin",
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 3`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 3`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -498,7 +498,7 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 4`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 4`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -517,7 +517,7 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with different macho files (shim and lipo) 5`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with different macho files (shim and lipo) 5`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -528,7 +528,7 @@ exports[`makeUniversalApp no asar mode different app dirs with different macho f
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 1`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -557,13 +557,13 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
} }
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 2`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 2`] = `
[ [
"private/var/i-aint-got-no-rhythm.bin", "private/var/i-aint-got-no-rhythm.bin",
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 3`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 3`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -582,7 +582,7 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 4`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 4`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -601,7 +601,7 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode different app dirs with universal macho files (shim but don't lipo) 5`] = ` exports[`makeUniversalApp > no asar mode > different app dirs with universal macho files (shim but don't lipo) 5`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -612,7 +612,7 @@ exports[`makeUniversalApp no asar mode different app dirs with universal macho f
} }
`; `;
exports[`makeUniversalApp no asar mode identical app dirs with different macho files (e.g. do not shim, but still lipo) 1`] = ` exports[`makeUniversalApp > no asar mode > identical app dirs with different macho files (e.g. do not shim, but still lipo) 1`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -630,13 +630,13 @@ exports[`makeUniversalApp no asar mode identical app dirs with different macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode identical app dirs with different macho files (e.g. do not shim, but still lipo) 2`] = ` exports[`makeUniversalApp > no asar mode > identical app dirs with different macho files (e.g. do not shim, but still lipo) 2`] = `
{ {
"Contents/Info.plist": {}, "Contents/Info.plist": {},
} }
`; `;
exports[`makeUniversalApp no asar mode identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 1`] = ` exports[`makeUniversalApp > no asar mode > identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 1`] = `
[ [
"hello-world", "hello-world",
"index.js", "index.js",
@@ -654,13 +654,13 @@ exports[`makeUniversalApp no asar mode identical app dirs with universal macho f
] ]
`; `;
exports[`makeUniversalApp no asar mode identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 2`] = ` exports[`makeUniversalApp > no asar mode > identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir) 2`] = `
{ {
"Contents/Info.plist": {}, "Contents/Info.plist": {},
} }
`; `;
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 1`] = ` exports[`makeUniversalApp > no asar mode > should correctly merge two identical app folders 1`] = `
[ [
"index.js", "index.js",
{ {
@@ -673,13 +673,13 @@ exports[`makeUniversalApp no asar mode should correctly merge two identical app
] ]
`; `;
exports[`makeUniversalApp no asar mode should correctly merge two identical app folders 2`] = ` exports[`makeUniversalApp > no asar mode > should correctly merge two identical app folders 2`] = `
{ {
"Contents/Info.plist": {}, "Contents/Info.plist": {},
} }
`; `;
exports[`makeUniversalApp no asar mode should shim two different app folders 1`] = ` exports[`makeUniversalApp > no asar mode > should shim two different app folders 1`] = `
{ {
"files": { "files": {
"index.js": { "index.js": {
@@ -708,13 +708,13 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 1`]
} }
`; `;
exports[`makeUniversalApp no asar mode should shim two different app folders 2`] = ` exports[`makeUniversalApp > no asar mode > should shim two different app folders 2`] = `
[ [
"private/var/i-aint-got-no-rhythm.bin", "private/var/i-aint-got-no-rhythm.bin",
] ]
`; `;
exports[`makeUniversalApp no asar mode should shim two different app folders 3`] = ` exports[`makeUniversalApp > no asar mode > should shim two different app folders 3`] = `
[ [
"index.js", "index.js",
{ {
@@ -732,7 +732,7 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 3`]
] ]
`; `;
exports[`makeUniversalApp no asar mode should shim two different app folders 4`] = ` exports[`makeUniversalApp > no asar mode > should shim two different app folders 4`] = `
[ [
"index.js", "index.js",
{ {
@@ -750,7 +750,7 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 4`]
] ]
`; `;
exports[`makeUniversalApp no asar mode should shim two different app folders 5`] = ` exports[`makeUniversalApp > no asar mode > should shim two different app folders 5`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {
@@ -761,7 +761,7 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 5`]
} }
`; `;
exports[`makeUniversalApp works for lipo binary resources 1`] = ` exports[`makeUniversalApp > works for lipo binary resources 1`] = `
{ {
"files": { "files": {
"hello-world": "<stripped>", "hello-world": "<stripped>",
@@ -820,15 +820,15 @@ exports[`makeUniversalApp works for lipo binary resources 1`] = `
} }
`; `;
exports[`makeUniversalApp works for lipo binary resources 2`] = `[]`; exports[`makeUniversalApp > works for lipo binary resources 2`] = `[]`;
exports[`makeUniversalApp works for lipo binary resources 3`] = ` exports[`makeUniversalApp > works for lipo binary resources 3`] = `
[ [
"hello-world", "hello-world",
] ]
`; `;
exports[`makeUniversalApp works for lipo binary resources 4`] = ` exports[`makeUniversalApp > works for lipo binary resources 4`] = `
{ {
"Contents/Info.plist": { "Contents/Info.plist": {
"Resources/app.asar": { "Resources/app.asar": {

View File

@@ -1,10 +1,11 @@
import * as path from 'path'; import * as path from 'node:path';
import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils'; import { describe, expect, it } from 'vitest';
import { describe, expect, it } from '@jest/globals';
const asarsPath = path.resolve(__dirname, 'fixtures', 'asars'); import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils.js';
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
const asarsPath = path.resolve(import.meta.dirname, 'fixtures', 'asars');
const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
describe('asar-utils', () => { describe('asar-utils', () => {
describe('detectAsarMode', () => { describe('detectAsarMode', () => {

View File

@@ -1,9 +1,10 @@
import * as path from 'path'; import * as path from 'node:path';
import { AppFile, AppFileType, getAllAppFiles } from '../src/file-utils'; import { beforeAll, describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from '@jest/globals';
const appsPath = path.resolve(__dirname, 'fixtures', 'apps'); import { AppFile, AppFileType, getAllAppFiles } from '../src/file-utils.js';
const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
describe('file-utils', () => { describe('file-utils', () => {
describe('getAllAppFiles', () => { describe('getAllAppFiles', () => {

View File

@@ -1,7 +1,8 @@
import { execFileSync } from 'child_process'; import { execFileSync } from 'node:child_process';
import * as fs from 'fs-extra'; import fs from 'node:fs';
import * as path from 'path'; import path from 'node:path';
import { appsDir, asarsDir, fixtureDir, templateApp } from './test/util';
import { appsDir, asarsDir, fixtureDir, templateApp } from './util.js';
// generates binaries from hello-world.c // generates binaries from hello-world.c
// hello-world-universal, hello-world-x86_64, hello-world-arm64 // hello-world-universal, hello-world-x86_64, hello-world-arm64
@@ -23,53 +24,59 @@ const generateMachO = () => {
}; };
export default async () => { export default async () => {
await fs.remove(appsDir); await fs.promises.rm(appsDir, { recursive: true, force: true });
await fs.mkdirp(appsDir); await fs.promises.mkdir(appsDir, { recursive: true });
// generate mach-o binaries to be leveraged in lipo tests // generate mach-o binaries to be leveraged in lipo tests
generateMachO(); generateMachO();
await templateApp('Arm64Asar.app', 'arm64', async (appPath) => { await templateApp('Arm64Asar.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app.asar'), path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
// contains `extra-file.txt` // contains `extra-file.txt`
await templateApp('Arm64AsarExtraFile.app', 'arm64', async (appPath) => { await templateApp('Arm64AsarExtraFile.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app2.asar'), path.resolve(asarsDir, 'app2.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
await templateApp('X64Asar.app', 'x64', async (appPath) => { await templateApp('X64Asar.app', 'x64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app.asar'), path.resolve(asarsDir, 'app.asar'),
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
await templateApp('Arm64NoAsar.app', 'arm64', async (appPath) => { await templateApp('Arm64NoAsar.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app'), path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'), path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
// contains `extra-file.txt` // contains `extra-file.txt`
await templateApp('Arm64NoAsarExtraFile.app', 'arm64', async (appPath) => { await templateApp('Arm64NoAsarExtraFile.app', 'arm64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app2'), path.resolve(asarsDir, 'app2'),
path.resolve(appPath, 'Contents', 'Resources', 'app'), path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
await templateApp('X64NoAsar.app', 'x64', async (appPath) => { await templateApp('X64NoAsar.app', 'x64', async (appPath) => {
await fs.copy( await fs.promises.cp(
path.resolve(asarsDir, 'app'), path.resolve(asarsDir, 'app'),
path.resolve(appPath, 'Contents', 'Resources', 'app'), path.resolve(appPath, 'Contents', 'Resources', 'app'),
{ recursive: true, verbatimSymlinks: true },
); );
}); });
}; };

View File

@@ -1,24 +1,27 @@
import * as fs from 'fs-extra'; import fs from 'node:fs';
import * as path from 'path'; import path from 'node:path';
import { makeUniversalApp } from '../dist/cjs/index'; import { afterEach, describe, expect, it } from 'vitest';
import { makeUniversalApp } from '../dist/index.js';
import { fsMove } from '../src/file-utils.js';
import { import {
createStagingAppDir, createStagingAppDir,
generateNativeApp, generateNativeApp,
templateApp, templateApp,
VERIFY_APP_TIMEOUT, VERIFY_APP_TIMEOUT,
verifyApp, verifyApp,
} from './util'; } from './util.js';
import { createPackage, createPackageWithOptions } from '@electron/asar'; import { createPackage, createPackageWithOptions } from '@electron/asar';
import { afterEach, describe, expect, it } from '@jest/globals';
const appsPath = path.resolve(__dirname, 'fixtures', 'apps'); const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
const appsOutPath = path.resolve(__dirname, 'fixtures', 'apps', 'out'); const appsOutPath = path.resolve(import.meta.dirname, 'fixtures', 'apps', 'out');
// See `jest.setup.ts` for app fixture setup process // See `globalSetup.ts` for app fixture setup process
describe('makeUniversalApp', () => { describe('makeUniversalApp', () => {
afterEach(async () => { afterEach(async () => {
await fs.emptyDir(appsOutPath); await fs.promises.rm(appsOutPath, { force: true, recursive: true });
await fs.promises.mkdir(appsOutPath, { recursive: true });
}); });
it('throws an error if asar is only detected in one arch', async () => { it('throws an error if asar is only detected in one arch', async () => {
@@ -34,9 +37,7 @@ describe('makeUniversalApp', () => {
); );
}); });
it( it('works for lipo binary resources', { timeout: VERIFY_APP_TIMEOUT }, async () => {
'works for lipo binary resources',
async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'LipoX64.app', appNameWithExtension: 'LipoX64.app',
arch: 'x64', arch: 'x64',
@@ -51,14 +52,12 @@ describe('makeUniversalApp', () => {
const out = path.resolve(appsOutPath, 'Lipo.app'); const out = path.resolve(appsOutPath, 'Lipo.app');
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out, mergeASARs: true }); await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out, mergeASARs: true });
await verifyApp(out, true); await verifyApp(out, true);
}, });
VERIFY_APP_TIMEOUT,
);
describe('force', () => { describe('force', () => {
it('throws an error if `out` bundle already exists and `force` is `false`', async () => { it('throws an error if `out` bundle already exists and `force` is `false`', async () => {
const out = path.resolve(appsOutPath, 'Error.app'); const out = path.resolve(appsOutPath, 'Error.app');
await fs.mkdirp(out); await fs.promises.mkdir(out, { recursive: true });
await expect( await expect(
makeUniversalApp({ makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'), x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
@@ -70,9 +69,10 @@ describe('makeUniversalApp', () => {
it( it(
'packages successfully if `out` bundle already exists and `force` is `true`', 'packages successfully if `out` bundle already exists and `force` is `true`',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'NoError.app'); const out = path.resolve(appsOutPath, 'NoError.app');
await fs.mkdirp(out); await fs.promises.mkdir(out, { recursive: true });
await makeUniversalApp({ await makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'), x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'), arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
@@ -81,14 +81,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
}); });
describe('asar mode', () => { describe('asar mode', () => {
it( it('should correctly merge two identical asars', { timeout: VERIFY_APP_TIMEOUT }, async () => {
'should correctly merge two identical asars',
async () => {
const out = path.resolve(appsOutPath, 'MergedAsar.app'); const out = path.resolve(appsOutPath, 'MergedAsar.app');
await makeUniversalApp({ await makeUniversalApp({
x64AppPath: path.resolve(appsPath, 'X64Asar.app'), x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
@@ -96,12 +93,11 @@ describe('makeUniversalApp', () => {
outAppPath: out, outAppPath: out,
}); });
await verifyApp(out); await verifyApp(out);
}, });
VERIFY_APP_TIMEOUT,
);
it( it(
'should create a shim if asars are different between architectures', 'should create a shim if asars are different between architectures',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'ShimmedAsar.app'); const out = path.resolve(appsOutPath, 'ShimmedAsar.app');
await makeUniversalApp({ await makeUniversalApp({
@@ -111,11 +107,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'should merge two different asars when `mergeASARs` is enabled', 'should merge two different asars when `mergeASARs` is enabled',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'MergedAsar.app'); const out = path.resolve(appsOutPath, 'MergedAsar.app');
await makeUniversalApp({ await makeUniversalApp({
@@ -127,11 +123,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique file', 'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique file',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'Error.app'); const out = path.resolve(appsOutPath, 'Error.app');
await expect( await expect(
@@ -144,17 +140,17 @@ describe('makeUniversalApp', () => {
}), }),
).rejects.toThrow(/Detected unique file "extra-file\.txt"/); ).rejects.toThrow(/Detected unique file "extra-file\.txt"/);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`', 'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => { const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => {
const { testPath } = await createStagingAppDir('Arm64-1'); const { testPath } = await createStagingAppDir('Arm64-1');
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar')); await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => { await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => {
await fs.move( await fsMove(
subArm64AppPath, subArm64AppPath,
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)), path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
); );
@@ -164,7 +160,7 @@ describe('makeUniversalApp', () => {
const { testPath } = await createStagingAppDir('X64-1'); const { testPath } = await createStagingAppDir('X64-1');
await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar')); await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => { await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => {
await fs.move( await fsMove(
subArm64AppPath, subArm64AppPath,
path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)), path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
); );
@@ -180,7 +176,6 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, },
VERIFY_APP_TIMEOUT,
); );
// TODO: Investigate if this should even be allowed. // TODO: Investigate if this should even be allowed.
@@ -188,6 +183,7 @@ describe('makeUniversalApp', () => {
// https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49 // https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
it.skip( it.skip(
'should shim asars with different unpacked dirs', 'should shim asars with different unpacked dirs',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => { const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
const { testPath } = await createStagingAppDir('UnpackedAppArm64'); const { testPath } = await createStagingAppDir('UnpackedAppArm64');
@@ -218,32 +214,32 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'should generate AsarIntegrity for all asars in the application', 'should generate AsarIntegrity for all asars in the application',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const { testPath } = await createStagingAppDir('app-2'); const { testPath } = await createStagingAppDir('app-2');
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar'); const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
await createPackage(testPath, testAsarPath); await createPackage(testPath, testAsarPath);
const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => { const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
); );
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'), path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
); );
}); });
const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => { const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
); );
await fs.copyFile( await fs.promises.copyFile(
testAsarPath, testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'), path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
); );
@@ -257,13 +253,13 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, },
VERIFY_APP_TIMEOUT,
); );
}); });
describe('no asar mode', () => { describe('no asar mode', () => {
it( it(
'should correctly merge two identical app folders', 'should correctly merge two identical app folders',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const out = path.resolve(appsOutPath, 'MergedNoAsar.app'); const out = path.resolve(appsOutPath, 'MergedNoAsar.app');
await makeUniversalApp({ await makeUniversalApp({
@@ -273,24 +269,27 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out); await verifyApp(out);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it('should shim two different app folders', { timeout: VERIFY_APP_TIMEOUT }, async () => {
'should shim two different app folders',
async () => {
const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => { const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => {
const { testPath } = await createStagingAppDir('shimArm64', { const { testPath } = await createStagingAppDir('shimArm64', {
'i-aint-got-no-rhythm.bin': 'boomshakalaka', 'i-aint-got-no-rhythm.bin': 'boomshakalaka',
}); });
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app')); await fs.promises.cp(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'), {
recursive: true,
verbatimSymlinks: true,
});
}); });
const x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => { const x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => {
const { testPath } = await createStagingAppDir('shimX64', { const { testPath } = await createStagingAppDir('shimX64', {
'hello-world.bin': 'Hello World', 'hello-world.bin': 'Hello World',
}); });
await fs.copy(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app')); await fs.promises.cp(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'), {
recursive: true,
verbatimSymlinks: true,
});
}); });
const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app'); const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app');
@@ -300,12 +299,11 @@ describe('makeUniversalApp', () => {
outAppPath, outAppPath,
}); });
await verifyApp(outAppPath); await verifyApp(outAppPath);
}, });
VERIFY_APP_TIMEOUT,
);
it( it(
'different app dirs with different macho files (shim and lipo)', 'different app dirs with different macho files (shim and lipo)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppX64-1.app', appNameWithExtension: 'DifferentMachoAppX64-1.app',
@@ -332,11 +330,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath, true); await verifyApp(outAppPath, true);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
"different app dirs with universal macho files (shim but don't lipo)", "different app dirs with universal macho files (shim but don't lipo)",
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentButUniversalMachoAppX64-2.app', appNameWithExtension: 'DifferentButUniversalMachoAppX64-2.app',
@@ -365,11 +363,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(outAppPath, true); await verifyApp(outAppPath, true);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'identical app dirs with different macho files (e.g. do not shim, but still lipo)', 'identical app dirs with different macho files (e.g. do not shim, but still lipo)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'DifferentMachoAppX64-2.app', appNameWithExtension: 'DifferentMachoAppX64-2.app',
@@ -390,11 +388,11 @@ describe('makeUniversalApp', () => {
}); });
await verifyApp(out, true); await verifyApp(out, true);
}, },
VERIFY_APP_TIMEOUT,
); );
it( it(
'identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir)', 'identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir)',
{ timeout: VERIFY_APP_TIMEOUT },
async () => { async () => {
const x64AppPath = await generateNativeApp({ const x64AppPath = await generateNativeApp({
appNameWithExtension: 'UniversalMachoAppX64.app', appNameWithExtension: 'UniversalMachoAppX64.app',
@@ -413,7 +411,6 @@ describe('makeUniversalApp', () => {
await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out }); await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out });
await verifyApp(out, true); await verifyApp(out, true);
}, },
VERIFY_APP_TIMEOUT,
); );
}); });
}); });

View File

@@ -1,11 +1,12 @@
import * as path from 'path'; import path from 'node:path';
import { sha } from '../src/sha'; import { describe, expect, it } from 'vitest';
import { describe, expect, it } from '@jest/globals';
import { sha } from '../src/sha.js';
describe('sha', () => { describe('sha', () => {
it('should correctly hash a file', async () => { it('should correctly hash a file', async () => {
expect(await sha(path.resolve(__dirname, 'fixtures', 'tohash'))).toEqual( expect(await sha(path.resolve(import.meta.dirname, 'fixtures', 'tohash'))).toEqual(
'12998c017066eb0d2a70b94e6ed3192985855ce390f321bbdb832022888bd251', '12998c017066eb0d2a70b94e6ed3192985855ce390f321bbdb832022888bd251',
); );
}); });

View File

@@ -1,29 +1,31 @@
import fs from 'node:fs';
import path from 'node:path';
import { createPackageWithOptions, getRawHeader } from '@electron/asar';
import { downloadArtifact } from '@electron/get'; import { downloadArtifact } from '@electron/get';
import { spawn } from '@malept/cross-spawn-promise'; import { spawn } from '@malept/cross-spawn-promise';
import * as zip from 'cross-zip'; import * as zip from 'cross-zip';
import * as fs from 'fs-extra';
import * as path from 'path';
import plist from 'plist'; import plist from 'plist';
import * as fileUtils from '../dist/cjs/file-utils';
import { createPackageWithOptions, getRawHeader } from '@electron/asar';
declare const expect: typeof import('@jest/globals').expect; import * as fileUtils from '../dist/file-utils.js';
// We do a LOT of verifications in `verifyApp` 😅 // We do a LOT of verifications in `verifyApp` 😅
// exec universal binary -> verify ALL asars -> verify ALL app dirs -> verify ALL asar integrity entries // exec universal binary -> verify ALL asars -> verify ALL app dirs -> verify ALL asar integrity entries
// plus some tests create fixtures at runtime // plus some tests create fixtures at runtime
export const VERIFY_APP_TIMEOUT = 80 * 1000; export const VERIFY_APP_TIMEOUT = 80 * 1000;
export const fixtureDir = path.resolve(__dirname, 'fixtures'); export const fixtureDir = path.resolve(import.meta.dirname, 'fixtures');
export const asarsDir = path.resolve(fixtureDir, 'asars'); export const asarsDir = path.resolve(fixtureDir, 'asars');
export const appsDir = path.resolve(fixtureDir, 'apps'); export const appsDir = path.resolve(fixtureDir, 'apps');
export const appsOutPath = path.resolve(appsDir, 'out'); export const appsOutPath = path.resolve(appsDir, 'out');
export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho = false) => { export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho = false) => {
const { expect } = await import('vitest');
await ensureUniversal(appPath); await ensureUniversal(appPath);
const resourcesDir = path.resolve(appPath, 'Contents', 'Resources'); const resourcesDir = path.resolve(appPath, 'Contents', 'Resources');
const resourcesDirContents = await fs.readdir(resourcesDir); const resourcesDirContents = await fs.promises.readdir(resourcesDir);
// sort for consistent result // sort for consistent result
const asars = resourcesDirContents.filter((p) => p.endsWith('.asar')).sort(); const asars = resourcesDirContents.filter((p) => p.endsWith('.asar')).sort();
@@ -79,12 +81,14 @@ export const verifyApp = async (appPath: string, containsRuntimeGeneratedMacho =
const extractAsarIntegrity = async (infoPlist: string) => { const extractAsarIntegrity = async (infoPlist: string) => {
const { ElectronAsarIntegrity: integrity, ...otherData } = plist.parse( const { ElectronAsarIntegrity: integrity, ...otherData } = plist.parse(
await fs.readFile(infoPlist, 'utf-8'), await fs.promises.readFile(infoPlist, 'utf-8'),
) as any; ) as any;
return integrity; return integrity;
}; };
export const verifyFileTree = async (dirPath: string) => { export const verifyFileTree = async (dirPath: string) => {
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);
@@ -98,6 +102,8 @@ export const verifyFileTree = async (dirPath: string) => {
}; };
export const ensureUniversal = async (app: string) => { export const ensureUniversal = async (app: string) => {
const { expect } = await import('vitest');
const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron'); const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron');
const result = await spawn(exe); const result = await spawn(exe);
expect(result).toContain('arm64'); expect(result).toContain('arm64');
@@ -162,15 +168,18 @@ export const createStagingAppDir = async (
) => { ) => {
const outDir = (testName || 'app') + Math.floor(Math.random() * 100); // tests run in parallel, randomize dir suffix to prevent naming collisions const outDir = (testName || 'app') + Math.floor(Math.random() * 100); // tests run in parallel, randomize dir suffix to prevent naming collisions
const testPath = path.join(appsDir, outDir); const testPath = path.join(appsDir, outDir);
await fs.remove(testPath); await fs.promises.rm(testPath, { recursive: true, force: true });
await fs.copy(path.join(asarsDir, 'app'), testPath); await fs.promises.cp(path.join(asarsDir, 'app'), testPath, {
recursive: true,
verbatimSymlinks: true,
});
const privateVarPath = path.join(testPath, 'private', 'var'); const privateVarPath = path.join(testPath, 'private', 'var');
const varPath = path.join(testPath, 'var'); const varPath = path.join(testPath, 'var');
await fs.mkdir(privateVarPath, { recursive: true }); await fs.promises.mkdir(privateVarPath, { recursive: true });
await fs.symlink(path.relative(testPath, privateVarPath), varPath); await fs.promises.symlink(path.relative(testPath, privateVarPath), varPath);
const files = { const files = {
'file.txt': 'hello world', 'file.txt': 'hello world',
@@ -178,11 +187,11 @@ export const createStagingAppDir = async (
}; };
for await (const [filename, fileData] of Object.entries(files)) { for await (const [filename, fileData] of Object.entries(files)) {
const originFilePath = path.join(varPath, filename); const originFilePath = path.join(varPath, filename);
await fs.writeFile(originFilePath, fileData); await fs.promises.writeFile(originFilePath, fileData);
} }
const appPath = path.join(varPath, 'app'); const appPath = path.join(varPath, 'app');
await fs.mkdirp(appPath); await fs.promises.mkdir(appPath, { recursive: true });
await fs.symlink('../file.txt', path.join(appPath, 'file.txt')); await fs.promises.symlink('../file.txt', path.join(appPath, 'file.txt'));
return { return {
testPath, testPath,
@@ -204,8 +213,11 @@ export const templateApp = async (
}); });
const appPath = path.resolve(appsDir, name); const appPath = path.resolve(appsDir, name);
zip.unzipSync(electronZip, appsDir); zip.unzipSync(electronZip, appsDir);
await fs.rename(path.resolve(appsDir, 'Electron.app'), appPath); await fs.promises.rename(path.resolve(appsDir, 'Electron.app'), appPath);
await fs.remove(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar')); await fs.promises.rm(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar'), {
recursive: true,
force: true,
});
await modify(appPath); await modify(appPath);
return appPath; return appPath;
@@ -229,22 +241,23 @@ export const generateNativeApp = async (options: {
const resources = path.join(appPath, 'Contents', 'Resources'); const resources = path.join(appPath, 'Contents', 'Resources');
const resourcesApp = path.resolve(resources, 'app'); const resourcesApp = path.resolve(resources, 'app');
if (!fs.existsSync(resourcesApp)) { if (!fs.existsSync(resourcesApp)) {
await fs.mkdir(resourcesApp); await fs.promises.mkdir(resourcesApp, { recursive: true });
} }
const { testPath } = await createStagingAppDir( const { testPath } = await createStagingAppDir(
path.basename(appNameWithExtension, '.app'), path.basename(appNameWithExtension, '.app'),
additionalFiles, additionalFiles,
); );
await fs.copy( await fs.promises.cp(
path.join(appsDir, `hello-world-${nativeModuleArch}`), path.join(appsDir, `hello-world-${nativeModuleArch}`),
path.join(testPath, 'hello-world'), path.join(testPath, 'hello-world'),
{ 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.copy(testPath, resourcesApp); await fs.promises.cp(testPath, resourcesApp, { recursive: true, verbatimSymlinks: true });
} }
}); });
return appPath; return appPath;

View File

@@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["src"]
}

View File

@@ -1,7 +1,20 @@
{ {
"extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"module": "commonjs",
"target": "es2017",
"lib": [
"es2017"
],
"sourceMap": true,
"strict": true,
"outDir": "entry-asar", "outDir": "entry-asar",
"types": [
"node",
],
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": false
}, },
"include": [ "include": [
"entry-asar" "entry-asar"

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm"
},
"include": ["src"]
}

View File

@@ -1,11 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"types": [
"jest"
]
},
"include": ["src"]
}

View File

@@ -1,23 +1,15 @@
{ {
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"module": "commonjs",
"target": "es2017",
"lib": [
"es2017"
],
"sourceMap": true, "sourceMap": true,
"strict": true, "outDir": "dist",
"outDir": "dist/cjs",
"types": [ "types": [
"node", "node",
], ],
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": true "declaration": true
}, },
"include": [ "include": [
"src", "src"
"entry-asar"
] ]
} }

7
vitest.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globalSetup: './test/globalSetup.ts',
},
});

3083
yarn.lock

File diff suppressed because it is too large Load Diff