Folo/forge.config.ts

240 lines
6.8 KiB
TypeScript

import crypto from "node:crypto"
import fs, { readdirSync } from "node:fs"
import { readdir } from "node:fs/promises"
import path, { resolve } from "node:path"
import { FuseV1Options, FuseVersion } from "@electron/fuses"
import { MakerDMG } from "@electron-forge/maker-dmg"
import { MakerSquirrel } from "@electron-forge/maker-squirrel"
import { MakerZIP } from "@electron-forge/maker-zip"
import { FusesPlugin } from "@electron-forge/plugin-fuses"
import type { ForgeConfig } from "@electron-forge/shared-types"
import MakerAppImage from "@pengx17/electron-forge-maker-appimage"
import setLanguages from "electron-packager-languages"
import { rimraf, rimrafSync } from "rimraf"
const artifactRegex = /.*\.(?:exe|dmg|AppImage|zip)$/
const platformNamesMap = {
darwin: "macos",
linux: "linux",
win32: "windows",
}
const ymlMapsMap = {
darwin: "latest-mac.yml",
linux: "latest-linux.yml",
win32: "latest.yml",
}
const keepModules = new Set(["font-list", "vscode-languagedetection"])
const keepLanguages = new Set(["en", "en_GB", "zh_CN", "zh-CN", "en-GB"])
// remove folders & files not to be included in the app
async function cleanSources(buildPath, electronVersion, platform, arch, callback) {
// folders & files to be included in the app
const appItems = new Set(["dist", "node_modules", "package.json", "resources"])
if (platform === "darwin") {
const frameworkResourcePath = resolve(
buildPath,
"../../Frameworks/Electron Framework.framework/Versions/A/Resources",
)
for (const file of readdirSync(frameworkResourcePath)) {
if (file.endsWith(".lproj") && !keepLanguages.has(file.split(".")[0])) {
rimrafSync(resolve(frameworkResourcePath, file))
}
}
}
// Keep only node_modules to be included in the app
await Promise.all([
...(await readdir(buildPath).then((items) =>
items.filter((item) => !appItems.has(item)).map((item) => rimraf(path.join(buildPath, item))),
)),
...(await readdir(path.join(buildPath, "node_modules")).then((items) =>
items
.filter((item) => !keepModules.has(item))
.map((item) => rimraf(path.join(buildPath, "node_modules", item))),
)),
])
callback()
}
const ignorePattern = new RegExp(`^/node_modules/(?!${[...keepModules].join("|")})`)
const config: ForgeConfig = {
packagerConfig: {
appBundleId: "is.follow",
icon: "resources/icon",
extraResource: ["./resources/app-update.yml"],
protocols: [
{
name: "Follow",
schemes: ["follow"],
},
],
afterCopy: [cleanSources, setLanguages([...keepLanguages])],
asar: true,
ignore: [ignorePattern],
prune: true,
...(process.env.APPLE_ID &&
process.env.APPLE_PASSWORD &&
process.env.APPLE_TEAM_ID && {
osxSign: {
optionsForFile: () => ({
entitlements: "build/entitlements.mac.plist",
}),
keychain: process.env.KEYCHAIN_PATH,
},
osxNotarize: {
appleId: process.env.APPLE_ID!,
appleIdPassword: process.env.APPLE_PASSWORD!,
teamId: process.env.APPLE_TEAM_ID!,
},
}),
},
rebuildConfig: {},
makers: [
new MakerZIP({}, ["darwin"]),
new MakerDMG({
overwrite: true,
background: "resources/dmg-background.png",
icon: "resources/dmg-icon.icns",
iconSize: 160,
additionalDMGOptions: {
window: {
size: {
width: 660,
height: 400,
},
},
},
contents: (opts) => [
{
x: 180,
y: 170,
type: "file",
path: (opts as any).appPath,
},
{
x: 480,
y: 170,
type: "link",
path: "/Applications",
},
],
}),
new MakerSquirrel({
name: "Follow",
setupIcon: "resources/icon.ico",
}),
new MakerAppImage({
options: {
icon: "resources/icon.png",
mimeType: ["x-scheme-handler/follow"],
},
}),
],
plugins: [
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
publishers: [
{
name: "@electron-forge/publisher-github",
config: {
repository: {
owner: "RSSNext",
name: "follow",
},
draft: true,
},
},
],
hooks: {
postMake: async (config, makeResults) => {
const yml: {
version?: string
files: {
url: string
sha512: string
size: number
}[]
releaseDate?: string
} = {
version: makeResults[0].packageJSON.version,
files: [],
}
makeResults = makeResults.map((result) => {
result.artifacts = result.artifacts.map((artifact) => {
if (artifactRegex.test(artifact)) {
const newArtifact = `${path.dirname(artifact)}/${
result.packageJSON.name
}-${result.packageJSON.version}-${
platformNamesMap[result.platform]
}-${result.arch}${path.extname(artifact)}`
fs.renameSync(artifact, newArtifact)
try {
const fileData = fs.readFileSync(newArtifact)
const hash = crypto.createHash("sha512").update(fileData).digest("base64")
const { size } = fs.statSync(newArtifact)
yml.files.push({
url: path.basename(newArtifact),
sha512: hash,
size,
})
} catch {
console.error(`Failed to hash ${newArtifact}`)
}
return newArtifact
} else {
return artifact
}
})
return result
})
yml.releaseDate = new Date().toISOString()
const ymlStr =
`version: ${yml.version}\n` +
`files:\n${yml.files
.map(
(file) =>
` - url: ${file.url}\n` +
` sha512: ${file.sha512}\n` +
` size: ${file.size}\n`,
)
.join("")}releaseDate: ${yml.releaseDate}\n`
const ymlPath = `${path.dirname(makeResults[0].artifacts[0])}/${
ymlMapsMap[makeResults[0].platform]
}`
fs.writeFileSync(ymlPath, ymlStr)
makeResults.push({
artifacts: [ymlPath],
platform: makeResults[0].platform,
arch: makeResults[0].arch,
packageJSON: makeResults[0].packageJSON,
})
return makeResults
},
},
}
export default config