Skip to content
Open
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
6 changes: 5 additions & 1 deletion src/command-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import debug from 'debug';

import { ElectronVersions } from './versions.js';
import { Fiddle, FiddleFactory } from './fiddle.js';
import { Runner } from './runner.js';
import { Runner } from './runner.js';

/**
* Handles command-line arguments, initializes objects, and executes commands based on input.
* Logs debug information and exits if invalid parameters are detected.
*/
export async function runFromCommandLine(argv: string[]): Promise<void> {
const d = debug('fiddle-core:runFromCommandLine');

Expand Down
52 changes: 44 additions & 8 deletions src/fiddle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ function hashString(str: string): string {
md5sum.update(str);
return md5sum.digest('hex');
}

/**
* A Fiddle instance, containing a main entry file and its source content.
*/
export class Fiddle {
constructor(
public readonly mainPath: string, // /path/to/main.js
/** Path to the main entry script file (e.g., `/path/to/main.js`). */
public readonly mainPath: string,

/** Source code for the Fiddle. */
public readonly source: string,
) {}

/** Deletes the Fiddle from the file system. */
public remove(): Promise<void> {
return fs.promises.rm(path.dirname(this.mainPath), {
recursive: true,
Expand All @@ -41,22 +47,31 @@ export interface FiddleFactoryCreateOptions {
packAsAsar?: boolean;
}

/**
* Factory class for creating Fiddle instances from different sources.
*/
export class FiddleFactory {
constructor(private readonly fiddles: string = DefaultPaths.fiddles) {}

/**
* Creates a Fiddle by fetching a GitHub Gist and cloning it into a temporary directory.
* @param gistId - The ID of the GitHub Gist to fetch.
*/
public async fromGist(gistId: string): Promise<Fiddle> {
return this.fromRepo(`https://gist.github.com/${gistId}.git`);
}

/**
* Creates a Fiddle by making a temporary copy from the specified source folder.
* @param source - The folder path containing the Fiddle source files.
*/
public async fromFolder(source: string): Promise<Fiddle> {
const d = debug('fiddle-core:FiddleFactory:fromFolder');

// make a tmp copy of this fiddle
const folder = path.join(this.fiddles, hashString(source));
d({ source, folder });
await fs.promises.rm(folder, { recursive: true, force: true });

// Disable asar in case any deps bundle Electron - ex. @electron/remote
const { noAsar } = process;
process.noAsar = true;
await fs.promises.cp(source, folder, { recursive: true });
Expand All @@ -65,12 +80,20 @@ export class FiddleFactory {
return new Fiddle(path.join(folder, 'main.js'), source);
}

/**
* Creates a Fiddle instance by cloning a Git repository into a temporary directory.
* Optionally checks out a specific branch and determines the main file path
* based on the cloned content.
*
* @param url - The Git repository URL to clone.
* @param checkout - The branch to check out (default is 'master').
* @returns A Promise that resolves to a Fiddle instance.
*/
public async fromRepo(url: string, checkout = 'master'): Promise<Fiddle> {
const d = debug('fiddle-core:FiddleFactory:fromRepo');
const folder = path.join(this.fiddles, hashString(url));
d({ url, checkout, folder });

// get the repo
if (!fs.existsSync(folder)) {
d(`cloning "${url}" into "${folder}"`);
const git = simpleGit();
Expand All @@ -83,20 +106,25 @@ export class FiddleFactory {

return new Fiddle(path.join(folder, 'main.js'), url);
}

/**
* Creates a Fiddle instance from an in-memory collection of files.
* Each entry consists of a filename and its corresponding file content.
* The files are saved to a temporary directory, and the main file path is set accordingly.
*
* @param src - An iterable of [filename, content] pairs.
* @returns A Promise that resolves to a Fiddle instance.
*/
public async fromEntries(src: Iterable<[string, string]>): Promise<Fiddle> {
const d = debug('fiddle-core:FiddleFactory:fromEntries');
const map = new Map<string, string>(src);

// make a name for the directory that will hold our temp copy of the fiddle
const md5sum = createHash('md5');
for (const content of map.values()) md5sum.update(content);
const hash = md5sum.digest('hex');
const folder = path.join(this.fiddles, hash);
await fs.promises.mkdir(folder, { recursive: true });
d({ folder });

// save content to that temp directory
await Promise.all(
[...map.entries()].map(([filename, content]) =>
util.promisify(fs.writeFile)(
Expand All @@ -110,6 +138,14 @@ export class FiddleFactory {
return new Fiddle(path.join(folder, 'main.js'), 'entries');
}

/**
* Determines the type of the provided source and delegates to the appropriate
* method to create a Fiddle instance.
*
* @param src - The source used to create the Fiddle. Can be an existing Fiddle instance,
* a local folder path, a Git repository URL, or a collection of file entries.
* @returns A Promise that resolves to a Fiddle instance or undefined if the source type is invalid.
*/
public async create(
src: FiddleSource,
options?: FiddleFactoryCreateOptions,
Expand Down
72 changes: 59 additions & 13 deletions src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { DefaultPaths, Paths } from './paths.js';
function getZipName(version: string): string {
return `electron-v${version}-${process.platform}-${process.arch}.zip`;
}

/** Tracks installation progress as a percentage. */
export type ProgressObject = { percent: number };

/**
Expand All @@ -22,30 +22,55 @@ export type ProgressObject = { percent: number };
* See Installer.on('state-changed') to watch for state changes.
*/
export enum InstallState {
/** No installation detected. */
missing = 'missing',

/** Files are being downloaded. */
downloading = 'downloading',

/** Download completed successfully. */
downloaded = 'downloaded',

/** Installation process is in progress. */
installing = 'installing',

/** Installation completed successfully. */
installed = 'installed',
}

/**
* Event emitted when an installation state changes for a specific Electron version.
*/
export interface InstallStateEvent {
/** Version identifier for the installation. */
version: string;
/** Current installation state. */
state: InstallState;
}

/**
* URLs for Electron mirrors used during downloads.
*/
export interface Mirrors {
/** Base URL for the Electron mirror. */
electronMirror: string;
/** Base URL for the Electron nightly mirror. */
electronNightlyMirror: string;
}

/**
* Configuration details for an Electron binary.
*/
export interface ElectronBinary {
/** Local path to the Electron binary. */
path: string;
alreadyExtracted: boolean; // to check if it's kept as zipped or not
/** Whether the binary has already been extracted. */
alreadyExtracted: boolean;
}

/**
* Parameters used when running an installer.
*/
export interface InstallerParams {
/** Callback invoked to report installation progress. */
progressCallback: (progress: ProgressObject) => void;
/** Mirror sources used for downloading. */
mirror: Mirrors;
}

Expand All @@ -70,7 +95,10 @@ export class Installer extends EventEmitter {
this.paths = Object.freeze({ ...DefaultPaths, ...pathsIn });
this.rebuildStates();
}

/**
* Returns the relative executable path based on the current platform.
* @param platform - Optional platform identifier; defaults to the host platform.
*/
public static execSubpath(platform: string = process.platform): string {
switch (platform) {
case 'darwin':
Expand All @@ -81,11 +109,17 @@ export class Installer extends EventEmitter {
return 'electron';
}
}

/**
* Resolves the full path to the Electron executable within a given folder.
* @param folder - Path to the Electron version folder.
*/
public static getExecPath(folder: string): string {
return path.join(folder, Installer.execSubpath());
}

/**
* Returns the current installation state for a given Electron version.
* @param version - Version identifier to check.
*/
public state(version: string): InstallState {
return this.stateMap.get(version) || InstallState.missing;
}
Expand Down Expand Up @@ -319,9 +353,15 @@ export class Installer extends EventEmitter {
};
}

/** map of version string to currently-running active Promise */
/**
* Tracks versions currently being downloaded, mapped to their active Promise.
*/
private downloading = new Map<string, Promise<ElectronBinary>>();

/**
* Ensures that the specified version of Electron is downloaded and ready for installation.
* @param version - Electron version to download.
* @param opts - Optional parameters such as progress callbacks or mirror configuration.
*/
public async ensureDownloaded(
version: string,
opts?: Partial<InstallerParams>,
Expand All @@ -337,9 +377,15 @@ export class Installer extends EventEmitter {
return promise;
}

/** keep a track of all currently installing versions */
/**
* Tracks all versions currently undergoing installation.
*/
private installing = new Set<string>();

/**
* Installs the specified Electron version after download.
* @param version - Electron version to install.
* @param opts - Optional installer configuration.
*/
public async install(
version: string,
opts?: Partial<InstallerParams>,
Expand Down
14 changes: 9 additions & 5 deletions src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,29 @@ import path from 'node:path';

import envPaths from 'env-paths';

/**
* Defines standard filesystem paths used by fiddle-core.
*/
export interface Paths {
// folder where electron zipfiles will be cached
/** Directory where Electron zip archives are cached. */
readonly electronDownloads: string;

// folder where an electron download will be unzipped to be run
/** Directory where Electron builds are extracted and executed. */
readonly electronInstall: string;

// folder where fiddles will be saved
/** Directory where user fiddles are stored. */
readonly fiddles: string;

// file where electron releases are cached
/** File path used to cache Electron release metadata. */
readonly versionsCache: string;
}

const paths = envPaths('fiddle-core', { suffix: '' });

/** Default set of resolved paths for fiddle-core operations. */
export const DefaultPaths: Paths = {
electronDownloads: path.join(paths.data, 'electron', 'zips'),
electronInstall: path.join(paths.data, 'electron', 'current'),
fiddles: path.join(paths.cache, 'fiddles'),
versionsCache: path.join(paths.cache, 'releases.json'),
};
};
Loading