Skip to content

Commit b6f0c88

Browse files
feat: bump minimum node version to 16 and add tests (#86)
BREAKING CHANGE: Minimum node version is now 16
1 parent bb304ce commit b6f0c88

18 files changed

+3622
-1463
lines changed

.circleci/config.yml

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
version: 2.1
22

33
orbs:
4-
cfa: continuousauth/npm@1.0.2
5-
node: electronjs/node@1.4.1
4+
cfa: continuousauth/npm@2.0.0
5+
node: electronjs/node@2.1.0
66

77
workflows:
88
test_and_release:
@@ -13,6 +13,7 @@ workflows:
1313
name: test-mac-<< matrix.node-version >>
1414
override-ci-command: yarn install --frozen-lockfile --ignore-engines
1515
test-steps:
16+
- node/install-rosetta
1617
- run: yarn build
1718
- run: yarn lint
1819
- run: yarn test
@@ -24,9 +25,6 @@ workflows:
2425
- 20.5.0
2526
- 18.17.0
2627
- 16.20.1
27-
- 14.21.3
28-
- 12.22.12
29-
- 10.24.1
3028
- cfa/release:
3129
requires:
3230
- test

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ dist
33
entry-asar/*.js*
44
entry-asar/*.ts
55
*.app
6+
test/fixtures/apps
7+
coverage

jest.config.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
transform: {
6+
'.': [
7+
'ts-jest',
8+
{
9+
tsconfig: 'tsconfig.jest.json'
10+
}
11+
]
12+
},
13+
globalSetup: './jest.setup.ts'
14+
};

jest.setup.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { downloadArtifact } from '@electron/get';
2+
import * as zip from 'cross-zip';
3+
import * as fs from 'fs-extra';
4+
import * as path from 'path';
5+
6+
const asarsDir = path.resolve(__dirname, 'test', 'fixtures', 'asars');
7+
const appsDir = path.resolve(__dirname, 'test', 'fixtures', 'apps');
8+
9+
const templateApp = async (
10+
name: string,
11+
arch: string,
12+
modify: (appPath: string) => Promise<void>,
13+
) => {
14+
const electronZip = await downloadArtifact({
15+
artifactName: 'electron',
16+
version: '27.0.0',
17+
platform: 'darwin',
18+
arch,
19+
});
20+
const appPath = path.resolve(appsDir, name);
21+
zip.unzipSync(electronZip, appsDir);
22+
await fs.rename(path.resolve(appsDir, 'Electron.app'), appPath);
23+
await fs.remove(path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar'));
24+
await modify(appPath);
25+
};
26+
27+
export default async () => {
28+
await fs.remove(appsDir);
29+
await fs.mkdirp(appsDir);
30+
await templateApp('Asar.app', 'arm64', async (appPath) => {
31+
await fs.copy(
32+
path.resolve(asarsDir, 'app.asar'),
33+
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
34+
);
35+
});
36+
37+
await templateApp('X64Asar.app', 'x64', async (appPath) => {
38+
await fs.copy(
39+
path.resolve(asarsDir, 'app.asar'),
40+
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
41+
);
42+
});
43+
44+
await templateApp('NoAsar.app', 'arm64', async (appPath) => {
45+
await fs.copy(
46+
path.resolve(asarsDir, 'app'),
47+
path.resolve(appPath, 'Contents', 'Resources', 'app'),
48+
);
49+
});
50+
51+
await templateApp('X64NoAsar.app', 'x64', async (appPath) => {
52+
await fs.copy(
53+
path.resolve(asarsDir, 'app'),
54+
path.resolve(appPath, 'Contents', 'Resources', 'app'),
55+
);
56+
});
57+
};

package.json

+29-20
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"url": "https://github.com/electron/universal.git"
1616
},
1717
"engines": {
18-
"node": ">=8.6"
18+
"node": ">=16.4"
1919
},
2020
"files": [
2121
"dist/*",
@@ -26,36 +26,45 @@
2626
"author": "Samuel Attard",
2727
"scripts": {
2828
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json",
29-
"lint": "prettier --check \"{src,entry-asar}/**/*.ts\"",
30-
"prettier:write": "prettier --write \"{src,entry-asar}/**/*.ts\"",
29+
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
30+
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
3131
"prepublishOnly": "npm run build",
32-
"test": "exit 0",
32+
"test": "jest",
3333
"prepare": "husky install"
3434
},
3535
"devDependencies": {
36-
"@continuous-auth/semantic-release-npm": "^3.0.0",
37-
"@types/debug": "^4.1.5",
38-
"@types/fs-extra": "^9.0.4",
39-
"@types/minimatch": "^3.0.5",
40-
"@types/node": "^14.14.7",
41-
"@types/plist": "^3.0.2",
42-
"husky": "^8.0.0",
43-
"lint-staged": "^10.5.1",
44-
"prettier": "^2.1.2",
45-
"typescript": "^4.0.5"
36+
"@continuous-auth/semantic-release-npm": "^4.0.0",
37+
"@electron/get": "^3.0.0",
38+
"@types/cross-zip": "^4.0.1",
39+
"@types/debug": "^4.1.10",
40+
"@types/fs-extra": "^11.0.3",
41+
"@types/jest": "^29.5.7",
42+
"@types/minimatch": "^5.1.2",
43+
"@types/node": "^20.8.10",
44+
"@types/plist": "^3.0.4",
45+
"cross-zip": "^4.0.0",
46+
"husky": "^8.0.3",
47+
"jest": "^29.7.0",
48+
"lint-staged": "^15.0.2",
49+
"prettier": "^3.0.3",
50+
"ts-jest": "^29.1.1",
51+
"typescript": "^5.2.2"
4652
},
4753
"dependencies": {
48-
"@electron/asar": "^3.2.1",
49-
"@malept/cross-spawn-promise": "^1.1.0",
54+
"@electron/asar": "^3.2.7",
55+
"@malept/cross-spawn-promise": "^2.0.0",
5056
"debug": "^4.3.1",
51-
"dir-compare": "^3.0.0",
52-
"fs-extra": "^9.0.1",
53-
"minimatch": "^3.0.4",
54-
"plist": "^3.0.4"
57+
"dir-compare": "^4.2.0",
58+
"fs-extra": "^11.1.1",
59+
"minimatch": "^9.0.3",
60+
"plist": "^3.1.0"
5561
},
5662
"lint-staged": {
5763
"*.ts": [
5864
"prettier --write"
5965
]
66+
},
67+
"resolutions": {
68+
"jackspeak": "2.1.1"
6069
}
6170
}

