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
|
@@ -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
|
+
}
|
package/src/asar/pack.ts
ADDED
|
@@ -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
|
+
}
|