From 6a930a09c2f97b29e56629b2ce2fbd8c259764f5 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sun, 16 Feb 2025 15:57:19 -0800 Subject: [PATCH 01/20] feat: allow usage of .cjs, .mjs, and type=module custom/generic publishers --- .changeset/hip-weeks-allow.md | 5 +++ .../src/publish/PublishManager.ts | 40 +++++++++---------- 2 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 .changeset/hip-weeks-allow.md diff --git a/.changeset/hip-weeks-allow.md b/.changeset/hip-weeks-allow.md new file mode 100644 index 00000000000..3fe2767f3e9 --- /dev/null +++ b/.changeset/hip-weeks-allow.md @@ -0,0 +1,5 @@ +--- +"app-builder-lib": minor +--- + +feat: allow usage of .cjs, .mjs, and type=module custom/generic publishers diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index 0584d8f69aa..76bca5b8e8b 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -38,6 +38,8 @@ import { PlatformPackager } from "../platformPackager" import { expandMacro } from "../util/macroExpander" import { WinPackager } from "../winPackager" import { createUpdateInfoTasks, UpdateInfoFileTask, writeUpdateInfoFiles } from "./updateInfoBuilder" +import { existsSync } from "fs-extra" +import { resolveModule } from "../util/resolve" const publishForPrWarning = "There are serious security concerns with PUBLISH_FOR_PULL_REQUEST=true (see the CircleCI documentation (https://circleci.com/docs/1.0/fork-pr-builds/) for details)" + @@ -135,7 +137,7 @@ export class PublishManager implements PublishContext { if (debug.enabled) { debug(`artifactCreated (isPublish: ${this.isPublish}): ${safeStringifyJson(event, new Set(["packager"]))},\n publishConfig: ${safeStringifyJson(publishConfiguration)}`) } - this.scheduleUpload(publishConfiguration, event, this.getAppInfo(event.packager)) + void this.scheduleUpload(publishConfiguration, event, this.getAppInfo(event.packager)) } }) } @@ -149,12 +151,12 @@ export class PublishManager implements PublishContext { return await resolvePublishConfigurations(publishers, null, this.packager, null, true) } - scheduleUpload(publishConfig: PublishConfiguration, event: UploadTask, appInfo: AppInfo): void { + async scheduleUpload(publishConfig: PublishConfiguration, event: UploadTask, appInfo: AppInfo): Promise { if (publishConfig.provider === "generic") { return } - const publisher = this.getOrCreatePublisher(publishConfig, appInfo) + const publisher = await this.getOrCreatePublisher(publishConfig, appInfo) if (publisher == null) { log.debug( { @@ -204,7 +206,7 @@ export class PublishManager implements PublishContext { break } - this.scheduleUpload(publishConfig, event, this.getAppInfo(platformPackager)) + await this.scheduleUpload(publishConfig, event, this.getAppInfo(platformPackager)) } } @@ -219,12 +221,12 @@ export class PublishManager implements PublishContext { } } - private getOrCreatePublisher(publishConfig: PublishConfiguration, appInfo: AppInfo): Publisher | null { + private async getOrCreatePublisher(publishConfig: PublishConfiguration, appInfo: AppInfo): Promise { // to not include token into cache key const providerCacheKey = safeStringifyJson(publishConfig) let publisher = this.nameToPublisher.get(providerCacheKey) if (publisher == null) { - publisher = createPublisher(this, appInfo.version, publishConfig, this.publishOptions, this.packager) + publisher = await createPublisher(this, appInfo.version, publishConfig, this.publishOptions, this.packager) this.nameToPublisher.set(providerCacheKey, publisher) log.info({ publisher: publisher!.toString() }, "publishing") } @@ -297,7 +299,7 @@ export async function getPublishConfigsForUpdateInfo( return publishConfigs } -export function createPublisher(context: PublishContext, version: string, publishConfig: PublishConfiguration, options: PublishOptions, packager: Packager): Publisher | null { +export async function createPublisher(context: PublishContext, version: string, publishConfig: PublishConfiguration, options: PublishOptions, packager: Packager): Promise { if (debug.enabled) { debug(`Create publisher: ${safeStringifyJson(publishConfig)}`) } @@ -317,13 +319,13 @@ export function createPublisher(context: PublishContext, version: string, publis return null default: { - const clazz = requireProviderClass(provider, packager) + const clazz = await requireProviderClass(provider, packager) return clazz == null ? null : new clazz(context, publishConfig) } } } -function requireProviderClass(provider: string, packager: Packager): any | null { +async function requireProviderClass(provider: string, packager: Packager): Promise { switch (provider) { case "github": return GitHubPublisher @@ -347,18 +349,14 @@ function requireProviderClass(provider: string, packager: Packager): any | null return BitbucketPublisher default: { - const name = `electron-publisher-${provider}` - let module: any = null - try { - module = require(path.join(packager.buildResourcesDir, name + ".js")) - } catch (_ignored) { - log.debug({ path: path.join(packager.buildResourcesDir, name + ".js") }, "Unable to find publish provider in build resources") - } - - if (module == null) { - module = require(name) + const name = (ext: string) => `electron-publisher-${provider}.${ext}` + const validPublisherFiles = [".mjs", ".js", ".cjs"].map(ext => name(ext)) + for (const publisherFilename of validPublisherFiles) { + const potentialFile = path.join(packager.buildResourcesDir, publisherFilename) + if (existsSync(potentialFile)) { + return await resolveModule(packager.appInfo.type, potentialFile) + } } - return module.default || module } } } @@ -515,7 +513,7 @@ async function getResolvedPublishConfig( return options } - const providerClass = requireProviderClass(options.provider, packager) + const providerClass = await requireProviderClass(options.provider, packager) if (providerClass != null && providerClass.checkAndResolveOptions != null) { await providerClass.checkAndResolveOptions(options, channelFromAppVersion, errorIfCannot) return options From 45922e7934a6ef38f03f8d79b319b866edada82d Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sun, 16 Feb 2025 16:00:36 -0800 Subject: [PATCH 02/20] other awaits --- packages/app-builder-lib/src/index.ts | 2 +- packages/app-builder-lib/src/publish/PublishManager.ts | 8 +++++++- packages/electron-builder/src/publish.ts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/app-builder-lib/src/index.ts b/packages/app-builder-lib/src/index.ts index ea13f0be9aa..2f6db9f53d8 100644 --- a/packages/app-builder-lib/src/index.ts +++ b/packages/app-builder-lib/src/index.ts @@ -109,7 +109,7 @@ export function build(options: PackagerOptions & PublishOptions, packager: Packa } buildResult.artifactPaths.push(newArtifact) for (const publishConfiguration of publishConfigurations) { - publishManager.scheduleUpload( + await publishManager.scheduleUpload( publishConfiguration, { file: newArtifact, diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index 76bca5b8e8b..e127f9363b0 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -299,7 +299,13 @@ export async function getPublishConfigsForUpdateInfo( return publishConfigs } -export async function createPublisher(context: PublishContext, version: string, publishConfig: PublishConfiguration, options: PublishOptions, packager: Packager): Promise { +export async function createPublisher( + context: PublishContext, + version: string, + publishConfig: PublishConfiguration, + options: PublishOptions, + packager: Packager +): Promise { if (debug.enabled) { debug(`Create publisher: ${safeStringifyJson(publishConfig)}`) } diff --git a/packages/electron-builder/src/publish.ts b/packages/electron-builder/src/publish.ts index d00afd0a485..14d7c06f975 100644 --- a/packages/electron-builder/src/publish.ts +++ b/packages/electron-builder/src/publish.ts @@ -96,7 +96,7 @@ async function publishPackageWithTasks( for (const newArtifact of uploadTasks) { for (const publishConfiguration of publishConfigurations) { - publishManager.scheduleUpload(publishConfiguration, newArtifact, appInfo) + await publishManager.scheduleUpload(publishConfiguration, newArtifact, appInfo) } } From 748e0c1806e6931ef06ddff8d8adfc14d5e20e20 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sun, 16 Feb 2025 16:06:44 -0800 Subject: [PATCH 03/20] add logging if no file found --- .../app-builder-lib/src/publish/PublishManager.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index e127f9363b0..2cdbf174182 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -355,14 +355,17 @@ async function requireProviderClass(provider: string, packager: Packager): Promi return BitbucketPublisher default: { - const name = (ext: string) => `electron-publisher-${provider}.${ext}` - const validPublisherFiles = [".mjs", ".js", ".cjs"].map(ext => name(ext)) - for (const publisherFilename of validPublisherFiles) { - const potentialFile = path.join(packager.buildResourcesDir, publisherFilename) + const extensions = [".mjs", ".js", ".cjs"] + const template = `electron-publisher-${provider}` + const name = (ext: string) => `${template}.${ext}` + + const validPublisherFiles = extensions.map(ext => path.join(packager.buildResourcesDir, name(ext))) + for (const potentialFile of validPublisherFiles) { if (existsSync(potentialFile)) { return await resolveModule(packager.appInfo.type, potentialFile) } } + log.warn({ path: log.filePath(packager.buildResourcesDir), template, extensionsChecked: extensions }, "unable to find publish provider in build resources") } } } From ad169e548d588392aa5f763a440a6738603d40d2 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sun, 16 Feb 2025 16:08:57 -0800 Subject: [PATCH 04/20] screw you changesets --- .changeset/hip-weeks-allow.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/hip-weeks-allow.md b/.changeset/hip-weeks-allow.md index 3fe2767f3e9..61b3e92eb45 100644 --- a/.changeset/hip-weeks-allow.md +++ b/.changeset/hip-weeks-allow.md @@ -1,5 +1,5 @@ --- -"app-builder-lib": minor +"app-builder-lib": patch --- -feat: allow usage of .cjs, .mjs, and type=module custom/generic publishers +fix: allow usage of .cjs, .mjs, and type=module custom/generic publishers From 272957e1c3f91c62467f8e2b54cb47912bd73fdb Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sun, 16 Feb 2025 16:19:26 -0800 Subject: [PATCH 05/20] fix test compiler issue --- test/src/ArtifactPublisherTest.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/src/ArtifactPublisherTest.ts b/test/src/ArtifactPublisherTest.ts index 7516eca301d..837c2bd6108 100644 --- a/test/src/ArtifactPublisherTest.ts +++ b/test/src/ArtifactPublisherTest.ts @@ -83,10 +83,10 @@ testAndIgnoreApiRate("GitHub upload", async () => { }) test.ifEnv(process.env.AWS_ACCESS_KEY_ID != null && process.env.AWS_SECRET_ACCESS_KEY != null)("S3 upload", async () => { - const publisher = createPublisher(publishContext, "0.0.1", { provider: "s3", bucket: "electron-builder-test" } as S3Options, {}, {} as any)! - await publisher.upload({ file: iconPath, arch: Arch.x64 }) + const publisher = await createPublisher(publishContext, "0.0.1", { provider: "s3", bucket: "electron-builder-test" } as S3Options, {}, {} as any) + await publisher!.upload({ file: iconPath, arch: Arch.x64 }) // test overwrite - await publisher.upload({ file: iconPath, arch: Arch.x64 }) + await publisher!.upload({ file: iconPath, arch: Arch.x64 }) }) test.ifEnv(process.env.DO_KEY_ID != null && process.env.DO_SECRET_KEY != null)("DO upload", async () => { @@ -95,10 +95,10 @@ test.ifEnv(process.env.DO_KEY_ID != null && process.env.DO_SECRET_KEY != null)(" name: "electron-builder-test", region: "nyc3", } - const publisher = createPublisher(publishContext, "0.0.1", configuration, {}, {} as any)! - await publisher.upload({ file: iconPath, arch: Arch.x64 }) + const publisher = await createPublisher(publishContext, "0.0.1", configuration, {}, {} as any) + await publisher!.upload({ file: iconPath, arch: Arch.x64 }) // test overwrite - await publisher.upload({ file: iconPath, arch: Arch.x64 }) + await publisher!.upload({ file: iconPath, arch: Arch.x64 }) }) testAndIgnoreApiRate("prerelease", async () => { From 70ed3792d6636a07f6ddde7e8f4b28621b1fe0f2 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 08:47:41 -0800 Subject: [PATCH 06/20] async event emitter typing complete --- packages/app-builder-lib/src/packager.ts | 22 +++++----- .../src/publish/PublishManager.ts | 5 +-- .../src/util/asyncEventEmitter.ts | 44 +++++++++++++++++++ 3 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 packages/app-builder-lib/src/util/asyncEventEmitter.ts diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index 67972d63780..369e9fb5f8f 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -15,7 +15,6 @@ import { TmpDir, } from "builder-util" import { CancellationToken } from "builder-util-runtime" -import { EventEmitter } from "events" import { chmod, mkdirs, outputFile } from "fs-extra" import * as isCI from "is-ci" import { Lazy } from "lazy-val" @@ -41,10 +40,7 @@ import { getRepositoryInfo } from "./util/repositoryInfo" import { resolveFunction } from "./util/resolve" import { installOrRebuild, nodeGypRebuild } from "./util/yarn" import { PACKAGE_VERSION } from "./version" - -function addHandler(emitter: EventEmitter, event: string, handler: (...args: Array) => void) { - emitter.on(event, handler) -} +import { AsyncEventEmitter } from "./util/asyncEventEmitter" async function createFrameworkInfo(configuration: Configuration, packager: Packager): Promise { let framework = configuration.framework @@ -72,6 +68,10 @@ async function createFrameworkInfo(configuration: Configuration, packager: Packa } } +type PackagerEvents = { + artifactCreated: (event: ArtifactCreated) => void | Promise +} + export class Packager { readonly projectDir: string @@ -110,7 +110,7 @@ export class Packager { isTwoPackageJsonProjectLayoutUsed = false - readonly eventEmitter = new EventEmitter() + readonly eventEmitter = new AsyncEventEmitter() _appInfo: AppInfo | null = null get appInfo(): AppInfo { @@ -260,8 +260,8 @@ export class Packager { this.afterPackHandlers.push(handler) } - artifactCreated(handler: (event: ArtifactCreated) => void): Packager { - addHandler(this.eventEmitter, "artifactCreated", handler) + artifactCreated(handler: (event: ArtifactCreated) => void | Promise): Packager { + this.eventEmitter.on("artifactCreated", handler) return this } @@ -283,8 +283,8 @@ export class Packager { /** * Only for sub artifacts (update info), for main artifacts use `callArtifactBuildCompleted`. */ - dispatchArtifactCreated(event: ArtifactCreated): void { - this.eventEmitter.emit("artifactCreated", event) + async dispatchArtifactCreated(event: ArtifactCreated): Promise { + await this.eventEmitter.emit("artifactCreated", event) } async callArtifactBuildCompleted(event: ArtifactCreated): Promise { @@ -293,7 +293,7 @@ export class Packager { await Promise.resolve(handler(event)) } - this.dispatchArtifactCreated(event) + await this.dispatchArtifactCreated(event) } async callAppxManifestCreated(path: string): Promise { diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index 2cdbf174182..4027dd227e8 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -1,4 +1,4 @@ -import { Arch, asArray, AsyncTaskManager, InvalidConfigurationError, isEmptyOrSpaces, isPullRequest, log, safeStringifyJson, serializeToYaml } from "builder-util" +import { Arch, asArray, AsyncTaskManager, exists, InvalidConfigurationError, isEmptyOrSpaces, isPullRequest, log, safeStringifyJson, serializeToYaml } from "builder-util" import { BitbucketOptions, CancellationToken, @@ -38,7 +38,6 @@ import { PlatformPackager } from "../platformPackager" import { expandMacro } from "../util/macroExpander" import { WinPackager } from "../winPackager" import { createUpdateInfoTasks, UpdateInfoFileTask, writeUpdateInfoFiles } from "./updateInfoBuilder" -import { existsSync } from "fs-extra" import { resolveModule } from "../util/resolve" const publishForPrWarning = @@ -361,7 +360,7 @@ async function requireProviderClass(provider: string, packager: Packager): Promi const validPublisherFiles = extensions.map(ext => path.join(packager.buildResourcesDir, name(ext))) for (const potentialFile of validPublisherFiles) { - if (existsSync(potentialFile)) { + if (await exists(potentialFile)) { return await resolveModule(packager.appInfo.type, potentialFile) } } diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts new file mode 100644 index 00000000000..692a73056cc --- /dev/null +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -0,0 +1,44 @@ +type Handler = (...args: any[]) => Promise | void + +export type EventMap = { + [key: string]: Handler +} + +interface TypedEventEmitter { + on(event: E, listener: Events[E]): this + off(event: E, listener: Events[E]): this + emit(event: E, ...args: Parameters): Promise | boolean +} + +export class AsyncEventEmitter implements TypedEventEmitter { + private readonly listeners: Map = new Map() + + on(event: E, listener: T[E]): this { + let listeners = this.listeners.get(event) + if (!listeners) { + listeners = [] + } + listeners.push(listener) + this.listeners.set(event, listeners) + return this + } + + off(event: E, listener: T[E]): this { + const listeners = this.listeners.get(event)?.filter(l => l !== listener) + if (!listeners?.length) { + this.listeners.delete(event) + return this + } + this.listeners.set(event, listeners) + return this + } + + async emit(event: E, ...args: Parameters): Promise { + const eventListeners = this.listeners.get(event) || [] + if (!eventListeners.length) { + return false + } + await Promise.all(eventListeners.map(listener => listener(...args))) + return true + } +} From 61495fecbb25dc8f0f85c57ae845ec5081c230aa Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 09:16:11 -0800 Subject: [PATCH 07/20] tmp save --- packages/app-builder-lib/src/configuration.ts | 16 ++-- packages/app-builder-lib/src/packager.ts | 83 +++++++++---------- .../app-builder-lib/src/platformPackager.ts | 2 +- .../src/publish/PublishManager.ts | 4 +- .../src/publish/updateInfoBuilder.ts | 4 +- .../src/targets/AppImageTarget.ts | 4 +- .../app-builder-lib/src/targets/AppxTarget.ts | 6 +- .../src/targets/ArchiveTarget.ts | 4 +- .../src/targets/FlatpakTarget.ts | 4 +- .../app-builder-lib/src/targets/FpmTarget.ts | 4 +- .../app-builder-lib/src/targets/MsiTarget.ts | 6 +- .../targets/differentialUpdateInfoBuilder.ts | 2 +- .../src/targets/nsis/NsisTarget.ts | 4 +- packages/app-builder-lib/src/targets/pkg.ts | 2 +- packages/app-builder-lib/src/targets/snap.ts | 4 +- .../src/util/asyncEventEmitter.ts | 13 ++- packages/app-builder-lib/src/util/resolve.ts | 1 + packages/dmg-builder/src/dmg.ts | 4 +- test/src/helpers/packTester.ts | 2 +- 19 files changed, 84 insertions(+), 85 deletions(-) diff --git a/packages/app-builder-lib/src/configuration.ts b/packages/app-builder-lib/src/configuration.ts index 37f55ca3189..215fa231a20 100644 --- a/packages/app-builder-lib/src/configuration.ts +++ b/packages/app-builder-lib/src/configuration.ts @@ -288,36 +288,36 @@ File `myBeforePackHook.js` in the project root directory: } ``` */ - readonly beforePack?: Hook | string | null + readonly beforePack?: Hook | string | null /** * The function (or path to file or module id) to be [run after the prebuilt Electron binary has been extracted to the output directory](#afterextract) * Same setup as {@link beforePack} */ - readonly afterExtract?: Hook | string | null + readonly afterExtract?: Hook | string | null /** * The function (or path to file or module id) to be [run after pack](#afterpack) (but before pack into distributable format and sign). * Same setup as {@link beforePack} */ - readonly afterPack?: Hook | string | null + readonly afterPack?: Hook | string | null /** * The function (or path to file or module id) to be [run after pack and sign](#aftersign) (but before pack into distributable format). * Same setup as {@link beforePack} */ - readonly afterSign?: Hook | string | null + readonly afterSign?: Hook | string | null /** * The function (or path to file or module id) to be run on artifact build start. * Same setup as {@link beforePack} */ - readonly artifactBuildStarted?: Hook | string | null + readonly artifactBuildStarted?: Hook | string | null /** * The function (or path to file or module id) to be run on artifact build completed. * Same setup as {@link beforePack} */ - readonly artifactBuildCompleted?: Hook | string | null + readonly artifactBuildCompleted?: Hook | string | null /** * The function (or path to file or module id) to be run after all artifacts are built. @@ -339,11 +339,11 @@ Configuration in the same way as `afterPack` (see above). /** * The function (or path to file or module id) to be run after MSI project created on disk - not packed into .msi package yet. */ - readonly msiProjectCreated?: Hook | string | null + readonly msiProjectCreated?: Hook | string | null /** * The function (or path to file or module id) to be run after Appx manifest created on disk - not packed into .appx package yet. */ - readonly appxManifestCreated?: Hook | string | null + readonly appxManifestCreated?: Hook | string | null /** * The function (or path to file or module id) to be [run on each node module](#onnodemodulefile) file. Returning `true`/`false` will determine whether to force include or to use the default copier logic */ diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index 369e9fb5f8f..e73e6741859 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -22,7 +22,7 @@ import { release as getOsRelease } from "os" import * as path from "path" import { AppInfo } from "./appInfo" import { readAsarJson } from "./asar/asar" -import { AfterPackContext, Configuration } from "./configuration" +import { AfterExtractContext, AfterPackContext, BeforePackContext, Configuration, Hook } from "./configuration" import { Platform, SourceRepositoryInfo, Target } from "./core" import { createElectronFrameworkSupport } from "./electron/ElectronFramework" import { Framework } from "./Framework" @@ -69,7 +69,18 @@ async function createFrameworkInfo(configuration: Configuration, packager: Packa } type PackagerEvents = { - artifactCreated: (event: ArtifactCreated) => void | Promise + artifactCreated: Hook + + beforePack: Hook + afterExtract: Hook + afterPack: Hook + + afterSign: Hook + artifactBuildStarted: Hook + artifactBuildCompleted: Hook + + msiProjectCreated: Hook + appxManifestCreated: Hook } export class Packager { @@ -121,8 +132,6 @@ export class Packager { private _repositoryInfo = new Lazy(() => getRepositoryInfo(this.projectDir, this.metadata, this.devMetadata)) - private readonly afterPackHandlers: Array<(context: AfterPackContext) => Promise | null> = [] - readonly options: PackagerOptions readonly debugLogger = new DebugLogger(log.isDebugEnabled) @@ -246,26 +255,27 @@ export class Packager { prepackaged: options.prepackaged == null ? null : path.resolve(this.projectDir, options.prepackaged), } - try { - log.info({ version: PACKAGE_VERSION, os: getOsRelease() }, "electron-builder") - } catch (e: any) { - // error in dev mode without babel - if (!(e instanceof ReferenceError)) { - throw e - } - } + log.info({ version: PACKAGE_VERSION, os: getOsRelease() }, "electron-builder") + } + + async addPackagerEventHandlers() { + this.eventEmitter.on("artifactBuildStarted", await resolveFunction(this.appInfo.type, this.config.artifactBuildStarted, "artifactBuildStarted")) + this.eventEmitter.on("artifactBuildCompleted", await resolveFunction(this.appInfo.type, this.config.artifactBuildCompleted, "artifactBuildCompleted")) + this.eventEmitter.on("appxManifestCreated", await resolveFunction(this.appInfo.type, this.config.appxManifestCreated, "appxManifestCreated")) + this.eventEmitter.on("msiProjectCreated", await resolveFunction(this.appInfo.type, this.config.msiProjectCreated, "msiProjectCreated")) } - addAfterPackHandler(handler: (context: AfterPackContext) => Promise | null) { - this.afterPackHandlers.push(handler) + onAfterPack(handler: PackagerEvents["afterPack"]): Packager { + this.eventEmitter.on("afterPack", handler) + return this } - artifactCreated(handler: (event: ArtifactCreated) => void | Promise): Packager { + onArtifactCreated(handler: PackagerEvents["artifactCreated"]): Packager { this.eventEmitter.on("artifactCreated", handler) return this } - async callArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any): Promise { + async emitArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any): Promise { log.info( logFields || { target: event.targetPresentableName, @@ -274,40 +284,27 @@ export class Packager { }, "building" ) - const handler = await resolveFunction(this.appInfo.type, this.config.artifactBuildStarted, "artifactBuildStarted") - if (handler != null) { - await Promise.resolve(handler(event)) - } + await this.eventEmitter.emit("artifactBuildStarted", event) } /** * Only for sub artifacts (update info), for main artifacts use `callArtifactBuildCompleted`. */ - async dispatchArtifactCreated(event: ArtifactCreated): Promise { + async emitArtifactCreated(event: ArtifactCreated): Promise { await this.eventEmitter.emit("artifactCreated", event) } - async callArtifactBuildCompleted(event: ArtifactCreated): Promise { - const handler = await resolveFunction(this.appInfo.type, this.config.artifactBuildCompleted, "artifactBuildCompleted") - if (handler != null) { - await Promise.resolve(handler(event)) - } - - await this.dispatchArtifactCreated(event) + async emitArtifactBuildCompleted(event: ArtifactCreated): Promise { + await this.eventEmitter.emit("artifactBuildCompleted", event) + await this.emitArtifactCreated(event) } - async callAppxManifestCreated(path: string): Promise { - const handler = await resolveFunction(this.appInfo.type, this.config.appxManifestCreated, "appxManifestCreated") - if (handler != null) { - await Promise.resolve(handler(path)) - } + async emitAppxManifestCreated(path: string): Promise { + await this.eventEmitter.emit("appxManifestCreated", path) } - async callMsiProjectCreated(path: string): Promise { - const handler = await resolveFunction(this.appInfo.type, this.config.msiProjectCreated, "msiProjectCreated") - if (handler != null) { - await Promise.resolve(handler(path)) - } + async emitMsiProjectCreated(path: string): Promise { + await this.eventEmitter.emit("msiProjectCreated", path) } async validateConfig(): Promise { @@ -383,7 +380,7 @@ export class Packager { // because artifact event maybe dispatched several times for different publish providers const artifactPaths = new Set() - this.artifactCreated(event => { + this.onArtifactCreated(event => { if (event.file != null) { artifactPaths.add(event.file) } @@ -549,15 +546,11 @@ export class Packager { } async afterPack(context: AfterPackContext): Promise { + await this.eventEmitter.emit("afterPack", context) const afterPack = await resolveFunction(this.appInfo.type, this.config.afterPack, "afterPack") - const handlers = this.afterPackHandlers.slice() if (afterPack != null) { // user handler should be last - handlers.push(afterPack) - } - - for (const handler of handlers) { - await Promise.resolve(handler(context)) + await Promise.resolve(afterPack(context)) } } } diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index aa85c765b47..39a75a5658a 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -155,7 +155,7 @@ export abstract class PlatformPackager } dispatchArtifactCreated(file: string, target: Target | null, arch: Arch | null, safeArtifactName?: string | null): Promise { - return this.info.callArtifactBuildCompleted({ + return this.info.emitArtifactBuildCompleted({ file, safeArtifactName, target, diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index 4027dd227e8..836186925d9 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -110,7 +110,7 @@ export class PublishManager implements PublishContext { ) } - packager.addAfterPackHandler(async event => { + packager.onAfterPack(async event => { const packager = event.packager if (event.electronPlatformName === "darwin") { if (!event.targets.some(it => it.name === "dmg" || it.name === "zip")) { @@ -128,7 +128,7 @@ export class PublishManager implements PublishContext { } }) - packager.artifactCreated(event => { + packager.onArtifactCreated(event => { const publishConfiguration = event.publishConfig if (publishConfiguration == null) { this.taskManager.addTask(this.artifactCreatedWithoutExplicitPublishConfig(event)) diff --git a/packages/app-builder-lib/src/publish/updateInfoBuilder.ts b/packages/app-builder-lib/src/publish/updateInfoBuilder.ts index 7a689621bae..76f3bce4213 100644 --- a/packages/app-builder-lib/src/publish/updateInfoBuilder.ts +++ b/packages/app-builder-lib/src/publish/updateInfoBuilder.ts @@ -228,7 +228,7 @@ export async function writeUpdateInfoFiles(updateInfoFileTasks: Array(["blockmap", "--input", file, "--output", blockMapFile]) - await packager.info.callArtifactBuildCompleted({ + await packager.info.emitArtifactBuildCompleted({ file: blockMapFile, safeArtifactName: safeArtifactName == null ? null : `${safeArtifactName}${BLOCK_MAP_FILE_SUFFIX}`, target, diff --git a/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts b/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts index 7a3e5693f5d..b53650e82a7 100644 --- a/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts +++ b/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts @@ -193,7 +193,7 @@ export class NsisTarget extends Target { logFields.perMachine = isPerMachine } - await packager.info.callArtifactBuildStarted( + await packager.info.emitArtifactBuildStarted( { targetPresentableName: this.name, file: installerPath, @@ -363,7 +363,7 @@ export class NsisTarget extends Target { updateInfo.isAdminRightsRequired = true } - await packager.info.callArtifactBuildCompleted({ + await packager.info.emitArtifactBuildCompleted({ file: installerPath, updateInfo, target: this, diff --git a/packages/app-builder-lib/src/targets/pkg.ts b/packages/app-builder-lib/src/targets/pkg.ts index 7aff9ee0163..6309ea3c9a2 100644 --- a/packages/app-builder-lib/src/targets/pkg.ts +++ b/packages/app-builder-lib/src/targets/pkg.ts @@ -46,7 +46,7 @@ export class PkgTarget extends Target { const artifactName = packager.expandArtifactNamePattern(options, "pkg", arch) const artifactPath = path.join(this.outDir, artifactName) - await packager.info.callArtifactBuildStarted({ + await packager.info.emitArtifactBuildStarted({ targetPresentableName: "pkg", file: artifactPath, arch, diff --git a/packages/app-builder-lib/src/targets/snap.ts b/packages/app-builder-lib/src/targets/snap.ts index 0c7f2464a00..eaf3e9359fe 100644 --- a/packages/app-builder-lib/src/targets/snap.ts +++ b/packages/app-builder-lib/src/targets/snap.ts @@ -180,7 +180,7 @@ export default class SnapTarget extends Target { // tslint:disable-next-line:no-invalid-template-strings const artifactName = packager.expandArtifactNamePattern(this.options, "snap", arch, "${name}_${version}_${arch}.${ext}", false) const artifactPath = path.join(this.outDir, artifactName) - await packager.info.callArtifactBuildStarted({ + await packager.info.emitArtifactBuildStarted({ targetPresentableName: "snap", file: artifactPath, arch, @@ -253,7 +253,7 @@ export default class SnapTarget extends Target { const publishConfig = findSnapPublishConfig(this.packager.config) - await packager.info.callArtifactBuildCompleted({ + await packager.info.emitArtifactBuildCompleted({ file: artifactPath, safeArtifactName: packager.computeSafeArtifactName(artifactName, "snap", arch, false), target: this, diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index 692a73056cc..197cd3047c5 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -1,3 +1,5 @@ +import { Nullish } from "builder-util-runtime" + type Handler = (...args: any[]) => Promise | void export type EventMap = { @@ -5,15 +7,18 @@ export type EventMap = { } interface TypedEventEmitter { - on(event: E, listener: Events[E]): this - off(event: E, listener: Events[E]): this + on(event: E, listener: Events[E] | Nullish): this + off(event: E, listener: Events[E] | Nullish): this emit(event: E, ...args: Parameters): Promise | boolean } export class AsyncEventEmitter implements TypedEventEmitter { private readonly listeners: Map = new Map() - on(event: E, listener: T[E]): this { + on(event: E, listener: T[E] | Nullish): this { + if (!listener) { + return this + } let listeners = this.listeners.get(event) if (!listeners) { listeners = [] @@ -23,7 +28,7 @@ export class AsyncEventEmitter implements TypedEventEmitter< return this } - off(event: E, listener: T[E]): this { + off(event: E, listener: T[E] | Nullish): this { const listeners = this.listeners.get(event)?.filter(l => l !== listener) if (!listeners?.length) { this.listeners.delete(event) diff --git a/packages/app-builder-lib/src/util/resolve.ts b/packages/app-builder-lib/src/util/resolve.ts index 68c82618151..a96a7352d2b 100644 --- a/packages/app-builder-lib/src/util/resolve.ts +++ b/packages/app-builder-lib/src/util/resolve.ts @@ -24,6 +24,7 @@ export async function resolveModule(type: string | undefined, name: string): export async function resolveFunction(type: string | undefined, executor: T | string, name: string): Promise { if (executor == null || typeof executor !== "string") { + // is already function or explicitly ignored by user return executor } diff --git a/packages/dmg-builder/src/dmg.ts b/packages/dmg-builder/src/dmg.ts index f89b5504a51..1892c669d77 100644 --- a/packages/dmg-builder/src/dmg.ts +++ b/packages/dmg-builder/src/dmg.ts @@ -36,7 +36,7 @@ export class DmgTarget extends Target { packager.platformSpecificBuildOptions.defaultArch ) const artifactPath = path.join(this.outDir, artifactName) - await packager.info.callArtifactBuildStarted({ + await packager.info.emitArtifactBuildStarted({ targetPresentableName: "DMG", file: artifactPath, arch, @@ -84,7 +84,7 @@ export class DmgTarget extends Target { const safeArtifactName = packager.computeSafeArtifactName(artifactName, "dmg") const updateInfo = this.options.writeUpdateInfo === false ? null : await createBlockmap(artifactPath, this, packager, safeArtifactName) - await packager.info.callArtifactBuildCompleted({ + await packager.info.emitArtifactBuildCompleted({ file: artifactPath, safeArtifactName, target: this, diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 336ed3daac6..0fd639f9c0f 100644 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -180,7 +180,7 @@ async function packAndCheck(packagerOptions: PackagerOptions, checkOptions: Asse const publishManager = new PublishManager(packager, { publish: "publish" in checkOptions ? checkOptions.publish : "never" }) const artifacts: Map> = new Map() - packager.artifactCreated(event => { + packager.onArtifactCreated(event => { if (event.file == null) { return } From ec355b94a3b5887b1da07630245a8b89116bd0c3 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 09:33:29 -0800 Subject: [PATCH 08/20] adding priority ordering to async event emitter --- packages/app-builder-lib/src/packager.ts | 12 +++-- .../app-builder-lib/src/platformPackager.ts | 19 ++++---- .../src/util/asyncEventEmitter.ts | 44 ++++++++++++++----- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index e73e6741859..401dd0b32de 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -259,10 +259,14 @@ export class Packager { } async addPackagerEventHandlers() { - this.eventEmitter.on("artifactBuildStarted", await resolveFunction(this.appInfo.type, this.config.artifactBuildStarted, "artifactBuildStarted")) - this.eventEmitter.on("artifactBuildCompleted", await resolveFunction(this.appInfo.type, this.config.artifactBuildCompleted, "artifactBuildCompleted")) - this.eventEmitter.on("appxManifestCreated", await resolveFunction(this.appInfo.type, this.config.appxManifestCreated, "appxManifestCreated")) - this.eventEmitter.on("msiProjectCreated", await resolveFunction(this.appInfo.type, this.config.msiProjectCreated, "msiProjectCreated")) + this.eventEmitter.on("artifactBuildStarted", await resolveFunction(this.appInfo.type, this.config.artifactBuildStarted, "artifactBuildStarted"), "user") + this.eventEmitter.on("artifactBuildCompleted", await resolveFunction(this.appInfo.type, this.config.artifactBuildCompleted, "artifactBuildCompleted"), "user") + this.eventEmitter.on("appxManifestCreated", await resolveFunction(this.appInfo.type, this.config.appxManifestCreated, "appxManifestCreated"), "user") + this.eventEmitter.on("msiProjectCreated", await resolveFunction(this.appInfo.type, this.config.msiProjectCreated, "msiProjectCreated"), "user") + + this.eventEmitter.on("beforePack", await resolveFunction(this.appInfo.type, this.config.beforePack, "beforePack"), "user") + this.eventEmitter.on("afterSign", await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign"), "user") + } onAfterPack(handler: PackagerEvents["afterPack"]): Packager { diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index 39a75a5658a..6919e196e27 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -237,17 +237,14 @@ export abstract class PlatformPackager const { outDir, appOutDir, platformName, arch, platformSpecificBuildOptions, targets, options } = packOptions - const beforePack = await resolveFunction(this.appInfo.type, this.config.beforePack, "beforePack") - if (beforePack != null) { - await beforePack({ - appOutDir, - outDir, - arch, - targets, - packager: this, - electronPlatformName: platformName, - }) - } + await this.info.eventEmitter.emit("beforePack", { + appOutDir, + outDir, + arch, + targets, + packager: this, + electronPlatformName: platformName, + }) await this.info.installAppDependencies(this.platform, arch) diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index 197cd3047c5..2a5db2788bd 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -1,21 +1,26 @@ -import { Nullish } from "builder-util-runtime" +import { CancellationToken, Nullish } from "builder-util-runtime" type Handler = (...args: any[]) => Promise | void +type HandlerType = "system" | "user" + +type Handle = { handler: Handler; type: HandlerType } + export type EventMap = { - [key: string]: Handler + [key: string]: Handle } interface TypedEventEmitter { - on(event: E, listener: Events[E] | Nullish): this - off(event: E, listener: Events[E] | Nullish): this - emit(event: E, ...args: Parameters): Promise | boolean + on(event: E, listener: Events[E]["handler"] | Nullish, priority: HandlerType): this + off(event: E, listener: Events[E]["handler"] | Nullish): this + emit(event: E, ...args: Parameters): Promise | boolean } export class AsyncEventEmitter implements TypedEventEmitter { - private readonly listeners: Map = new Map() + private readonly listeners: Map = new Map() + private readonly cancellationToken = new CancellationToken() - on(event: E, listener: T[E] | Nullish): this { + on(event: E, listener: T[E]["handler"] | Nullish, priority: HandlerType = "system"): this { if (!listener) { return this } @@ -23,13 +28,13 @@ export class AsyncEventEmitter implements TypedEventEmitter< if (!listeners) { listeners = [] } - listeners.push(listener) + listeners.push({ handler: listener, type: priority }) this.listeners.set(event, listeners) return this } - off(event: E, listener: T[E] | Nullish): this { - const listeners = this.listeners.get(event)?.filter(l => l !== listener) + off(event: E, listener: T[E]["handler"] | Nullish): this { + const listeners = this.listeners.get(event)?.filter(l => l.handler !== listener) if (!listeners?.length) { this.listeners.delete(event) return this @@ -38,12 +43,27 @@ export class AsyncEventEmitter implements TypedEventEmitter< return this } - async emit(event: E, ...args: Parameters): Promise { + async emit(event: E, ...args: Parameters): Promise { const eventListeners = this.listeners.get(event) || [] if (!eventListeners.length) { return false } - await Promise.all(eventListeners.map(listener => listener(...args))) + const emitInternal = async (listeners: Handle[]) => { + for (const listener of listeners) { + if (this.cancellationToken.cancelled) { + return false + } + await listener.handler(...args) + } + return true + } + if (!(await emitInternal(eventListeners.filter(l => l.type === "system")))) { + return false + } + // user handlers are always last + if (!(await emitInternal(eventListeners.filter(l => l.type === "user")))) { + return false + } return true } } From f6dd8e170cc735631456cf3afde68f3ee83b72ac Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 13:26:32 -0800 Subject: [PATCH 09/20] register all "user" event handlers at beginning of `build` (after `_appInfo` is created). clear listeners after build. enable listeners to be added as promises --- packages/app-builder-lib/src/macPackager.ts | 2 +- packages/app-builder-lib/src/packager.ts | 59 ++++++++++++------- .../app-builder-lib/src/platformPackager.ts | 35 +++++------ .../src/util/asyncEventEmitter.ts | 52 +++++++++++----- 4 files changed, 89 insertions(+), 59 deletions(-) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index 17f1d77cc18..2f39263e28e 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -173,7 +173,7 @@ export class MacPackager extends PlatformPackager { packager: this, electronPlatformName: platformName, } - await this.info.afterPack(packContext) + await this.info.emitAfterPack(packContext) if (this.info.cancellationToken.cancelled) { return diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index 401dd0b32de..f8f13a1730f 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -40,7 +40,7 @@ import { getRepositoryInfo } from "./util/repositoryInfo" import { resolveFunction } from "./util/resolve" import { installOrRebuild, nodeGypRebuild } from "./util/yarn" import { PACKAGE_VERSION } from "./version" -import { AsyncEventEmitter } from "./util/asyncEventEmitter" +import { AsyncEventEmitter, HandlerType } from "./util/asyncEventEmitter" async function createFrameworkInfo(configuration: Configuration, packager: Packager): Promise { let framework = configuration.framework @@ -121,7 +121,7 @@ export class Packager { isTwoPackageJsonProjectLayoutUsed = false - readonly eventEmitter = new AsyncEventEmitter() + private readonly eventEmitter = new AsyncEventEmitter() _appInfo: AppInfo | null = null get appInfo(): AppInfo { @@ -258,15 +258,15 @@ export class Packager { log.info({ version: PACKAGE_VERSION, os: getOsRelease() }, "electron-builder") } - async addPackagerEventHandlers() { - this.eventEmitter.on("artifactBuildStarted", await resolveFunction(this.appInfo.type, this.config.artifactBuildStarted, "artifactBuildStarted"), "user") - this.eventEmitter.on("artifactBuildCompleted", await resolveFunction(this.appInfo.type, this.config.artifactBuildCompleted, "artifactBuildCompleted"), "user") - this.eventEmitter.on("appxManifestCreated", await resolveFunction(this.appInfo.type, this.config.appxManifestCreated, "appxManifestCreated"), "user") - this.eventEmitter.on("msiProjectCreated", await resolveFunction(this.appInfo.type, this.config.msiProjectCreated, "msiProjectCreated"), "user") + addPackagerEventHandlers() { + this.eventEmitter.on("artifactBuildStarted", resolveFunction(this.appInfo.type, this.config.artifactBuildStarted, "artifactBuildStarted"), "user") + this.eventEmitter.on("artifactBuildCompleted", resolveFunction(this.appInfo.type, this.config.artifactBuildCompleted, "artifactBuildCompleted"), "user") - this.eventEmitter.on("beforePack", await resolveFunction(this.appInfo.type, this.config.beforePack, "beforePack"), "user") - this.eventEmitter.on("afterSign", await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign"), "user") + this.eventEmitter.on("appxManifestCreated", resolveFunction(this.appInfo.type, this.config.appxManifestCreated, "appxManifestCreated"), "user") + this.eventEmitter.on("msiProjectCreated", resolveFunction(this.appInfo.type, this.config.msiProjectCreated, "msiProjectCreated"), "user") + this.eventEmitter.on("beforePack", resolveFunction(this.appInfo.type, this.config.beforePack, "beforePack"), "user") + this.eventEmitter.on("afterSign", resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign"), "user") } onAfterPack(handler: PackagerEvents["afterPack"]): Packager { @@ -279,6 +279,10 @@ export class Packager { return this } + filterEventListeners(event: keyof PackagerEvents, type: HandlerType | undefined) { + return this.eventEmitter.filterListeners(event, type) + } + async emitArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any): Promise { log.info( logFields || { @@ -294,23 +298,39 @@ export class Packager { /** * Only for sub artifacts (update info), for main artifacts use `callArtifactBuildCompleted`. */ - async emitArtifactCreated(event: ArtifactCreated): Promise { + async emitArtifactCreated(event: ArtifactCreated) { await this.eventEmitter.emit("artifactCreated", event) } - async emitArtifactBuildCompleted(event: ArtifactCreated): Promise { + async emitArtifactBuildCompleted(event: ArtifactCreated) { await this.eventEmitter.emit("artifactBuildCompleted", event) await this.emitArtifactCreated(event) } - async emitAppxManifestCreated(path: string): Promise { + async emitAppxManifestCreated(path: string) { await this.eventEmitter.emit("appxManifestCreated", path) } - async emitMsiProjectCreated(path: string): Promise { + async emitMsiProjectCreated(path: string) { await this.eventEmitter.emit("msiProjectCreated", path) } + async emitBeforePack(context: BeforePackContext) { + await this.eventEmitter.emit("beforePack", context) + } + + async emitAfterPack(context: AfterPackContext) { + await this.eventEmitter.emit("afterPack", context) + } + + async emitAfterSign(context: AfterPackContext) { + await this.eventEmitter.emit("afterSign", context) + } + + async emitAfterExtract(context: AfterPackContext) { + await this.eventEmitter.emit("afterExtract", context) + } + async validateConfig(): Promise { let configPath: string | null = null let configFromOptions = this.options.config @@ -367,6 +387,8 @@ export class Packager { } this._appInfo = new AppInfo(this, null) + this.addPackagerEventHandlers() + this._framework = await createFrameworkInfo(this.config, this) const commonOutDirWithoutPossibleOsMacro = path.resolve( @@ -396,6 +418,8 @@ export class Packager { await this.debugLogger.save(path.join(commonOutDirWithoutPossibleOsMacro, "builder-debug.yml")) } + this.eventEmitter.clear() + const toDispose = this.toDispose.slice() this.toDispose.length = 0 for (const disposer of toDispose) { @@ -548,15 +572,6 @@ export class Packager { }) } } - - async afterPack(context: AfterPackContext): Promise { - await this.eventEmitter.emit("afterPack", context) - const afterPack = await resolveFunction(this.appInfo.type, this.config.afterPack, "afterPack") - if (afterPack != null) { - // user handler should be last - await Promise.resolve(afterPack(context)) - } - } } function createOutDirIfNeed(targetList: Array, createdOutDirs: Set): Promise { diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index 6919e196e27..bd92cf60b99 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -46,7 +46,6 @@ import { import { executeAppBuilderAsJson } from "./util/appBuilder" import { computeFileSets, computeNodeModuleFileSets, copyAppFiles, ELECTRON_COMPILE_SHIM_FILENAME, transformFiles } from "./util/appFileCopier" import { expandMacro as doExpandMacro } from "./util/macroExpander" -import { resolveFunction } from "./util/resolve" export type DoPackOptions = { outDir: string @@ -237,7 +236,7 @@ export abstract class PlatformPackager const { outDir, appOutDir, platformName, arch, platformSpecificBuildOptions, targets, options } = packOptions - await this.info.eventEmitter.emit("beforePack", { + await this.info.emitBeforePack({ appOutDir, outDir, arch, @@ -271,17 +270,14 @@ export abstract class PlatformPackager version: framework.version, }) - const afterExtract = await resolveFunction(this.appInfo.type, this.config.afterExtract, "afterExtract") - if (afterExtract != null) { - await afterExtract({ - appOutDir, - outDir, - arch, - targets, - packager: this, - electronPlatformName: platformName, - }) - } + await this.info.emitAfterExtract({ + appOutDir, + outDir, + arch, + targets, + packager: this, + electronPlatformName: platformName, + }) const excludePatterns: Array = [] @@ -352,7 +348,7 @@ export abstract class PlatformPackager return } - await this.info.afterPack(packContext) + await this.info.emitAfterPack(packContext) if (framework.afterPack != null) { await framework.afterPack(packContext) @@ -449,13 +445,10 @@ export abstract class PlatformPackager electronPlatformName: platformName, } const didSign = await this.signApp(packContext, isAsar) - const afterSign = await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign") - if (afterSign != null) { - if (didSign) { - await Promise.resolve(afterSign(packContext)) - } else { - log.warn(null, `skipping "afterSign" hook as no signing occurred, perhaps you intended "afterPack"?`) - } + if (didSign) { + await this.info.emitAfterSign(packContext) + } else if (this.info.filterEventListeners("afterSign", "user").length) { + log.warn(null, `skipping "afterSign" hook as no signing occurred, perhaps you intended "afterPack"?`) } } diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index 2a5db2788bd..84c660a0634 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -1,26 +1,31 @@ +import { log } from "builder-util"; import { CancellationToken, Nullish } from "builder-util-runtime" type Handler = (...args: any[]) => Promise | void -type HandlerType = "system" | "user" +export type HandlerType = "system" | "user" -type Handle = { handler: Handler; type: HandlerType } +type Handle = { handler: Handler | Promise; type: HandlerType } + +export type Listener = Promise | T[E] | Nullish export type EventMap = { - [key: string]: Handle + [key: string]: Handler } interface TypedEventEmitter { - on(event: E, listener: Events[E]["handler"] | Nullish, priority: HandlerType): this - off(event: E, listener: Events[E]["handler"] | Nullish): this - emit(event: E, ...args: Parameters): Promise | boolean + on(event: E, listener: Events[E] | Nullish, type: HandlerType): this + off(event: E, listener: Events[E] | Nullish): this + emit(event: E, ...args: Parameters): Promise<{ emittedSystem: boolean; emittedUser: boolean }> + filterListeners(event: E, type: HandlerType): Handle[] + clear(): void } export class AsyncEventEmitter implements TypedEventEmitter { private readonly listeners: Map = new Map() private readonly cancellationToken = new CancellationToken() - on(event: E, listener: T[E]["handler"] | Nullish, priority: HandlerType = "system"): this { + on(event: E, listener: T[E] | Promise | Nullish, type: HandlerType = "system"): this { if (!listener) { return this } @@ -28,12 +33,12 @@ export class AsyncEventEmitter implements TypedEventEmitter< if (!listeners) { listeners = [] } - listeners.push({ handler: listener, type: priority }) + listeners.push({ handler: listener, type }) this.listeners.set(event, listeners) return this } - off(event: E, listener: T[E]["handler"] | Nullish): this { + off(event: E, listener: T[E] | Nullish): this { const listeners = this.listeners.get(event)?.filter(l => l.handler !== listener) if (!listeners?.length) { this.listeners.delete(event) @@ -43,27 +48,44 @@ export class AsyncEventEmitter implements TypedEventEmitter< return this } - async emit(event: E, ...args: Parameters): Promise { + async emit(event: E, ...args: Parameters): Promise<{ emittedSystem: boolean; emittedUser: boolean }> { + const result = { emittedSystem: false, emittedUser: false } + const eventListeners = this.listeners.get(event) || [] if (!eventListeners.length) { - return false + log.debug({ event }, "no event listeners found") + return result } const emitInternal = async (listeners: Handle[]) => { for (const listener of listeners) { if (this.cancellationToken.cancelled) { return false } - await listener.handler(...args) + await ( + await listener.handler + )?.(...args) } return true } if (!(await emitInternal(eventListeners.filter(l => l.type === "system")))) { - return false + result.emittedSystem = true } // user handlers are always last if (!(await emitInternal(eventListeners.filter(l => l.type === "user")))) { - return false + result.emittedUser = true } - return true + return result + } + + filterListeners(event: E, type: HandlerType | undefined): Handle[] { + const listeners = this.listeners.get(event) ?? [] + if (type) { + return listeners.filter(l => l.type === type) + } + return listeners + } + + clear() { + this.listeners.clear() } } From 15cb285115e87767b9b24323b52314c512aa021f Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 14:45:26 -0800 Subject: [PATCH 10/20] cleanup --- packages/app-builder-lib/src/packager.ts | 2 -- packages/app-builder-lib/src/publish/PublishManager.ts | 4 ++-- packages/app-builder-lib/src/util/asyncEventEmitter.ts | 5 ----- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index f8f13a1730f..fc2010961e5 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -418,8 +418,6 @@ export class Packager { await this.debugLogger.save(path.join(commonOutDirWithoutPossibleOsMacro, "builder-debug.yml")) } - this.eventEmitter.clear() - const toDispose = this.toDispose.slice() this.toDispose.length = 0 for (const disposer of toDispose) { diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index 836186925d9..e66db426e76 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -128,7 +128,7 @@ export class PublishManager implements PublishContext { } }) - packager.onArtifactCreated(event => { + packager.onArtifactCreated(async event => { const publishConfiguration = event.publishConfig if (publishConfiguration == null) { this.taskManager.addTask(this.artifactCreatedWithoutExplicitPublishConfig(event)) @@ -136,7 +136,7 @@ export class PublishManager implements PublishContext { if (debug.enabled) { debug(`artifactCreated (isPublish: ${this.isPublish}): ${safeStringifyJson(event, new Set(["packager"]))},\n publishConfig: ${safeStringifyJson(publishConfiguration)}`) } - void this.scheduleUpload(publishConfiguration, event, this.getAppInfo(event.packager)) + await this.scheduleUpload(publishConfiguration, event, this.getAppInfo(event.packager)) } }) } diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index 84c660a0634..62d9329c1cf 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -18,7 +18,6 @@ interface TypedEventEmitter { off(event: E, listener: Events[E] | Nullish): this emit(event: E, ...args: Parameters): Promise<{ emittedSystem: boolean; emittedUser: boolean }> filterListeners(event: E, type: HandlerType): Handle[] - clear(): void } export class AsyncEventEmitter implements TypedEventEmitter { @@ -84,8 +83,4 @@ export class AsyncEventEmitter implements TypedEventEmitter< } return listeners } - - clear() { - this.listeners.clear() - } } From c1aac4cc005fc22c5eecb7df998cdeeebee476c4 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 14:48:59 -0800 Subject: [PATCH 11/20] fix compiler error --- .../src/SquirrelWindowsTarget.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 6e23c93778b..cb74a7b3371 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -29,7 +29,7 @@ export default class SquirrelWindowsTarget extends Target { const installerOutDir = path.join(this.outDir, `squirrel-windows${getArchSuffix(arch)}`) const artifactPath = path.join(installerOutDir, setupFile) - await packager.info.callArtifactBuildStarted({ + await packager.info.emitArtifactBuildStarted({ targetPresentableName: "Squirrel.Windows", file: artifactPath, arch, @@ -48,7 +48,7 @@ export default class SquirrelWindowsTarget extends Target { await createWindowsInstaller(distOptions) - await packager.info.callArtifactBuildCompleted({ + await packager.info.emitArtifactBuildCompleted({ file: artifactPath, target: this, arch, @@ -57,14 +57,14 @@ export default class SquirrelWindowsTarget extends Target { }) const packagePrefix = `${this.appName}-${convertVersion(version)}-` - packager.info.dispatchArtifactCreated({ + await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, `${packagePrefix}full.nupkg`), target: this, arch, packager, }) if (distOptions.remoteReleases != null) { - packager.info.dispatchArtifactCreated({ + await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, `${packagePrefix}delta.nupkg`), target: this, arch, @@ -72,7 +72,7 @@ export default class SquirrelWindowsTarget extends Target { }) } - packager.info.dispatchArtifactCreated({ + await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, "RELEASES"), target: this, arch, From 0f4654c877795e084ad87aadab9fc3c84453ee3d Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 14:54:42 -0800 Subject: [PATCH 12/20] clear event listeners after build --- packages/app-builder-lib/src/index.ts | 5 ++++- packages/app-builder-lib/src/packager.ts | 6 +++++- packages/app-builder-lib/src/platformPackager.ts | 2 +- .../app-builder-lib/src/util/asyncEventEmitter.ts | 13 ++++++++----- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/app-builder-lib/src/index.ts b/packages/app-builder-lib/src/index.ts index 2f6db9f53d8..b2c2b69707b 100644 --- a/packages/app-builder-lib/src/index.ts +++ b/packages/app-builder-lib/src/index.ts @@ -132,6 +132,9 @@ export function build(options: PackagerOptions & PublishOptions, packager: Packa promise = publishManager.awaitTasks() } - return promise.then(() => process.removeListener("SIGINT", sigIntHandler)) + return promise.then(() => { + packager.clearPackagerEventListeners() + process.removeListener("SIGINT", sigIntHandler) + }) }) } diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index fc2010961e5..35980f89cd7 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -279,10 +279,14 @@ export class Packager { return this } - filterEventListeners(event: keyof PackagerEvents, type: HandlerType | undefined) { + filterPackagerEventListeners(event: keyof PackagerEvents, type: HandlerType | undefined) { return this.eventEmitter.filterListeners(event, type) } + clearPackagerEventListeners() { + this.eventEmitter.clear() + } + async emitArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any): Promise { log.info( logFields || { diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index bd92cf60b99..8591bfff5e4 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -447,7 +447,7 @@ export abstract class PlatformPackager const didSign = await this.signApp(packContext, isAsar) if (didSign) { await this.info.emitAfterSign(packContext) - } else if (this.info.filterEventListeners("afterSign", "user").length) { + } else if (this.info.filterPackagerEventListeners("afterSign", "user").length) { log.warn(null, `skipping "afterSign" hook as no signing occurred, perhaps you intended "afterPack"?`) } } diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index 62d9329c1cf..d8420ebf8a6 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -1,4 +1,4 @@ -import { log } from "builder-util"; +import { log } from "builder-util" import { CancellationToken, Nullish } from "builder-util-runtime" type Handler = (...args: any[]) => Promise | void @@ -7,8 +7,6 @@ export type HandlerType = "system" | "user" type Handle = { handler: Handler | Promise; type: HandlerType } -export type Listener = Promise | T[E] | Nullish - export type EventMap = { [key: string]: Handler } @@ -18,6 +16,7 @@ interface TypedEventEmitter { off(event: E, listener: Events[E] | Nullish): this emit(event: E, ...args: Parameters): Promise<{ emittedSystem: boolean; emittedUser: boolean }> filterListeners(event: E, type: HandlerType): Handle[] + clear(): void } export class AsyncEventEmitter implements TypedEventEmitter { @@ -66,11 +65,11 @@ export class AsyncEventEmitter implements TypedEventEmitter< } return true } - if (!(await emitInternal(eventListeners.filter(l => l.type === "system")))) { + if (await emitInternal(eventListeners.filter(l => l.type === "system"))) { result.emittedSystem = true } // user handlers are always last - if (!(await emitInternal(eventListeners.filter(l => l.type === "user")))) { + if (await emitInternal(eventListeners.filter(l => l.type === "user"))) { result.emittedUser = true } return result @@ -83,4 +82,8 @@ export class AsyncEventEmitter implements TypedEventEmitter< } return listeners } + + clear() { + this.listeners.clear() + } } From 5269947f06bbd0833d8a0fbf8fe49992c3727b07 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 15:04:03 -0800 Subject: [PATCH 13/20] Forgot `afterPack` --- .github/workflows/test.yaml | 2 +- packages/app-builder-lib/src/packager.ts | 14 ++++++++------ test/src/BuildTest.ts | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 95e37acbaad..5a1f847a57d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -73,7 +73,7 @@ jobs: testFiles: - ArtifactPublisherTest,BuildTest,ExtraBuildTest,RepoSlugTest,binDownloadTest,configurationValidationTest,filenameUtilTest,filesTest,globTest,ignoreTest,macroExpanderTest,mainEntryTest,urlUtilTest,extraMetadataTest,linuxArchiveTest,linuxPackagerTest,HoistedNodeModuleTest,MemoLazyTest,HoistTest - snapTest,debTest,fpmTest,protonTest - - winPackagerTest,BuildTest,winCodeSignTest,webInstallerTest + - winPackagerTest,winCodeSignTest,webInstallerTest - oneClickInstallerTest,assistedInstallerTest steps: - name: Checkout code repository diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index 35980f89cd7..38d9d4fd9e8 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -259,14 +259,16 @@ export class Packager { } addPackagerEventHandlers() { - this.eventEmitter.on("artifactBuildStarted", resolveFunction(this.appInfo.type, this.config.artifactBuildStarted, "artifactBuildStarted"), "user") - this.eventEmitter.on("artifactBuildCompleted", resolveFunction(this.appInfo.type, this.config.artifactBuildCompleted, "artifactBuildCompleted"), "user") + const { type } = this.appInfo + this.eventEmitter.on("artifactBuildStarted", resolveFunction(type, this.config.artifactBuildStarted, "artifactBuildStarted"), "user") + this.eventEmitter.on("artifactBuildCompleted", resolveFunction(type, this.config.artifactBuildCompleted, "artifactBuildCompleted"), "user") - this.eventEmitter.on("appxManifestCreated", resolveFunction(this.appInfo.type, this.config.appxManifestCreated, "appxManifestCreated"), "user") - this.eventEmitter.on("msiProjectCreated", resolveFunction(this.appInfo.type, this.config.msiProjectCreated, "msiProjectCreated"), "user") + this.eventEmitter.on("appxManifestCreated", resolveFunction(type, this.config.appxManifestCreated, "appxManifestCreated"), "user") + this.eventEmitter.on("msiProjectCreated", resolveFunction(type, this.config.msiProjectCreated, "msiProjectCreated"), "user") - this.eventEmitter.on("beforePack", resolveFunction(this.appInfo.type, this.config.beforePack, "beforePack"), "user") - this.eventEmitter.on("afterSign", resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign"), "user") + this.eventEmitter.on("beforePack", resolveFunction(type, this.config.beforePack, "beforePack"), "user") + this.eventEmitter.on("afterPack", resolveFunction(type, this.config.afterPack, "afterPack"), "user") + this.eventEmitter.on("afterSign", resolveFunction(type, this.config.afterSign, "afterSign"), "user") } onAfterPack(handler: PackagerEvents["afterPack"]): Packager { diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index d48da12be2d..a7063754094 100644 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -235,6 +235,7 @@ test.ifLinuxOrDevMac("afterPack", () => { { packed: async () => { expect(called).toEqual(2) + return Promise.resolve() }, } ) @@ -257,6 +258,7 @@ test.ifWindows("afterSign", () => { packed: async () => { // afterSign is only called when an app is actually signed and ignored otherwise. expect(called).toEqual(1) + return Promise.resolve() }, } ) From 9159d6b7d427c65bc6e187d1b3c5548d57d783cc Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 15:14:25 -0800 Subject: [PATCH 14/20] update test to include all hooks --- packages/app-builder-lib/src/packager.ts | 9 ++++--- test/snapshots/BuildTest.js.snap | 14 +++++----- test/src/BuildTest.ts | 34 +++++++++++++++++++++--- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index 38d9d4fd9e8..62e2656930f 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -69,18 +69,20 @@ async function createFrameworkInfo(configuration: Configuration, packager: Packa } type PackagerEvents = { - artifactCreated: Hook + artifactBuildStarted: Hook beforePack: Hook afterExtract: Hook afterPack: Hook - afterSign: Hook - artifactBuildStarted: Hook + artifactBuildCompleted: Hook msiProjectCreated: Hook appxManifestCreated: Hook + + // internal-use only, prefer usage of `artifactBuildCompleted` + artifactCreated: Hook } export class Packager { @@ -267,6 +269,7 @@ export class Packager { this.eventEmitter.on("msiProjectCreated", resolveFunction(type, this.config.msiProjectCreated, "msiProjectCreated"), "user") this.eventEmitter.on("beforePack", resolveFunction(type, this.config.beforePack, "beforePack"), "user") + this.eventEmitter.on("afterExtract", resolveFunction(type, this.config.afterExtract, "afterExtract"), "user") this.eventEmitter.on("afterPack", resolveFunction(type, this.config.afterPack, "afterPack"), "user") this.eventEmitter.on("afterSign", resolveFunction(type, this.config.afterSign, "afterSign"), "user") } diff --git a/test/snapshots/BuildTest.js.snap b/test/snapshots/BuildTest.js.snap index 30ad6f26b09..385df19324f 100644 --- a/test/snapshots/BuildTest.js.snap +++ b/test/snapshots/BuildTest.js.snap @@ -1,12 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`afterPack 1`] = ` -{ - "linux": [], - "mac": [], -} -`; - exports[`afterSign 1`] = ` { "linux": [], @@ -212,6 +205,13 @@ exports[`electron version from electron-prebuilt dependency 1`] = ` } `; +exports[`hooks 1`] = ` +{ + "linux": [], + "mac": [], +} +`; + exports[`posix smart unpack 1`] = ` { "linux": [], diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index a7063754094..8c69940b86c 100644 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -219,22 +219,48 @@ test( ) ) -test.ifLinuxOrDevMac("afterPack", () => { - let called = 0 +test.ifLinuxOrDevMac("hooks", () => { + let artifactBuildStartedCalled = 0 + let artifactBuildCompletedCalled = 0 + let beforePackCalled = 0 + let afterPackCalled = 0 + let afterExtractCalled = 0 return assertPack( "test-app-one", { targets: createTargets([Platform.LINUX, Platform.MAC], DIR_TARGET), config: { + artifactBuildStarted: () => { + artifactBuildStartedCalled++ + return Promise.resolve() + }, + artifactBuildCompleted: () => { + artifactBuildCompletedCalled++ + return Promise.resolve() + }, + beforePack: () => { + beforePackCalled++ + return Promise.resolve() + }, + afterExtract: () => { + afterExtractCalled++ + return Promise.resolve() + }, afterPack: () => { - called++ + afterPackCalled++ return Promise.resolve() }, + }, }, { packed: async () => { - expect(called).toEqual(2) + expect(artifactBuildStartedCalled).toEqual(2) + expect(artifactBuildCompletedCalled).toEqual(2) + expect(beforePackCalled).toEqual(2) + expect(afterExtractCalled).toEqual(2) + expect(afterPackCalled).toEqual(2) + expect(afterPackCalled).toEqual(2) return Promise.resolve() }, } From 88c816ed38c25bb90c8530500989731c596a3f96 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 15:28:09 -0800 Subject: [PATCH 15/20] update snapshot and test --- packages/app-builder-lib/src/packager.ts | 2 +- test/snapshots/BuildTest.js.snap | 74 +++++++++++++++++++++++- test/src/BuildTest.ts | 5 +- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/packages/app-builder-lib/src/packager.ts b/packages/app-builder-lib/src/packager.ts index 62e2656930f..2ac701945e6 100644 --- a/packages/app-builder-lib/src/packager.ts +++ b/packages/app-builder-lib/src/packager.ts @@ -292,7 +292,7 @@ export class Packager { this.eventEmitter.clear() } - async emitArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any): Promise { + async emitArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any) { log.info( logFields || { target: event.targetPresentableName, diff --git a/test/snapshots/BuildTest.js.snap b/test/snapshots/BuildTest.js.snap index 385df19324f..2daa72ba90e 100644 --- a/test/snapshots/BuildTest.js.snap +++ b/test/snapshots/BuildTest.js.snap @@ -207,8 +207,78 @@ exports[`electron version from electron-prebuilt dependency 1`] = ` exports[`hooks 1`] = ` { - "linux": [], - "mac": [], + "linux": [ + { + "arch": "x64", + "file": "TestApp-1.1.0.zip", + }, + ], + "mac": [ + { + "arch": "x64", + "file": "Test App ßW-1.1.0-mac.zip", + "safeArtifactName": "TestApp-1.1.0-mac.zip", + "updateInfo": { + "sha512": "@sha512", + "size": "@size", + }, + }, + { + "file": "Test App ßW-1.1.0-mac.zip.blockmap", + "safeArtifactName": "Test App ßW-1.1.0-mac.zip.blockmap", + "updateInfo": { + "sha512": "@sha512", + "size": "@size", + }, + }, + ], +} +`; + +exports[`hooks 2`] = ` +{ + "CFBundleDisplayName": "Test App ßW", + "CFBundleExecutable": "Test App ßW", + "CFBundleIconFile": "icon.icns", + "CFBundleIdentifier": "org.electron-builder.testApp", + "CFBundleInfoDictionaryVersion": "6.0", + "CFBundleName": "Test App ßW", + "CFBundlePackageType": "APPL", + "CFBundleShortVersionString": "1.1.0", + "ElectronAsarIntegrity": { + "Resources/app.asar": { + "algorithm": "SHA256", + "hash": "hash", + }, + }, + "LSApplicationCategoryType": "your.app.category.type", + "LSEnvironment": { + "MallocNanoZone": "0", + }, + "NSAppTransportSecurity": { + "NSAllowsLocalNetworking": true, + "NSExceptionDomains": { + "127.0.0.1": { + "NSIncludesSubdomains": false, + "NSTemporaryExceptionAllowsInsecureHTTPLoads": true, + "NSTemporaryExceptionAllowsInsecureHTTPSLoads": false, + "NSTemporaryExceptionMinimumTLSVersion": "1.0", + "NSTemporaryExceptionRequiresForwardSecrecy": false, + }, + "localhost": { + "NSIncludesSubdomains": false, + "NSTemporaryExceptionAllowsInsecureHTTPLoads": true, + "NSTemporaryExceptionAllowsInsecureHTTPSLoads": false, + "NSTemporaryExceptionMinimumTLSVersion": "1.0", + "NSTemporaryExceptionRequiresForwardSecrecy": false, + }, + }, + }, + "NSBluetoothAlwaysUsageDescription": "This app needs access to Bluetooth", + "NSBluetoothPeripheralUsageDescription": "This app needs access to Bluetooth", + "NSHighResolutionCapable": true, + "NSPrincipalClass": "AtomApplication", + "NSSupportsAutomaticGraphicsSwitching": true, } `; diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index 8c69940b86c..12bfb0834f3 100644 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -228,7 +228,7 @@ test.ifLinuxOrDevMac("hooks", () => { return assertPack( "test-app-one", { - targets: createTargets([Platform.LINUX, Platform.MAC], DIR_TARGET), + targets: createTargets([Platform.LINUX, Platform.MAC], "zip", "x64"), config: { artifactBuildStarted: () => { artifactBuildStartedCalled++ @@ -250,13 +250,12 @@ test.ifLinuxOrDevMac("hooks", () => { afterPackCalled++ return Promise.resolve() }, - }, }, { packed: async () => { expect(artifactBuildStartedCalled).toEqual(2) - expect(artifactBuildCompletedCalled).toEqual(2) + expect(artifactBuildCompletedCalled).toEqual(3) // 2 artifacts + blockmap expect(beforePackCalled).toEqual(2) expect(afterExtractCalled).toEqual(2) expect(afterPackCalled).toEqual(2) From caf2f5706de80cab8d1c196ed400b4d992689e6b Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Mon, 17 Feb 2025 15:48:17 -0800 Subject: [PATCH 16/20] fix merge conflicts --- .../src/SquirrelWindowsTarget.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 0d83498b26e..a5dc0ca5f19 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -53,7 +53,7 @@ export default class SquirrelWindowsTarget extends Target { const artifactPath = path.join(installerOutDir, setupFile) const msiArtifactPath = path.join(installerOutDir, packager.expandArtifactNamePattern(this.options, "msi", arch, "${productName} Setup ${version}.${ext}")) - await packager.info.callArtifactBuildStarted({ + await packager.info.emitArtifactBuildStarted({ targetPresentableName: "Squirrel.Windows", file: artifactPath, arch, @@ -69,7 +69,7 @@ export default class SquirrelWindowsTarget extends Target { const safeArtifactName = (ext: string) => `${sanitizedName}-Setup-${version}${getArchSuffix(arch)}.${ext}` - await packager.info.callArtifactBuildCompleted({ + await packager.info.emitArtifactBuildCompleted({ file: artifactPath, target: this, arch, @@ -78,7 +78,7 @@ export default class SquirrelWindowsTarget extends Target { }) if (this.options.msi) { - await packager.info.callArtifactBuildCompleted({ + await packager.info.emitArtifactBuildCompleted({ file: msiArtifactPath, target: this, arch, @@ -88,14 +88,14 @@ export default class SquirrelWindowsTarget extends Target { } const packagePrefix = `${this.appName}-${convertVersion(version)}-` - packager.info.dispatchArtifactCreated({ + await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, `${packagePrefix}full.nupkg`), target: this, arch, packager, }) if (distOptions.remoteReleases != null) { - packager.info.dispatchArtifactCreated({ + await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, `${packagePrefix}delta.nupkg`), target: this, arch, @@ -103,7 +103,7 @@ export default class SquirrelWindowsTarget extends Target { }) } - packager.info.dispatchArtifactCreated({ + await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, "RELEASES"), target: this, arch, From d9878fd983165b936fb94f39be3f705fe1e56640 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Wed, 19 Feb 2025 17:13:55 -0800 Subject: [PATCH 17/20] prettier + simplify --- .../app-builder-lib/src/util/asyncEventEmitter.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index d8420ebf8a6..a2df7229d05 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -27,10 +27,7 @@ export class AsyncEventEmitter implements TypedEventEmitter< if (!listener) { return this } - let listeners = this.listeners.get(event) - if (!listeners) { - listeners = [] - } + const listeners = this.listeners.get(event) ?? [] listeners.push({ handler: listener, type }) this.listeners.set(event, listeners) return this @@ -65,13 +62,11 @@ export class AsyncEventEmitter implements TypedEventEmitter< } return true } - if (await emitInternal(eventListeners.filter(l => l.type === "system"))) { - result.emittedSystem = true - } + + result.emittedSystem = await emitInternal(eventListeners.filter(l => l.type === "system")) // user handlers are always last - if (await emitInternal(eventListeners.filter(l => l.type === "user"))) { - result.emittedUser = true - } + result.emittedUser = await emitInternal(eventListeners.filter(l => l.type === "user")) + return result } From 7f83d296acbe5f708d6b60209d82518579721926 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 20 Feb 2025 08:56:05 -0800 Subject: [PATCH 18/20] add additional test for loading hooks from files --- .../src/util/asyncEventEmitter.ts | 6 +- test/fixtures/build-hook.cjs | 3 + test/fixtures/build-hook.mjs | 9 +++ test/snapshots/BuildTest.js.snap | 81 ++++++++++++++++++- test/src/BuildTest.ts | 36 ++++++++- 5 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/build-hook.cjs create mode 100644 test/fixtures/build-hook.mjs diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index a2df7229d05..fb1ac9cf32b 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -51,14 +51,14 @@ export class AsyncEventEmitter implements TypedEventEmitter< log.debug({ event }, "no event listeners found") return result } + const emitInternal = async (listeners: Handle[]) => { for (const listener of listeners) { if (this.cancellationToken.cancelled) { return false } - await ( - await listener.handler - )?.(...args) + const handler = await listener.handler + await Promise.resolve(handler?.(...args)) } return true } diff --git a/test/fixtures/build-hook.cjs b/test/fixtures/build-hook.cjs new file mode 100644 index 00000000000..91aad0ec873 --- /dev/null +++ b/test/fixtures/build-hook.cjs @@ -0,0 +1,3 @@ +module.exports = (event) => { + return +} \ No newline at end of file diff --git a/test/fixtures/build-hook.mjs b/test/fixtures/build-hook.mjs new file mode 100644 index 00000000000..ac97d97eb68 --- /dev/null +++ b/test/fixtures/build-hook.mjs @@ -0,0 +1,9 @@ +const func = (event) => { + return +} + +export const artifactBuildStarted = func +export const artifactBuildCompleted = func +export const beforePack = func +export const afterExtract = func +export const afterPack = func \ No newline at end of file diff --git a/test/snapshots/BuildTest.js.snap b/test/snapshots/BuildTest.js.snap index 2daa72ba90e..d1649b96153 100644 --- a/test/snapshots/BuildTest.js.snap +++ b/test/snapshots/BuildTest.js.snap @@ -205,7 +205,7 @@ exports[`electron version from electron-prebuilt dependency 1`] = ` } `; -exports[`hooks 1`] = ` +exports[`hooks as file - cjs 1`] = ` { "linux": [ { @@ -235,7 +235,84 @@ exports[`hooks 1`] = ` } `; -exports[`hooks 2`] = ` +exports[`hooks as file - cjs 2`] = ` +{ + "CFBundleDisplayName": "Test App ßW", + "CFBundleExecutable": "Test App ßW", + "CFBundleIconFile": "icon.icns", + "CFBundleIdentifier": "org.electron-builder.testApp", + "CFBundleInfoDictionaryVersion": "6.0", + "CFBundleName": "Test App ßW", + "CFBundlePackageType": "APPL", + "CFBundleShortVersionString": "1.1.0", + "ElectronAsarIntegrity": { + "Resources/app.asar": { + "algorithm": "SHA256", + "hash": "hash", + }, + }, + "LSApplicationCategoryType": "your.app.category.type", + "LSEnvironment": { + "MallocNanoZone": "0", + }, + "NSAppTransportSecurity": { + "NSAllowsLocalNetworking": true, + "NSExceptionDomains": { + "127.0.0.1": { + "NSIncludesSubdomains": false, + "NSTemporaryExceptionAllowsInsecureHTTPLoads": true, + "NSTemporaryExceptionAllowsInsecureHTTPSLoads": false, + "NSTemporaryExceptionMinimumTLSVersion": "1.0", + "NSTemporaryExceptionRequiresForwardSecrecy": false, + }, + "localhost": { + "NSIncludesSubdomains": false, + "NSTemporaryExceptionAllowsInsecureHTTPLoads": true, + "NSTemporaryExceptionAllowsInsecureHTTPSLoads": false, + "NSTemporaryExceptionMinimumTLSVersion": "1.0", + "NSTemporaryExceptionRequiresForwardSecrecy": false, + }, + }, + }, + "NSBluetoothAlwaysUsageDescription": "This app needs access to Bluetooth", + "NSBluetoothPeripheralUsageDescription": "This app needs access to Bluetooth", + "NSHighResolutionCapable": true, + "NSPrincipalClass": "AtomApplication", + "NSSupportsAutomaticGraphicsSwitching": true, +} +`; + +exports[`hooks as functions 1`] = ` +{ + "linux": [ + { + "arch": "x64", + "file": "TestApp-1.1.0.zip", + }, + ], + "mac": [ + { + "arch": "x64", + "file": "Test App ßW-1.1.0-mac.zip", + "safeArtifactName": "TestApp-1.1.0-mac.zip", + "updateInfo": { + "sha512": "@sha512", + "size": "@size", + }, + }, + { + "file": "Test App ßW-1.1.0-mac.zip.blockmap", + "safeArtifactName": "Test App ßW-1.1.0-mac.zip.blockmap", + "updateInfo": { + "sha512": "@sha512", + "size": "@size", + }, + }, + ], +} +`; + +exports[`hooks as functions 2`] = ` { "CFBundleDisplayName": "Test App ßW", "CFBundleExecutable": "Test App ßW", diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index 12bfb0834f3..9da15fb1272 100644 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -5,11 +5,11 @@ import { createYargs } from "electron-builder/out/builder" import { promises as fs } from "fs" import { outputFile, outputJson } from "fs-extra" import * as path from "path" -import { app, appTwo, appTwoThrows, assertPack, linuxDirTarget, modifyPackageJson, packageJson, toSystemIndependentPath } from "./helpers/packTester" +import { app, appTwo, appTwoThrows, assertPack, getFixtureDir, linuxDirTarget, modifyPackageJson, packageJson, toSystemIndependentPath } from "./helpers/packTester" import { ELECTRON_VERSION } from "./helpers/testConfig" import { verifySmartUnpack } from "./helpers/verifySmartUnpack" -test("cli", async () => { +test("cli", () => { // because these methods are internal const { configureBuildCommand, normalizeOptions } = require("electron-builder/out/builder") const yargs = createYargs() @@ -219,7 +219,7 @@ test( ) ) -test.ifLinuxOrDevMac("hooks", () => { +test.ifLinuxOrDevMac("hooks as functions", () => { let artifactBuildStartedCalled = 0 let artifactBuildCompletedCalled = 0 let beforePackCalled = 0 @@ -266,6 +266,34 @@ test.ifLinuxOrDevMac("hooks", () => { ) }) +test.ifLinuxOrDevMac("hooks as file - cjs", async () => { + const hookScript = path.join(getFixtureDir(), "build-hook.cjs") + return assertPack("test-app-one", { + targets: createTargets([Platform.LINUX, Platform.MAC], "zip", "x64"), + config: { + artifactBuildStarted: hookScript, + artifactBuildCompleted: hookScript, + beforePack: hookScript, + afterExtract: hookScript, + afterPack: hookScript, + }, + }) +}) + +// test.only("hooks as file - mjs exported functions", async () => { +// const hookScript = path.join(getFixtureDir(), "build-hook.mjs") +// return assertPack("test-app-one", { +// targets: createTargets([Platform.LINUX, Platform.MAC], "zip", "x64"), +// config: { +// artifactBuildStarted: hookScript, +// artifactBuildCompleted: hookScript, +// beforePack: hookScript, +// afterExtract: hookScript, +// afterPack: hookScript, +// }, +// }) +// }) + test.ifWindows("afterSign", () => { let called = 0 return assertPack( @@ -299,12 +327,14 @@ test.ifLinuxOrDevMac("beforeBuild", () => { npmRebuild: true, beforeBuild: async () => { called++ + return Promise.resolve() }, }, }, { packed: async () => { expect(called).toEqual(2) + return Promise.resolve() }, } ) From a429a1352f0b7d08143f8d6f0584c86ee95adb71 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 20 Feb 2025 18:29:18 -0800 Subject: [PATCH 19/20] `return module.default || module` --- packages/app-builder-lib/src/publish/PublishManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index e66db426e76..fe55f7092b1 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -361,7 +361,8 @@ async function requireProviderClass(provider: string, packager: Packager): Promi const validPublisherFiles = extensions.map(ext => path.join(packager.buildResourcesDir, name(ext))) for (const potentialFile of validPublisherFiles) { if (await exists(potentialFile)) { - return await resolveModule(packager.appInfo.type, potentialFile) + const module: any = await resolveModule(packager.appInfo.type, potentialFile) + return module.default || module } } log.warn({ path: log.filePath(packager.buildResourcesDir), template, extensionsChecked: extensions }, "unable to find publish provider in build resources") From 7bae4e9b282851ab350c065e1954b76ebf11bf76 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 20 Feb 2025 22:19:29 -0800 Subject: [PATCH 20/20] additional Promise.resolve just in case --- packages/app-builder-lib/src/util/asyncEventEmitter.ts | 2 +- test/fixtures/build-hook.cjs | 2 +- test/src/BuildTest.ts | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/app-builder-lib/src/util/asyncEventEmitter.ts b/packages/app-builder-lib/src/util/asyncEventEmitter.ts index fb1ac9cf32b..e5c1cb01428 100644 --- a/packages/app-builder-lib/src/util/asyncEventEmitter.ts +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -57,7 +57,7 @@ export class AsyncEventEmitter implements TypedEventEmitter< if (this.cancellationToken.cancelled) { return false } - const handler = await listener.handler + const handler = await Promise.resolve(listener.handler) await Promise.resolve(handler?.(...args)) } return true diff --git a/test/fixtures/build-hook.cjs b/test/fixtures/build-hook.cjs index 91aad0ec873..5f3f629ece4 100644 --- a/test/fixtures/build-hook.cjs +++ b/test/fixtures/build-hook.cjs @@ -1,3 +1,3 @@ module.exports = (event) => { - return + return Promise.resolve('foobar'); } \ No newline at end of file diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index 9da15fb1272..f09b6ff0699 100644 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -232,11 +232,9 @@ test.ifLinuxOrDevMac("hooks as functions", () => { config: { artifactBuildStarted: () => { artifactBuildStartedCalled++ - return Promise.resolve() }, artifactBuildCompleted: () => { artifactBuildCompletedCalled++ - return Promise.resolve() }, beforePack: () => { beforePackCalled++