Skip to content

feat: add support for packaging fiddles as ASAR using @electron/asar #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion etc/fiddle-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class Fiddle {
export class FiddleFactory {
constructor(fiddles?: string);
// (undocumented)
create(src: FiddleSource): Promise<Fiddle | undefined>;
create(src: FiddleSource, options?: FiddleFactoryCreateOptions): Promise<Fiddle | undefined>;
// (undocumented)
fromEntries(src: Iterable<[string, string]>): Promise<Fiddle>;
// (undocumented)
Expand All @@ -122,6 +122,12 @@ export class FiddleFactory {
fromRepo(url: string, checkout?: string): Promise<Fiddle>;
}

// @public (undocumented)
export interface FiddleFactoryCreateOptions {
// (undocumented)
packAsAsar?: boolean;
}

// @public
export type FiddleSource = Fiddle | string | Iterable<[string, string]>;

Expand Down Expand Up @@ -242,6 +248,8 @@ export interface RunnerOptions {
// (undocumented)
out?: Writable;
// (undocumented)
runFromAsar?: boolean;
// (undocumented)
showConfig?: boolean;
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"test:ci": "jest --runInBand --coverage"
},
"dependencies": {
"@electron/asar": "^3.3.1",
"@electron/get": "^2.0.0",
"debug": "^4.3.3",
"env-paths": "^2.2.1",
Expand Down
48 changes: 40 additions & 8 deletions src/fiddle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as asar from '@electron/asar';
import debug from 'debug';
import simpleGit from 'simple-git';
import { createHash } from 'crypto';
Expand Down Expand Up @@ -31,6 +32,10 @@ export class Fiddle {
*/
export type FiddleSource = Fiddle | string | Iterable<[string, string]>;

export interface FiddleFactoryCreateOptions {
packAsAsar?: boolean;
}

export class FiddleFactory {
constructor(private readonly fiddles: string = DefaultPaths.fiddles) {}

Expand Down Expand Up @@ -95,16 +100,43 @@ export class FiddleFactory {
return new Fiddle(path.join(folder, 'main.js'), 'entries');
}

public async create(src: FiddleSource): Promise<Fiddle | undefined> {
if (src instanceof Fiddle) return src;
public async create(
src: FiddleSource,
options?: FiddleFactoryCreateOptions,
): Promise<Fiddle | undefined> {
let fiddle: Fiddle;
if (src instanceof Fiddle) {
fiddle = src;
} else if (typeof src === 'string') {
if (fs.existsSync(src)) {
fiddle = await this.fromFolder(src);
} else if (/^[0-9A-Fa-f]{32}$/.test(src)) {
fiddle = await this.fromGist(src);
} else if (/^https:/.test(src) || /\.git$/.test(src)) {
fiddle = await this.fromRepo(src);
} else {
return;
}
} else {
fiddle = await this.fromEntries(src as Iterable<[string, string]>);
}

if (typeof src === 'string') {
if (fs.existsSync(src)) return this.fromFolder(src);
if (/^[0-9A-Fa-f]{32}$/.test(src)) return this.fromGist(src);
if (/^https:/.test(src) || /\.git$/.test(src)) return this.fromRepo(src);
return;
const { packAsAsar } = options || {};
if (packAsAsar) {
fiddle = await this.packageFiddleAsAsar(fiddle);
}
return fiddle;
}

private async packageFiddleAsAsar(fiddle: Fiddle): Promise<Fiddle> {
const sourceDir = path.dirname(fiddle.mainPath);
const asarOutputDir = path.join(this.fiddles, hashString(sourceDir));
const asarFilePath = path.join(asarOutputDir, 'app.asar');

await asar.createPackage(sourceDir, asarFilePath);
const packagedFiddle = new Fiddle(asarFilePath, fiddle.source);

return this.fromEntries(src);
await fs.remove(sourceDir);
return packagedFiddle;
}
}
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
Mirrors,
ProgressObject,
} from './installer';
import { Fiddle, FiddleFactory, FiddleSource } from './fiddle';
import {
Fiddle,
FiddleFactory,
FiddleSource,
FiddleFactoryCreateOptions,
} from './fiddle';
import {
BisectResult,
Runner,
Expand Down Expand Up @@ -39,6 +44,7 @@ export {
ElectronVersionsCreateOptions,
Fiddle,
FiddleFactory,
FiddleFactoryCreateOptions,
FiddleSource,
InstallState,
InstallStateEvent,
Expand Down
6 changes: 5 additions & 1 deletion src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface RunnerOptions {
out?: Writable;
// whether to show config info (e.g. platform os & arch) in the log
showConfig?: boolean;
// whether to run the fiddle from asar
runFromAsar?: boolean;
}

const DefaultRunnerOpts: RunnerOptions = {
Expand Down Expand Up @@ -142,7 +144,9 @@ export class Runner {
// process the input parameters
opts = { ...DefaultRunnerOpts, ...opts };
const version = versionIn instanceof SemVer ? versionIn.version : versionIn;
const fiddle = await this.fiddleFactory.create(fiddleIn);
const fiddle = await this.fiddleFactory.create(fiddleIn, {
packAsAsar: opts.runFromAsar,
});
if (!fiddle) throw new Error(`Invalid fiddle: "${inspect(fiddleIn)}"`);

// set up the electron binary and the fiddle
Expand Down
39 changes: 39 additions & 0 deletions tests/fiddle.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import asar from '@electron/asar';

import { Fiddle, FiddleFactory } from '../src/index';

Expand Down Expand Up @@ -35,6 +36,9 @@ describe('FiddleFactory', () => {
const dirname = path.dirname(fiddle!.mainPath);
expect(dirname).not.toEqual(sourceDir);

// test that main.js file is created (not app.asar)
expect(path.basename(fiddle!.mainPath)).toBe('main.js');

// test that the fiddle is kept in the fiddle cache
expect(path.dirname(dirname)).toBe(fiddleDir);

Expand Down Expand Up @@ -93,6 +97,41 @@ describe('FiddleFactory', () => {
expect(fiddle).toBe(fiddleIn);
});

it('packages fiddle into ASAR archive', async () => {
const sourceDir = fiddleFixture('642fa8daaebea6044c9079e3f8a46390');
const fiddle = await fiddleFactory.create(sourceDir, {
packAsAsar: true,
});

function normalizeAsarFiles(files: string[]): string[] {
return files.map(
(f) => f.replace(/^[\\/]/, ''), // Remove leading slash or backslash
);
}

// test that app.asar file is created
expect(fiddle).toBeTruthy();
expect(path.basename(fiddle!.mainPath)).toBe('app.asar');

// test that the file list is identical
const dirname: string = fiddle!.mainPath;
const sourceFiles = fs.readdirSync(sourceDir);
const asarFiles = normalizeAsarFiles(
asar.listPackage(dirname, { isPack: false }),
);
expect(asarFiles).toStrictEqual(sourceFiles);

// test that the files' contents are identical
for (const file of sourceFiles) {
const sourceFileContent = fs.readFileSync(
path.join(sourceDir, file),
'utf-8',
);
const asarFileContent = asar.extractFile(dirname, file).toString();
expect(asarFileContent).toStrictEqual(sourceFileContent);
}
});

it.todo('reads fiddles from git repositories');
it.todo('refreshes the cache if given a previously-cached git repository');

Expand Down
38 changes: 36 additions & 2 deletions tests/runner.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Installer, FiddleFactory, Runner, TestResult } from '../src/index';
import {
Installer,
FiddleFactory,
Runner,
TestResult,
FiddleFactoryCreateOptions,
} from '../src/index';
import child_process from 'child_process';
import { EventEmitter } from 'events';
import * as fs from 'fs-extra';
Expand Down Expand Up @@ -58,7 +64,16 @@ async function createFakeRunner({
install: jest.fn().mockResolvedValue(pathToExecutable),
} as Pick<Installer, 'install'> as Installer,
fiddleFactory: {
create: jest.fn().mockResolvedValue(generatedFiddle),
create: jest
.fn()
.mockImplementation((_, options?: FiddleFactoryCreateOptions) => {
if (options?.packAsAsar)
return Promise.resolve({
...generatedFiddle,
mainPath: '/path/to/fiddle/app.asar',
});
return Promise.resolve(generatedFiddle);
}),
} as Pick<FiddleFactory, 'create'> as FiddleFactory,
paths: {
versionsCache,
Expand Down Expand Up @@ -181,6 +196,25 @@ describe('Runner', () => {
new Error(`Invalid fiddle: "'invalid-fiddle'"`),
);
});

it('spawns a subprocess with ASAR path when runFromAsar is true', async () => {
const runner = await createFakeRunner({});
(child_process.spawn as jest.Mock).mockReturnValueOnce(mockSubprocess);

await runner.spawn('12.0.1', '642fa8daaebea6044c9079e3f8a46390', {
out: {
write: mockStdout,
} as Pick<Writable, 'write'> as Writable,
runFromAsar: true,
});

expect(child_process.spawn).toHaveBeenCalledTimes(1);
expect(child_process.spawn).toHaveBeenCalledWith(
'/path/to/electron/executable',
['/path/to/fiddle/app.asar'],
expect.anything(),
);
});
});

describe('run()', () => {
Expand Down
16 changes: 15 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,15 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==

"@electron/asar@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.3.1.tgz#cd14e897770d9844673dd7c1dc8944e086e1e0ea"
integrity sha512-WtpC/+34p0skWZiarRjLAyqaAX78DofhDxnREy/V5XHfu1XEXbFCSSMcDQ6hNCPJFaPy8/NnUgYuf9uiCkvKPg==
dependencies:
commander "^5.0.0"
glob "^7.1.6"
minimatch "^3.0.4"

"@electron/get@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.0.tgz#d991e68dc089fc66b521ec3ca4021515482bef91"
Expand Down Expand Up @@ -1733,6 +1742,11 @@ commander@^2.7.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==

commander@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==

compress-brotli@^1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.8.tgz#0c0a60c97a989145314ec381e84e26682e7b38db"
Expand Down Expand Up @@ -2362,7 +2376,7 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"

glob@^7.1.3:
glob@^7.1.3, glob@^7.1.6:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
Expand Down