Skip to content

Commit 08893e3

Browse files
committed
fix: compile using electron-compile not in place, but using cache
Close #807
1 parent e208f53 commit 08893e3

File tree

5 files changed

+176
-146
lines changed

5 files changed

+176
-146
lines changed

packages/electron-builder/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
"isbinaryfile": "^3.0.2",
6464
"js-yaml": "^3.8.2",
6565
"minimatch": "^3.0.3",
66-
"mime": "^1.3.4",
6766
"node-forge": "^0.7.0",
6867
"normalize-package-data": "^2.3.6",
6968
"parse-color": "^1.0.0",

packages/electron-builder/src/asarUtil.ts

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { AsarOptions } from "electron-builder-core"
33
import { debug } from "electron-builder-util"
44
import { CONCURRENCY, FileCopier, FileTransformer, Filter, MAX_FILE_REQUESTS, statOrNull, walk } from "electron-builder-util/out/fs"
55
import { log } from "electron-builder-util/out/log"
6-
import { createReadStream, createWriteStream, ensureDir, readFile, readlink, stat, Stats, writeFile } from "fs-extra-p"
6+
import { createReadStream, createWriteStream, emptyDir, ensureDir, readFile, readlink, stat, Stats, writeFile } from "fs-extra-p"
77
import * as path from "path"
88
import { AsarFilesystem, Node, readAsar } from "./asar"
9+
import { createElectronCompilerHost } from "./fileTransformer"
910

