cross-packer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +151 -0
- package/README.zh-CN.md +151 -0
- package/dist/archive/ar.d.ts +12 -0
- package/dist/archive/seven-za.d.ts +32 -0
- package/dist/archive/tar.d.ts +30 -0
- package/dist/archive/zip-reader.d.ts +17 -0
- package/dist/asar/dependency/collect.d.ts +2 -0
- package/dist/asar/dependency/detector.d.ts +8 -0
- package/dist/asar/dependency/hoister.d.ts +27 -0
- package/dist/asar/dependency/utils.d.ts +59 -0
- package/dist/asar/fileset/matcher.d.ts +12 -0
- package/dist/asar/fileset/platform-fileset.d.ts +36 -0
- package/dist/asar/fileset/transformer.d.ts +2 -0
- package/dist/asar/fileset/unpack-detector.d.ts +10 -0
- package/dist/asar/fileset/walker.d.ts +14 -0
- package/dist/asar/pack.d.ts +23 -0
- package/dist/asar/packer/app-collector.d.ts +2 -0
- package/dist/asar/packer/collector.d.ts +18 -0
- package/dist/asar/packer/concurrency.d.ts +1 -0
- package/dist/asar/packer/integrity.d.ts +8 -0
- package/dist/asar/packer/nm-collector.d.ts +7 -0
- package/dist/asar/packer/stream-builder.d.ts +18 -0
- package/dist/cli.d.ts +2 -0
- package/dist/config/index.d.ts +97 -0
- package/dist/electron/dist-zip.d.ts +18 -0
- package/dist/electron/download.d.ts +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/packer/common/app-update-yml.d.ts +5 -0
- package/dist/packer/common/extra-resources.d.ts +39 -0
- package/dist/packer/common/staging.d.ts +3 -0
- package/dist/packer/common/template.d.ts +2 -0
- package/dist/packer/linux/linux.d.ts +2 -0
- package/dist/packer/mac/mac.d.ts +23 -0
- package/dist/packer/win/nsis.d.ts +24 -0
- package/dist/packer/win/resedit.d.ts +5 -0
- package/dist/packer/win/toolchain.d.ts +8 -0
- package/dist/packer/win/win.d.ts +12 -0
- package/dist/pipeline/pipeline.d.ts +41 -0
- package/dist/pipeline/targets.d.ts +28 -0
- package/dist/shared/hash.d.ts +2 -0
- package/dist/shared/logger.d.ts +39 -0
- package/dist/shared/platform.d.ts +26 -0
- package/dist/shared/types.d.ts +68 -0
- package/dist/update/blockmap.d.ts +13 -0
- package/dist/update/metadata.d.ts +20 -0
- package/dist/update/version.d.ts +7 -0
- package/package.json +71 -0
- package/src/archive/ar.ts +41 -0
- package/src/archive/seven-za.ts +160 -0
- package/src/archive/tar.ts +142 -0
- package/src/archive/zip-reader.ts +61 -0
- package/src/asar/dependency/collect.ts +437 -0
- package/src/asar/dependency/detector.ts +73 -0
- package/src/asar/dependency/hoister.ts +714 -0
- package/src/asar/dependency/utils.ts +225 -0
- package/src/asar/fileset/matcher.ts +101 -0
- package/src/asar/fileset/platform-fileset.ts +416 -0
- package/src/asar/fileset/transformer.ts +154 -0
- package/src/asar/fileset/unpack-detector.ts +38 -0
- package/src/asar/fileset/walker.ts +115 -0
- package/src/asar/pack.ts +186 -0
- package/src/asar/packer/app-collector.ts +74 -0
- package/src/asar/packer/collector.ts +134 -0
- package/src/asar/packer/concurrency.ts +24 -0
- package/src/asar/packer/integrity.ts +53 -0
- package/src/asar/packer/nm-collector.ts +194 -0
- package/src/asar/packer/stream-builder.ts +723 -0
- package/src/cli.ts +182 -0
- package/src/config/index.ts +365 -0
- package/src/electron/dist-zip.ts +65 -0
- package/src/electron/download.ts +121 -0
- package/src/index.ts +14 -0
- package/src/packer/common/app-update-yml.ts +21 -0
- package/src/packer/common/extra-resources.ts +83 -0
- package/src/packer/common/staging.ts +33 -0
- package/src/packer/common/template.ts +24 -0
- package/src/packer/linux/linux.ts +329 -0
- package/src/packer/mac/mac.ts +495 -0
- package/src/packer/win/nsis.ts +244 -0
- package/src/packer/win/resedit.ts +60 -0
- package/src/packer/win/toolchain.ts +63 -0
- package/src/packer/win/win.ts +421 -0
- package/src/pipeline/pipeline.ts +149 -0
- package/src/pipeline/targets.ts +67 -0
- package/src/shared/hash.ts +15 -0
- package/src/shared/logger.ts +91 -0
- package/src/shared/platform.ts +41 -0
- package/src/shared/types.ts +82 -0
- package/src/update/blockmap.ts +247 -0
- package/src/update/metadata.ts +97 -0
- package/src/update/version.ts +59 -0
- package/templates/linux/app.desktop +11 -0
- package/templates/linux/control +9 -0
- package/templates/linux/postinst +10 -0
- package/templates/linux/postrm +8 -0
- package/templates/linux/preinst +49 -0
- package/templates/linux/prerm +43 -0
- package/templates/mac/Info.plist +18 -0
- package/templates/mac/app-update.yml +4 -0
- package/templates/win/nsis/README.md +51 -0
- package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
- package/templates/win/nsis/assistedInstaller.nsh +154 -0
- package/templates/win/nsis/assistedMessages.yml +368 -0
- package/templates/win/nsis/common.nsh +147 -0
- package/templates/win/nsis/empty-license.txt +1 -0
- package/templates/win/nsis/include/FileAssociation.nsh +123 -0
- package/templates/win/nsis/include/StdUtils.nsh +496 -0
- package/templates/win/nsis/include/StrContains.nsh +48 -0
- package/templates/win/nsis/include/UAC.nsh +299 -0
- package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
- package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
- package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
- package/templates/win/nsis/include/installUtil.nsh +248 -0
- package/templates/win/nsis/include/installer.nsh +247 -0
- package/templates/win/nsis/include/nsProcess.nsh +28 -0
- package/templates/win/nsis/include/webPackage.nsh +64 -0
- package/templates/win/nsis/installSection.nsh +110 -0
- package/templates/win/nsis/installer.nsi +132 -0
- package/templates/win/nsis/messages.yml +240 -0
- package/templates/win/nsis/multiUser.nsh +134 -0
- package/templates/win/nsis/multiUserUi.nsh +230 -0
- package/templates/win/nsis/oneClick.nsh +19 -0
- package/templates/win/nsis/portable.nsi +91 -0
- package/templates/win/nsis/uninstaller.nsh +263 -0
- package/templates/win/nsis-header.nsh +57 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cross-packer CLI entry.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* cross-packer --win --amd64
|
|
7
|
+
* cross-packer --mac --arm64
|
|
8
|
+
* cross-packer --linux --all-arch
|
|
9
|
+
* cross-packer --config ./cross-packer.config.mjs --win --mac --linux
|
|
10
|
+
*/
|
|
11
|
+
import { parseArgs } from 'node:util'
|
|
12
|
+
import { setPrebuiltDirResolver } from './asar/fileset/platform-fileset.ts'
|
|
13
|
+
import { loadConfig, resolveConfigPath } from './config/index.ts'
|
|
14
|
+
import { runPack } from './pipeline/pipeline.ts'
|
|
15
|
+
import { createLogger } from './shared/logger.ts'
|
|
16
|
+
import { Arch, Platform } from './shared/platform.ts'
|
|
17
|
+
|
|
18
|
+
const SUPPORTED_PLATFORMS: Platform[] = [Platform.WIN, Platform.MAC, Platform.LINUX]
|
|
19
|
+
const SUPPORTED_ARCHES: Arch[] = [Arch.AMD64, Arch.ARM64]
|
|
20
|
+
|
|
21
|
+
interface CliOptions {
|
|
22
|
+
platforms: Platform[]
|
|
23
|
+
arches: Arch[]
|
|
24
|
+
configPath: string
|
|
25
|
+
output: string
|
|
26
|
+
channel: string
|
|
27
|
+
signEnabled: boolean | undefined
|
|
28
|
+
dryRun: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseCli(argv: string[]): CliOptions {
|
|
32
|
+
const { values } = parseArgs({
|
|
33
|
+
options: {
|
|
34
|
+
win: { type: 'boolean', default: false },
|
|
35
|
+
mac: { type: 'boolean', default: false },
|
|
36
|
+
linux: { type: 'boolean', default: false },
|
|
37
|
+
'all-platforms': { type: 'boolean', default: false },
|
|
38
|
+
amd64: { type: 'boolean', default: false },
|
|
39
|
+
x64: { type: 'boolean', default: false },
|
|
40
|
+
arm64: { type: 'boolean', default: false },
|
|
41
|
+
'all-arch': { type: 'boolean', default: false },
|
|
42
|
+
arch: { type: 'string', default: '' },
|
|
43
|
+
config: { type: 'string', default: '' },
|
|
44
|
+
output: { type: 'string', default: '' },
|
|
45
|
+
channel: { type: 'string', default: '' },
|
|
46
|
+
sign: { type: 'boolean' },
|
|
47
|
+
'dry-run': { type: 'boolean', default: false },
|
|
48
|
+
help: { type: 'boolean', default: false },
|
|
49
|
+
},
|
|
50
|
+
strict: true,
|
|
51
|
+
argv,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const platformFlags: Record<Platform, boolean> = {
|
|
55
|
+
win: values.win,
|
|
56
|
+
mac: values.mac,
|
|
57
|
+
linux: values.linux,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (values.help) {
|
|
61
|
+
printHelp()
|
|
62
|
+
process.exit(0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (values['all-platforms']) {
|
|
66
|
+
platformFlags.win = true
|
|
67
|
+
platformFlags.mac = true
|
|
68
|
+
platformFlags.linux = true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const platforms = SUPPORTED_PLATFORMS.filter((p) => platformFlags[p])
|
|
72
|
+
if (platforms.length === 0) {
|
|
73
|
+
fail('Missing target platform: expected --win, --mac, --linux, or --all-platforms')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let arches: Arch[] = []
|
|
77
|
+
if (values['all-arch']) {
|
|
78
|
+
arches = [...SUPPORTED_ARCHES]
|
|
79
|
+
} else if (values.arch) {
|
|
80
|
+
const requested = values.arch
|
|
81
|
+
.split(',')
|
|
82
|
+
.map((a) => a.trim())
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
const invalid = requested.filter((a) => !SUPPORTED_ARCHES.includes(a as Arch))
|
|
85
|
+
if (invalid.length > 0) {
|
|
86
|
+
fail(
|
|
87
|
+
`Unsupported architecture(s): ${invalid.join(', ')} (supported: ${SUPPORTED_ARCHES.join(', ')})`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
arches = requested as Arch[]
|
|
91
|
+
} else {
|
|
92
|
+
if (values.amd64 || values.x64) arches.push(Arch.AMD64)
|
|
93
|
+
if (values.arm64) arches.push(Arch.ARM64)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (arches.length === 0) {
|
|
97
|
+
// No arch flags given: use the platform's default for a single target,
|
|
98
|
+
// or all supported architectures when multiple platforms are requested.
|
|
99
|
+
arches = platforms.length === 1 ? defaultArchFor(platforms[0]) : [...SUPPORTED_ARCHES]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
platforms,
|
|
104
|
+
arches: [...new Set(arches)],
|
|
105
|
+
configPath: values.config,
|
|
106
|
+
output: values.output,
|
|
107
|
+
channel: values.channel,
|
|
108
|
+
signEnabled: values.sign,
|
|
109
|
+
dryRun: values['dry-run'],
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function defaultArchFor(platform: Platform): Arch[] {
|
|
114
|
+
if (platform === Platform.MAC) return [Arch.ARM64]
|
|
115
|
+
return [Arch.AMD64]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Print an error message and exit with a non-zero code. */
|
|
119
|
+
function fail(message: string): never {
|
|
120
|
+
console.error(`cross-packer: ${message}`)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function printHelp(): void {
|
|
125
|
+
process.stdout.write(`cross-packer — cross-platform Electron app packager
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
cross-packer [options]
|
|
129
|
+
|
|
130
|
+
Platforms:
|
|
131
|
+
--win Build Windows NSIS installer
|
|
132
|
+
--mac Build macOS .app (zipped)
|
|
133
|
+
--linux Build Linux .deb package
|
|
134
|
+
--all-platforms Equivalent to --win --mac --linux
|
|
135
|
+
|
|
136
|
+
Architectures:
|
|
137
|
+
--amd64 Target amd64 (x86-64, legacy alias: --x64)
|
|
138
|
+
--arm64 Target arm64
|
|
139
|
+
--all-arch Target both amd64 and arm64
|
|
140
|
+
--arch <list> Comma-separated list, e.g. --arch amd64,arm64
|
|
141
|
+
|
|
142
|
+
Options:
|
|
143
|
+
--config <path> Path to config file (.mjs or .json), defaults to cross-packer.config.mjs
|
|
144
|
+
--output <path> Output directory (default: dist-<timestamp>)
|
|
145
|
+
--channel <name> Update channel override (e.g. latest, beta)
|
|
146
|
+
--sign Enable code signing hooks (see config.sign)
|
|
147
|
+
--dry-run Validate config and targets without building
|
|
148
|
+
-h, --help Show this help
|
|
149
|
+
`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function main(): Promise<void> {
|
|
153
|
+
const cli = parseCli(process.argv.slice(2))
|
|
154
|
+
const logger = createLogger()
|
|
155
|
+
|
|
156
|
+
const configPath = resolveConfigPath(cli.configPath)
|
|
157
|
+
const config = await loadConfig(configPath, {
|
|
158
|
+
channel: cli.channel,
|
|
159
|
+
signEnabled: cli.signEnabled,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Install the prebuilt native-module replacement rules (see platform-fileset.ts).
|
|
163
|
+
if (config.nativeModules?.prebuiltRules) {
|
|
164
|
+
setPrebuiltDirResolver(config.nativeModules.prebuiltRules)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
logger.banner(config, cli)
|
|
168
|
+
|
|
169
|
+
const result = await runPack(config, cli)
|
|
170
|
+
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
process.exitCode = 1
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
logger.summary(result)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
main().catch((err: unknown) => {
|
|
179
|
+
const message = err instanceof Error ? err.stack || err.message : String(err)
|
|
180
|
+
console.error('cross-packer: fatal error:', message)
|
|
181
|
+
process.exit(1)
|
|
182
|
+
})
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loading and normalization.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { pathToFileURL } from 'node:url'
|
|
8
|
+
import type { PrebuiltModuleRule } from '../asar/fileset/platform-fileset.ts'
|
|
9
|
+
import type { ExtraResourceEntry } from '../packer/common/extra-resources.ts'
|
|
10
|
+
import type { Arch, NodePlatform } from '../shared/platform.ts'
|
|
11
|
+
import type { FileVisitor, JsonObject } from '../shared/types.ts'
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_CONFIG_FILENAME = 'cross-packer.config.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Raw user configuration loaded from disk. This is an untyped trust
|
|
17
|
+
* boundary: every field is validated and normalized before it reaches
|
|
18
|
+
* NormalizedConfig.
|
|
19
|
+
*/
|
|
20
|
+
type RawConfig = Record<string, unknown>
|
|
21
|
+
|
|
22
|
+
const REQUIRED_FIELDS = ['name', 'productName', 'appId', 'electronVersion']
|
|
23
|
+
|
|
24
|
+
const CONFIG_FILENAME_CANDIDATES = [
|
|
25
|
+
DEFAULT_CONFIG_FILENAME,
|
|
26
|
+
'cross-packer.config.json',
|
|
27
|
+
'cross-packer.json',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export interface NormalizedConfig {
|
|
31
|
+
name: string
|
|
32
|
+
productName: string
|
|
33
|
+
artifactName: string
|
|
34
|
+
appId: string
|
|
35
|
+
version: string
|
|
36
|
+
buildNumber?: number | string
|
|
37
|
+
description: string
|
|
38
|
+
author: string
|
|
39
|
+
homepage: string
|
|
40
|
+
electronVersion: string
|
|
41
|
+
electronMirror: string
|
|
42
|
+
copyright: string
|
|
43
|
+
projectDir: string
|
|
44
|
+
files?: JsonObject
|
|
45
|
+
appDistDir: string
|
|
46
|
+
asarUnpack: string[]
|
|
47
|
+
smartUnpack: boolean
|
|
48
|
+
extraResources: Array<ExtraResourceEntry>
|
|
49
|
+
extraMetadata?: Record<string, unknown>
|
|
50
|
+
onNodeModuleFile?: FileVisitor
|
|
51
|
+
disableDefaultIgnoredFiles: boolean
|
|
52
|
+
asar: { path?: string; unpackedDir?: string }
|
|
53
|
+
nativeModules: {
|
|
54
|
+
prebuiltRules?: (
|
|
55
|
+
platform: NodePlatform,
|
|
56
|
+
arch: Arch,
|
|
57
|
+
) => ReadonlyArray<PrebuiltModuleRule> | null | undefined
|
|
58
|
+
}
|
|
59
|
+
mac: {
|
|
60
|
+
bundleName: string
|
|
61
|
+
category: string
|
|
62
|
+
iconFile?: string
|
|
63
|
+
minimumSystemVersion: string
|
|
64
|
+
urlSchemes: string[]
|
|
65
|
+
/** Extra Info.plist entries merged on top of the generated keys. */
|
|
66
|
+
plist: Record<string, unknown>
|
|
67
|
+
}
|
|
68
|
+
win: {
|
|
69
|
+
iconFile?: string
|
|
70
|
+
compressionLevel: number
|
|
71
|
+
nsisVersion: string
|
|
72
|
+
nsisResourcesVersion: string
|
|
73
|
+
nsisCacheDir: string
|
|
74
|
+
shortcutName: string
|
|
75
|
+
}
|
|
76
|
+
linux: {
|
|
77
|
+
installDir: string
|
|
78
|
+
iconDir: string
|
|
79
|
+
iconSizes: number[]
|
|
80
|
+
debDepends: string[]
|
|
81
|
+
debRecommends: string
|
|
82
|
+
maintainer: string
|
|
83
|
+
mimeType: string
|
|
84
|
+
desktopExtraFile?: string
|
|
85
|
+
}
|
|
86
|
+
update: { channel: string; generateMetadata: boolean; url: string; extraArtifacts?: string[] }
|
|
87
|
+
sign: {
|
|
88
|
+
enabled: boolean
|
|
89
|
+
win?: {
|
|
90
|
+
enabled?: boolean
|
|
91
|
+
hook?: (
|
|
92
|
+
target: string,
|
|
93
|
+
ctx: { config: NormalizedConfig; arch: Arch; outputDir: string; singleFile?: boolean },
|
|
94
|
+
) => Promise<void> | void
|
|
95
|
+
}
|
|
96
|
+
mac?: {
|
|
97
|
+
enabled?: boolean
|
|
98
|
+
hook?: (
|
|
99
|
+
target: string,
|
|
100
|
+
ctx: { config: NormalizedConfig; arch: Arch; outputDir: string },
|
|
101
|
+
) => Promise<string> | string
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Overrides applied on top of the raw config during normalization. */
|
|
107
|
+
export interface LoadConfigOverrides {
|
|
108
|
+
channel?: string
|
|
109
|
+
signEnabled?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Resolve the config path: an explicit path, or the first default candidate. */
|
|
113
|
+
export function resolveConfigPath(explicitPath = ''): string {
|
|
114
|
+
if (explicitPath) {
|
|
115
|
+
const resolved = path.resolve(explicitPath)
|
|
116
|
+
if (!existsSync(resolved)) {
|
|
117
|
+
throw new Error(`Config file not found: ${resolved}`)
|
|
118
|
+
}
|
|
119
|
+
return resolved
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const candidates = CONFIG_FILENAME_CANDIDATES
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const resolved = path.resolve(candidate)
|
|
125
|
+
if (existsSync(resolved)) return resolved
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new Error(
|
|
129
|
+
`No config found. Create ${DEFAULT_CONFIG_FILENAME} in the project root, or pass --config <path>.`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function loadConfig(
|
|
134
|
+
configPath: string,
|
|
135
|
+
overrides: LoadConfigOverrides = {},
|
|
136
|
+
): Promise<NormalizedConfig> {
|
|
137
|
+
const raw = await importConfig(configPath)
|
|
138
|
+
|
|
139
|
+
validateRawConfig(raw, configPath)
|
|
140
|
+
|
|
141
|
+
const config = normalize(raw, {
|
|
142
|
+
configDir: path.dirname(configPath),
|
|
143
|
+
overrides,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return config
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function normalizeConfig(
|
|
150
|
+
raw: Record<string, unknown>,
|
|
151
|
+
overrides: LoadConfigOverrides = {},
|
|
152
|
+
): NormalizedConfig {
|
|
153
|
+
validateRawConfig(raw, '<in-memory config>')
|
|
154
|
+
return normalize(raw, { configDir: process.cwd(), overrides })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function importConfig(configPath: string): Promise<RawConfig> {
|
|
158
|
+
if (configPath.endsWith('.json')) {
|
|
159
|
+
return JSON.parse(await readFile(configPath, 'utf8')) as RawConfig
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const imported: Record<string, unknown> = await import(pathToFileURL(configPath).href)
|
|
163
|
+
const raw = imported.default ?? imported
|
|
164
|
+
if (typeof raw === 'function') {
|
|
165
|
+
return (raw as (env: NodeJS.ProcessEnv) => RawConfig)(process.env)
|
|
166
|
+
}
|
|
167
|
+
return raw as RawConfig
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateRawConfig(raw: RawConfig, configPath: string): void {
|
|
171
|
+
if (!raw || typeof raw !== 'object') {
|
|
172
|
+
throw new Error(`Config must export an object or a function returning one: ${configPath}`)
|
|
173
|
+
}
|
|
174
|
+
for (const field of REQUIRED_FIELDS) {
|
|
175
|
+
const value = raw[field]
|
|
176
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
177
|
+
throw new Error(`Config is missing required string field "${field}": ${configPath}`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Read a string field from a raw config, falling back to a default value. */
|
|
183
|
+
function str(value: unknown, fallback = ''): string {
|
|
184
|
+
return typeof value === 'string' ? value : fallback
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Narrow an unknown value to a record for nested section access. */
|
|
188
|
+
function rec(value: unknown): Record<string, unknown> {
|
|
189
|
+
return value != null && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Read a string array field, falling back to a default. */
|
|
193
|
+
function strArray(value: unknown, fallback: string[]): string[] {
|
|
194
|
+
return Array.isArray(value) && value.every((it) => typeof it === 'string') ? value : fallback
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Read a number array field, falling back to a default. */
|
|
198
|
+
function numArray(value: unknown, fallback: number[]): number[] {
|
|
199
|
+
return Array.isArray(value) && value.every((it) => typeof it === 'number') ? value : fallback
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Read an optional string field (undefined passes through). */
|
|
203
|
+
function strOptional(value: unknown): string | undefined {
|
|
204
|
+
return typeof value === 'string' ? value : undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalize(
|
|
208
|
+
raw: RawConfig,
|
|
209
|
+
{
|
|
210
|
+
configDir,
|
|
211
|
+
overrides,
|
|
212
|
+
}: {
|
|
213
|
+
configDir: string
|
|
214
|
+
overrides: LoadConfigOverrides
|
|
215
|
+
},
|
|
216
|
+
): NormalizedConfig {
|
|
217
|
+
// Required string fields are validated by validateRawConfig before normalization.
|
|
218
|
+
const required = raw as {
|
|
219
|
+
name: string
|
|
220
|
+
productName: string
|
|
221
|
+
appId: string
|
|
222
|
+
electronVersion: string
|
|
223
|
+
}
|
|
224
|
+
const pkgVersion = str(raw.version, '1.0.0')
|
|
225
|
+
const buildNumber =
|
|
226
|
+
typeof raw.buildNumber === 'number'
|
|
227
|
+
? raw.buildNumber
|
|
228
|
+
: typeof raw.buildNumber === 'string' && raw.buildNumber.trim()
|
|
229
|
+
? raw.buildNumber.trim()
|
|
230
|
+
: undefined
|
|
231
|
+
const mac = rec(raw.mac)
|
|
232
|
+
const win = rec(raw.win)
|
|
233
|
+
const linux = rec(raw.linux)
|
|
234
|
+
const update = rec(raw.update)
|
|
235
|
+
const asar = rec(raw.asar)
|
|
236
|
+
const files = rec(raw.files)
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
name: required.name,
|
|
240
|
+
productName: required.productName,
|
|
241
|
+
artifactName: str(raw.artifactName, required.productName),
|
|
242
|
+
appId: required.appId,
|
|
243
|
+
version: pkgVersion,
|
|
244
|
+
buildNumber,
|
|
245
|
+
description: str(raw.description),
|
|
246
|
+
author: str(raw.author),
|
|
247
|
+
homepage: str(raw.homepage),
|
|
248
|
+
|
|
249
|
+
electronVersion: required.electronVersion,
|
|
250
|
+
// Default to the official download host; regional mirrors are configured via electronMirror.
|
|
251
|
+
electronMirror: str(
|
|
252
|
+
raw.electronMirror,
|
|
253
|
+
'https://github.com/electron/electron/releases/download/',
|
|
254
|
+
),
|
|
255
|
+
|
|
256
|
+
copyright:
|
|
257
|
+
str(raw.copyright) || `Copyright © ${new Date().getFullYear()} ${str(raw.author)}`.trim(),
|
|
258
|
+
|
|
259
|
+
projectDir:
|
|
260
|
+
typeof raw.projectDir === 'string' ? path.resolve(configDir, raw.projectDir) : process.cwd(),
|
|
261
|
+
|
|
262
|
+
// Application payload collected from the project directory.
|
|
263
|
+
files,
|
|
264
|
+
appDistDir: str(files.appDistDir, 'dist'),
|
|
265
|
+
asarUnpack: strArray(raw.asarUnpack, ['**/*.node']),
|
|
266
|
+
smartUnpack: raw.smartUnpack !== false,
|
|
267
|
+
extraResources: (Array.isArray(raw.extraResources)
|
|
268
|
+
? raw.extraResources
|
|
269
|
+
: []) as NormalizedConfig['extraResources'],
|
|
270
|
+
extraMetadata: rec(raw.extraMetadata),
|
|
271
|
+
onNodeModuleFile: raw.onNodeModuleFile as FileVisitor | undefined,
|
|
272
|
+
disableDefaultIgnoredFiles: raw.disableDefaultIgnoredFiles === true,
|
|
273
|
+
|
|
274
|
+
// Asar payload source: omit asar.path to build it from projectDir, or set it
|
|
275
|
+
// to a pre-built asar produced by a bundler such as electron-vite or forge.
|
|
276
|
+
asar: {
|
|
277
|
+
path: strOptional(asar.path),
|
|
278
|
+
unpackedDir: strOptional(asar.unpackedDir),
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// Optional rules for projects that ship prebuilt native modules under a
|
|
282
|
+
// custom directory. Signature: prebuiltRules(platform, arch) → rules.
|
|
283
|
+
nativeModules: rec(raw.nativeModules) as NormalizedConfig['nativeModules'],
|
|
284
|
+
|
|
285
|
+
mac: {
|
|
286
|
+
bundleName: str(mac.bundleName, required.productName),
|
|
287
|
+
category: str(mac.category, 'public.app-category.productivity'),
|
|
288
|
+
iconFile: strOptional(mac.iconFile),
|
|
289
|
+
minimumSystemVersion: str(mac.minimumSystemVersion, '10.13'),
|
|
290
|
+
// URL schemes registered as CFBundleURLTypes entries in Info.plist.
|
|
291
|
+
urlSchemes: strArray(mac.urlSchemes, []),
|
|
292
|
+
plist: rec(mac.plist),
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
win: {
|
|
296
|
+
iconFile: strOptional(win.iconFile),
|
|
297
|
+
compressionLevel: typeof win.compressionLevel === 'number' ? win.compressionLevel : 5,
|
|
298
|
+
nsisVersion: str(win.nsisVersion, '3.0.4.1'),
|
|
299
|
+
nsisResourcesVersion: str(win.nsisResourcesVersion, '3.4.1'),
|
|
300
|
+
nsisCacheDir: str(win.nsisCacheDir, '.nsis-cache'),
|
|
301
|
+
shortcutName: str(win.shortcutName, required.productName),
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
linux: {
|
|
305
|
+
installDir: str(linux.installDir, required.name),
|
|
306
|
+
iconDir: str(linux.iconDir, 'build/icons'),
|
|
307
|
+
iconSizes: numArray(linux.iconSizes, [16, 24, 32, 48, 64, 128, 256, 512]),
|
|
308
|
+
debDepends: strArray(linux.debDepends, defaultDebDepends()),
|
|
309
|
+
debRecommends: str(linux.debRecommends),
|
|
310
|
+
maintainer: str(linux.maintainer, str(raw.author)),
|
|
311
|
+
// MIME types for desktop integration; deep-link schemes use the
|
|
312
|
+
// x-scheme-handler prefix.
|
|
313
|
+
mimeType: str(linux.mimeType),
|
|
314
|
+
desktopExtraFile: strOptional(linux.desktopExtraFile),
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// Update metadata (electron-updater compatible); all fields are optional.
|
|
318
|
+
update: {
|
|
319
|
+
channel: overrides.channel ?? str(update.channel, 'latest'),
|
|
320
|
+
generateMetadata: update.generateMetadata !== false,
|
|
321
|
+
// Base URL written to the embedded app-update.yml; an empty value omits embedding.
|
|
322
|
+
url: str(update.url),
|
|
323
|
+
extraArtifacts: Array.isArray(update.extraArtifacts)
|
|
324
|
+
? (update.extraArtifacts as string[])
|
|
325
|
+
: undefined,
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
sign: normalizeSign(raw.sign, overrides.signEnabled),
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeSign(
|
|
333
|
+
rawSign: unknown,
|
|
334
|
+
signEnabledOverride: boolean | undefined,
|
|
335
|
+
): NormalizedConfig['sign'] {
|
|
336
|
+
const sign = rec(rawSign)
|
|
337
|
+
const win = rec(sign.win)
|
|
338
|
+
const mac = rec(sign.mac)
|
|
339
|
+
return {
|
|
340
|
+
...(sign as NormalizedConfig['sign']),
|
|
341
|
+
enabled: signEnabledOverride ?? sign.enabled === true,
|
|
342
|
+
win: {
|
|
343
|
+
...(win as NormalizedConfig['sign']['win']),
|
|
344
|
+
enabled: win.enabled !== false,
|
|
345
|
+
},
|
|
346
|
+
mac: {
|
|
347
|
+
...(mac as NormalizedConfig['sign']['mac']),
|
|
348
|
+
enabled: mac.enabled !== false,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function defaultDebDepends(): string[] {
|
|
354
|
+
return [
|
|
355
|
+
'libgtk-3-0',
|
|
356
|
+
'libnotify4',
|
|
357
|
+
'libnss3',
|
|
358
|
+
'libxss1',
|
|
359
|
+
'libxtst6',
|
|
360
|
+
'xdg-utils',
|
|
361
|
+
'libatspi2.0-0',
|
|
362
|
+
'libuuid1',
|
|
363
|
+
'libsecret-1-0',
|
|
364
|
+
]
|
|
365
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { readZip } from '../archive/zip-reader.ts'
|
|
4
|
+
import type { Arch, NodePlatform } from '../shared/platform.ts'
|
|
5
|
+
import { downloadElectron, electronArchiveName } from './download.ts'
|
|
6
|
+
|
|
7
|
+
interface ElectronZipConfig {
|
|
8
|
+
electronVersion: string
|
|
9
|
+
electronMirror?: string
|
|
10
|
+
checksums?: Record<string, string>
|
|
11
|
+
cacheDir?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Download (with cache) and read the Electron distribution zip for a target.
|
|
16
|
+
*
|
|
17
|
+
* Checksums are resolved from the electron package's checksums.json when
|
|
18
|
+
* present; otherwise the caller provides config.checksums keyed by archive name.
|
|
19
|
+
*/
|
|
20
|
+
export async function downloadAndReadElectron(
|
|
21
|
+
platform: NodePlatform,
|
|
22
|
+
arch: Arch,
|
|
23
|
+
config: ElectronZipConfig,
|
|
24
|
+
) {
|
|
25
|
+
const expectedChecksum = await resolveChecksum(config, platform, arch)
|
|
26
|
+
const zipPath = await downloadElectron({
|
|
27
|
+
version: config.electronVersion,
|
|
28
|
+
platform,
|
|
29
|
+
arch,
|
|
30
|
+
mirror: config.electronMirror,
|
|
31
|
+
cacheDir: config.cacheDir,
|
|
32
|
+
expectedChecksum,
|
|
33
|
+
})
|
|
34
|
+
const { entries } = await readZip(zipPath)
|
|
35
|
+
return { zipPath, entries }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function resolveChecksum(
|
|
39
|
+
config: ElectronZipConfig,
|
|
40
|
+
platform: NodePlatform,
|
|
41
|
+
arch: Arch,
|
|
42
|
+
): Promise<string> {
|
|
43
|
+
const archiveName = electronArchiveName(config.electronVersion, platform, arch)
|
|
44
|
+
|
|
45
|
+
if (config.checksums?.[archiveName]) {
|
|
46
|
+
return config.checksums[archiveName]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const require = createRequire(import.meta.url)
|
|
51
|
+
const checksumsPath = require.resolve('electron/checksums.json', {
|
|
52
|
+
paths: [process.cwd()],
|
|
53
|
+
})
|
|
54
|
+
const checksums = JSON.parse(await readFile(checksumsPath, 'utf8')) as Record<string, string>
|
|
55
|
+
if (!checksums[archiveName]) {
|
|
56
|
+
throw new Error(`No official checksum for ${archiveName}`)
|
|
57
|
+
}
|
|
58
|
+
return checksums[archiveName]
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Cannot verify Electron archive ${archiveName}: ${message}. Install the "electron" package in the target project or provide config.checksums.`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|