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

View File

@@ -1,11 +1,13 @@
import asar from '@electron/asar';
import { execFileSync } from 'child_process';
import crypto from 'crypto';
import fs from 'fs-extra';
import path from 'path';
import { execFileSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import * as asar from '@electron/asar';
import { minimatch } from 'minimatch';
import os from 'os';
import { d } from './debug';
import { d } from './debug.js';
const LIPO = 'lipo';
@@ -40,7 +42,7 @@ export const detectAsarMode = async (appPath: string) => {
d('checking asar mode of', appPath);
const asarPath = path.resolve(appPath, 'Contents', 'Resources', 'app.asar');
if (!(await fs.pathExists(asarPath))) {
if (!fs.existsSync(asarPath)) {
d('determined no asar');
return AsarMode.NO_ASAR;
}
@@ -169,8 +171,8 @@ export const mergeASARs = async ({
// Extract both
//
const x64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'x64-'));
const arm64Dir = await fs.mkdtemp(path.join(os.tmpdir(), 'arm64-'));
const x64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'x64-'));
const arm64Dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'arm64-'));
try {
d(`extracting ${x64AsarPath} to ${x64Dir}`);
@@ -185,18 +187,22 @@ export const mergeASARs = async ({
if (isDirectory(arm64AsarPath, file)) {
d(`creating unique directory: ${file}`);
await fs.mkdirp(destination);
await fs.promises.mkdir(destination, { recursive: true });
continue;
}
d(`xopying unique file: ${file}`);
await fs.mkdirp(path.dirname(destination));
await fs.copy(source, destination);
d(`copying unique file: ${file}`);
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
await fs.promises.cp(source, destination, {
force: true,
recursive: true,
verbatimSymlinks: true,
});
}
for (const binding of commonBindings) {
const source = await fs.realpath(path.resolve(arm64Dir, binding));
const destination = await fs.realpath(path.resolve(x64Dir, binding));
const source = await fs.promises.realpath(path.resolve(arm64Dir, binding));
const destination = await fs.promises.realpath(path.resolve(x64Dir, binding));
d(`merging binding: ${binding}`);
execFileSync(LIPO, [source, destination, '-create', '-output', destination]);
@@ -219,7 +225,10 @@ export const mergeASARs = async ({
d('done merging');
} 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 * as fs from 'fs-extra';
import * as path from 'path';
import fs from 'node:fs';
import path from 'node:path';
import { promises as stream } from 'node:stream';
import { spawn, ExitCodeError } from '@malept/cross-spawn-promise';
const MACHO_PREFIX = 'Mach-O ';
export enum AppFileType {
@@ -27,11 +28,11 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
const visited = new Set<string>();
const traverse = async (p: string) => {
p = await fs.realpath(p);
p = await fs.promises.realpath(p);
if (visited.has(p)) return;
visited.add(p);
const info = await fs.stat(p);
const info = await fs.promises.stat(p);
if (info.isSymbolicLink()) return;
if (info.isFile()) {
let fileType = AppFileType.PLAIN;
@@ -63,7 +64,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
}
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));
}
}
@@ -83,3 +84,21 @@ export const readMachOHeader = async (path: string) => {
});
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 { spawn } from '@malept/cross-spawn-promise';
import * as dircompare from 'dir-compare';
import * as fs from 'fs-extra';
import { minimatch } from 'minimatch';
import * as os from 'os';
import * as path from 'path';
import * as plist from 'plist';
import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils';
import { AppFile, AppFileType, getAllAppFiles, readMachOHeader } from './file-utils';
import { sha } from './sha';
import { d } from './debug';
import { computeIntegrityData } from './integrity';
import { AsarMode, detectAsarMode, isUniversalMachO, mergeASARs } from './asar-utils.js';
import { AppFile, AppFileType, fsMove, getAllAppFiles, readMachOHeader } from './file-utils.js';
import { sha } from './sha.js';
import { d } from './debug.js';
import { computeIntegrityData } from './integrity.js';
/**
* 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))
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');
if (!opts.force) {
throw new Error(
@@ -96,7 +97,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
);
} else {
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)',
);
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);
try {
@@ -120,8 +121,8 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const uniqueToX64: string[] = [];
const uniqueToArm64: string[] = [];
const x64Files = await getAllAppFiles(await fs.realpath(tmpApp));
const arm64Files = await getAllAppFiles(await fs.realpath(opts.arm64AppPath));
const x64Files = await getAllAppFiles(await fs.promises.realpath(tmpApp));
const arm64Files = await getAllAppFiles(await fs.promises.realpath(opts.arm64AppPath));
for (const file of dupedFiles(x64Files)) {
if (!arm64Files.some((f) => f.relativePath === file.relativePath))
@@ -159,8 +160,10 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
}
const knownMergedMachOFiles = new Set();
for (const machOFile of x64Files.filter((f) => f.type === AppFileType.MACHO)) {
const first = await fs.realpath(path.resolve(tmpApp, machOFile.relativePath));
const second = await fs.realpath(path.resolve(opts.arm64AppPath, machOFile.relativePath));
const first = await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath));
const second = await fs.promises.realpath(
path.resolve(opts.arm64AppPath, machOFile.relativePath),
);
if (
isUniversalMachO(await readMachOHeader(first)) &&
@@ -201,7 +204,7 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
second,
'-create',
'-output',
await fs.realpath(path.resolve(tmpApp, machOFile.relativePath)),
await fs.promises.realpath(path.resolve(tmpApp, machOFile.relativePath)),
]);
knownMergedMachOFiles.add(machOFile.relativePath);
}
@@ -232,26 +235,34 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (nonMergedDifferences.length > 0) {
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-x64'),
);
await fs.copy(
await fs.promises.cp(
path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app'),
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64'),
{ force: true, recursive: true, verbatimSymlinks: true },
);
const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.mkdir(entryAsar);
await fs.copy(
path.resolve(__dirname, '..', '..', 'entry-asar', 'no-asar.js'),
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 = await fs.readJson(
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
let pj = JSON.parse(
await fs.promises.readFile(
path.resolve(opts.x64AppPath, 'Contents', 'Resources', 'app', 'package.json'),
'utf8',
),
);
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(
entryAsar,
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
@@ -288,19 +299,20 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
if (x64AsarSha !== arm64AsarSha) {
d('x64 and arm64 asars are different');
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');
if (await fs.pathExists(x64Unpacked)) {
await fs.move(
if (fs.existsSync(x64Unpacked)) {
await fsMove(
x64Unpacked,
path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar.unpacked'),
);
}
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'),
arm64AsarPath,
{ force: true, recursive: true, verbatimSymlinks: true },
);
const arm64Unpacked = path.resolve(
opts.arm64AppPath,
@@ -308,17 +320,18 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
'Resources',
'app.asar.unpacked',
);
if (await fs.pathExists(arm64Unpacked)) {
await fs.copy(
if (fs.existsSync(arm64Unpacked)) {
await fs.promises.cp(
arm64Unpacked,
path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar.unpacked'),
{ force: true, recursive: true, verbatimSymlinks: true },
);
}
const entryAsar = path.resolve(tmpDir, 'entry-asar');
await fs.mkdir(entryAsar);
await fs.copy(
path.resolve(__dirname, '..', '..', 'entry-asar', 'has-asar.js'),
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(
@@ -330,7 +343,11 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
).toString('utf8'),
);
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');
await asar.createPackage(entryAsar, asarPath);
} else {
@@ -346,10 +363,10 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath);
const { ElectronAsarIntegrity: x64Integrity, ...x64Plist } = plist.parse(
await fs.readFile(x64PlistPath, 'utf8'),
await fs.promises.readFile(x64PlistPath, 'utf8'),
) as any;
const { ElectronAsarIntegrity: arm64Integrity, ...arm64Plist } = plist.parse(
await fs.readFile(arm64PlistPath, 'utf8'),
await fs.promises.readFile(arm64PlistPath, 'utf8'),
) as any;
if (JSON.stringify(x64Plist) !== JSON.stringify(arm64Plist)) {
throw new Error(
@@ -364,23 +381,26 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
? { ...x64Plist, ElectronAsarIntegrity: generatedIntegrity }
: { ...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)) {
d('copying snapshot file', snapshotsFile.relativePath, 'to target application');
await fs.copy(
await fs.promises.cp(
path.resolve(opts.arm64AppPath, snapshotsFile.relativePath),
path.resolve(tmpApp, snapshotsFile.relativePath),
);
}
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]);
} catch (err) {
throw err;
} 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 path from 'path';
import { AppFileType, getAllAppFiles } from './file-utils';
import { sha } from './sha';
import { generateAsarIntegrity } from './asar-utils';
import fs from 'node:fs';
import path from 'node:path';
import { AppFileType, getAllAppFiles } from './file-utils.js';
import { generateAsarIntegrity } from './asar-utils.js';
type IntegrityMap = {
[filepath: string]: string;
@@ -18,7 +18,7 @@ export interface 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 resourcesPath = path.resolve(root, resourcesRelativePath);

View File

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