diff --git a/.changeset/hip-weeks-allow.md b/.changeset/hip-weeks-allow.md new file mode 100644 index 00000000000..61b3e92eb45 --- /dev/null +++ b/.changeset/hip-weeks-allow.md @@ -0,0 +1,5 @@ +--- +"app-builder-lib": patch +--- + +fix: allow usage of .cjs, .mjs, and type=module custom/generic publishers diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fc67b27a5e7..de17b6e4101 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/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/index.ts b/packages/app-builder-lib/src/index.ts index ea13f0be9aa..b2c2b69707b 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, @@ -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/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 67972d63780..2ac701945e6 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" @@ -23,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" @@ -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, HandlerType } from "./util/asyncEventEmitter" async function createFrameworkInfo(configuration: Configuration, packager: Packager): Promise { let framework = configuration.framework @@ -72,6 +68,23 @@ async function createFrameworkInfo(configuration: Configuration, packager: Packa } } +type PackagerEvents = { + artifactBuildStarted: Hook + + beforePack: Hook + afterExtract: Hook + afterPack: Hook + afterSign: Hook + + artifactBuildCompleted: Hook + + msiProjectCreated: Hook + appxManifestCreated: Hook + + // internal-use only, prefer usage of `artifactBuildCompleted` + artifactCreated: Hook +} + export class Packager { readonly projectDir: string @@ -110,7 +123,7 @@ export class Packager { isTwoPackageJsonProjectLayoutUsed = false - readonly eventEmitter = new EventEmitter() + private readonly eventEmitter = new AsyncEventEmitter() _appInfo: AppInfo | null = null get appInfo(): AppInfo { @@ -121,8 +134,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 +257,42 @@ 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") } - addAfterPackHandler(handler: (context: AfterPackContext) => Promise | null) { - this.afterPackHandlers.push(handler) + addPackagerEventHandlers() { + 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(type, this.config.appxManifestCreated, "appxManifestCreated"), "user") + 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") } - artifactCreated(handler: (event: ArtifactCreated) => void): Packager { - addHandler(this.eventEmitter, "artifactCreated", handler) + onAfterPack(handler: PackagerEvents["afterPack"]): Packager { + this.eventEmitter.on("afterPack", handler) return this } - async callArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any): Promise { + onArtifactCreated(handler: PackagerEvents["artifactCreated"]): Packager { + this.eventEmitter.on("artifactCreated", handler) + return this + } + + filterPackagerEventListeners(event: keyof PackagerEvents, type: HandlerType | undefined) { + return this.eventEmitter.filterListeners(event, type) + } + + clearPackagerEventListeners() { + this.eventEmitter.clear() + } + + async emitArtifactBuildStarted(event: ArtifactBuildStarted, logFields?: any) { log.info( logFields || { target: event.targetPresentableName, @@ -274,40 +301,43 @@ 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`. */ - dispatchArtifactCreated(event: ArtifactCreated): void { - this.eventEmitter.emit("artifactCreated", event) + async emitArtifactCreated(event: ArtifactCreated) { + 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)) - } + async emitArtifactBuildCompleted(event: ArtifactCreated) { + await this.eventEmitter.emit("artifactBuildCompleted", event) + await this.emitArtifactCreated(event) + } - this.dispatchArtifactCreated(event) + async emitAppxManifestCreated(path: string) { + await this.eventEmitter.emit("appxManifestCreated", path) } - 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 emitMsiProjectCreated(path: string) { + await this.eventEmitter.emit("msiProjectCreated", 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 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 { @@ -366,6 +396,8 @@ export class Packager { } this._appInfo = new AppInfo(this, null) + this.addPackagerEventHandlers() + this._framework = await createFrameworkInfo(this.config, this) const commonOutDirWithoutPossibleOsMacro = path.resolve( @@ -383,7 +415,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) } @@ -547,19 +579,6 @@ export class Packager { }) } } - - async afterPack(context: AfterPackContext): Promise { - 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)) - } - } } 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 aa85c765b47..8591bfff5e4 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 @@ -155,7 +154,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, @@ -237,17 +236,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.emitBeforePack({ + appOutDir, + outDir, + arch, + targets, + packager: this, + electronPlatformName: platformName, + }) await this.info.installAppDependencies(this.platform, arch) @@ -274,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 = [] @@ -355,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) @@ -452,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.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/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index 0584d8f69aa..fe55f7092b1 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,6 +38,7 @@ import { PlatformPackager } from "../platformPackager" import { expandMacro } from "../util/macroExpander" import { WinPackager } from "../winPackager" import { createUpdateInfoTasks, UpdateInfoFileTask, writeUpdateInfoFiles } from "./updateInfoBuilder" +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)" + @@ -109,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")) { @@ -127,7 +128,7 @@ export class PublishManager implements PublishContext { } }) - packager.artifactCreated(event => { + packager.onArtifactCreated(async event => { const publishConfiguration = event.publishConfig if (publishConfiguration == null) { this.taskManager.addTask(this.artifactCreatedWithoutExplicitPublishConfig(event)) @@ -135,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)}`) } - this.scheduleUpload(publishConfiguration, event, this.getAppInfo(event.packager)) + await this.scheduleUpload(publishConfiguration, event, this.getAppInfo(event.packager)) } }) } @@ -149,12 +150,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 +205,7 @@ export class PublishManager implements PublishContext { break } - this.scheduleUpload(publishConfig, event, this.getAppInfo(platformPackager)) + await this.scheduleUpload(publishConfig, event, this.getAppInfo(platformPackager)) } } @@ -219,12 +220,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 +298,13 @@ 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 +324,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 +354,18 @@ 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 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 (await exists(potentialFile)) { + const module: any = await resolveModule(packager.appInfo.type, potentialFile) + return module.default || module + } } - return module.default || module + log.warn({ path: log.filePath(packager.buildResourcesDir), template, extensionsChecked: extensions }, "unable to find publish provider in build resources") } } } @@ -515,7 +522,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 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 new file mode 100644 index 00000000000..e5c1cb01428 --- /dev/null +++ b/packages/app-builder-lib/src/util/asyncEventEmitter.ts @@ -0,0 +1,84 @@ +import { log } from "builder-util" +import { CancellationToken, Nullish } from "builder-util-runtime" + +type Handler = (...args: any[]) => Promise | void + +export type HandlerType = "system" | "user" + +type Handle = { handler: Handler | Promise; type: HandlerType } + +export type EventMap = { + [key: string]: Handler +} + +interface TypedEventEmitter { + 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] | Promise | Nullish, type: HandlerType = "system"): this { + if (!listener) { + return this + } + const listeners = this.listeners.get(event) ?? [] + listeners.push({ handler: listener, type }) + this.listeners.set(event, listeners) + return 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) + return this + } + this.listeners.set(event, listeners) + return this + } + + 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) { + 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 + } + const handler = await Promise.resolve(listener.handler) + await Promise.resolve(handler?.(...args)) + } + return true + } + + result.emittedSystem = await emitInternal(eventListeners.filter(l => l.type === "system")) + // user handlers are always last + result.emittedUser = await emitInternal(eventListeners.filter(l => l.type === "user")) + + 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() + } +} 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/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, 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) } } diff --git a/test/fixtures/build-hook.cjs b/test/fixtures/build-hook.cjs new file mode 100644 index 00000000000..5f3f629ece4 --- /dev/null +++ b/test/fixtures/build-hook.cjs @@ -0,0 +1,3 @@ +module.exports = (event) => { + return Promise.resolve('foobar'); +} \ 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 30ad6f26b09..d1649b96153 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,160 @@ exports[`electron version from electron-prebuilt dependency 1`] = ` } `; +exports[`hooks as file - cjs 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 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", + "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[`posix smart unpack 1`] = ` { "linux": [], 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 () => { diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index d48da12be2d..f09b6ff0699 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,27 +219,79 @@ test( ) ) -test.ifLinuxOrDevMac("afterPack", () => { - let called = 0 +test.ifLinuxOrDevMac("hooks as functions", () => { + 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), + targets: createTargets([Platform.LINUX, Platform.MAC], "zip", "x64"), config: { + artifactBuildStarted: () => { + artifactBuildStartedCalled++ + }, + artifactBuildCompleted: () => { + artifactBuildCompletedCalled++ + }, + 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(3) // 2 artifacts + blockmap + expect(beforePackCalled).toEqual(2) + expect(afterExtractCalled).toEqual(2) + expect(afterPackCalled).toEqual(2) + expect(afterPackCalled).toEqual(2) + return Promise.resolve() }, } ) }) +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( @@ -257,6 +309,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() }, } ) @@ -272,12 +325,14 @@ test.ifLinuxOrDevMac("beforeBuild", () => { npmRebuild: true, beforeBuild: async () => { called++ + return Promise.resolve() }, }, }, { packed: async () => { expect(called).toEqual(2) + return Promise.resolve() }, } ) 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 }