jgb-cli
Version:
```shell npm i -g jgb-cli #全局安装 ```
197 lines (166 loc) • 5.33 kB
text/typescript
import * as glob from 'fast-glob';
import * as fs from 'fs';
import * as fsExtra from 'fs-extra';
import * as isGlob from 'is-glob';
import { IInitOptions } from 'jgb-shared/lib';
import { logger } from 'jgb-shared/lib/Logger';
import { md5, objectHash } from 'jgb-shared/lib/utils';
import * as mkdir from 'mkdirp';
import * as path from 'path';
import { promisify } from 'util';
import * as VError from 'verror';
import * as pkg from '../package.json';
type optionsKey = keyof IInitOptions;
// These keys can affect the output, so if they differ, the cache should not match
const OPTION_KEYS: optionsKey[] = [
'alias',
'target',
'source',
'presets',
'plugins',
'minify',
'outDir',
'sourceDir',
'rootDir'
];
const mkdirp = promisify(mkdir);
const statp = promisify(fs.stat);
export default class FSCache {
dir: string;
dirExistsPromise: Promise<void>;
invalidated = new Set();
optionsHash: any;
constructor(options: IInitOptions) {
this.dir = path.resolve(options.cacheDir || '.cache');
const hash = OPTION_KEYS.reduce((p: any, k) => ((p[k] = options[k]), p), {
version: pkg.version
});
this.optionsHash = objectHash(hash);
this.ensureDirExists();
}
async ensureDirExists() {
if (this.dirExistsPromise) {
return this.dirExistsPromise;
}
const task = async () => {
await mkdirp(this.dir);
// Create sub-directories for every possible hex value
// This speeds up large caches on many file systems since there are fewer files in a single directory.
for (let i = 0; i < 256; i++) {
await mkdirp(path.join(this.dir, ('00' + i.toString(16)).slice(-2)));
}
};
this.dirExistsPromise = task();
}
async getCacheFile(filename: string) {
const hash = md5((await this.optionsHash) + filename);
return path.join(this.dir, hash.slice(0, 2), hash.slice(2) + '.json');
}
getLastModifiedSync(filename: string) {
const stat = fs.statSync(filename);
return stat.mtime.getTime();
}
async getLastModified(filename: string) {
// if (isGlob(filename)) {
//
// const files = await glob(filename, {
// onlyFiles: true
// });
// return (await Promise.all(
// files.map((file: any) =>
// statp(file).then(({ mtime }) => mtime.getTime())
// )
// )).reduce((a, b) => Math.max(a, b), 0);
// }
const stat = await statp(filename);
return stat.mtime.getTime();
}
async writeDepMtimes(data: IPipelineProcessed) {
// Write mtimes for each dependent file that is already compiled into this asset
const dependencies = data.dependencies;
if (dependencies instanceof Map) {
// @ts-ignore
dependencies = dependencies.values();
}
for (const dep of new Set(dependencies)) {
if (dep && dep.includedInParent) {
try {
dep.mtime = this.getLastModifiedSync(dep.name);
} catch (error) {
logger.error(VError.fullStack(error));
}
}
}
}
async write(filename: string, data: IPipelineProcessed) {
try {
await this.ensureDirExists();
const cacheFile = await this.getCacheFile(filename);
if (data.dependencies instanceof Map) {
data.dependencies = [...data.dependencies].map(([fileName, asset]) => {
return [fileName, { ...asset, asset: null }];
});
} else if (Array.isArray(data.dependencies)) {
data.dependencies = [...data.dependencies].map(asset => {
return [asset.name, { ...asset, asset: null }];
});
}
await this.writeDepMtimes(data);
await fsExtra.writeFile(cacheFile, JSON.stringify(data));
this.invalidated.delete(filename);
} catch (err) {
err.name = 'FSCache Error write cache';
logger.error(VError.fullStack(err));
}
}
async checkDepMtimes(data: any) {
// Check mtimes for files that are already compiled into this asset
// If any of them changed, invalidate.
if (Array.isArray(data.dependencies)) {
for (const [assetName, dep] of new Set<any>(data.dependencies)) {
if (dep && dep.includedInParent) {
if ((await this.getLastModified(dep.name)) > dep.mtime) {
return false;
}
}
}
}
return true;
}
async read(filename: string) {
if (this.invalidated.has(filename)) {
return null;
}
const cacheFile = await this.getCacheFile(filename);
try {
const stats = await statp(filename);
const cacheStats = await statp(cacheFile);
if (stats.mtime > cacheStats.mtime) {
return null;
}
const json = await promisify(fs.readFile)(cacheFile, {
encoding: 'utf-8'
});
const data = JSON.parse(json);
if (!(await this.checkDepMtimes(data))) {
return null;
}
return data;
} catch (err) {
// logger.error(`cannot found ${filename}'s cacheFile: ${cacheFile}`);
return null;
}
}
invalidate(filename: string) {
this.invalidated.add(filename);
}
async delete(filename: string) {
try {
const cacheFile = await this.getCacheFile(filename);
await promisify(fs.unlink)(cacheFile);
this.invalidated.delete(filename);
} catch (err) {
// Fail silently
}
}
}