Skip to content

Commit 8c7f685

Browse files
feat: add support for packaging fiddles as ASAR using @electron/asar (#138)
Co-authored-by: Erick Zhao <[email protected]>
1 parent ad34262 commit 8c7f685

File tree

8 files changed

+152
-14
lines changed

8 files changed

+152
-14
lines changed

etc/fiddle-core.api.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class Fiddle {
111111
export class FiddleFactory {
112112
constructor(fiddles?: string);
113113
// (undocumented)
114-
create(src: FiddleSource): Promise<Fiddle | undefined>;
114+
create(src: FiddleSource, options?: FiddleFactoryCreateOptions): Promise<Fiddle | undefined>;
115115
// (undocumented)
116116
fromEntries(src: Iterable<[string, string]>): Promise<Fiddle>;
117117
// (undocumented)
@@ -122,6 +122,12 @@ export class FiddleFactory {
122122
fromRepo(url: string, checkout?: string): Promise<Fiddle>;
123123
}
124124

125+
// @public (undocumented)
126+
export interface FiddleFactoryCreateOptions {
127+
// (undocumented)
128+
packAsAsar?: boolean;
129+
}
130+
125131
// @public
126132
export type FiddleSource = Fiddle | string | Iterable<[string, string]>;
127133

@@ -242,6 +248,8 @@ export interface RunnerOptions {
242248
// (undocumented)
243249
out?: Writable;
244250
// (undocumented)
251+
runFromAsar?: boolean;
252+
// (undocumented)
245253
showConfig?: boolean;
246254
}
247255

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"test:ci": "jest --runInBand --coverage"
3535
},
3636
"dependencies": {
37+
"@electron/asar": "^3.3.1",
3738
"@electron/get": "^2.0.0",
3839
"debug": "^4.3.3",
3940
"env-paths": "^2.2.1",

src/fiddle.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from 'fs-extra';
22
import * as path from 'path';
3+
import * as asar from '@electron/asar';
34
import debug from 'debug';
45
import simpleGit from 'simple-git';
56
import { createHash } from 'crypto';
@@ -31,6 +32,10 @@ export class Fiddle {
3132
*/
3233
export type FiddleSource = Fiddle | string | Iterable<[string, string]>;
3334

35+
export interface FiddleFactoryCreateOptions {
36+
packAsAsar?: boolean;
37+
}
38+
3439
export class FiddleFactory {
3540
constructor(private readonly fiddles: string = DefaultPaths.fiddles) {}
3641

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

98-
public async create(src: FiddleSource): Promise<Fiddle | undefined> {
99-
if (src instanceof Fiddle) return src;
103+
public async create(
104+
src: FiddleSource,
105+
options?: FiddleFactoryCreateOptions,
106+
): Promise<Fiddle | undefined> {
107+
let fiddle: Fiddle;
108+
if (src instanceof Fiddle) {
109+
fiddle = src;
110+
} else if (typeof src === 'string') {
111+
if (fs.existsSync(src)) {
112+
fiddle = await this.fromFolder(src);
113+
} else if (/^[0-9A-Fa-f]{32}$/.test(src)) {
114+
fiddle = await this.fromGist(src);
115+
} else if (/^https:/.test(src) || /\.git$/.test(src)) {
116+
fiddle = await this.fromRepo(src);
117+
} else {
118+
return;
119+
}
120+
} else {
121+
fiddle = await this.fromEntries(src as Iterable<[string, string]>);
122+
}
100123

101-
if (typeof src === 'string') {
102-
if (fs.existsSync(src)) return this.fromFolder(src);
103-
if (/^[0-9A-Fa-f]{32}$/.test(src)) return this.fromGist(src);
104-
if (/^https:/.test(src) || /\.git$/.test(src)) return this.fromRepo(src);
105-
return;
124+
const { packAsAsar } = options || {};
125+
if (packAsAsar) {
126+
fiddle = await this.packageFiddleAsAsar(fiddle);
106127
}
128+
return fiddle;
129+
}
130+
131+
private async packageFiddleAsAsar(fiddle: Fiddle): Promise<Fiddle> {
132+
const sourceDir = path.dirname(fiddle.mainPath);
133+
const asarOutputDir = path.join(this.fiddles, hashString(sourceDir));
134+
const asarFilePath = path.join(asarOutputDir, 'app.asar');
135+
136+
await asar.createPackage(sourceDir, asarFilePath);
137+
const packagedFiddle = new Fiddle(asarFilePath, fiddle.source);
107138

108-
return this.fromEntries(src);
139+
await fs.remove(sourceDir);
140+
return packagedFiddle;
109141
}
110142
}

src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
Mirrors,
1111
ProgressObject,
1212
} from './installer';
13-
import { Fiddle, FiddleFactory, FiddleSource } from './fiddle';
13+
import {
14+
Fiddle,
15+
FiddleFactory,
16+
FiddleSource,
17+
FiddleFactoryCreateOptions,
18+
} from './fiddle';
1419
import {
1520
BisectResult,
1621
Runner,
@@ -39,6 +44,7 @@ export {
3944
ElectronVersionsCreateOptions,
4045
Fiddle,
4146
FiddleFactory,
47+
FiddleFactoryCreateOptions,
4248
FiddleSource,
4349
InstallState,
4450
InstallStateEvent,

src/runner.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface RunnerOptions {
2222
out?: Writable;
2323
// whether to show config info (e.g. platform os & arch) in the log
2424
showConfig?: boolean;
25+
// whether to run the fiddle from asar
26+
runFromAsar?: boolean;
2527
}
2628

2729
const DefaultRunnerOpts: RunnerOptions = {
@@ -142,7 +144,9 @@ export class Runner {
142144
// process the input parameters
143145
opts = { ...DefaultRunnerOpts, ...opts };
144146
const version = versionIn instanceof SemVer ? versionIn.version : versionIn;
145-
const fiddle = await this.fiddleFactory.create(fiddleIn);
147+
const fiddle = await this.fiddleFactory.create(fiddleIn, {
148+
packAsAsar: opts.runFromAsar,
149+
});
146150
if (!fiddle) throw new Error(`Invalid fiddle: "${inspect(fiddleIn)}"`);
147151

148152
// set up the electron binary and the fiddle

tests/fiddle.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs-extra';
22
import * as os from 'os';
33
import * as path from 'path';
4+
import asar from '@electron/asar';
45

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

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

39+
// test that main.js file is created (not app.asar)
40+
expect(path.basename(fiddle!.mainPath)).toBe('main.js');
41+
3842
// test that the fiddle is kept in the fiddle cache
3943
expect(path.dirname(dirname)).toBe(fiddleDir);
4044

@@ -93,6 +97,41 @@ describe('FiddleFactory', () => {
9397
expect(fiddle).toBe(fiddleIn);
9498
});
9599

100+
it('packages fiddle into ASAR archive', async () => {
101+
const sourceDir = fiddleFixture('642fa8daaebea6044c9079e3f8a46390');
102+
const fiddle = await fiddleFactory.create(sourceDir, {
103+
packAsAsar: true,
104+
});
105+
106+
function normalizeAsarFiles(files: string[]): string[] {
107+
return files.map(
108+
(f) => f.replace(/^[\\/]/, ''), // Remove leading slash or backslash
109+
);
110+
}
111+
112+
// test that app.asar file is created
113+
expect(fiddle).toBeTruthy();
114+
expect(path.basename(fiddle!.mainPath)).toBe('app.asar');
115+
116+
// test that the file list is identical
117+
const dirname: string = fiddle!.mainPath;
118+
const sourceFiles = fs.readdirSync(sourceDir);
119+
const asarFiles = normalizeAsarFiles(
120+
asar.listPackage(dirname, { isPack: false }),
121+
);
122+
expect(asarFiles).toStrictEqual(sourceFiles);
123+
124+
// test that the files' contents are identical
125+
for (const file of sourceFiles) {
126+
const sourceFileContent = fs.readFileSync(
127+
path.join(sourceDir, file),
128+
'utf-8',
129+
);
130+
const asarFileContent = asar.extractFile(dirname, file).toString();
131+
expect(asarFileContent).toStrictEqual(sourceFileContent);
132+
}
133+
});
134+
96135
it.todo('reads fiddles from git repositories');
97136
it.todo('refreshes the cache if given a previously-cached git repository');
98137

tests/runner.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Installer, FiddleFactory, Runner, TestResult } from '../src/index';
1+
import {
2+
Installer,
3+
FiddleFactory,
4+
Runner,
5+
TestResult,
6+
FiddleFactoryCreateOptions,
7+
} from '../src/index';
28
import child_process from 'child_process';
39
import { EventEmitter } from 'events';
410
import * as fs from 'fs-extra';
@@ -58,7 +64,16 @@ async function createFakeRunner({
5864
install: jest.fn().mockResolvedValue(pathToExecutable),
5965
} as Pick<Installer, 'install'> as Installer,
6066
fiddleFactory: {
61-
create: jest.fn().mockResolvedValue(generatedFiddle),
67+
create: jest
68+
.fn()
69+
.mockImplementation((_, options?: FiddleFactoryCreateOptions) => {
70+
if (options?.packAsAsar)
71+
return Promise.resolve({
72+
...generatedFiddle,
73+
mainPath: '/path/to/fiddle/app.asar',
74+
});
75+
return Promise.resolve(generatedFiddle);
76+
}),
6277
} as Pick<FiddleFactory, 'create'> as FiddleFactory,
6378
paths: {
6479
versionsCache,
@@ -181,6 +196,25 @@ describe('Runner', () => {
181196
new Error(`Invalid fiddle: "'invalid-fiddle'"`),
182197
);
183198
});
199+
200+
it('spawns a subprocess with ASAR path when runFromAsar is true', async () => {
201+
const runner = await createFakeRunner({});
202+
(child_process.spawn as jest.Mock).mockReturnValueOnce(mockSubprocess);
203+
204+
await runner.spawn('12.0.1', '642fa8daaebea6044c9079e3f8a46390', {
205+
out: {
206+
write: mockStdout,
207+
} as Pick<Writable, 'write'> as Writable,
208+
runFromAsar: true,
209+
});
210+
211+
expect(child_process.spawn).toHaveBeenCalledTimes(1);
212+
expect(child_process.spawn).toHaveBeenCalledWith(
213+
'/path/to/electron/executable',
214+
['/path/to/fiddle/app.asar'],
215+
expect.anything(),
216+
);
217+
});
184218
});
185219

186220
describe('run()', () => {

yarn.lock

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,15 @@
575575
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
576576
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
577577

578+
"@electron/asar@^3.3.1":
579+
version "3.3.1"
580+
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.3.1.tgz#cd14e897770d9844673dd7c1dc8944e086e1e0ea"
581+
integrity sha512-WtpC/+34p0skWZiarRjLAyqaAX78DofhDxnREy/V5XHfu1XEXbFCSSMcDQ6hNCPJFaPy8/NnUgYuf9uiCkvKPg==
582+
dependencies:
583+
commander "^5.0.0"
584+
glob "^7.1.6"
585+
minimatch "^3.0.4"
586+
578587
"@electron/get@^2.0.0":
579588
version "2.0.0"
580589
resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.0.tgz#d991e68dc089fc66b521ec3ca4021515482bef91"
@@ -1733,6 +1742,11 @@ commander@^2.7.1:
17331742
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
17341743
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
17351744

1745+
commander@^5.0.0:
1746+
version "5.1.0"
1747+
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
1748+
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
1749+
17361750
compress-brotli@^1.3.8:
17371751
version "1.3.8"
17381752
resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.8.tgz#0c0a60c97a989145314ec381e84e26682e7b38db"
@@ -2362,7 +2376,7 @@ glob-parent@^6.0.2:
23622376
dependencies:
23632377
is-glob "^4.0.3"
23642378

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

0 commit comments

Comments
 (0)