import { FilterStats, MAX_FILE_REQUESTS } from "builder-util" import { realpathSync } from "fs" import { lstat, lstatSync, readdir } from "fs-extra" import * as path from "path" import asyncPool from "tiny-async-pool" import { excludedNames, FileMatcher } from "../fileMatcher" import { Packager } from "../packager" import { FileCopyHelper } from "./AppFileWalker" import { NodeModuleInfo } from "./packageDependencies" import { resolveFunction } from "./resolve" const excludedFiles = new Set( [ ".DS_Store", "node_modules" /* already in the queue */, "CHANGELOG.md", "ChangeLog", "changelog.md", "Changelog.md", "Changelog", "binding.gyp", ".npmignore", "node_gyp_bins", ].concat(excludedNames.split(",")) ) const topLevelExcludedFiles = new Set([ "karma.conf.js", ".coveralls.yml", "README.md", "readme.markdown", "README", "readme.md", "Readme.md", "Readme", "readme", "test", "tests", "__tests__", "powered-test", "example", "examples", ".bin", ]) /** @internal */ export class NodeModuleCopyHelper extends FileCopyHelper { constructor(matcher: FileMatcher, packager: Packager) { super(matcher, matcher.isEmpty() ? null : matcher.createFilter(), packager) } async collectNodeModules(moduleInfo: NodeModuleInfo, nodeModuleExcludedExts: Array, destination: string): Promise> { const filter = this.filter const metadata = this.metadata const onNodeModuleFile = await resolveFunction(this.packager.appInfo.type, this.packager.config.onNodeModuleFile, "onNodeModuleFile") const result: Array = [] const queue: Array = [] const emptyDirs: Set = new Set() const symlinkFiles: Map = new Map() const tmpPath = moduleInfo.dir const moduleName = moduleInfo.name queue.length = 1 // The path should be corrected in Windows that when the moduleName is Scoped packages named. const depPath = path.normalize(tmpPath) queue[0] = depPath while (queue.length > 0) { const dirPath = queue.pop()! const childNames = await readdir(dirPath) childNames.sort() const isTopLevel = dirPath === depPath const dirs: Array = [] // our handler is async, but we should add sorted files, so, we add file to result not in the mapper, but after map const sortedFilePaths = await asyncPool(MAX_FILE_REQUESTS, childNames, async name => { const filePath = path.join(dirPath, name) const forceIncluded = onNodeModuleFile != null && !!onNodeModuleFile(filePath) if (excludedFiles.has(name) || name.startsWith("._")) { return null } // check if filematcher matches the files array as more important than the default excluded files. const fileMatched = filter != null && filter(dirPath, lstatSync(dirPath)) if (!fileMatched || !forceIncluded || !!this.packager.config.disableDefaultIgnoredFiles) { for (const ext of nodeModuleExcludedExts) { if (name.endsWith(ext)) { return null } } // noinspection SpellCheckingInspection if (isTopLevel && (topLevelExcludedFiles.has(name) || (moduleName === "libui-node" && (name === "build" || name === "docs" || name === "src")))) { return null } if (dirPath.endsWith("build")) { if (name === "gyp-mac-tool" || name === "Makefile" || name.endsWith(".mk") || name.endsWith(".gypi") || name.endsWith(".Makefile")) { return null } } else if (dirPath.endsWith("Release") && (name === ".deps" || name === "obj.target")) { return null } else if (name === "src" && (dirPath.endsWith("keytar") || dirPath.endsWith("keytar-prebuild"))) { return null } else if (dirPath.endsWith("lzma-native") && (name === "build" || name === "deps")) { return null } } return lstat(filePath).then((stat: FilterStats) => { stat.moduleName = moduleName stat.moduleRootPath = destination stat.moduleFullFilePath = path.join(destination, path.relative(depPath, filePath)) if (filter != null && !filter(filePath, stat)) { return null } if (!stat.isDirectory()) { metadata.set(filePath, stat) } const consumerResult = this.handleFile(filePath, dirPath, stat) if (consumerResult == null) { if (stat.isDirectory()) { dirs.push(name) return null } else { return filePath } } else { return consumerResult.then(it => { // asarUtil can return modified stat (symlink handling) if ((it == null ? stat : it).isDirectory()) { dirs.push(name) return null } else { return filePath } }) } }) }) let isEmpty = true for (const child of sortedFilePaths) { if (child != null) { result.push(child) if (this.metadata.get(child)?.isSymbolicLink()) { symlinkFiles.set(child, result.length - 1) } isEmpty = false } } if (isEmpty) { emptyDirs.add(dirPath) } dirs.sort() for (const child of dirs) { queue.push(dirPath + path.sep + child) } } for (const [file, index] of symlinkFiles) { const resolvedPath = realpathSync(file) if (emptyDirs.has(resolvedPath)) { // delete symlink file if target is a empty dir result[index] = undefined } } return result.filter((it): it is string => it !== undefined) } }