import { asArray, copyDir, copyOrLinkFile, FileTransformer, Filter, log, statOrNull, USE_HARD_LINKS } from "builder-util"
import { Nullish } from "builder-util-runtime"
import { mkdir } from "fs/promises"
import { Minimatch } from "minimatch"
import * as path from "path"
import { Configuration, FileSet, Packager, PlatformSpecificBuildOptions } from "./index"
import { PlatformPackager } from "./platformPackager"
import { createFilter, hasMagic } from "./util/filter"
const minimatchOptions = { dot: true }
export const excludedNames =
".git,.hg,.svn,CVS,RCS,SCCS," +
"__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore," +
".idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci," +
".yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml," +
"appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env"
export const excludedExts =
"iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts," +
"mk,a,o,obj,forge-meta"
function ensureNoEndSlash(file: string): string {
if (path.sep !== "/") {
file = file.replace(/\//g, path.sep)
}
if (path.sep !== "\\") {
file = file.replace(/\\/g, path.sep)
}
if (file.endsWith(path.sep)) {
return file.substring(0, file.length - 1)
} else {
return file
}
}
export class FileMatcher {
readonly from: string
readonly to: string
readonly patterns: Array<string>
excludePatterns: Array<Minimatch> | null = null
readonly isSpecifiedAsEmptyArray: boolean
constructor(
from: string,
to: string,
readonly macroExpander: (pattern: string) => string,
patterns?: Array<string> | string | null
) {
this.from = ensureNoEndSlash(macroExpander(from))
this.to = ensureNoEndSlash(macroExpander(to))
this.patterns = asArray(patterns).map(it => this.normalizePattern(it))
this.isSpecifiedAsEmptyArray = Array.isArray(patterns) && patterns.length === 0
}
normalizePattern(pattern: string) {
if (pattern.startsWith("./")) {
pattern = pattern.substring("./".length)
}
return path.posix.normalize(this.macroExpander(pattern.replace(/\\/g, "/")))
}
addPattern(pattern: string) {
this.patterns.push(this.normalizePattern(pattern))
}
prependPattern(pattern: string) {
this.patterns.unshift(this.normalizePattern(pattern))
}
isEmpty() {
return this.patterns.length === 0
}
containsOnlyIgnore(): boolean {
return !this.isEmpty() && this.patterns.find(it => !it.startsWith("!")) == null
}
computeParsedPatterns(result: Array<Minimatch>, fromDir?: string): void {
const relativeFrom = fromDir == null ? null : path.relative(fromDir, this.from)
if (this.patterns.length === 0 && relativeFrom != null) {
result.push(new Minimatch(relativeFrom, minimatchOptions))
return
}
for (let pattern of this.patterns) {
if (relativeFrom != null) {
pattern = path.join(relativeFrom, pattern)
}
const parsedPattern = new Minimatch(pattern, minimatchOptions)
result.push(parsedPattern)
if (!pattern.includes(".") && !hasMagic(parsedPattern)) {
result.push(new Minimatch(`${pattern}/**/*`, minimatchOptions))
}
}
}
createFilter(): Filter {
const parsedPatterns: Array<Minimatch> = []
this.computeParsedPatterns(parsedPatterns)
return createFilter(this.from, parsedPatterns, this.excludePatterns)
}
toString() {
return `from: ${this.from}, to: ${this.to}, patterns: ${this.patterns.join(", ")}`
}
}
export function getMainFileMatchers(
appDir: string,
destination: string,
macroExpander: (pattern: string) => string,
platformSpecificBuildOptions: PlatformSpecificBuildOptions,
platformPackager: PlatformPackager<any>,
outDir: string,
isElectronCompile: boolean
): Array<FileMatcher> {
const packager = platformPackager.info
const buildResourceDir = path.resolve(packager.projectDir, packager.buildResourcesDir)
let matchers = packager.isPrepackedAppAsar
? null
: getFileMatchers(packager.config, "files", destination, {
macroExpander,
customBuildOptions: platformSpecificBuildOptions,
globalOutDir: outDir,
defaultSrc: appDir,
})
if (matchers == null) {
matchers = [new FileMatcher(appDir, destination, macroExpander)]
}
const matcher = matchers[0]
if (matcher.from !== appDir) {
return matchers
}
const patterns = matcher.patterns
const customFirstPatterns: Array<string> = []
if (!matcher.isSpecifiedAsEmptyArray && (matcher.isEmpty() || matcher.containsOnlyIgnore())) {
customFirstPatterns.push("**/*")
} else if (!patterns.includes("package.json")) {
patterns.push("package.json")
}
let insertExculdeNodeModulesIndex = -1
for (let i = 0; i < patterns.length; i++) {
if (!patterns[i].startsWith("!") && (patterns[i].includes("/node_modules") || patterns[i].includes("node_modules/"))) {
insertExculdeNodeModulesIndex = i
break
}
}
if (insertExculdeNodeModulesIndex !== -1) {
patterns.splice(insertExculdeNodeModulesIndex, 0, ...["!**/node_modules/**"])
} else {
customFirstPatterns.push("!**/node_modules/**")
}
const relativeBuildResourceDir = path.relative(matcher.from, buildResourceDir)
if (relativeBuildResourceDir.length !== 0 && !relativeBuildResourceDir.startsWith(".")) {
customFirstPatterns.push(`!${relativeBuildResourceDir}{,/**/*}`)
}
const relativeOutDir = matcher.normalizePattern(path.relative(packager.projectDir, outDir))
if (!relativeOutDir.startsWith(".")) {
customFirstPatterns.push(`!${relativeOutDir}{,/**/*}`)
}
let insertIndex = 0
for (let i = patterns.length - 1; i >= 0; i--) {
if (patterns[i].startsWith("**/")) {
insertIndex = i + 1
break
}
}
patterns.splice(insertIndex, 0, ...customFirstPatterns)
patterns.push(`!**/*.{${excludedExts}${packager.config.includePdb === true ? "" : ",pdb"}}`)
patterns.push("!**/._*")
patterns.push("!**/electron-builder.{yaml,yml,json,json5,toml,ts}")
patterns.push(`!**/{${excludedNames}}`)
if (isElectronCompile) {
patterns.push("!.cache{,/**/*}")
}
patterns.push("!.yarn{,/**/*}")
patterns.push("!.editorconfig")
patterns.push("!.yarnrc.yml")
const debugLogger = packager.debugLogger
if (debugLogger.isEnabled) {
debugLogger.add(`${macroExpander("${arch}")}.firstOrDefaultFilePatterns`, patterns)
}
return matchers
}
export function getNodeModuleFileMatcher(
appDir: string,
destination: string,
macroExpander: (pattern: string) => string,
platformSpecificBuildOptions: PlatformSpecificBuildOptions,
packager: Packager
): FileMatcher {
const matcher = new FileMatcher(appDir, destination, macroExpander)
function addPatterns(patterns: Array<string | FileSet> | string | Nullish | FileSet) {
if (patterns == null) {
return
} else if (!Array.isArray(patterns)) {
if (typeof patterns === "string" && patterns.startsWith("!")) {
matcher.addPattern(patterns)
return
}
return
}
for (const pattern of patterns) {
if (typeof pattern === "string") {
if (pattern.startsWith("!")) {
matcher.addPattern(pattern)
}
} else {
const fileSet = pattern
if (fileSet.from == null || fileSet.from === ".") {
for (const p of asArray(fileSet.filter)) {
matcher.addPattern(p)
}
}
}
}
}
addPatterns(packager.config.files)
addPatterns(platformSpecificBuildOptions.files)
if (!matcher.isEmpty()) {
matcher.prependPattern("**/*")
}
const debugLogger = packager.debugLogger
if (debugLogger.isEnabled) {
debugLogger.add(`${macroExpander("${arch}")}.nodeModuleFilePatterns`, matcher.patterns)
}
return matcher
}
export interface GetFileMatchersOptions {
readonly macroExpander: (pattern: string) => string
readonly customBuildOptions: PlatformSpecificBuildOptions
readonly globalOutDir: string
readonly defaultSrc: string
}
export function getFileMatchers(
config: Configuration,
name: "files" | "extraFiles" | "extraResources" | "asarUnpack" | "extraDistFiles",
defaultDestination: string,
options: GetFileMatchersOptions
): Array<FileMatcher> | null {
const defaultMatcher = new FileMatcher(options.defaultSrc, defaultDestination, options.macroExpander)
const fileMatchers: Array<FileMatcher> = []
function addPatterns(patterns: Array<string | FileSet> | string | Nullish | FileSet) {
if (patterns == null) {
return
} else if (!Array.isArray(patterns)) {
if (typeof patterns === "string") {
defaultMatcher.addPattern(patterns)
return
}
patterns = [patterns]
}
for (const pattern of patterns) {
if (typeof pattern === "string") {
defaultMatcher.addPattern(pattern)
} else if (name === "asarUnpack") {
throw new Error(`Advanced file copying not supported for "${name}"`)
} else {
const from = pattern.from == null ? options.defaultSrc : path.resolve(options.defaultSrc, pattern.from)
const to = pattern.to == null ? defaultDestination : path.resolve(defaultDestination, pattern.to)
fileMatchers.push(new FileMatcher(from, to, options.macroExpander, pattern.filter))
}
}
}
if (name !== "extraDistFiles") {
addPatterns((config as any)[name])
}
addPatterns((options.customBuildOptions as any)[name])
if (!defaultMatcher.isEmpty()) {
fileMatchers.unshift(defaultMatcher)
}
const relativeOutDir = defaultMatcher.normalizePattern(path.relative(options.defaultSrc, options.globalOutDir))
if (!relativeOutDir.startsWith(".")) {
defaultMatcher.addPattern(`!${relativeOutDir}/*-unpacked{,/**/*}`)
}
return fileMatchers.length === 0 ? null : fileMatchers
}
export function copyFiles(matchers: Array<FileMatcher> | null, transformer: FileTransformer | null, isUseHardLink?: boolean): Promise<any> {
if (matchers == null || matchers.length === 0) {
return Promise.resolve()
}
return Promise.all(
matchers.map(async (matcher: FileMatcher) => {
const fromStat = await statOrNull(matcher.from)
if (fromStat == null) {
log.warn({ from: matcher.from }, `file source doesn't exist`)
return
}
if (fromStat.isFile()) {
const toStat = await statOrNull(matcher.to)
if (toStat != null && toStat.isDirectory()) {
return await copyOrLinkFile(matcher.from, path.join(matcher.to, path.basename(matcher.from)), fromStat, isUseHardLink)
}
await mkdir(path.dirname(matcher.to), { recursive: true })
return await copyOrLinkFile(matcher.from, matcher.to, fromStat)
}
if (matcher.isEmpty() || matcher.containsOnlyIgnore()) {
matcher.prependPattern("**/*")
}
log.debug({ matcher }, "copying files using pattern")
return await copyDir(matcher.from, matcher.to, { filter: matcher.createFilter(), transformer, isUseHardLink: isUseHardLink ? USE_HARD_LINKS : null })
})
)
}