1011
const isBinaryFile: any = BluebirdPromise.promisify(require("isbinaryfile"))
1112
const pickle = require ("chromium-pickle-js")
@@ -26,7 +27,7 @@ function addValue(map: Map<string, Array<string>>, key: string, value: string) {
2627
interface UnpackedFileTask {
2728
stats: Stats
2829
src?: string
29-
data?: string
30+
data?: string | Buffer
3031
destination: string
3132
}
3233

@@ -44,15 +45,18 @@ function writeUnpackedFiles(filesToUnpack: Array<UnpackedFileTask>, fileCopier:
4445
export class AsarPackager {
4546
private readonly fs = new AsarFilesystem(this.src)
4647
private readonly outFile: string
48+
49+
private transformedFiles: Array<string | Buffer | true | null>
50+
private readonly metadata = new Map<string, Stats>()
4751

48-
constructor(private readonly src: string, destination: string, private readonly options: AsarOptions, private readonly unpackPattern: Filter | null) {
52+
constructor(private readonly src: string, destination: string, private readonly options: AsarOptions, private readonly unpackPattern: Filter | null, private readonly transformer: FileTransformer) {
4953
this.outFile = path.join(destination, "app.asar")
5054
}
5155

5256
// sort files to minimize file change (i.e. asar file is not changed dramatically on small change)
53-
async pack(filter: Filter, transformer: ((path: string) => any) | null) {
54-
const metadata = new Map<string, Stats>()
55-
const files = await walk(this.src, filter, (file, fileStat) => {
57+
async pack(filter: Filter, isElectronCompile: boolean) {
58+
const metadata = this.metadata
59+
let files = await walk(this.src, filter, (file, fileStat) => {
5660
metadata.set(file, fileStat)
5761
if (fileStat.isSymbolicLink()) {
5862
return readlink(file)
@@ -76,13 +80,61 @@ export class AsarPackager {
7680
}
7781
return null
7882
})
83+
84+
// transform before electron-compile to avoid filtering (cache files in any case should be not transformed)
85+
const transformer = this.transformer
86+
this.transformedFiles = await BluebirdPromise.map(files, it => metadata.get(it)!.isFile() ? transformer(it) : null, CONCURRENCY)
87+
88+
if (isElectronCompile) {
89+
files = await this.compileUsingElectronCompile(files)
90+
}
91+
92+
await this.createPackageFromFiles(this.options.ordering == null ? files : await this.order(files))
93+
}
94+
95+
async compileUsingElectronCompile(files: Array<string>): Promise<Array<string>> {
96+
log("Compiling using electron-compile")
97+
98+
const metadata = this.metadata
99+
const cacheDir = path.join(this.src, ".cache")
100+
// clear and create cache dir
101+
await emptyDir(cacheDir)
102+
const compilerHost = await createElectronCompilerHost(this.src, cacheDir)
103+
const nextSlashIndex = this.src.length + 1
104+
// pre-compute electron-compile to cache dir - we need to process only subdirectories, not direct files of app dir
105+
await BluebirdPromise.map(files, file => {
106+
if (file.includes("/node_modules/") || file.includes("/bower_components/")
107+
|| !file.includes("/", nextSlashIndex) // ignore not root files
108+
|| !metadata.get(file)!.isFile()) {
109+
return null
110+
}
111+
return compilerHost.compile(file)
112+
.then((it: any) => null)
113+
}, CONCURRENCY)
79114

80-
await this.createPackageFromFiles(this.options.ordering == null ? files : await this.order(files), metadata, transformer)
115+
await compilerHost.saveConfiguration()
116+
117+
const cacheFiles = await walk(cacheDir, (file, stat) => !file.startsWith("."), (file, fileStat) => {
118+
this.metadata.set(file, fileStat)
119+
return null
120+
})
121+
122+
// add es6-shim.js
123+
const es6ShimPath = `${this.src}/es6-shim.js`
124+
cacheFiles.push(es6ShimPath)
125+
metadata.set(es6ShimPath, <any>{isFile: () => true, isDirectory: () => false})
126+
127+
this.transformedFiles = (new Array(cacheFiles.length)).concat(this.transformedFiles)
128+
129+
this.transformedFiles[cacheFiles.length - 1] = await readFile(path.join(this.src, "node_modules", "electron-compile", "lib", "es6-shim.js"))
130+
131+
// cache files should be first (better IO)
132+
return cacheFiles.concat(files)
81133
}
82134

83-
async detectUnpackedDirs(files: Array<string>, metadata: Map<string, Stats>, autoUnpackDirs: Set<string>, unpackedDest: string) {
135+
async detectUnpackedDirs(files: Array<string>, autoUnpackDirs: Set<string>, unpackedDest: string) {
84136
const dirToCreate = new Map<string, Array<string>>()
85-
137+
const metadata = this.metadata
86138
/* tslint:disable:rule1 prefer-const */
87139
for (let i = 0, n = files.length; i < n; i++) {
88140
const file = files[i]
@@ -147,19 +199,20 @@ export class AsarPackager {
147199
}
148200
}
149201

150-
private async createPackageFromFiles(files: Array<string>, metadata: Map<string, Stats>, transformer: FileTransformer | null) {
202+
async createPackageFromFiles(files: Array<string>) {
203+
const metadata = this.metadata
151204
// search auto unpacked dir
152205
const unpackedDirs = new Set<string>()
153206
const unpackedDest = `${this.outFile}.unpacked`
154207
await ensureDir(path.dirname(this.outFile))
155208

156209
if (this.options.smartUnpack !== false) {
157-
await this.detectUnpackedDirs(files, metadata, unpackedDirs, unpackedDest)
210+
await this.detectUnpackedDirs(files, unpackedDirs, unpackedDest)
158211
}
159212

160213
const dirToCreateForUnpackedFiles = new Set<string>(unpackedDirs)
161214

162-
const transformedFiles = transformer == null ? new Array(files.length) : await BluebirdPromise.map(files, it => metadata.get(it)!.isFile() ? transformer(it) : null, CONCURRENCY)
215+
const transformedFiles = this.transformedFiles
163216
const filesToUnpack: Array<UnpackedFileTask> = []
164217
const fileCopier = new FileCopier()
165218
/* tslint:disable:rule1 prefer-const */
@@ -170,9 +223,9 @@ export class AsarPackager {
170223
const fileParent = path.dirname(file)
171224
const dirNode = this.fs.getOrCreateNode(fileParent)
172225

173-
const newData = transformedFiles == null ? null : transformedFiles[i]
226+
const newData = transformedFiles == null ? null : <string | Buffer>transformedFiles[i]
174227
const node = this.fs.getOrCreateNode(file)
175-
node.size = newData == null ? stat.size : Buffer.byteLength(newData)
228+
node.size = newData == null ? stat.size : Buffer.byteLength(<any>newData)
176229
if (dirNode.unpacked || (this.unpackPattern != null && this.unpackPattern(file, stat))) {
177230
node.unpacked = true
178231
if (newData != null) {
@@ -227,10 +280,10 @@ export class AsarPackager {
227280
await writeUnpackedFiles(filesToUnpack, fileCopier)
228281
}
229282

230-
await this.writeAsarFile(files, transformedFiles)
283+
await this.writeAsarFile(files)
231284
}
232285

233-
private writeAsarFile(files: Array<string>, transformedFiles: Array<string | Buffer | null | true>): Promise<any> {
286+
private writeAsarFile(files: Array<string>): Promise<any> {
234287
const headerPickle = pickle.createEmpty()
235288
headerPickle.writeString(JSON.stringify(this.fs.header))
236289
const headerBuf = headerPickle.toBuffer()
@@ -239,6 +292,7 @@ export class AsarPackager {
239292
sizePickle.writeUInt32(headerBuf.length)
240293
const sizeBuf = sizePickle.toBuffer()
241294

295+
const transformedFiles = this.transformedFiles
242296
const writeStream = createWriteStream(this.outFile)
243297
return new BluebirdPromise((resolve, reject) => {
244298
writeStream.on("error", reject)

packages/electron-builder/src/fileMatcher.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import BluebirdPromise from "bluebird-lst"
2+
import { FilePattern, PlatformSpecificBuildOptions } from "electron-builder-core"
23
import { asArray } from "electron-builder-util"
34
import { copyDir, copyFile, Filter, statOrNull } from "electron-builder-util/out/fs"
45
import { warn } from "electron-builder-util/out/log"
56
import { mkdirs } from "fs-extra-p"
67
import { Minimatch } from "minimatch"
78
import * as path from "path"
9+
import { Config } from "./metadata"
10+
import { BuildInfo } from "./packagerApi"
811
import { createFilter, hasMagic } from "./util/filter"
912

1013
export class FileMatcher {
@@ -71,6 +74,78 @@ export class FileMatcher {
7174
}
7275
}
7376

77+
export function createFileMatcher(info: BuildInfo, appDir: string, resourcesPath: string, macroExpander: (pattern: string) => string, platformSpecificBuildOptions: PlatformSpecificBuildOptions) {
78+
const patterns = info.isPrepackedAppAsar ? null : getFileMatchers(info.config, "files", appDir, path.join(resourcesPath, "app"), false, macroExpander, platformSpecificBuildOptions)
79+
const matcher = patterns == null ? new FileMatcher(appDir, path.join(resourcesPath, "app"), macroExpander) : patterns[0]
80+
if (matcher.isEmpty() || matcher.containsOnlyIgnore()) {
81+
matcher.addAllPattern()
82+
}
83+
else {
84+
matcher.addPattern("package.json")
85+
}
86+
matcher.addPattern("!**/node_modules/*/{CHANGELOG.md,ChangeLog,changelog.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}")
87+
matcher.addPattern("!**/node_modules/.bin")
88+
matcher.addPattern("!**/*.{o,hprof,orig,pyc,pyo,rbc,swp}")
89+
matcher.addPattern("!**/._*")
90+
matcher.addPattern("!*.iml")
91+
//noinspection SpellCheckingInspection
92+
matcher.addPattern("!**/{.git,.hg,.svn,CVS,RCS,SCCS," +
93+
"__pycache__,.DS_Store,thumbs.db,.gitignore,.gitattributes," +
94+
".editorconfig,.flowconfig,.jshintrc,.eslintrc," +
95+
".yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,npm-debug.log," +
96+
".idea," +
97+
"appveyor.yml,.travis.yml,circle.yml," +
98+
".nyc_output}")
99+
100+
return matcher
101+
}
102+
103+
export function getFileMatchers(config: Config, name: "files" | "extraFiles" | "extraResources" | "asarUnpack", defaultSrc: string, defaultDest: string, allowAdvancedMatching: boolean, macroExpander: (pattern: string) => string, customBuildOptions: PlatformSpecificBuildOptions): Array<FileMatcher> | null {
104+
const globalPatterns: Array<string | FilePattern> | string | n | FilePattern = (<any>config)[name]
105+
const platformSpecificPatterns: Array<string | FilePattern> | string | n = (<any>customBuildOptions)[name]
106+
107+
const defaultMatcher = new FileMatcher(defaultSrc, defaultDest, macroExpander)
108+
const fileMatchers: Array<FileMatcher> = []
109+
110+
function addPatterns(patterns: Array<string | FilePattern> | string | n | FilePattern) {
111+
if (patterns == null) {
112+
return
113+
}
114+
else if (!Array.isArray(patterns)) {
115+
if (typeof patterns === "string") {
116+
defaultMatcher.addPattern(patterns)
117+
return
118+
}
119+
patterns = [patterns]
120+
}
121+
122+
for (const pattern of patterns) {
123+
if (typeof pattern === "string") {
124+
// use normalize to transform ./foo to foo
125+
defaultMatcher.addPattern(pattern)
126+
}
127+
else if (allowAdvancedMatching) {
128+
const from = pattern.from == null ? defaultSrc : path.resolve(defaultSrc, pattern.from)
129+
const to = pattern.to == null ? defaultDest : path.resolve(defaultDest, pattern.to)
130+
fileMatchers.push(new FileMatcher(from, to, macroExpander, pattern.filter))
131+
}
132+
else {
133+
throw new Error(`Advanced file copying not supported for "${name}"`)
134+
}
135+
}
136+
}
137+
138+
addPatterns(globalPatterns)
139+
addPatterns(platformSpecificPatterns)
140+
141+
if (!defaultMatcher.isEmpty()) {
142+
// default matcher should be first in the array
143+
fileMatchers.unshift(defaultMatcher)
144+
}
145+
146+
return fileMatchers.length === 0 ? null : fileMatchers
147+
}
148+
74149
export function copyFiles(patterns: Array<FileMatcher> | null): Promise<any> {
75150
if (patterns == null || patterns.length === 0) {
76151
return BluebirdPromise.resolve()

packages/electron-builder/src/fileTransformer.ts

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { debug } from "electron-builder-util"
22
import { deepAssign } from "electron-builder-util/out/deepAssign"
33
import { FileTransformer } from "electron-builder-util/out/fs"
4-
import { log, warn } from "electron-builder-util/out/log"
4+
import { warn } from "electron-builder-util/out/log"
55
import { readJson } from "fs-extra-p"
6-
import mime from "mime"
76
import * as path from "path"
87
import { BuildInfo } from "./packagerApi"
9-
import { PlatformPackager } from "./platformPackager"
108

11-
function isElectronCompileUsed(info: BuildInfo): boolean {
9+
export function isElectronCompileUsed(info: BuildInfo): boolean {
1210
const depList = [(<any>info.metadata).devDependencies, info.metadata.dependencies]
1311
if (info.isTwoPackageJsonProjectLayoutUsed) {
1412
depList.push((<any>info.devMetadata).devDependencies)
@@ -17,19 +15,17 @@ function isElectronCompileUsed(info: BuildInfo): boolean {
1715

1816
for (const deps of depList) {
1917
if (deps != null && "electron-compile" in deps) {
20-
log("electron-compile detected — files will be compiled")
2118
return true
2219
}
2320
}
2421

2522
return false
2623
}
2724

28-
export async function createTransformer(projectDir: string, srcDir: string, packager: PlatformPackager<any>): Promise<FileTransformer> {
29-
const extraMetadata = packager.packagerOptions.extraMetadata
25+
export async function createTransformer(srcDir: string, extraMetadata: any): Promise<FileTransformer> {
3026
const mainPackageJson = path.join(srcDir, "package.json")
31-
32-
const defaultTransformer: FileTransformer = file => {
27+
28+
return file => {
3329
if (file === mainPackageJson) {
3430
return modifyMainPackageJson(file, extraMetadata)
3531
}
@@ -42,43 +38,17 @@ export async function createTransformer(projectDir: string, srcDir: string, pack
4238
return null
4339
}
4440
}
41+
}
42+
43+
export interface CompilerHost {
44+
compile(file: string): any
4545

46-
return isElectronCompileUsed(packager.info) ? await createElectronCompileTransformer(projectDir, defaultTransformer) : defaultTransformer
46+
saveConfiguration(): Promise<any>
4747
}
4848

49-
async function createElectronCompileTransformer(projectDir: string, defaultTransformer: FileTransformer) {
49+
export function createElectronCompilerHost(projectDir: string, cacheDir: string): Promise<CompilerHost> {
5050
const electronCompilePath = path.join(projectDir, "node_modules", "electron-compile", "lib")
51-
const CompilerHost = require(path.join(electronCompilePath, "compiler-host")).default
52-
const compilerHost = await require(path.join(electronCompilePath, "config-parser")).createCompilerHostFromProjectRoot(projectDir)
53-
return async (file: string) => {
54-
const defaultResult = defaultTransformer(file)
55-
if (defaultResult != null) {
56-
return await defaultResult
57-
}
58-
59-
if (file.includes("/node_modules/") || file.includes("/bower_components/")) {
60-
return null
61-
}
62-
63-
const hashInfo = await compilerHost.fileChangeCache.getHashForPath(file)
64-
65-
if (CompilerHost.shouldPassthrough(hashInfo)) {
66-
return null
67-
}
68-
69-
// we don't use @paulcbetts/mime-types to lookup mime-type because it doesn't any value except size (@develar 20.03.17)
70-
// as we already depends on mime module (github publisher)
71-
// https://github.com/electron/electron-compile/pull/148#issuecomment-266669293
72-
const type = mime.lookup(file)
73-
const compiler = type == null ? null : compilerHost.compilersByMimeType[type]
74-
if (compiler == null) {
75-
return null
76-
}
77-
78-
const cache = compilerHost.cachesForCompilers.get(compiler)
79-
const result = await cache.getOrFetch(file, (file: string, hashInfo: any) => compilerHost.compileUncached(file, hashInfo, compiler))
80-
return result.code || result.binaryData
81-
}
51+
return require(path.join(electronCompilePath, "config-parser")).createCompilerHostFromProjectRoot(projectDir, cacheDir)
8252
}
8353

8454
function cleanupPackageJson(data: any): any {

0 commit comments

Comments
 (0)