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
@@ -0,0 +1,115 @@
1
+ import { type Dirent, existsSync, lstatSync, readdirSync, readlinkSync, statSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { FileStat } from '../../shared/types.ts'
4
+ import { EntryKind, entryKind } from '../../shared/types.ts'
5
+
6
+ export function walkDirectory(
7
+ dirPath: string,
8
+ files: string[],
9
+ metadata: Map<string, FileStat>,
10
+ projectDir: string,
11
+ ): void {
12
+ if (!existsSync(dirPath)) return
13
+
14
+ let entries: Dirent[]
15
+ try {
16
+ entries = readdirSync(dirPath, { withFileTypes: true })
17
+ } catch {
18
+ return
19
+ }
20
+
21
+ for (const entry of entries) {
22
+ const fullPath = path.join(dirPath, entry.name)
23
+
24
+ let stat: ReturnType<typeof lstatSync>
25
+ try {
26
+ stat = lstatSync(fullPath)
27
+ } catch {
28
+ continue
29
+ }
30
+
31
+ const type = entryKind(stat)
32
+ if (type === null) continue
33
+
34
+ const meta: FileStat = { type, size: stat.size, mode: stat.mode }
35
+
36
+ const nodeModulesIdx = fullPath.lastIndexOf(`${path.sep}node_modules${path.sep}`)
37
+ if (nodeModulesIdx !== -1) {
38
+ const afterNodeModules = fullPath.slice(nodeModulesIdx + '/node_modules/'.length)
39
+ const moduleName = afterNodeModules.split(path.sep)[0]
40
+ if (moduleName.startsWith('@')) {
41
+ const scopedName = afterNodeModules.split(path.sep).slice(0, 2).join(path.sep)
42
+ meta.moduleRootPath = fullPath.slice(
43
+ 0,
44
+ nodeModulesIdx + '/node_modules/'.length + scopedName.length,
45
+ )
46
+ meta.moduleName = scopedName
47
+ } else {
48
+ meta.moduleRootPath = fullPath.slice(
49
+ 0,
50
+ nodeModulesIdx + '/node_modules/'.length + moduleName.length,
51
+ )
52
+ meta.moduleName = moduleName
53
+ }
54
+ meta.moduleFullFilePath = fullPath
55
+ }
56
+
57
+ if (meta.type === EntryKind.LINK) {
58
+ let linkTarget: string
59
+ try {
60
+ linkTarget = readlinkSync(fullPath)
61
+ } catch {
62
+ continue
63
+ }
64
+ const resolvedLinkTarget = path.resolve(dirPath, linkTarget)
65
+ const link = path.relative(projectDir, resolvedLinkTarget)
66
+ if (link.startsWith('..')) {
67
+ try {
68
+ const targetStat = statSync(resolvedLinkTarget)
69
+ const targetType = entryKind(targetStat)
70
+ if (targetType === null) continue
71
+ metadata.set(fullPath, {
72
+ type: targetType,
73
+ size: targetStat.size,
74
+ mode: targetStat.mode,
75
+ })
76
+ files.push(fullPath)
77
+ continue
78
+ } catch {
79
+ continue
80
+ }
81
+ }
82
+ meta.relativeLink = link
83
+ meta.linkRelativeToFile = path.relative(dirPath, resolvedLinkTarget)
84
+ }
85
+
86
+ files.push(fullPath)
87
+ metadata.set(fullPath, meta)
88
+
89
+ if (meta.type === EntryKind.DIRECTORY) {
90
+ walkDirectory(fullPath, files, metadata, projectDir)
91
+ }
92
+ }
93
+ }
94
+
95
+ export function mergeFileSets(
96
+ set1: { files: string[]; metadata: Map<string, FileStat> },
97
+ set2: { files: string[]; metadata: Map<string, FileStat> },
98
+ ): { files: string[]; metadata: Map<string, FileStat> } {
99
+ const metadata = new Map([...set1.metadata, ...set2.metadata])
100
+ const fileSet = new Set([...set1.files, ...set2.files])
101
+ const files = Array.from(fileSet).sort()
102
+ return { files, metadata }
103
+ }
104
+
105
+ /** Derive the deepest common ancestor directory of a file list. */
106
+ export function deriveProjectDir(files: string[]): string {
107
+ if (files.length === 0) return process.cwd()
108
+ let prefix = files[0]
109
+ for (const f of files) {
110
+ while (!f.startsWith(prefix + path.sep) && f !== prefix) {
111
+ prefix = path.dirname(prefix)
112
+ }
113
+ }
114
+ return prefix
115
+ }
@@ -0,0 +1,186 @@
1
+ import os from 'node:os'
2
+ import path from 'node:path'
3
+ import { Minimatch } from 'minimatch'
4
+ import type { NormalizedConfig } from '../config/index.ts'
5
+ import { logger } from '../shared/logger.ts'
6
+ import { type Arch, NodePlatform, nodePlatformOf, Platform } from '../shared/platform.ts'
7
+ import type { FileFilter, FileSet, FileStat, JsonObject } from '../shared/types.ts'
8
+ import { createFilter, hasMagic } from './fileset/matcher.ts'
9
+ import { derivePlatformFileSet } from './fileset/platform-fileset.ts'
10
+ import { collectAllFiles } from './packer/collector.ts'
11
+ import { mapWithConcurrency } from './packer/concurrency.ts'
12
+ import { packAsarFromFileSets } from './packer/stream-builder.ts'
13
+
14
+ const TRANSFORM_CONCURRENCY = Number(
15
+ process.env.CROSS_PACKER_ASAR_TRANSFORM_CONCURRENCY ?? Math.max(4, os.cpus().length),
16
+ )
17
+
18
+ const PLATFORM_NAMES: Record<Platform, NodePlatform> = {
19
+ [Platform.WIN]: NodePlatform.WIN32,
20
+ [Platform.MAC]: NodePlatform.DARWIN,
21
+ [Platform.LINUX]: NodePlatform.LINUX,
22
+ }
23
+
24
+ /** A FileSet plus the transformed contents collected during packing. */
25
+ interface PackedFileSet extends FileSet {
26
+ transformedFiles?: Map<number, string>
27
+ }
28
+
29
+ type Transformer = (file: string) => Promise<string | null> | string | null
30
+
31
+ /** A files-config entry: either a glob string or a mapping object with a filter. */
32
+ interface FilesConfigEntry {
33
+ from?: string
34
+ filter?: string | string[]
35
+ }
36
+
37
+ export async function collectProjectFiles(projectDir: string, config: NormalizedConfig) {
38
+ const nodeModuleFileFilter = createNodeModuleFileFilter(config.files, projectDir)
39
+
40
+ logger.info('\n Collecting all files (single traversal) ...')
41
+ const hostPlatform = nodePlatformOf(process.platform)
42
+ const { appFileSet, nmFileSets, transformer } = await collectAllFiles(projectDir, {
43
+ platform: hostPlatform,
44
+ appDistDir: config.appDistDir,
45
+ includePdb: false,
46
+ disableDefaultIgnoredFiles: config.disableDefaultIgnoredFiles ?? false,
47
+ workspaceRoot: projectDir,
48
+ extraMetadata: config.extraMetadata,
49
+ nodeModuleFileFilter: nodeModuleFileFilter,
50
+ onNodeModuleFile: config.onNodeModuleFile,
51
+ })
52
+ logger.info(` App files: ${appFileSet.files.length} entries`)
53
+ logger.info(` node_modules fileSets: ${nmFileSets.length}`)
54
+
55
+ return { appFileSet, nmFileSets, transformer }
56
+ }
57
+
58
+ export async function packAsarForTarget({
59
+ platform,
60
+ arch,
61
+ appFileSet,
62
+ nmFileSets,
63
+ transformer,
64
+ config,
65
+ projectDir,
66
+ stagingDir,
67
+ }: {
68
+ platform: Platform
69
+ arch: Arch
70
+ appFileSet: FileSet
71
+ nmFileSets: FileSet[]
72
+ transformer: Transformer
73
+ config: NormalizedConfig
74
+ projectDir: string
75
+ stagingDir: string
76
+ }) {
77
+ const platformName = PLATFORM_NAMES[platform]
78
+ const asarOutputPath = path.join(stagingDir, `app-${platform}-${arch}.asar`)
79
+
80
+ const label = `${platform}-${arch}`
81
+ logger.info(` [${label}] Deriving platform file set ...`)
82
+ const platformAppFileSet = derivePlatformFileSet(appFileSet, platformName, arch, projectDir)
83
+ const platformNmFileSets = nmFileSets.map((s) =>
84
+ derivePlatformFileSet(s, platformName, arch, projectDir),
85
+ )
86
+ const allFileSets: PackedFileSet[] = [platformAppFileSet, ...platformNmFileSets]
87
+ logger.info(` [${label}] Total fileSets: ${allFileSets.length}`)
88
+
89
+ interface TransformTask {
90
+ fileSet: PackedFileSet
91
+ index: number
92
+ file: string
93
+ fileStat: FileStat
94
+ }
95
+ const transformTasks: TransformTask[] = []
96
+ for (const fileSet of allFileSets) {
97
+ fileSet.transformedFiles = new Map()
98
+ for (let i = 0; i < fileSet.files.length; i++) {
99
+ const file = fileSet.files[i]
100
+ const fileStat = fileSet.metadata.get(file)
101
+ if (fileStat?.type !== 'file') continue
102
+ transformTasks.push({ fileSet, index: i, file, fileStat })
103
+ }
104
+ }
105
+
106
+ await mapWithConcurrency(
107
+ transformTasks,
108
+ TRANSFORM_CONCURRENCY,
109
+ async ({ fileSet, index, file, fileStat }: TransformTask) => {
110
+ const fileToRead = fileStat.realFilePath || file
111
+ const transformedValue = await transformer(fileToRead)
112
+ if (transformedValue != null) {
113
+ fileSet.transformedFiles?.set(index, transformedValue)
114
+ }
115
+ },
116
+ )
117
+
118
+ const packStartedAt = performance.now()
119
+ logger.info(` [${label}] Packing asar ...`)
120
+ const { asarPath, unpackedDir } = await packAsarFromFileSets(allFileSets, {
121
+ outputPath: asarOutputPath,
122
+ unpackPattern: createAsarUnpackFilter(config.asarUnpack, projectDir),
123
+ smartUnpack: config.smartUnpack,
124
+ defaultDestination: projectDir,
125
+ workspaceRoot: projectDir,
126
+ })
127
+ const packElapsedSec = ((performance.now() - packStartedAt) / 1000).toFixed(1)
128
+ logger.info(` [${label}] app.asar: ${asarPath} (${packElapsedSec}s)`)
129
+
130
+ return { asarPath, unpackedDir }
131
+ }
132
+
133
+ function isFilesConfigEntry(value: unknown): value is FilesConfigEntry {
134
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
135
+ }
136
+
137
+ /** Collect exclusion patterns from a files config entry. */
138
+ function addPatterns(patterns: unknown, excludePatterns: string[]): void {
139
+ if (patterns == null) return
140
+ if (!Array.isArray(patterns)) {
141
+ if (typeof patterns === 'string' && patterns.startsWith('!')) {
142
+ excludePatterns.push(patterns)
143
+ }
144
+ return
145
+ }
146
+ for (const pattern of patterns) {
147
+ if (typeof pattern === 'string') {
148
+ if (pattern.startsWith('!')) {
149
+ excludePatterns.push(pattern)
150
+ }
151
+ } else if (isFilesConfigEntry(pattern) && (pattern.from == null || pattern.from === '.')) {
152
+ const filter = pattern.filter
153
+ const filterList = Array.isArray(filter) ? filter : typeof filter === 'string' ? [filter] : []
154
+ for (const p of filterList) {
155
+ excludePatterns.push(p)
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ function buildMinimatchPatterns(patterns: string[]): Minimatch[] {
162
+ const parsedPatterns: Minimatch[] = []
163
+ for (const pattern of patterns) {
164
+ const parsedPattern = new Minimatch(pattern, { dot: true })
165
+ parsedPatterns.push(parsedPattern)
166
+ if (!pattern.includes('.') && !pattern.startsWith('!') && !hasMagic(parsedPattern)) {
167
+ parsedPatterns.push(new Minimatch(`${pattern}/**/*`, { dot: true }))
168
+ }
169
+ }
170
+ return parsedPatterns
171
+ }
172
+
173
+ function createAsarUnpackFilter(patterns: string[], baseDir: string): FileFilter {
174
+ return createFilter(baseDir, buildMinimatchPatterns(patterns))
175
+ }
176
+
177
+ function createNodeModuleFileFilter(
178
+ filesConfig: JsonObject | undefined,
179
+ baseDir: string,
180
+ ): FileFilter | undefined {
181
+ const excludePatterns: string[] = []
182
+ addPatterns(filesConfig, excludePatterns)
183
+ if (excludePatterns.length === 0) return undefined
184
+ const allPatterns = ['**/*', ...excludePatterns.map((p) => (p.startsWith('!') ? p : `!${p}`))]
185
+ return createFilter(baseDir, buildMinimatchPatterns(allPatterns))
186
+ }
@@ -0,0 +1,74 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { lstat } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import { Minimatch } from 'minimatch'
5
+ import { entryKind, type FileSet, type FileStat } from '../../shared/types.ts'
6
+ import { createFilter, excludedExts, excludedNames } from '../fileset/matcher.ts'
7
+ import { walkDirectory } from '../fileset/walker.ts'
8
+
9
+ export async function collectAppFileSet(
10
+ projectDir: string,
11
+ distDir: string,
12
+ includePdb: boolean,
13
+ ): Promise<FileSet> {
14
+ const metadata = new Map<string, FileStat>()
15
+ const files: string[] = []
16
+
17
+ walkDirectory(distDir, files, metadata, projectDir)
18
+
19
+ const pkgJsonPath = path.join(projectDir, 'package.json')
20
+ if (existsSync(pkgJsonPath)) {
21
+ const stat = await lstat(pkgJsonPath)
22
+ const type = entryKind(stat)
23
+ if (type === null) {
24
+ throw new Error(`package.json is not a regular file: ${pkgJsonPath}`)
25
+ }
26
+ files.push(pkgJsonPath)
27
+ metadata.set(pkgJsonPath, { type, size: stat.size, mode: stat.mode })
28
+ }
29
+
30
+ const patterns = computeAppPatterns(projectDir, includePdb)
31
+ const appFilter = createFilter(projectDir, patterns)
32
+
33
+ const filtered = files.filter((f) => {
34
+ if (f === projectDir) return true
35
+ const stat = metadata.get(f)
36
+ if (!stat) return false
37
+ return appFilter(f, stat)
38
+ })
39
+ const filteredMetadata = new Map<string, FileStat>()
40
+ for (const f of filtered) {
41
+ const m = metadata.get(f)
42
+ if (m) filteredMetadata.set(f, m)
43
+ }
44
+
45
+ filtered.sort()
46
+ return {
47
+ src: projectDir,
48
+ destination: projectDir,
49
+ files: filtered,
50
+ metadata: filteredMetadata,
51
+ }
52
+ }
53
+
54
+ function computeAppPatterns(src: string, includePdb: boolean): Minimatch[] {
55
+ const patterns: Minimatch[] = []
56
+ patterns.push(new Minimatch('**/*', { dot: true }))
57
+ patterns.push(new Minimatch('!**/node_modules/**', { dot: true }))
58
+ const relativeOutDir = path.relative(src, path.join(src, '..', 'dist'))
59
+ if (relativeOutDir.length !== 0 && !relativeOutDir.startsWith('.')) {
60
+ patterns.push(new Minimatch(`!${relativeOutDir}{,/**/*}`, { dot: true }))
61
+ }
62
+ const effectiveExts = includePdb === true ? excludedExts : `${excludedExts},pdb`
63
+ patterns.push(new Minimatch(`!**/*.{${effectiveExts}}`, { dot: true }))
64
+ patterns.push(new Minimatch('!**/._*', { dot: true }))
65
+ patterns.push(new Minimatch('!**/electron-builder.{yaml,yml,json,json5,toml,ts}', { dot: true }))
66
+ const excludedNamesList = excludedNames.split(',')
67
+ for (const name of excludedNamesList) {
68
+ patterns.push(new Minimatch(`!**/${name}`, { dot: true }))
69
+ }
70
+ patterns.push(new Minimatch('!.yarn{,/**/*}', { dot: true }))
71
+ patterns.push(new Minimatch('!.editorconfig', { dot: true }))
72
+ patterns.push(new Minimatch('!.yarnrc.yml', { dot: true }))
73
+ return patterns
74
+ }
@@ -0,0 +1,134 @@
1
+ import { existsSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { logger } from '../../shared/logger.ts'
4
+ import type { NodePlatform } from '../../shared/platform.ts'
5
+ import type { DependencyEntry, FileFilter, FileSet, FileVisitor } from '../../shared/types.ts'
6
+ import { collectProductionDeps } from '../dependency/collect.ts'
7
+ import { createTransformer } from '../fileset/transformer.ts'
8
+ import { collectAppFileSet } from './app-collector.ts'
9
+ import { collectNodeModuleFiles } from './nm-collector.ts'
10
+
11
+ export interface CollectAllFilesOptions {
12
+ platform: NodePlatform
13
+ /** Directory under projectDir holding the built app (e.g. "out" for electron-vite). */
14
+ appDistDir: string
15
+ includePdb: boolean
16
+ disableDefaultIgnoredFiles: boolean
17
+ workspaceRoot?: string
18
+ extraMetadata?: Record<string, unknown>
19
+ nodeModuleFileFilter?: FileFilter
20
+ onNodeModuleFile?: FileVisitor
21
+ }
22
+
23
+ export async function collectAllFiles(
24
+ projectDir: string,
25
+ {
26
+ platform,
27
+ appDistDir,
28
+ includePdb,
29
+ disableDefaultIgnoredFiles,
30
+ workspaceRoot,
31
+ extraMetadata,
32
+ nodeModuleFileFilter,
33
+ onNodeModuleFile,
34
+ }: CollectAllFilesOptions,
35
+ ): Promise<{
36
+ appFileSet: FileSet
37
+ nmFileSets: FileSet[]
38
+ transformer: (file: string) => Promise<string | null> | string | null
39
+ }> {
40
+ const distDir = path.join(projectDir, appDistDir)
41
+ if (!existsSync(distDir)) {
42
+ throw new Error(
43
+ `${appDistDir}/ directory not found. Build the app first, or set files.appDistDir in the config.`,
44
+ )
45
+ }
46
+
47
+ logger.info(` Collecting app files (${appDistDir}/ + package.json) ...`)
48
+ const appFileSet = await collectAppFileSet(projectDir, distDir, includePdb)
49
+ logger.info(` App files: ${appFileSet.files.length} entries`)
50
+
51
+ logger.info(' Collecting production node_modules ...')
52
+ const nmFileSets = await collectNodeModuleFileSets(projectDir, {
53
+ platform,
54
+ includePdb,
55
+ disableDefaultIgnoredFiles,
56
+ workspaceRoot,
57
+ filter: nodeModuleFileFilter,
58
+ onNodeModuleFile,
59
+ })
60
+ logger.info(` node_modules fileSets: ${nmFileSets.length}`)
61
+
62
+ const transformer = createTransformer(projectDir, extraMetadata)
63
+
64
+ return { appFileSet, nmFileSets, transformer }
65
+ }
66
+
67
+ interface CollectNodeModuleFileSetsOptions {
68
+ platform: NodePlatform
69
+ includePdb: boolean
70
+ disableDefaultIgnoredFiles: boolean
71
+ workspaceRoot?: string
72
+ filter?: FileFilter
73
+ onNodeModuleFile?: FileVisitor
74
+ }
75
+
76
+ async function collectNodeModuleFileSets(
77
+ projectDir: string,
78
+ {
79
+ platform,
80
+ includePdb,
81
+ disableDefaultIgnoredFiles,
82
+ workspaceRoot,
83
+ filter,
84
+ onNodeModuleFile,
85
+ }: CollectNodeModuleFileSetsOptions,
86
+ ): Promise<FileSet[]> {
87
+ const nmDir = path.join(projectDir, 'node_modules')
88
+ if (!existsSync(nmDir)) {
89
+ return []
90
+ }
91
+
92
+ const productionPackages = await collectProductionDeps(projectDir, workspaceRoot)
93
+ logger.info(` Production packages: ${productionPackages.length}`)
94
+
95
+ const result: FileSet[] = []
96
+
97
+ const collectModule = async (dep: DependencyEntry, parentDestination: string): Promise<void> => {
98
+ const pkgDir = dep.dir
99
+ if (!pkgDir || !existsSync(pkgDir)) return
100
+
101
+ const NODE_MODULES = 'node_modules'
102
+ const destination = path.join(parentDestination, NODE_MODULES, dep.name)
103
+
104
+ const { files: pkgFiles, metadata: pkgMeta } = await collectNodeModuleFiles(
105
+ pkgDir,
106
+ dep.name,
107
+ destination,
108
+ platform,
109
+ includePdb,
110
+ disableDefaultIgnoredFiles,
111
+ filter,
112
+ onNodeModuleFile,
113
+ )
114
+
115
+ result.push({
116
+ src: pkgDir,
117
+ destination,
118
+ files: pkgFiles,
119
+ metadata: pkgMeta,
120
+ })
121
+
122
+ if (dep.dependencies && dep.dependencies.length > 0) {
123
+ for (const child of dep.dependencies) {
124
+ await collectModule(child, destination)
125
+ }
126
+ }
127
+ }
128
+
129
+ for (const dep of productionPackages) {
130
+ await collectModule(dep, projectDir)
131
+ }
132
+
133
+ return result
134
+ }
@@ -0,0 +1,24 @@
1
+ export async function mapWithConcurrency<T, R>(
2
+ items: T[],
3
+ concurrency: number,
4
+ mapper: (item: T, index: number) => Promise<R>,
5
+ ): Promise<R[]> {
6
+ if (items.length === 0) {
7
+ return []
8
+ }
9
+
10
+ const limit = Math.max(1, Math.min(concurrency, items.length))
11
+ const results = new Array<R>(items.length)
12
+ let nextIndex = 0
13
+
14
+ async function worker(): Promise<void> {
15
+ while (nextIndex < items.length) {
16
+ const index = nextIndex
17
+ nextIndex += 1
18
+ results[index] = await mapper(items[index], index)
19
+ }
20
+ }
21
+
22
+ await Promise.all(Array.from({ length: limit }, () => worker()))
23
+ return results
24
+ }
@@ -0,0 +1,53 @@
1
+ import crypto from 'node:crypto'
2
+ import { createRequire } from 'node:module'
3
+ import fsExtra from 'fs-extra'
4
+
5
+ const require = createRequire(import.meta.url)
6
+ const chromiumPickle = require('chromium-pickle-js')
7
+
8
+ interface PickleIterator {
9
+ readUInt32(): number
10
+ readString(): string
11
+ }
12
+
13
+ interface Pickle {
14
+ createIterator(): PickleIterator
15
+ }
16
+
17
+ export async function readAsarHeader(archive: string): Promise<{ header: string; size: number }> {
18
+ const fd = await fsExtra.open(archive, 'r')
19
+ let size: number
20
+ let headerBuf: Buffer
21
+
22
+ try {
23
+ const sizeBuf = Buffer.allocUnsafe(8)
24
+ const { bytesRead: sizeBytesRead } = await fsExtra.read(fd, sizeBuf, 0, 8, null)
25
+ if (sizeBytesRead !== 8) {
26
+ throw new Error('Unable to read header size')
27
+ }
28
+
29
+ const sizePickle: Pickle = chromiumPickle.createFromBuffer(sizeBuf)
30
+ size = sizePickle.createIterator().readUInt32()
31
+
32
+ headerBuf = Buffer.allocUnsafe(size)
33
+ const { bytesRead: headerBytesRead } = await fsExtra.read(fd, headerBuf, 0, size, null)
34
+ if (headerBytesRead !== size) {
35
+ throw new Error('Unable to read header')
36
+ }
37
+ } finally {
38
+ await fsExtra.close(fd)
39
+ }
40
+
41
+ const headerPickle: Pickle = chromiumPickle.createFromBuffer(headerBuf)
42
+ return { header: headerPickle.createIterator().readString(), size }
43
+ }
44
+
45
+ export async function hashHeader(file: string): Promise<{ algorithm: string; hash: string }> {
46
+ const hash = crypto.createHash('sha256')
47
+ const { header } = await readAsarHeader(file)
48
+ hash.update(header)
49
+ return {
50
+ algorithm: 'SHA256',
51
+ hash: hash.digest('hex'),
52
+ }
53
+ }