jgb-cli
Version:
```shell npm i -g jgb-cli #全局安装 ```
352 lines (279 loc) • 8.94 kB
text/typescript
import * as Debug from 'debug';
import * as fs from 'fs';
import { Asset, IInitOptions, Resolver } from 'jgb-shared/lib';
import AwaitEventEmitter from 'jgb-shared/lib/awaitEventEmitter';
import Logger, { logger, LogType } from 'jgb-shared/lib/Logger';
import { normalizeAlias, pathToUnixType } from 'jgb-shared/lib/utils/index';
import WorkerFarm from 'jgb-shared/lib/workerfarm/WorkerFarm';
import * as Path from 'path';
import { promisify } from 'util';
import Compiler from './Compiler';
import FSCache from './FSCache';
import PromiseQueue from './utils/PromiseQueue';
import Watcher from './Watcher';
const debug = Debug('core');
export default class Core extends AwaitEventEmitter {
private currentDir = process.cwd();
private loadedAssets = new Map<string, Asset>();
private options: IInitOptions;
private entryFiles: string[];
buildQueue: PromiseQueue;
resolver: Resolver;
watcher: Watcher;
compiler: Compiler;
watchedAssets = new Map();
farm: WorkerFarm;
cache: FSCache;
hooks: Array<(...args: any[]) => Promise<void>>;
constructor(options: IInitOptions) {
super();
this.hooks = options.hooks || [];
this.options = this.normalizeOptions(options);
if (options.rootDir) {
this.currentDir = options.rootDir;
}
this.entryFiles = this.normalizeEntryFiles();
this.resolver = new Resolver(this.options);
this.compiler = new Compiler(this.options);
this.buildQueue = new PromiseQueue(this.processAsset.bind(this));
if (this.options.cache) {
this.cache = new FSCache(this.options);
}
}
normalizeEntryFiles() {
return []
.concat(this.options.entryFiles || [])
.filter(Boolean)
.map(f => Path.resolve(this.options.sourceDir, f));
}
normalizeOptions(options: IInitOptions): IInitOptions {
const rootDir = Path.resolve(options.rootDir || this.currentDir);
return {
plugins: options.plugins,
presets: options.presets,
watch: !!options.watch,
rootDir,
useLocalWorker: !!options.useLocalWorker,
outDir: Path.resolve(options.outDir || 'dist'),
npmDir: Path.resolve(options.npmDir || 'node_modules'),
entryFiles: [].concat(options.entryFiles),
cache: !!options.cache,
sourceDir: Path.resolve(options.sourceDir || 'src'),
alias: aliasResolve(options, rootDir),
minify: !!options.minify,
source: options.source || 'wx',
target: options.target || 'wx',
lib: options.lib
};
}
async initHook() {
if (!this.hooks || this.hooks.length === 0) {
return;
}
const allHooks = this.hooks.map(async fn => await fn(this));
await Promise.all(allHooks);
}
async init() {
await this.compiler.init(this.resolver);
}
async start() {
const startTime = new Date();
if (this.farm) {
return;
}
await this.initHook();
await this.emit('before-init');
await this.init();
await this.emit('before-compiler');
// another channce to modify entryFiles
this.entryFiles = this.normalizeEntryFiles();
if (this.options.watch) {
this.watcher = new Watcher();
this.watcher.on('change', this.onChange.bind(this));
}
this.farm = WorkerFarm.getShared(this.options, {
workerPath: require.resolve('./worker'),
core: this
});
for (const entry of new Set(this.entryFiles)) {
const asset = await this.resolveAsset(entry);
this.buildQueue.add(asset);
}
await this.buildQueue.run();
const endTime = new Date();
logger.info(`编译耗时:${endTime.getTime() - startTime.getTime()}ms`);
await this.emit('end-build');
this.farm.stopPref();
if (!this.options.watch) {
await this.stop();
}
}
async processAsset(asset: Asset, isRebuild = false) {
if (isRebuild) {
asset.invalidate();
if (this.cache) {
this.cache.invalidate(asset.name);
}
}
await this.loadAsset(asset);
}
async getAsset(name: string, parent: string) {
const asset = await this.resolveAsset(name, parent);
this.buildQueue.add(asset);
await this.buildQueue.run();
return asset;
}
async resolveAsset(name: string, parent?: string) {
const { path } = await this.resolver.resolve(name, parent);
return this.getLoadedAsset(path);
}
async loadAsset(asset: Asset) {
if (asset.processed) {
return;
}
// logger.info(asset.name);
asset.processed = true;
asset.startTime = Date.now();
let processed: IPipelineProcessed =
this.cache && (await this.cache.read(asset.name));
let cacheMiss = false;
if (!processed || asset.shouldInvalidate(processed.cacheData)) {
processed = await this.farm.run(asset.name, asset.distPath);
cacheMiss = true;
}
asset.endTime = Date.now();
debug(`${asset.name} processd time: ${asset.endTime - asset.startTime}ms`);
asset.id = processed.id;
// asset.generated = processed.generated;
asset.hash = processed.hash;
const dependencies = processed.dependencies;
const assetDeps = await Promise.all(
dependencies.map(async dep => {
// from cache dep
if (Array.isArray(dep) && dep.length > 1) {
dep = dep[1];
}
// This dependency is already included in the parent's generated output,
// so no need to load it. We map the name back to the parent asset so
// that changing it triggers a recompile of the parent.
if (dep.includedInParent) {
this.watch(dep.name, asset);
return;
}
dep.parent = asset.name;
const assetDep = await this.resolveDep(asset, dep);
if (assetDep) {
if (dep.distPath) {
assetDep.distPath = dep.distPath;
}
await this.loadAsset(assetDep);
dep.asset = assetDep;
dep.resolved = assetDep.name;
} else {
logger.warning(`can not resolveDep: ${dep.name}`);
}
return assetDep;
})
);
if (this.cache && cacheMiss) {
await this.cache.write(asset.name, processed);
}
}
async resolveDep(asset: Asset, dep: any, install = true) {
try {
if (Array.isArray(dep) && dep.length === 2) {
dep = dep[1];
}
if (dep.resolved) {
return this.getLoadedAsset(dep.resolved);
}
return await this.resolveAsset(dep.name, asset.name);
} catch (err) {
// If the dep is optional, return before we throw
if (dep.optional) {
return;
}
throw err;
}
}
getLoadedAsset(path: string) {
if (this.loadedAssets.has(path)) {
return this.loadedAssets.get(path);
}
const asset = this.compiler.getAsset(path);
if (this.loadedAssets.has(asset.name)) {
return this.loadedAssets.get(asset.name);
}
this.loadedAssets.set(path, asset);
this.loadedAssets.set(asset.name, asset);
this.watch(path, asset);
return asset;
}
async watch(path: string, asset: Asset) {
if (!this.watcher) {
return;
}
path = await promisify(fs.realpath)(path);
if (!this.watchedAssets.has(path)) {
this.watcher.watch(path);
this.watchedAssets.set(path, new Set());
}
this.watchedAssets.get(path).add(asset);
}
async stop() {
if (this.watcher) {
this.watcher.stop();
}
if (this.farm) {
await this.farm.end();
}
}
async unwatch(path: string, asset: Asset) {
path = await promisify(fs.realpath)(path);
if (!this.watchedAssets.has(path)) {
return;
}
const watched = this.watchedAssets.get(path);
watched.delete(asset);
if (watched.size === 0) {
this.watchedAssets.delete(path);
this.watcher.unwatch(path);
}
}
async onChange(path: string) {
const assets: Asset[] = this.watchedAssets.get(path);
if (!assets) {
return;
}
// Add the asset to the rebuild queue, and reset the timeout.
for (const asset of assets) {
this.buildQueue.add(asset, true);
}
await this.buildQueue.run();
}
}
function aliasResolve(options: IInitOptions, root: string) {
const alias = options.alias || {};
const newAlias: { [key: string]: any } = {};
/**
* 先排序 字符长度由长到短排序 (优先匹配)
* 再补充 resolve(aliasValue)
*/
Object.keys(alias)
.sort((a1, a2) => a2.length - a1.length)
.forEach(key => {
const aliasValue = normalizeAlias(alias[key]);
const aliasPath = aliasValue.path;
if (!Path.isAbsolute(aliasPath)) {
if (aliasPath.startsWith('.')) {
aliasValue.path = pathToUnixType(Path.resolve(root, aliasPath));
} else {
aliasValue.path = pathToUnixType(
Path.resolve(root, 'node_modules', aliasPath)
);
}
}
newAlias[key] = aliasValue;
});
return newAlias;
}