src/asar-utils.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execFileSync } from 'child_process';
33
import crypto from 'crypto';
44
import fs from 'fs-extra';
55
import path from 'path';
6-
import minimatch from 'minimatch';
6+
import { minimatch } from 'minimatch';
77
import os from 'os';
88
import { d } from './debug';
99

@@ -25,18 +25,15 @@ export type MergeASARsOptions = {
2525
// See: https://github.com/apple-opensource-mirror/llvmCore/blob/0c60489d96c87140db9a6a14c6e82b15f5e5d252/include/llvm/Object/MachOFormat.h#L108-L112
2626
const MACHO_MAGIC = new Set([
2727
// 32-bit Mach-O
28-
0xfeedface,
29-
0xcefaedfe,
28+
0xfeedface, 0xcefaedfe,
3029

3130
// 64-bit Mach-O
32-
0xfeedfacf,
33-
0xcffaedfe,
31+
0xfeedfacf, 0xcffaedfe,
3432
]);
3533

3634
const MACHO_UNIVERSAL_MAGIC = new Set([
3735
// universal
38-
0xcafebabe,
39-
0xbebafeca,
36+
0xcafebabe, 0xbebafeca,
4037
]);
4138

4239
export const detectAsarMode = async (appPath: string) => {

src/index.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { spawn } from '@malept/cross-spawn-promise';
22
import * as asar from '@electron/asar';
3-
import * as crypto from 'crypto';
43
import * as fs from 'fs-extra';
5-
import minimatch from 'minimatch';
4+
import { minimatch } from 'minimatch';
65
import * as os from 'os';
76
import * as path from 'path';
87
import * as plist from 'plist';
@@ -31,7 +30,7 @@ export type MakeUniversalOpts = {
3130
/**
3231
* Forcefully overwrite any existing files that are in the way of generating the universal application
3332
*/
34-
force: boolean;
33+
force?: boolean;
3534
/**
3635
* Merge x64 and arm64 ASARs into one.
3736
*/

src/sha.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import * as fs from 'fs-extra';
22
import * as crypto from 'crypto';
3+
import { pipeline } from 'stream/promises';
4+
35
import { d } from './debug';
46

57
export const sha = async (filePath: string) => {
68
d('hashing', filePath);
79
const hash = crypto.createHash('sha256');
810
hash.setEncoding('hex');
9-
const fileStream = fs.createReadStream(filePath);
10-
fileStream.pipe(hash);
11-
await new Promise((resolve, reject) => {
12-
fileStream.on('end', () => resolve());
13-
fileStream.on('error', (err) => reject(err));
14-
});
11+
await pipeline(fs.createReadStream(filePath), hash);
1512
return hash.read();
1613
};

test/asar-utils.spec.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as path from 'path';
2+
3+
import { AsarMode, detectAsarMode, generateAsarIntegrity } from '../src/asar-utils';
4+
5+
const asarsPath = path.resolve(__dirname, 'fixtures', 'asars');
6+
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
7+
8+
describe('asar-utils', () => {
9+
describe('detectAsarMode', () => {
10+
it('should correctly detect an asar enabled app', async () => {
11+
expect(await detectAsarMode(path.resolve(appsPath, 'Asar.app'))).toBe(AsarMode.HAS_ASAR);
12+
});
13+
14+
it('should correctly detect an app without an asar', async () => {
15+
expect(await detectAsarMode(path.resolve(appsPath, 'NoAsar.app'))).toBe(AsarMode.NO_ASAR);
16+
});
17+
});
18+
19+
describe('generateAsarIntegrity', () => {
20+
it('should deterministically hash an asar header', async () => {
21+
expect(generateAsarIntegrity(path.resolve(asarsPath, 'app.asar')).hash).toEqual(
22+
'85fff474383bd8df11cd9c5784e8fcd1525af71ff140a8a882e1dc9d5b39fcbf',
23+
);
24+
});
25+
});
26+
});

test/file-utils.spec.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as path from 'path';
2+
3+
import { AppFile, AppFileType, getAllAppFiles } from '../src/file-utils';
4+
5+
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
6+
7+
describe('file-utils', () => {
8+
describe('getAllAppFiles', () => {
9+
let asarFiles: AppFile[];
10+
let noAsarFiles: AppFile[];
11+
12+
beforeAll(async () => {
13+
asarFiles = await getAllAppFiles(path.resolve(appsPath, 'Asar.app'));
14+
noAsarFiles = await getAllAppFiles(path.resolve(appsPath, 'NoAsar.app'));
15+
});
16+
17+
it('should correctly identify plist files', async () => {
18+
expect(asarFiles.find((f) => f.relativePath === 'Contents/Info.plist')?.type).toBe(
19+
AppFileType.INFO_PLIST,
20+
);
21+
});
22+
23+
it('should correctly identify asar files as app code', async () => {
24+
expect(asarFiles.find((f) => f.relativePath === 'Contents/Resources/app.asar')?.type).toBe(
25+
AppFileType.APP_CODE,
26+
);
27+
});
28+
29+
it('should correctly identify non-asar code files as plain text', async () => {
30+
expect(
31+
noAsarFiles.find((f) => f.relativePath === 'Contents/Resources/app/index.js')?.type,
32+
).toBe(AppFileType.PLAIN);
33+
});
34+
35+
it('should correctly identify the Electron binary as Mach-O', async () => {
36+
expect(noAsarFiles.find((f) => f.relativePath === 'Contents/MacOS/Electron')?.type).toBe(
37+
AppFileType.MACHO,
38+
);
39+
});
40+
41+
it('should correctly identify the Electron Framework as Mach-O', async () => {
42+
expect(
43+
noAsarFiles.find(
44+
(f) =>
45+
f.relativePath ===
46+
'Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework',
47+
)?.type,
48+
).toBe(AppFileType.MACHO);
49+
});
50+
51+
it('should correctly identify the v8 context snapshot', async () => {
52+
expect(
53+
noAsarFiles.find(
54+
(f) =>
55+
f.relativePath ===
56+
'Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/v8_context_snapshot.arm64.bin',
57+
)?.type,
58+
).toBe(AppFileType.SNAPSHOT);
59+
});
60+
});
61+
});

test/fixtures/asars/app.asar

625 Bytes
Binary file not shown.

test/fixtures/asars/app/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console.log('I am an app folder', process.arch);
2+
process.exit(0);

test/fixtures/asars/app/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "app",
3+
"main": "index.js"
4+
}

test/fixtures/tohash

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello there

test/index.spec.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { spawn } from '@malept/cross-spawn-promise';
2+
import * as fs from 'fs-extra';
3+
import * as path from 'path';
4+
5+
import { makeUniversalApp } from '../src/index';
6+
7+
const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
8+
9+
async function ensureUniversal(app: string) {
10+
const exe = path.resolve(app, 'Contents', 'MacOS', 'Electron');
11+
const result = await spawn(exe);
12+
expect(result).toContain('arm64');
13+
const result2 = await spawn('arch', ['-x86_64', exe]);
14+
expect(result2).toContain('x64');
15+
}
16+
17+
describe('makeUniversalApp', () => {
18+
it('should correctly merge two identical asars', async () => {
19+
const out = path.resolve(appsPath, 'MergedAsar.app');
20+
await makeUniversalApp({
21+
x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
22+
arm64AppPath: path.resolve(appsPath, 'Asar.app'),
23+
outAppPath: out,
24+
});
25+
await ensureUniversal(out);
26+
// Only a single asar as they were identical
27+
expect(
28+
(await fs.readdir(path.resolve(out, 'Contents', 'Resources'))).filter((p) =>
29+
p.endsWith('asar'),
30+
),
31+
).toEqual(['app.asar']);
32+
}, 60000);
33+
34+
// TODO: Add tests for
35+
// * different asar files
36+
// * identical app dirs
37+
// * different app dirs
38+
// * different app dirs with different macho files
39+
// * identical app dirs with universal macho files
40+
});

0 commit comments

Comments
 (0)