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,416 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { logger } from '../../shared/logger.ts'
|
|
4
|
+
import { type Arch, NodePlatform } from '../../shared/platform.ts'
|
|
5
|
+
import { EntryKind, type FileSet, type FileStat } from '../../shared/types.ts'
|
|
6
|
+
import { isLibOrExe } from './unpack-detector.ts'
|
|
7
|
+
import { walkDirectory } from './walker.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A prebuilt native-module replacement rule: replace the node_modules copy
|
|
11
|
+
* of `moduleName` with files from `<projectDir>/<from>` in the packed fileset,
|
|
12
|
+
* optionally renaming the module directory.
|
|
13
|
+
*/
|
|
14
|
+
export interface PrebuiltModuleRule {
|
|
15
|
+
/** Module name as it appears in node_modules (may be scoped). */
|
|
16
|
+
moduleName: string
|
|
17
|
+
/** Source directory (relative to projectDir) holding the prebuilt files. */
|
|
18
|
+
from: string
|
|
19
|
+
/**
|
|
20
|
+
* Rename the module directory in the packed fileset (e.g. `@napi-rs/canvas-darwin-arm64`
|
|
21
|
+
* → `@napi-rs/canvas-<suffix>`); omit to keep the original name.
|
|
22
|
+
*/
|
|
23
|
+
renameTo?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolver producing the replacement rules for a platform/arch. Returning
|
|
28
|
+
* undefined/null skips prebuilt replacement and node_modules copies are
|
|
29
|
+
* used as-is.
|
|
30
|
+
*/
|
|
31
|
+
export type PrebuiltModuleRuleResolver = (
|
|
32
|
+
platform: NodePlatform,
|
|
33
|
+
arch: Arch,
|
|
34
|
+
) => ReadonlyArray<PrebuiltModuleRule> | null | undefined
|
|
35
|
+
|
|
36
|
+
let prebuiltRuleResolver: PrebuiltModuleRuleResolver | null = null
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Install the resolver used to derive prebuilt module replacement rules.
|
|
40
|
+
* Hosts inject their own rules through config.nativeModules.prebuiltRules.
|
|
41
|
+
*/
|
|
42
|
+
export function setPrebuiltDirResolver(resolver: PrebuiltModuleRuleResolver | null): void {
|
|
43
|
+
prebuiltRuleResolver = typeof resolver === 'function' ? resolver : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Compare module names independent of the host path separator. */
|
|
47
|
+
function normalizeModuleName(name: string): string {
|
|
48
|
+
return path.toNamespacedPath(name).split(path.sep).join('/')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function derivePlatformFileSet(
|
|
52
|
+
fileSet: FileSet,
|
|
53
|
+
platform: NodePlatform,
|
|
54
|
+
arch: Arch,
|
|
55
|
+
projectDir: string,
|
|
56
|
+
): FileSet {
|
|
57
|
+
const { files, metadata, src, destination } = fileSet
|
|
58
|
+
|
|
59
|
+
// Rules from the host-injected resolver; moduleName may end with "-*" to
|
|
60
|
+
// match a family of platform-variant modules (e.g. "@napi-rs/canvas-*").
|
|
61
|
+
const rules = (prebuiltRuleResolver?.(platform, arch) ?? []).filter((r) => r.from)
|
|
62
|
+
const exactRules = rules.filter((r) => !r.moduleName.endsWith('-*'))
|
|
63
|
+
const prefixRules = rules.filter((r) => r.moduleName.endsWith('-*'))
|
|
64
|
+
const prefixes = prefixRules.map((r) => ({
|
|
65
|
+
rule: r,
|
|
66
|
+
prefix: normalizeModuleName(r.moduleName.slice(0, -1)),
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
// First pass: find module roots to replace.
|
|
70
|
+
const exactModuleRoots = new Map<string, PrebuiltModuleRule[]>()
|
|
71
|
+
let prefixMatch: {
|
|
72
|
+
rule: PrebuiltModuleRule
|
|
73
|
+
moduleRootDir: string
|
|
74
|
+
originalModuleName: string
|
|
75
|
+
originalSegment: string
|
|
76
|
+
} | null = null
|
|
77
|
+
|
|
78
|
+
const seenModuleNames = new Set<string>()
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const info = parseModuleInfo(file)
|
|
82
|
+
if (!info) continue
|
|
83
|
+
const normalizedModuleName = normalizeModuleName(info.moduleName)
|
|
84
|
+
seenModuleNames.add(normalizedModuleName)
|
|
85
|
+
|
|
86
|
+
const exact = exactRules.filter(
|
|
87
|
+
(r) => normalizeModuleName(r.moduleName) === normalizedModuleName,
|
|
88
|
+
)
|
|
89
|
+
if (exact.length > 0) {
|
|
90
|
+
const list = exactModuleRoots.get(info.moduleRootPath) ?? []
|
|
91
|
+
list.push(...exact)
|
|
92
|
+
exactModuleRoots.set(info.moduleRootPath, list)
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
for (const { rule, prefix } of prefixes) {
|
|
96
|
+
if (normalizedModuleName.startsWith(prefix)) {
|
|
97
|
+
// Only the first platform variant encountered drives the rename.
|
|
98
|
+
if (!prefixMatch) {
|
|
99
|
+
const segments = normalizedModuleName.split('/')
|
|
100
|
+
prefixMatch = {
|
|
101
|
+
rule,
|
|
102
|
+
moduleRootDir: path.dirname(info.moduleRootPath),
|
|
103
|
+
originalModuleName: info.moduleName,
|
|
104
|
+
originalSegment: segments[segments.length - 1] ?? info.moduleName,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const rule of exactRules) {
|
|
113
|
+
const normalizedRuleName = normalizeModuleName(rule.moduleName)
|
|
114
|
+
if (
|
|
115
|
+
[...seenModuleNames].some(
|
|
116
|
+
(name) => name !== normalizedRuleName && name.startsWith(`${normalizedRuleName}-`),
|
|
117
|
+
)
|
|
118
|
+
) {
|
|
119
|
+
logger.warn(
|
|
120
|
+
`prebuiltRules: rule "${rule.moduleName}" is exact, but platform variants of the same family exist in node_modules; consider "${rule.moduleName}-*"`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const exactReplacements = new Map<
|
|
126
|
+
string,
|
|
127
|
+
{
|
|
128
|
+
entries: Array<{ rule: PrebuiltModuleRule; content: PrebuiltContent }>
|
|
129
|
+
replacedRelPaths: Set<string>
|
|
130
|
+
}
|
|
131
|
+
>()
|
|
132
|
+
for (const [moduleRootPath, ruleList] of exactModuleRoots) {
|
|
133
|
+
const entries: Array<{ rule: PrebuiltModuleRule; content: PrebuiltContent }> = []
|
|
134
|
+
const replacedRelPaths = new Set<string>()
|
|
135
|
+
for (const rule of ruleList) {
|
|
136
|
+
const content = collectPrebuiltContent(projectDir, rule)
|
|
137
|
+
if (!content) continue
|
|
138
|
+
entries.push({ rule, content })
|
|
139
|
+
for (const pf of content.files) {
|
|
140
|
+
replacedRelPaths.add(path.relative(content.prebuiltBase, pf))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (entries.length > 0) exactReplacements.set(moduleRootPath, { entries, replacedRelPaths })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Second pass: drop files belonging to replaced modules, keep the rest.
|
|
147
|
+
const newFiles: string[] = []
|
|
148
|
+
const newMetadata = new Map<string, FileStat>()
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
const meta = metadata.get(file)
|
|
151
|
+
if (!meta) continue
|
|
152
|
+
|
|
153
|
+
const info = parseModuleInfo(file)
|
|
154
|
+
if (info) {
|
|
155
|
+
const replacement = exactReplacements.get(info.moduleRootPath)
|
|
156
|
+
if (replacement) {
|
|
157
|
+
const relPath = path.relative(info.moduleRootPath, file)
|
|
158
|
+
if (replacement.replacedRelPaths.has(relPath) || isLibOrExe(file)) continue
|
|
159
|
+
}
|
|
160
|
+
const normalizedModuleName = normalizeModuleName(info.moduleName)
|
|
161
|
+
let matchedPrefix = false
|
|
162
|
+
for (const { prefix } of prefixes) {
|
|
163
|
+
if (normalizedModuleName.startsWith(prefix)) {
|
|
164
|
+
matchedPrefix = true
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (matchedPrefix) continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (info && isExcludedPlatformPath(file, info.moduleRootPath, platform, info.moduleName)) {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
newFiles.push(file)
|
|
176
|
+
newMetadata.set(file, meta)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let adjustedSrc = src
|
|
180
|
+
let adjustedDestination = destination
|
|
181
|
+
|
|
182
|
+
// Emit prebuilt files for exact-name rules into each original module root.
|
|
183
|
+
for (const [moduleRootPath, { entries }] of exactReplacements) {
|
|
184
|
+
for (const { rule, content } of entries) {
|
|
185
|
+
emitPrebuiltContent({
|
|
186
|
+
platform,
|
|
187
|
+
content,
|
|
188
|
+
targetModuleRoot: moduleRootPath,
|
|
189
|
+
targetModuleName: rule.moduleName,
|
|
190
|
+
newFiles,
|
|
191
|
+
newMetadata,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Emit prebuilt files for the platform-variant rule, renaming the module
|
|
197
|
+
// directory in the packed fileset.
|
|
198
|
+
if (prefixMatch) {
|
|
199
|
+
const { rule, moduleRootDir, originalModuleName, originalSegment } = prefixMatch
|
|
200
|
+
const renamedSegment = rule.renameTo ?? originalSegment
|
|
201
|
+
const renamedModuleName = rule.renameTo
|
|
202
|
+
? originalModuleName.replace(originalSegment, rule.renameTo)
|
|
203
|
+
: originalModuleName
|
|
204
|
+
const targetRoot = path.join(moduleRootDir, renamedSegment)
|
|
205
|
+
|
|
206
|
+
const content = collectPrebuiltContent(projectDir, rule)
|
|
207
|
+
if (content) {
|
|
208
|
+
emitPrebuiltContent({
|
|
209
|
+
platform,
|
|
210
|
+
content,
|
|
211
|
+
targetModuleRoot: targetRoot,
|
|
212
|
+
targetModuleName: renamedModuleName,
|
|
213
|
+
newFiles,
|
|
214
|
+
newMetadata,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// When the whole fileset src was the renamed module (sub-fileset case),
|
|
218
|
+
// rebase src/destination onto the renamed directory.
|
|
219
|
+
if (newFiles.length > 0 && !newFiles.some((f) => f.startsWith(src))) {
|
|
220
|
+
adjustedSrc = targetRoot
|
|
221
|
+
adjustedDestination = destination.replace(originalSegment, renamedSegment)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
newFiles.sort()
|
|
227
|
+
return {
|
|
228
|
+
src: adjustedSrc,
|
|
229
|
+
destination: adjustedDestination,
|
|
230
|
+
files: newFiles,
|
|
231
|
+
metadata: newMetadata,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Walked content of one prebuilt source directory. */
|
|
236
|
+
interface PrebuiltContent {
|
|
237
|
+
prebuiltBase: string
|
|
238
|
+
files: string[]
|
|
239
|
+
metadata: Map<string, FileStat>
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function collectPrebuiltContent(
|
|
243
|
+
projectDir: string,
|
|
244
|
+
rule: PrebuiltModuleRule,
|
|
245
|
+
): PrebuiltContent | null {
|
|
246
|
+
const prebuiltBase = path.join(projectDir, rule.from)
|
|
247
|
+
if (!existsSync(prebuiltBase)) return null
|
|
248
|
+
const files: string[] = []
|
|
249
|
+
const metadata = new Map<string, FileStat>()
|
|
250
|
+
walkDirectory(prebuiltBase, files, metadata, prebuiltBase)
|
|
251
|
+
return { prebuiltBase, files, metadata }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Copy prebuilt directory files into a target module root with override metadata. */
|
|
255
|
+
function emitPrebuiltContent({
|
|
256
|
+
platform,
|
|
257
|
+
content,
|
|
258
|
+
targetModuleRoot,
|
|
259
|
+
targetModuleName,
|
|
260
|
+
newFiles,
|
|
261
|
+
newMetadata,
|
|
262
|
+
}: {
|
|
263
|
+
platform: NodePlatform
|
|
264
|
+
content: PrebuiltContent
|
|
265
|
+
targetModuleRoot: string
|
|
266
|
+
targetModuleName: string
|
|
267
|
+
newFiles: string[]
|
|
268
|
+
newMetadata: Map<string, FileStat>
|
|
269
|
+
}): void {
|
|
270
|
+
for (const pf of content.files) {
|
|
271
|
+
const pm = content.metadata.get(pf)
|
|
272
|
+
if (!pm) continue
|
|
273
|
+
|
|
274
|
+
const relFromPrebuilt = path.relative(content.prebuiltBase, pf)
|
|
275
|
+
const targetPath = path.join(targetModuleRoot, relFromPrebuilt)
|
|
276
|
+
|
|
277
|
+
const overrideMeta: FileStat = {
|
|
278
|
+
...pm,
|
|
279
|
+
realFilePath: pf,
|
|
280
|
+
moduleRootPath: targetModuleRoot,
|
|
281
|
+
moduleName: targetModuleName,
|
|
282
|
+
}
|
|
283
|
+
if (pm.type === EntryKind.FILE && pf.endsWith('.node')) {
|
|
284
|
+
if (platform === NodePlatform.DARWIN || platform === NodePlatform.LINUX) {
|
|
285
|
+
overrideMeta.mode = 0o100755
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
newFiles.push(targetPath)
|
|
289
|
+
newMetadata.set(targetPath, overrideMeta)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Parse a file path inside node_modules into module name / subpath info. */
|
|
294
|
+
export function parseModuleInfo(
|
|
295
|
+
filePath: string,
|
|
296
|
+
): { moduleName: string; moduleRootPath: string } | null {
|
|
297
|
+
const nmIdx = filePath.indexOf(`${path.sep}node_modules${path.sep}`)
|
|
298
|
+
if (nmIdx !== -1) {
|
|
299
|
+
const after = filePath.slice(nmIdx + path.sep.length + 'node_modules'.length + path.sep.length)
|
|
300
|
+
const parts = after.split(path.sep)
|
|
301
|
+
if (parts[0].startsWith('@') && parts.length >= 2) {
|
|
302
|
+
const moduleName = parts[0] + path.sep + parts[1]
|
|
303
|
+
const moduleRootPath = filePath.slice(
|
|
304
|
+
0,
|
|
305
|
+
nmIdx + path.sep.length + 'node_modules'.length + path.sep.length + moduleName.length,
|
|
306
|
+
)
|
|
307
|
+
return { moduleName, moduleRootPath }
|
|
308
|
+
}
|
|
309
|
+
if (parts.length >= 1) {
|
|
310
|
+
const moduleName = parts[0]
|
|
311
|
+
const moduleRootPath = filePath.slice(
|
|
312
|
+
0,
|
|
313
|
+
nmIdx + path.sep.length + 'node_modules'.length + path.sep.length + moduleName.length,
|
|
314
|
+
)
|
|
315
|
+
return { moduleName, moduleRootPath }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (filePath.startsWith(`node_modules${path.sep}`) || filePath.startsWith('node_modules/')) {
|
|
320
|
+
const after = filePath.slice('node_modules/'.length)
|
|
321
|
+
const parts = after.split(/[/\\]/)
|
|
322
|
+
if (parts[0].startsWith('@') && parts.length >= 2) {
|
|
323
|
+
const moduleName = `${parts[0]}/${parts[1]}`
|
|
324
|
+
const moduleRootPath = `node_modules/${moduleName}`
|
|
325
|
+
return { moduleName, moduleRootPath }
|
|
326
|
+
}
|
|
327
|
+
if (parts.length >= 1) {
|
|
328
|
+
const moduleName = parts[0]
|
|
329
|
+
const moduleRootPath = `node_modules/${moduleName}`
|
|
330
|
+
return { moduleName, moduleRootPath }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Directory segments that mark a path as platform-specific, per platform.
|
|
339
|
+
* Used to drop node_modules entries that only apply to other platforms.
|
|
340
|
+
*/
|
|
341
|
+
const PLATFORM_DIR_SEGMENTS: Record<NodePlatform, string[]> = {
|
|
342
|
+
darwin: ['mac', 'darwin'],
|
|
343
|
+
linux: ['linux'],
|
|
344
|
+
win32: ['win', 'win32'],
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Module name prefixes that mark a module as platform-specific, per platform. */
|
|
348
|
+
const PLATFORM_MODULE_NAMES: Record<NodePlatform, RegExp[]> = {
|
|
349
|
+
darwin: [/^mac-/, /^darwin-/],
|
|
350
|
+
linux: [/^linux-/],
|
|
351
|
+
win32: [/^win-/, /^windows-/],
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Path segment prefixes that mark a part as platform-specific, per platform. */
|
|
355
|
+
const PLATFORM_PREFIX_PATTERNS: Record<NodePlatform, RegExp> = {
|
|
356
|
+
darwin: /^darwin-/,
|
|
357
|
+
linux: /^linux-/,
|
|
358
|
+
win32: /^win32-/,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const PLATFORM_TRIPLE_RE = /-(win32|darwin|linux)-(x64|arm64|ia32|arm)(?:-|$)/
|
|
362
|
+
|
|
363
|
+
const TRIPLE_PLATFORMS: Record<string, NodePlatform> = {
|
|
364
|
+
win32: NodePlatform.WIN32,
|
|
365
|
+
darwin: NodePlatform.DARWIN,
|
|
366
|
+
linux: NodePlatform.LINUX,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function isExcludedPlatformPath(
|
|
370
|
+
filePath: string,
|
|
371
|
+
moduleRootPath: string,
|
|
372
|
+
targetPlatform: NodePlatform,
|
|
373
|
+
moduleName: string,
|
|
374
|
+
): boolean {
|
|
375
|
+
const relFromModuleRoot = path.relative(moduleRootPath, filePath)
|
|
376
|
+
if (!relFromModuleRoot) return false
|
|
377
|
+
|
|
378
|
+
const parts = relFromModuleRoot.split(/[/\\]/)
|
|
379
|
+
|
|
380
|
+
for (const [platform, segments] of Object.entries(PLATFORM_DIR_SEGMENTS)) {
|
|
381
|
+
if (platform === targetPlatform) continue
|
|
382
|
+
for (const segment of segments) {
|
|
383
|
+
if (parts.includes(segment)) return true
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (moduleName) {
|
|
388
|
+
const segments = moduleName.split(path.sep)
|
|
389
|
+
const shortName = segments.length > 1 ? (segments[segments.length - 1] as string) : moduleName
|
|
390
|
+
const triple = PLATFORM_TRIPLE_RE.exec(shortName)
|
|
391
|
+
if (triple) {
|
|
392
|
+
const variantPlatform = TRIPLE_PLATFORMS[triple[1] as string]
|
|
393
|
+
if (variantPlatform !== undefined && variantPlatform !== targetPlatform) return true
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (const [platform, pattern] of Object.entries(PLATFORM_PREFIX_PATTERNS)) {
|
|
398
|
+
if (platform === targetPlatform) continue
|
|
399
|
+
for (const part of parts) {
|
|
400
|
+
if (pattern.test(part)) return true
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (moduleName) {
|
|
405
|
+
const segments = moduleName.split(path.sep)
|
|
406
|
+
const shortName = segments.length > 1 ? (segments[segments.length - 1] as string) : moduleName
|
|
407
|
+
for (const [platform, patterns] of Object.entries(PLATFORM_MODULE_NAMES)) {
|
|
408
|
+
if (platform === targetPlatform) continue
|
|
409
|
+
for (const pattern of patterns) {
|
|
410
|
+
if (pattern.test(shortName)) return true
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return false
|
|
416
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { JsonObject } from '../../shared/types.ts'
|
|
4
|
+
|
|
5
|
+
const NODE_MODULES_PATTERN = `${path.sep}node_modules${path.sep}`
|
|
6
|
+
|
|
7
|
+
const ignoredPackageMetadataProperties = new Set([
|
|
8
|
+
'dist',
|
|
9
|
+
'gitHead',
|
|
10
|
+
'build',
|
|
11
|
+
'jspm',
|
|
12
|
+
'ava',
|
|
13
|
+
'xo',
|
|
14
|
+
'nyc',
|
|
15
|
+
'eslintConfig',
|
|
16
|
+
'contributors',
|
|
17
|
+
'bundleDependencies',
|
|
18
|
+
'tags',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
const PROTECTED_KEYS = new Set(['__proto__', 'prototype', 'constructor'])
|
|
22
|
+
|
|
23
|
+
export function createTransformer(
|
|
24
|
+
srcDir: string,
|
|
25
|
+
extraMetadata: JsonObject | undefined,
|
|
26
|
+
extraTransformer?: (file: string) => Promise<string | null> | string | null,
|
|
27
|
+
): (file: string) => Promise<string | null> | string | null {
|
|
28
|
+
const mainPackageJson = path.join(srcDir, 'package.json')
|
|
29
|
+
const isRemovePackageScripts = true
|
|
30
|
+
const isRemovePackageKeywords = true
|
|
31
|
+
const packageJson = `${path.sep}package.json`
|
|
32
|
+
|
|
33
|
+
// Transform the contents of a single file on read; return the modified
|
|
34
|
+
// content for touched files and the raw content for untouched files.
|
|
35
|
+
return (file) => {
|
|
36
|
+
if (file === mainPackageJson) {
|
|
37
|
+
return modifyMainPackageJson(
|
|
38
|
+
file,
|
|
39
|
+
extraMetadata,
|
|
40
|
+
isRemovePackageScripts,
|
|
41
|
+
isRemovePackageKeywords,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
if (file.endsWith(packageJson) && file.includes(NODE_MODULES_PATTERN)) {
|
|
45
|
+
return readFile(file, 'utf-8')
|
|
46
|
+
.then((it) =>
|
|
47
|
+
cleanupPackageJson(JSON.parse(it) as JsonObject, {
|
|
48
|
+
isMain: false,
|
|
49
|
+
isRemovePackageScripts,
|
|
50
|
+
isRemovePackageKeywords,
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
.catch((e: unknown) => {
|
|
54
|
+
console.warn(e)
|
|
55
|
+
return null
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
if (extraTransformer != null) {
|
|
59
|
+
return extraTransformer(file)
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cleanupPackageJson(
|
|
66
|
+
data: JsonObject,
|
|
67
|
+
options: { isMain: boolean; isRemovePackageScripts: boolean; isRemovePackageKeywords: boolean },
|
|
68
|
+
): string | null {
|
|
69
|
+
const deps = data.dependencies
|
|
70
|
+
const isRemoveBabel =
|
|
71
|
+
deps != null &&
|
|
72
|
+
typeof deps === 'object' &&
|
|
73
|
+
!Object.getOwnPropertyNames(deps).some((it) => it.startsWith('babel'))
|
|
74
|
+
let changed = false
|
|
75
|
+
for (const prop of Object.getOwnPropertyNames(data)) {
|
|
76
|
+
if (
|
|
77
|
+
prop[0] === '_' ||
|
|
78
|
+
ignoredPackageMetadataProperties.has(prop) ||
|
|
79
|
+
(options.isRemovePackageScripts && prop === 'scripts') ||
|
|
80
|
+
(options.isRemovePackageKeywords && prop === 'keywords') ||
|
|
81
|
+
(options.isMain && prop === 'devDependencies') ||
|
|
82
|
+
(!options.isMain && prop === 'bugs') ||
|
|
83
|
+
(isRemoveBabel && prop === 'babel')
|
|
84
|
+
) {
|
|
85
|
+
delete data[prop]
|
|
86
|
+
changed = true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (changed) {
|
|
90
|
+
return JSON.stringify(data, null, 2)
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function modifyMainPackageJson(
|
|
96
|
+
file: string,
|
|
97
|
+
extraMetadata: JsonObject | undefined,
|
|
98
|
+
isRemovePackageScripts: boolean,
|
|
99
|
+
isRemovePackageKeywords: boolean,
|
|
100
|
+
): Promise<string | null> {
|
|
101
|
+
const mainPackageData = JSON.parse(await readFile(file, 'utf-8')) as JsonObject
|
|
102
|
+
if (extraMetadata != null) {
|
|
103
|
+
deepAssign(mainPackageData, extraMetadata)
|
|
104
|
+
}
|
|
105
|
+
const serializedDataIfChanged = cleanupPackageJson(mainPackageData, {
|
|
106
|
+
isMain: true,
|
|
107
|
+
isRemovePackageScripts,
|
|
108
|
+
isRemovePackageKeywords,
|
|
109
|
+
})
|
|
110
|
+
if (serializedDataIfChanged != null) {
|
|
111
|
+
return serializedDataIfChanged
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Check that a key is safe for assignment that avoids prototype pollution. */
|
|
117
|
+
function isValidKey(key: string): boolean {
|
|
118
|
+
return !PROTECTED_KEYS.has(key)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Check whether a value is a non-array object (or a function). */
|
|
122
|
+
function isObject(x: unknown): boolean {
|
|
123
|
+
if (Array.isArray(x)) return false
|
|
124
|
+
const type = typeof x
|
|
125
|
+
return type === 'object' || type === 'function'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function assignKey(target: JsonObject, from: JsonObject, key: string): void {
|
|
129
|
+
const value = from[key]
|
|
130
|
+
if (value === undefined) return
|
|
131
|
+
const prevValue = target[key]
|
|
132
|
+
if (prevValue == null || value == null || !isObject(prevValue) || !isObject(value)) {
|
|
133
|
+
if (Array.isArray(prevValue) && Array.isArray(value)) {
|
|
134
|
+
target[key] = Array.from(new Set(prevValue.concat(value)))
|
|
135
|
+
} else {
|
|
136
|
+
target[key] = value
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
deepAssign(prevValue as JsonObject, value as JsonObject)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deepAssign(target: JsonObject, ...objects: (JsonObject | undefined)[]): JsonObject {
|
|
144
|
+
for (const o of objects) {
|
|
145
|
+
if (o != null) {
|
|
146
|
+
for (const key of Object.getOwnPropertyNames(o)) {
|
|
147
|
+
if (isValidKey(key)) {
|
|
148
|
+
assignKey(target, o, key)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return target
|
|
154
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FileStat } from '../../shared/types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check whether a file is a native binary library or executable that
|
|
5
|
+
* should be unpacked from the asar archive by default.
|
|
6
|
+
*/
|
|
7
|
+
export function isLibOrExe(file: string): boolean {
|
|
8
|
+
return (
|
|
9
|
+
file.endsWith('.dll') ||
|
|
10
|
+
file.endsWith('.exe') ||
|
|
11
|
+
file.endsWith('.dylib') ||
|
|
12
|
+
file.endsWith('.so') ||
|
|
13
|
+
file.endsWith('.node')
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function detectUnpackedDirs(
|
|
18
|
+
fileSet: { files: string[]; metadata: Map<string, FileStat> },
|
|
19
|
+
autoUnpackDirs: Set<string>,
|
|
20
|
+
): void {
|
|
21
|
+
const metadata = fileSet.metadata
|
|
22
|
+
for (let i = 0, n = fileSet.files.length; i < n; i++) {
|
|
23
|
+
const file = fileSet.files[i]
|
|
24
|
+
const stat = metadata.get(file)
|
|
25
|
+
if (!stat?.moduleRootPath || autoUnpackDirs.has(stat.moduleRootPath)) continue
|
|
26
|
+
if (stat.type !== 'file') continue
|
|
27
|
+
|
|
28
|
+
let shouldUnpack = false
|
|
29
|
+
const moduleName = stat.moduleName
|
|
30
|
+
|
|
31
|
+
if (moduleName === 'ffprobe-static' || moduleName === 'ffmpeg-static' || isLibOrExe(file)) {
|
|
32
|
+
shouldUnpack = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!shouldUnpack) continue
|
|
36
|
+
autoUnpackDirs.add(stat.moduleRootPath)
|
|
37
|
+
}
|
|
38
|
+
}
|