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.
Files changed (126) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +151 -0
  3. package/README.zh-CN.md +151 -0
  4. package/dist/archive/ar.d.ts +12 -0
  5. package/dist/archive/seven-za.d.ts +32 -0
  6. package/dist/archive/tar.d.ts +30 -0
  7. package/dist/archive/zip-reader.d.ts +17 -0
  8. package/dist/asar/dependency/collect.d.ts +2 -0
  9. package/dist/asar/dependency/detector.d.ts +8 -0
  10. package/dist/asar/dependency/hoister.d.ts +27 -0
  11. package/dist/asar/dependency/utils.d.ts +59 -0
  12. package/dist/asar/fileset/matcher.d.ts +12 -0
  13. package/dist/asar/fileset/platform-fileset.d.ts +36 -0
  14. package/dist/asar/fileset/transformer.d.ts +2 -0
  15. package/dist/asar/fileset/unpack-detector.d.ts +10 -0
  16. package/dist/asar/fileset/walker.d.ts +14 -0
  17. package/dist/asar/pack.d.ts +23 -0
  18. package/dist/asar/packer/app-collector.d.ts +2 -0
  19. package/dist/asar/packer/collector.d.ts +18 -0
  20. package/dist/asar/packer/concurrency.d.ts +1 -0
  21. package/dist/asar/packer/integrity.d.ts +8 -0
  22. package/dist/asar/packer/nm-collector.d.ts +7 -0
  23. package/dist/asar/packer/stream-builder.d.ts +18 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/config/index.d.ts +97 -0
  26. package/dist/electron/dist-zip.d.ts +18 -0
  27. package/dist/electron/download.d.ts +21 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/packer/common/app-update-yml.d.ts +5 -0
  30. package/dist/packer/common/extra-resources.d.ts +39 -0
  31. package/dist/packer/common/staging.d.ts +3 -0
  32. package/dist/packer/common/template.d.ts +2 -0
  33. package/dist/packer/linux/linux.d.ts +2 -0
  34. package/dist/packer/mac/mac.d.ts +23 -0
  35. package/dist/packer/win/nsis.d.ts +24 -0
  36. package/dist/packer/win/resedit.d.ts +5 -0
  37. package/dist/packer/win/toolchain.d.ts +8 -0
  38. package/dist/packer/win/win.d.ts +12 -0
  39. package/dist/pipeline/pipeline.d.ts +41 -0
  40. package/dist/pipeline/targets.d.ts +28 -0
  41. package/dist/shared/hash.d.ts +2 -0
  42. package/dist/shared/logger.d.ts +39 -0
  43. package/dist/shared/platform.d.ts +26 -0
  44. package/dist/shared/types.d.ts +68 -0
  45. package/dist/update/blockmap.d.ts +13 -0
  46. package/dist/update/metadata.d.ts +20 -0
  47. package/dist/update/version.d.ts +7 -0
  48. package/package.json +71 -0
  49. package/src/archive/ar.ts +41 -0
  50. package/src/archive/seven-za.ts +160 -0
  51. package/src/archive/tar.ts +142 -0
  52. package/src/archive/zip-reader.ts +61 -0
  53. package/src/asar/dependency/collect.ts +437 -0
  54. package/src/asar/dependency/detector.ts +73 -0
  55. package/src/asar/dependency/hoister.ts +714 -0
  56. package/src/asar/dependency/utils.ts +225 -0
  57. package/src/asar/fileset/matcher.ts +101 -0
  58. package/src/asar/fileset/platform-fileset.ts +416 -0
  59. package/src/asar/fileset/transformer.ts +154 -0
  60. package/src/asar/fileset/unpack-detector.ts +38 -0
  61. package/src/asar/fileset/walker.ts +115 -0
  62. package/src/asar/pack.ts +186 -0
  63. package/src/asar/packer/app-collector.ts +74 -0
  64. package/src/asar/packer/collector.ts +134 -0
  65. package/src/asar/packer/concurrency.ts +24 -0
  66. package/src/asar/packer/integrity.ts +53 -0
  67. package/src/asar/packer/nm-collector.ts +194 -0
  68. package/src/asar/packer/stream-builder.ts +723 -0
  69. package/src/cli.ts +182 -0
  70. package/src/config/index.ts +365 -0
  71. package/src/electron/dist-zip.ts +65 -0
  72. package/src/electron/download.ts +121 -0
  73. package/src/index.ts +14 -0
  74. package/src/packer/common/app-update-yml.ts +21 -0
  75. package/src/packer/common/extra-resources.ts +83 -0
  76. package/src/packer/common/staging.ts +33 -0
  77. package/src/packer/common/template.ts +24 -0
  78. package/src/packer/linux/linux.ts +329 -0
  79. package/src/packer/mac/mac.ts +495 -0
  80. package/src/packer/win/nsis.ts +244 -0
  81. package/src/packer/win/resedit.ts +60 -0
  82. package/src/packer/win/toolchain.ts +63 -0
  83. package/src/packer/win/win.ts +421 -0
  84. package/src/pipeline/pipeline.ts +149 -0
  85. package/src/pipeline/targets.ts +67 -0
  86. package/src/shared/hash.ts +15 -0
  87. package/src/shared/logger.ts +91 -0
  88. package/src/shared/platform.ts +41 -0
  89. package/src/shared/types.ts +82 -0
  90. package/src/update/blockmap.ts +247 -0
  91. package/src/update/metadata.ts +97 -0
  92. package/src/update/version.ts +59 -0
  93. package/templates/linux/app.desktop +11 -0
  94. package/templates/linux/control +9 -0
  95. package/templates/linux/postinst +10 -0
  96. package/templates/linux/postrm +8 -0
  97. package/templates/linux/preinst +49 -0
  98. package/templates/linux/prerm +43 -0
  99. package/templates/mac/Info.plist +18 -0
  100. package/templates/mac/app-update.yml +4 -0
  101. package/templates/win/nsis/README.md +51 -0
  102. package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
  103. package/templates/win/nsis/assistedInstaller.nsh +154 -0
  104. package/templates/win/nsis/assistedMessages.yml +368 -0
  105. package/templates/win/nsis/common.nsh +147 -0
  106. package/templates/win/nsis/empty-license.txt +1 -0
  107. package/templates/win/nsis/include/FileAssociation.nsh +123 -0
  108. package/templates/win/nsis/include/StdUtils.nsh +496 -0
  109. package/templates/win/nsis/include/StrContains.nsh +48 -0
  110. package/templates/win/nsis/include/UAC.nsh +299 -0
  111. package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
  112. package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
  113. package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
  114. package/templates/win/nsis/include/installUtil.nsh +248 -0
  115. package/templates/win/nsis/include/installer.nsh +247 -0
  116. package/templates/win/nsis/include/nsProcess.nsh +28 -0
  117. package/templates/win/nsis/include/webPackage.nsh +64 -0
  118. package/templates/win/nsis/installSection.nsh +110 -0
  119. package/templates/win/nsis/installer.nsi +132 -0
  120. package/templates/win/nsis/messages.yml +240 -0
  121. package/templates/win/nsis/multiUser.nsh +134 -0
  122. package/templates/win/nsis/multiUserUi.nsh +230 -0
  123. package/templates/win/nsis/oneClick.nsh +19 -0
  124. package/templates/win/nsis/portable.nsi +91 -0
  125. package/templates/win/nsis/uninstaller.nsh +263 -0
  126. 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
+ }