fuse-box
Version:
Fuse-Box a bundler that does it right
407 lines (371 loc) • 12.6 kB
text/typescript
import { WorkFlowContext } from "./core/WorkflowContext";
import { IPackageInformation } from "./core/PathMaster";
import { ModuleCollection } from "./core/ModuleCollection";
import { File } from "./core/File";
import { Config } from "./Config";
import { each } from "realm-utils";
import { AbsDir } from "./Types";
import * as fsExtra from "fs-extra";
import * as fs from "fs";
import * as path from "path";
const MEMORY_CACHE = {};
/**
*
* @class ModuleCache
*/
export class ModuleCache {
/**
*
*
* @type {AbsDir}
* @memberOf ModuleCache
*/
public cacheFolder: AbsDir;
/**
*
*
* @private
* @type {string}
* @memberOf ModuleCache
*/
private cacheFile: string;
/**
*
*
* @private
* @type {string}
* @memberOf ModuleCache
*/
private staticCacheFolder: string;
private permanentCacheFolder: string;
/**
*
*
* @private
*
* @memberOf ModuleCache
*/
private cachedDeps = {
tree: {},
flat: {},
};
/**
* Creates an instance of ModuleCache.
*
* @param {WorkFlowContext} context
*
* @memberOf ModuleCache
*/
constructor(public context: WorkFlowContext) { }
public initialize() {
this.cacheFolder = path.join(Config.TEMP_FOLDER, "cache",
Config.FUSEBOX_VERSION, this.context.output.getUniqueHash());
this.permanentCacheFolder = path.join(this.cacheFolder, "permanent");
fsExtra.ensureDirSync(this.permanentCacheFolder);
this.staticCacheFolder = path.join(this.cacheFolder, "static");
fsExtra.ensureDirSync(this.staticCacheFolder);
this.cacheFile = path.join(this.cacheFolder, "deps.json");
if (fs.existsSync(this.cacheFile)) {
try {
this.cachedDeps = require(this.cacheFile);
} catch (e) {
this.cachedDeps = {
tree: {},
flat: {},
};
}
}
}
public setPermanentCache(key: string, contents: string) {
key = encodeURIComponent(key);
let filePath = path.join(this.permanentCacheFolder, key);
fs.writeFile(filePath, contents, () => { });
MEMORY_CACHE[filePath] = contents;
}
public getPermanentCache(key: string) {
key = encodeURIComponent(key);
let filePath = path.join(this.permanentCacheFolder, key);
if (MEMORY_CACHE[filePath]) {
return MEMORY_CACHE[filePath];
}
if (fs.existsSync(filePath)) {
const contents = fs.readFileSync(filePath).toString();
MEMORY_CACHE[filePath] = contents;
return contents;
}
}
public getStaticCacheKey(file: File) {
return encodeURIComponent(this.context.bundle.name + file.absPath);
}
public encodeCacheFileName(str: string) {
let ext = path.extname(str);
if (ext !== ".js") {
str = str + ".js"
}
return encodeURIComponent(str);
}
/**
*
*
* @param {File} file
* @returns
*
* @memberOf ModuleCache
*/
public getStaticCache(file: File) {
let stats = fs.statSync(file.absPath);
let fileName = this.encodeCacheFileName(file.info.fuseBoxPath);
let memCacheKey = this.getStaticCacheKey(file);
let data;
if (MEMORY_CACHE[memCacheKey]) {
data = MEMORY_CACHE[memCacheKey];
if (data.mtime !== stats.mtime.getTime()) {
return;
}
return data;
} else {
let dest = path.join(this.staticCacheFolder, fileName);
if (fs.existsSync(dest)) {
try {
data = require(dest);
} catch (e) {
console.log(e);
return;
}
if (data.mtime !== stats.mtime.getTime()) {
return;
}
MEMORY_CACHE[memCacheKey] = data;
return data;
}
}
}
/**
*
*
* @param {File} file
* @param {any} dependencies
* @param {string} sourcemaps
*
* @memberOf ModuleCache
*/
public writeStaticCache(file: File, sourcemaps: string) {
let fileName = this.encodeCacheFileName(file.info.fuseBoxPath);
let memCacheKey = this.getStaticCacheKey(file);
let dest = path.join(this.staticCacheFolder, fileName);
let stats: any = fs.statSync(file.absPath);
let cacheData: any = {
contents: file.contents,
dependencies: file.analysis.dependencies,
sourceMap: sourcemaps || {},
headerContent: file.headerContent,
mtime: stats.mtime.getTime()
};
if (file.devLibsRequired) {
cacheData.devLibsRequired = file.devLibsRequired;
}
let data = `module.exports = { contents: ${JSON.stringify(cacheData.contents)},
dependencies: ${JSON.stringify(cacheData.dependencies)},
sourceMap: ${JSON.stringify(cacheData.sourceMap)},
headerContent: ${JSON.stringify(cacheData.headerContent)},
mtime: ${cacheData.mtime},
devLibsRequired : ${JSON.stringify(cacheData.devLibsRequired)}
};`;
MEMORY_CACHE[memCacheKey] = cacheData;
fs.writeFileSync(dest, data);
}
/**
*
*
* @param {File[]} files
* @returns {Promise<File[]>}
*
* @memberOf ModuleCache
*/
public resolve(files: File[]): Promise<File[]> {
let through: File[] = [];
let valid4Caching = [];
const moduleFileCollection = new Map<string, Map<string, File>>();
files.forEach(file => {
let info = file.info.nodeModuleInfo;
if (!moduleFileCollection.get(info.name)) {
moduleFileCollection.set(info.name, new Map<string, File>());
}
moduleFileCollection.get(info.name).set(file.info.fuseBoxPath, file);
});
files.forEach(file => {
let info = file.info.nodeModuleInfo;
let key = `${info.name}@${info.version}`;
let cachePath = path.join(this.cacheFolder, encodeURIComponent(key));
let cached = this.cachedDeps.flat[key];
if (!cached || !fs.existsSync(cachePath)) {
through.push(file);
} else {
if (cached.version !== info.version || cached.files.indexOf(file.info.fuseBoxPath) === -1) {
through.push(file);
for (let i = 0; i < cached.files.length; i++) {
let cachedFileName = cached.files[i];
let f = moduleFileCollection.get(info.name).get(cachedFileName);
if (f) {
through.push(f);
}
}
let index = valid4Caching.indexOf(key);
if (index !== -1) {
valid4Caching.splice(index, 1);
}
} else {
if (valid4Caching.indexOf(key) === -1) {
valid4Caching.push(key);
}
}
}
});
const required = [];
const operations: Promise<any>[] = [];
let cacheReset = false;
/**
*
*
* @param {any} err
* @param {any} result
* @returns
*/
const getAllRequired = (key, json: any) => {
if (required.indexOf(key) === -1) {
if (json) {
let collection = new ModuleCollection(this.context, json.name);
let cacheKey = encodeURIComponent(key);
collection.cached = true;
collection.cachedName = key;
collection.cacheFile = path.join(this.cacheFolder, cacheKey);
operations.push(new Promise((resolve, reject) => {
if (MEMORY_CACHE[collection.cacheFile]) {
collection.cachedContent = MEMORY_CACHE[collection.cacheFile];
return resolve();
}
if (fs.existsSync(collection.cacheFile)) {
fs.readFile(collection.cacheFile, (err, result) => {
collection.cachedContent = result.toString();
MEMORY_CACHE[collection.cacheFile] = collection.cachedContent;
return resolve();
});
} else {
// reset cache
valid4Caching = [];
cacheReset = true;
return resolve();
}
}));
this.context.addNodeModule(key, collection);
required.push(key);
if (json.deps) {
for (let k in json.deps) { if (json.deps.hasOwnProperty(k)) { getAllRequired(k, json.deps[k]); } }
}
}
}
};
valid4Caching.forEach(key => {
getAllRequired(key, this.cachedDeps.tree[key]);
});
return Promise.all(operations).then(() => {
if (cacheReset) {
this.context.resetNodeModules();
return files;
}
return through;
});
}
/**
*
*
* @param {ModuleCollection} rootCollection
*
* @memberOf ModuleCache
*/
public buildMap(rootCollection: ModuleCollection) {
let json = this.cachedDeps;
/**
*
*
* @param {Map<string, ModuleCollection>} modules
* @param {*} root
* @returns
*/
/**
*
*
* @param {ModuleCollection} collection
* @returns
*/
/**
*
*
* @param {any} file
*/
/**
*
*
* @param {any} resolve
* @param {any} reject
* @returns
*/
const traverse = (modules: Map<string, ModuleCollection>, root: any) => {
return each(modules, (collection: ModuleCollection) => {
if (collection.traversed) {
return;
}
let dependencies = {};
let flatFiles;
if (collection.cached) {
return;
}
let key = `${collection.info.name}@${collection.info.version}`;
if (!json.flat[key]) {
json.flat[key] = {
name: collection.name,
version: collection.info.version,
files: [],
};
}
flatFiles = json.flat[key].files;
collection.dependencies.forEach(file => {
if (flatFiles.indexOf(file.info.fuseBoxPath) < 0) {
flatFiles.push(file.info.fuseBoxPath);
}
});
root[key] = {
deps: dependencies,
name: collection.info.name,
version: collection.info.version,
};
collection.traversed = true;
return traverse(collection.nodeModules, dependencies);
});
};
//console.log("traverse...", rootCollection.nodeModules);
traverse(rootCollection.nodeModules, json.tree).then(() => {
fs.writeFile(this.cacheFile, JSON.stringify(json, undefined, 2), () => { });
});
}
/**
*
*
* @param {IPackageInformation} info
* @param {string} contents
* @returns
*
* @memberOf ModuleCache
*/
public set(info: IPackageInformation, contents: string) {
return new Promise((resolve, reject) => {
const cacheKey = encodeURIComponent(`${info.name}@${info.version}`);
const targetName = path.join(this.cacheFolder, cacheKey);
// storing to memory
MEMORY_CACHE[cacheKey] = contents;
fs.writeFile(targetName, contents, (err) => {
return resolve();
});
});
}
}