diff --git a/etc/fiddle-core.api.md b/etc/fiddle-core.api.md index 061d0bb..d288fb0 100644 --- a/etc/fiddle-core.api.md +++ b/etc/fiddle-core.api.md @@ -111,7 +111,7 @@ export class Fiddle { export class FiddleFactory { constructor(fiddles?: string); // (undocumented) - create(src: FiddleSource): Promise; + create(src: FiddleSource, options?: FiddleFactoryCreateOptions): Promise; // (undocumented) fromEntries(src: Iterable<[string, string]>): Promise; // (undocumented) @@ -122,6 +122,12 @@ export class FiddleFactory { fromRepo(url: string, checkout?: string): Promise; } +// @public (undocumented) +export interface FiddleFactoryCreateOptions { + // (undocumented) + packAsAsar?: boolean; +} + // @public export type FiddleSource = Fiddle | string | Iterable<[string, string]>; @@ -242,6 +248,8 @@ export interface RunnerOptions { // (undocumented) out?: Writable; // (undocumented) + runFromAsar?: boolean; + // (undocumented) showConfig?: boolean; } diff --git a/package.json b/package.json index 27fb1b7..377a288 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/fiddle.ts b/src/fiddle.ts index 17b8864..214b1db 100644 --- a/src/fiddle.ts +++ b/src/fiddle.ts @@ -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'; @@ -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) {} @@ -95,16 +100,43 @@ export class FiddleFactory { return new Fiddle(path.join(folder, 'main.js'), 'entries'); } - public async create(src: FiddleSource): Promise { - if (src instanceof Fiddle) return src; + public async create( + src: FiddleSource, + options?: FiddleFactoryCreateOptions, + ): Promise { + 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 { + 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; } } diff --git a/src/index.ts b/src/index.ts index ed47d46..dc3105d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, @@ -39,6 +44,7 @@ export { ElectronVersionsCreateOptions, Fiddle, FiddleFactory, + FiddleFactoryCreateOptions, FiddleSource, InstallState, InstallStateEvent, diff --git a/src/runner.ts b/src/runner.ts index f23ec19..2c593c1 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -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 = { @@ -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 diff --git a/tests/fiddle.test.ts b/tests/fiddle.test.ts index a352ad7..5f4a259 100644 --- a/tests/fiddle.test.ts +++ b/tests/fiddle.test.ts @@ -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'; @@ -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); @@ -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'); diff --git a/tests/runner.test.ts b/tests/runner.test.ts index 334d41e..77f86c9 100644 --- a/tests/runner.test.ts +++ b/tests/runner.test.ts @@ -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'; @@ -58,7 +64,16 @@ async function createFakeRunner({ install: jest.fn().mockResolvedValue(pathToExecutable), } as Pick 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 as FiddleFactory, paths: { versionsCache, @@ -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 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()', () => { diff --git a/yarn.lock b/yarn.lock index 098522e..b044274 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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==