diff --git a/@types/arguments.d.ts b/@types/arguments.d.ts new file mode 100644 index 0000000..0a134a4 --- /dev/null +++ b/@types/arguments.d.ts @@ -0,0 +1,8 @@ +import { OptionsDto } from "./options"; +export interface Arguments { + pattern: string; + options: OptionsDto; + uglifyProcessLimit: number; +} +declare const _default: Arguments; +export default _default; diff --git a/dist/cli.d.ts b/@types/cli.d.ts similarity index 100% rename from dist/cli.d.ts rename to @types/cli.d.ts diff --git a/@types/main.d.ts b/@types/main.d.ts new file mode 100644 index 0000000..e3068f4 --- /dev/null +++ b/@types/main.d.ts @@ -0,0 +1,29 @@ +import { OptionsDto } from "./options"; +export declare class GlobsUglifyJs { + constructor(globPattern: string, options?: OptionsDto); + private globOptions; + private globPattern; + private options; + private filesList; + private filesDetails; + private uglified; + GetFilesList(): Promise; + Uglify(processLimit?: number): Promise; + private handlerInfo; + private handleStarter(processLimit); + private tryToStartHandle(); + private onFileHandled(); + private startHandlingFile(index); + private handleError(error); + private removeSources(successFiles); + private uglifyItem(file); + private buildOutFilePath(filePath); + private uglifyFile(file, options?); + /** + * Asynchronously return files list by pattern. + * + * @param {string} pattern + * @param {glob.IOptions} [options={}] + */ + private getGlobFilesList(pattern, options?); +} diff --git a/dist/options.d.ts b/@types/options.d.ts similarity index 68% rename from dist/options.d.ts rename to @types/options.d.ts index 31d7847..e15678b 100644 --- a/dist/options.d.ts +++ b/@types/options.d.ts @@ -1,6 +1,6 @@ /// import * as uglifyjs from "uglify-js"; -export interface Options { +export interface OptionsDto { [key: string]: any; UseMinExt?: boolean; MinifyOptions?: uglifyjs.MinifyOptions; @@ -9,11 +9,13 @@ export interface Options { RootDir?: string; RemoveSource?: boolean; Debug?: boolean; - exclude?: Array | string; + Exclude?: Array | string; + Silence?: boolean; } -export default class OptionsConstructor implements Options { - constructor(importData?: Options); +export declare class Options implements OptionsDto { + constructor(importData?: OptionsDto); private options; + ToObject(): OptionsDto; readonly UseMinExt: boolean; readonly MinifyOptions: uglifyjs.MinifyOptions; readonly OutDir: string; @@ -22,4 +24,5 @@ export default class OptionsConstructor implements Options { readonly RemoveSource: boolean; readonly Debug: boolean; readonly Exclude: Array | string | undefined; + readonly Silence: boolean; } diff --git a/dist/rejection-error.d.ts b/@types/rejection-error.d.ts similarity index 68% rename from dist/rejection-error.d.ts rename to @types/rejection-error.d.ts index 2ff91eb..c6cb945 100644 --- a/dist/rejection-error.d.ts +++ b/@types/rejection-error.d.ts @@ -1,11 +1,11 @@ /// -export default class RejectionError { +export declare class RejectionError { private error; private type; private uniqId; - constructor(error: NodeJS.ErrnoException | Error, type?: string, uniqId?: string); + constructor(error: NodeJS.ErrnoException | Error, type?: string | undefined, uniqId?: string | undefined); readonly Type: string | undefined; - readonly Error: Error | NodeJS.ErrnoException; + readonly Error: NodeJS.ErrnoException | Error; readonly UniqId: string | undefined; private showErrorDetails(); LogError(debug?: boolean): void; diff --git a/@types/utils/directories.d.ts b/@types/utils/directories.d.ts new file mode 100644 index 0000000..50f7235 --- /dev/null +++ b/@types/utils/directories.d.ts @@ -0,0 +1,3 @@ +export declare function Exists(path: string): Promise; +export declare function RemoveEmptyDirectories(directoryPath: string): Promise; +export declare function MakeTree(filePath: string): Promise; diff --git a/README.md b/README.md index 3279383..46a8ca0 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ $ glob-uglifyjs -h | RootDir | `string` | ` ` | Specifies the root directory of input files. | | RemoveSource | `boolean` | `false` | Remove all source files specified by glob pattern. | | Debug | `boolean` | `false` | Show errors details information. | -| exclude | `string` \| `string[]` | `undefined` | Add a pattern or an array of glob patterns to exclude matches. Read more in [node-glob options](https://github.com/isaacs/node-glob#options) `ignore`. | +| Silence | `boolean` | `false` | Silence all messages in console. | +| Exclude | `string` \| `string[]` | `undefined` | Add a pattern or an array of glob patterns to exclude matches. Read more in [node-glob options](https://github.com/isaacs/node-glob#options) `ignore`. | | Cwd | `string` | `process.cwd()` | Current working directory. | diff --git a/dist/arguments.d.ts b/dist/arguments.d.ts deleted file mode 100644 index b94f2d3..0000000 --- a/dist/arguments.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Options } from "./options"; -export interface Arguments { - pattern: string; - options: Options; -} -declare var _default: Arguments; -export default _default; diff --git a/dist/arguments.js b/dist/arguments.js index b6496ef..7be2575 100644 --- a/dist/arguments.js +++ b/dist/arguments.js @@ -17,6 +17,13 @@ exports.default = yargs type: "string" }) .require("pattern", "Pattern required") + .option("uglifyProcessLimit", { + describe: "Uglify process limit", + type: "number" +}) + .default({ + "uglifyProcessLimit": 3 +}) .config("config") .alias("c", "config") .default("config", "glob-uglifyjs.config.json") diff --git a/dist/cli.js b/dist/cli.js index cfc23ee..84ba05f 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -1,5 +1,22 @@ #!/usr/bin/env node +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; Object.defineProperty(exports, "__esModule", { value: true }); const arguments_1 = require("./arguments"); const main_1 = require("./main"); -new main_1.GlobsUglifyJs(arguments_1.default.pattern, arguments_1.default.options || {}); +function CliStarter() { + return __awaiter(this, void 0, void 0, function* () { + if (arguments_1.default.uglifyProcessLimit <= 0) { + throw new Error("Uglify process limit must be at least 1."); + } + const globUglifier = new main_1.GlobsUglifyJs(arguments_1.default.pattern, arguments_1.default.options || {}); + yield globUglifier.Uglify(arguments_1.default.uglifyProcessLimit); + }); +} +CliStarter(); diff --git a/dist/main.d.ts b/dist/main.d.ts deleted file mode 100644 index 0a6c6a8..0000000 --- a/dist/main.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Options } from "./options"; -export declare class GlobsUglifyJs { - private globPattern; - private options; - constructor(globPattern: string, options?: Options); - private main(); - private deleteFiles(fileList); - private deleteFile(filePath); - private uglifyFile(file, options?); - private startRecursiveUglify(filesList, results?); - private recursiveUglify(file); - private ensureDirectoryExistence(filePath); - private directoryExists(path); - private deleteEmptyDirectories(directoryPath); - private removeDirectory(directoryPath); - private readFilesInDirectory(directoryPath); - /** - * Check if parsed name without extension has minified extension prefix. - * - * @private - * @param {string} nameWithoutExt Parsed file name without extension. - * @returns - * - * @memberOf GlobsUglifyJs - */ - private hasMinifiedExt(nameWithoutExt); - private resolveOutFilePath(filePath); - /** - * Asynchronously write data to file with flag "wx". - * - * @private - * @param {string} filePath File path. - * @param {string} data Data in "utf-8". - * @returns - * - * @memberOf GlobsUglifyJs - */ - private writeToFile(filePath, data); - /** - * Asynchronously return files list by pattern. - * - * @private - * @param {string} pattern - * @param {glob.IOptions} [options={}] - * @returns - * - * @memberOf GlobsUglifyJs - */ - private getGlobs(pattern, options?); - /** - * Validate JS extension. - * - * @private - * @param {string} pattern - * @returns {boolean} True if extension exist. - * - * @memberOf GlobsUglifyJs - */ - private validateExtension(pattern); - /** - * Add .js to glob pattern. - * - * @private - * @param {string} pattern - * @returns {string} - * - * @memberOf GlobsUglifyJs - */ - private addJsExtension(pattern); -} diff --git a/dist/main.js b/dist/main.js index ef03571..2318ac1 100644 --- a/dist/main.js +++ b/dist/main.js @@ -11,301 +11,220 @@ const uglifyjs = require("uglify-js"); const glob = require("glob"); const path = require("path"); const options_1 = require("./options"); -const fs = require("fs"); +const fs = require("mz/fs"); const rejection_error_1 = require("./rejection-error"); +const Directories = require("./utils/directories"); const JS_EXTENSION = ".js"; const MINIFY_EXTENSION_PREFIX = ".min"; -class RecursiveUglifyResults { - constructor() { - this.succeed = 0; - this.failed = 0; - } - get Succeed() { - return this.succeed; - } - get Failed() { - return this.failed; - } - OnSucceed() { - this.succeed++; - } - OnFailed() { - this.failed++; - } -} +var Status; +(function (Status) { + Status[Status["Init"] = 0] = "Init"; + Status[Status["Pending"] = 1] = "Pending"; + Status[Status["Completed"] = 2] = "Completed"; + Status[Status["Failed"] = 3] = "Failed"; +})(Status || (Status = {})); class GlobsUglifyJs { constructor(globPattern, options) { - this.options = new options_1.default(options); - if (!this.validateExtension(globPattern)) { - globPattern = this.addJsExtension(globPattern); + this.uglified = false; + this.options = new options_1.Options(options); + const globExt = path.extname(globPattern); + if (!globExt) { + globPattern += JS_EXTENSION; + } + else if (globExt === ".") { + globPattern += JS_EXTENSION.slice(1); + } + else if (!this.options.Silence) { + console.log(`Using custom '${globExt}' extension.`); + } + if (this.options.Exclude !== undefined) { + this.globOptions = { ignore: this.options.Exclude }; } this.globPattern = path.join(this.options.RootDir, globPattern); - this.main(); } - main() { + GetFilesList() { return __awaiter(this, void 0, void 0, function* () { - let globOptions; - if (this.options.Exclude !== undefined) { - globOptions = { ignore: this.options.Exclude }; + if (this.filesList != null) { + return this.filesList; } + let filesList; try { - let filesList = yield this.getGlobs(this.globPattern, globOptions); - if (filesList.length === 0) { - console.log("No files found."); - return; - } - let results = yield this.startRecursiveUglify(filesList.slice(0)); - if (this.options.RemoveSource) { - yield this.deleteFiles(filesList.slice(0)); - yield this.deleteEmptyDirectories(this.options.RootDir); - } - if (results.Failed > 0) { - console.warn(`Failed to minify ${results.Failed} file${(results.Failed > 1) ? "s" : ""}.`); - } - if (results.Succeed > 0) { - console.log(`Successfully minified ${results.Succeed} file${(results.Succeed > 1) ? "s" : ""}.`); - } + filesList = yield this.getGlobFilesList(this.globPattern, this.globOptions); } catch (error) { - if (error instanceof rejection_error_1.default) { - error.ThrowError(); - } - else { - throw error; + if (this.options.Debug && !this.options.Silence) { + console.error(error); } + throw new Error(`Failed to find files by specified glob '${this.globPattern}'.`); } - }); - } - deleteFiles(fileList) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { - let rejected = false; - let file = fileList.shift(); - if (file == null) { - resolve(); - return; - } - yield this.deleteFile(file) - .catch(error => { - rejected = true; - reject(new rejection_error_1.default(error, "deleteFile")); - }); - if (!rejected) { - yield this.deleteFiles(fileList); - resolve(); - } - })); - }); - } - deleteFile(filePath) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => { - fs.unlink(filePath, error => { - if (error) { - reject(error); - } - else { - resolve(); - } - }); + if (!this.options.Silence) { + console.log(`Found ${filesList.length} files with glob pattern '${this.globPattern}'.`); + } + this.filesList = filesList; + this.filesDetails = this.filesList.map((value, index) => { + return { Index: index, Status: Status.Init }; }); + return this.filesList; }); } - uglifyFile(file, options) { + Uglify(processLimit = 3) { return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => { + if (this.uglified && !this.options.Silence) { + console.warn("Files already uglified."); + return; + } + let filesList; + if (this.filesList == null) { + filesList = yield this.GetFilesList(); + } + else { + filesList = this.filesList; + } + if (filesList.length === 0 && !this.options.Silence) { + console.warn(`No files found matching specified glob pattern (${this.globPattern}).`); + return; + } + yield this.handleStarter(processLimit); + const results = { + Success: this.filesDetails.filter(x => x.Status === Status.Completed).map(x => filesList[x.Index]), + Failed: this.filesDetails.filter(x => x.Status === Status.Failed).map(x => filesList[x.Index]) + }; + if (this.options.RemoveSource) { try { - let outputData = uglifyjs.minify(file, options); - resolve(outputData); + yield this.removeSources(results.Success); } catch (error) { - reject(error); + this.handleError(error); } - }); - }); - } - startRecursiveUglify(filesList, results) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { - if (results == null) { - results = new RecursiveUglifyResults(); - } - let file = filesList.shift(); - if (file != null) { - try { - yield this.recursiveUglify(file); - results.OnSucceed(); - } - catch (error) { - if (error instanceof rejection_error_1.default) { - error.LogError(this.options.Debug); - } - else if (this.options.Debug) { - console.error(error); - } - results.OnFailed(); - } - resolve(yield this.startRecursiveUglify(filesList, results)); + } + if (!this.options.Silence) { + if (results.Failed.length > 0) { + console.warn(`Failed to minify ${results.Failed.length} file${(results.Failed.length > 1) ? "s" : ""}.`); } - else { - resolve(results); + if (results.Success.length > 0) { + console.log(`Successfully minified ${results.Success.length} file${(results.Success.length > 1) ? "s" : ""}.`); } - })); + } }); } - recursiveUglify(file) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { - try { - let outputData = yield this.uglifyFile(file, this.options.MinifyOptions) - .catch(error => { - throw new rejection_error_1.default(error, "uglifyFile", file); - }); - let outPath = this.resolveOutFilePath(file); - yield this.ensureDirectoryExistence(outPath) - .catch(error => { - throw new rejection_error_1.default(error, "ensureDirectoryExistence", file); - }); - yield this.writeToFile(outPath, outputData.code) - .catch(error => { - throw new rejection_error_1.default(error, "writeToFile", file); - }); - resolve(); - } - catch (error) { - reject(error); - } - })); + handleStarter(processLimit) { + return new Promise((resolve, reject) => { + this.handlerInfo = { + Reject: reject, + Resolve: resolve, + ProcessLimit: processLimit, + ActiveProcess: 0 + }; + this.tryToStartHandle(); }); } - ensureDirectoryExistence(filePath) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { - let rejected = false; - let dirname = path.dirname(filePath); - let directoryExist = yield this.directoryExists(dirname); - if (directoryExist) { - resolve(); - return; - } - yield this.ensureDirectoryExistence(dirname) - .catch(error => { - reject(error); - rejected = true; - }); - if (rejected) { - return; - } - fs.mkdir(dirname, error => { - if (error) { - reject(error); - } - else { - resolve(); - } - }); - })); - }); + tryToStartHandle() { + if (this.handlerInfo.ActiveProcess >= this.handlerInfo.ProcessLimit) { + return; + } + if (this.filesDetails == null) { + return; + } + const index = this.filesDetails.findIndex(x => x.Status === Status.Init); + if (index === -1) { + return; + } + this.handlerInfo.ActiveProcess++; + this.startHandlingFile(index); + this.tryToStartHandle(); } - directoryExists(path) { + onFileHandled() { + this.handlerInfo.ActiveProcess--; + if (this.filesDetails == null) { + return; + } + const completedList = this.filesDetails.filter(x => x.Status === Status.Completed || x.Status === Status.Failed); + if (completedList.length !== this.filesDetails.length) { + this.tryToStartHandle(); + return; + } + this.uglified = true; + this.handlerInfo.Resolve(); + } + startHandlingFile(index) { return __awaiter(this, void 0, void 0, function* () { - return new Promise(resolve => { - fs.stat(path, (error, stats) => { - if (error) { - resolve(false); - } - else { - resolve(stats.isDirectory()); - } - }); - }); + if (this.filesDetails == null || this.filesList == null) { + this.onFileHandled(); + return; + } + const file = this.filesList[this.filesDetails[index].Index]; + if (file == null) { + this.onFileHandled(); + return; + } + try { + this.filesDetails[index].Status = Status.Pending; + yield this.uglifyItem(file); + this.filesDetails[index].Status = Status.Completed; + } + catch (error) { + this.filesDetails[index].Status = Status.Failed; + this.handleError(error); + } + this.onFileHandled(); }); } - deleteEmptyDirectories(directoryPath) { + handleError(error) { + if (!this.options.Silence) { + if (error instanceof rejection_error_1.RejectionError) { + error.LogError(this.options.Debug); + } + else if (this.options.Debug && !this.options.Silence) { + console.error(error); + } + } + } + removeSources(successFiles) { return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { - let rejected = false; - let isExist = yield this.directoryExists(directoryPath); - if (!isExist) { - resolve(); // or reject? - return; - } - let files = yield this.readFilesInDirectory(directoryPath); - if (files.length > 0) { - for (let i = 0; i < files.length; i++) { - let file = files[i]; - var fullPath = path.join(directoryPath, file); - yield this.deleteEmptyDirectories(fullPath) - .catch(error => { - reject(new rejection_error_1.default(error, "deleteEmptyDirectories")); - rejected = true; - }); - if (rejected) { - break; - } - } - if (rejected) { - return; - } - files = yield this.readFilesInDirectory(directoryPath); - } - if (files.length === 0) { - yield this.removeDirectory(directoryPath) - .catch(error => { - reject(new rejection_error_1.default(error, "removeDirectory")); - rejected = true; - }); + for (const file of successFiles) { + try { + yield fs.unlink(file); } - if (!rejected) { - resolve(); + catch (error) { + throw new rejection_error_1.RejectionError(error, "deleteFile"); } - })); - }); - } - removeDirectory(directoryPath) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => { - fs.rmdir(directoryPath, error => { - if (error) { - reject(error); - } - else { - resolve(); - } - }); - }); + } + try { + yield Directories.RemoveEmptyDirectories(this.options.RootDir); + } + catch (error) { + throw new rejection_error_1.RejectionError(error, "deleteEmptyDirectories"); + } }); } - readFilesInDirectory(directoryPath) { + uglifyItem(file) { return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => { - fs.readdir(directoryPath, (error, files) => { - if (error) { - reject(error); - } - else { - resolve(files); - } - }); - }); + let outputData; + try { + outputData = yield this.uglifyFile(file, this.options.MinifyOptions); + } + catch (error) { + throw new rejection_error_1.RejectionError(error, "uglifyFile", file); + } + const outPath = this.buildOutFilePath(file); + try { + yield Directories.MakeTree(outPath); + } + catch (error) { + throw new rejection_error_1.RejectionError(error, "ensureDirectoryExistence", file); + } + try { + yield fs.writeFile(outPath, outputData.code, { encoding: "utf-8", flag: "w" }); + } + catch (error) { + throw new rejection_error_1.RejectionError(error, "writeToFile", file); + } }); } - /** - * Check if parsed name without extension has minified extension prefix. - * - * @private - * @param {string} nameWithoutExt Parsed file name without extension. - * @returns - * - * @memberOf GlobsUglifyJs - */ - hasMinifiedExt(nameWithoutExt) { - let ext = path.extname(nameWithoutExt); - return (ext != null && ext === MINIFY_EXTENSION_PREFIX); - } - resolveOutFilePath(filePath) { - let parsedPath = path.parse(filePath), targetExt = parsedPath.ext; - if (this.options.UseMinExt && !this.hasMinifiedExt(parsedPath.name)) { + buildOutFilePath(filePath) { + const parsedPath = path.parse(filePath); + let targetExt = parsedPath.ext; + if (this.options.UseMinExt && targetExt !== MINIFY_EXTENSION_PREFIX) { targetExt = MINIFY_EXTENSION_PREFIX + targetExt; } let relativeDir = path.relative(this.options.RootDir, parsedPath.dir); @@ -317,41 +236,26 @@ class GlobsUglifyJs { root: parsedPath.root }); } - /** - * Asynchronously write data to file with flag "wx". - * - * @private - * @param {string} filePath File path. - * @param {string} data Data in "utf-8". - * @returns - * - * @memberOf GlobsUglifyJs - */ - writeToFile(filePath, data) { + uglifyFile(file, options) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { - fs.writeFile(filePath, data, { encoding: "utf-8", flag: "w" }, (error) => { - if (error) { - reject(error); - } - else { - resolve(); - } - }); + try { + let outputData = uglifyjs.minify(file, options); + resolve(outputData); + } + catch (error) { + reject(error); + } }); }); } /** * Asynchronously return files list by pattern. * - * @private * @param {string} pattern * @param {glob.IOptions} [options={}] - * @returns - * - * @memberOf GlobsUglifyJs */ - getGlobs(pattern, options = {}) { + getGlobFilesList(pattern, options = {}) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { glob(pattern, options, (err, matches) => { @@ -365,33 +269,5 @@ class GlobsUglifyJs { }); }); } - /** - * Validate JS extension. - * - * @private - * @param {string} pattern - * @returns {boolean} True if extension exist. - * - * @memberOf GlobsUglifyJs - */ - validateExtension(pattern) { - let ext = path.extname(pattern); - if (ext.length !== 0 && ext !== JS_EXTENSION) { - console.warn("Using custom extension: ", ext); - } - return (ext != null && ext.length > 0); - } - /** - * Add .js to glob pattern. - * - * @private - * @param {string} pattern - * @returns {string} - * - * @memberOf GlobsUglifyJs - */ - addJsExtension(pattern) { - return pattern + JS_EXTENSION; - } } exports.GlobsUglifyJs = GlobsUglifyJs; diff --git a/dist/options.js b/dist/options.js index 861bf6c..ce1f20e 100644 --- a/dist/options.js +++ b/dist/options.js @@ -1,6 +1,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); const process = require("process"); -class OptionsConstructor { +class Options { constructor(importData) { this.options = { MinifyOptions: {}, @@ -10,7 +10,8 @@ class OptionsConstructor { RootDir: "", RemoveSource: false, Debug: false, - exclude: undefined + Exclude: undefined, + Silence: false }; if (importData != null) { if (importData.Cwd != null) { @@ -22,11 +23,20 @@ class OptionsConstructor { this.options.Cwd = process.cwd(); Object.keys(this.options).forEach(key => { if (importData[key] !== undefined) { - this.options[key] = importData[key]; + // Deprecated: now use Exclude key. + if (key === "exclude") { + this.options["Exclude"] = importData[key]; + } + else { + this.options[key] = importData[key]; + } } }); } } + ToObject() { + return this.options; + } get UseMinExt() { return this.options.UseMinExt; } @@ -49,7 +59,10 @@ class OptionsConstructor { return this.options.Debug; } get Exclude() { - return this.options.exclude; + return this.options.Exclude; + } + get Silence() { + return this.options.Silence; } } -exports.default = OptionsConstructor; +exports.Options = Options; diff --git a/dist/rejection-error.js b/dist/rejection-error.js index 4609a2e..1a6605b 100644 --- a/dist/rejection-error.js +++ b/dist/rejection-error.js @@ -35,4 +35,4 @@ class RejectionError { throw this.error; } } -exports.default = RejectionError; +exports.RejectionError = RejectionError; diff --git a/dist/utils/directories.js b/dist/utils/directories.js new file mode 100644 index 0000000..5d75645 --- /dev/null +++ b/dist/utils/directories.js @@ -0,0 +1,59 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const fs = require("mz/fs"); +const mkdirp = require("mkdirp"); +function Exists(path) { + return __awaiter(this, void 0, void 0, function* () { + try { + const stat = yield fs.stat(path); + return stat.isDirectory(); + } + catch (error) { + return false; + } + }); +} +exports.Exists = Exists; +function RemoveEmptyDirectories(directoryPath) { + return __awaiter(this, void 0, void 0, function* () { + const isExist = yield Exists(directoryPath); + if (!isExist) { + return; + } + let files = yield fs.readdir(directoryPath); + if (files.length > 0) { + for (const file of files) { + const fullPath = path.join(directoryPath, file); + yield RemoveEmptyDirectories(fullPath); + } + files = yield fs.readdir(directoryPath); + } + if (files.length === 0) { + yield fs.rmdir(directoryPath); + } + }); +} +exports.RemoveEmptyDirectories = RemoveEmptyDirectories; +function MakeTree(filePath) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + const dirname = path.dirname(filePath); + mkdirp(dirname, (error, data) => { + if (error) { + reject(error); + return; + } + resolve(data); + }); + }); + }); +} +exports.MakeTree = MakeTree; diff --git a/package.json b/package.json index f57b0cf..fd17c80 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "glob-uglifyjs", - "version": "0.4.3", + "version": "1.0.0", "description": "Uglify JS files with glob pattern.", "main": "dist/main.js", - "types": "dist/main.d.ts", + "types": "@types/main.d.ts", "scripts": { "build": "tsc -p .", "watch": "tsc -p . -w", @@ -21,6 +21,7 @@ ], "files": [ "dist", + "@types", "**/*.md" ], "author": "Giedrius Grabauskas (https://github.com/GiedriusGrabauskas)", @@ -32,15 +33,20 @@ "node": ">=6.0.0" }, "devDependencies": { + "@types/glob": "^5.0.30", + "@types/mkdirp": "^0.3.29", + "@types/mz": "0.0.31", + "@types/yargs": "6.6.0", + "fs-extra": "^2.1.2", "typescript": "^2.2.2" }, "dependencies": { + "@types/uglify-js": "^2.6.28", "glob": "^7.1.1", + "mkdirp": "^0.5.1", + "mz": "^2.6.0", "uglify-js": "^2.8.21", - "yargs": "^7.0.2", - "@types/glob": "^5.0.30", - "@types/uglify-js": "^2.6.28", - "@types/yargs": "6.6.0" + "yargs": "^7.0.2" }, "bin": { "glob-uglifyjs": "./dist/cli.js" diff --git a/src/arguments.ts b/src/arguments.ts index 3ad0094..691425d 100644 --- a/src/arguments.ts +++ b/src/arguments.ts @@ -1,9 +1,10 @@ import * as yargs from "yargs"; -import { Options } from "./options"; +import { OptionsDto } from "./options"; export interface Arguments { pattern: string; - options: Options; + options: OptionsDto; + uglifyProcessLimit: number; } @@ -25,6 +26,13 @@ export default yargs type: "string" }) .require("pattern", "Pattern required") + .option("uglifyProcessLimit", { + describe: "Uglify process limit", + type: "number" + }) + .default({ + "uglifyProcessLimit": 3 + }) .config("config") .alias("c", "config") .default("config", "glob-uglifyjs.config.json") diff --git a/src/cli.ts b/src/cli.ts index 1b45f8d..be1fa34 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,4 +2,13 @@ import Arguments from "./arguments"; import { GlobsUglifyJs } from "./main"; -new GlobsUglifyJs(Arguments.pattern, Arguments.options || {}); +async function CliStarter() { + if (Arguments.uglifyProcessLimit <= 0) { + throw new Error("Uglify process limit must be at least 1."); + } + + const globUglifier = new GlobsUglifyJs(Arguments.pattern, Arguments.options || {}); + await globUglifier.Uglify(Arguments.uglifyProcessLimit); +} + +CliStarter(); diff --git a/src/main.ts b/src/main.ts index 61e492f..9a4fcc8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,324 +1,250 @@ import * as uglifyjs from "uglify-js"; import * as glob from "glob"; import * as path from "path"; -import OptionsConstructor, { Options } from "./options"; -import * as fs from "fs"; -import RejectionError from "./rejection-error"; +import { Options, OptionsDto } from "./options"; +import * as fs from "mz/fs"; +import { RejectionError } from "./rejection-error"; + +import * as Directories from "./utils/directories"; const JS_EXTENSION = ".js"; const MINIFY_EXTENSION_PREFIX = ".min"; - - -class RecursiveUglifyResults { - private succeed = 0; - private failed = 0; - - get Succeed() { - return this.succeed; - } - - get Failed() { - return this.failed; - } - - OnSucceed() { - this.succeed++; - } - - OnFailed() { - this.failed++; - } - +enum Status { + Init, + Pending, + Completed, + Failed } - - export class GlobsUglifyJs { - private globPattern: string; - - private options: OptionsConstructor; - - constructor(globPattern: string, options?: Options) { - this.options = new OptionsConstructor(options); - if (!this.validateExtension(globPattern)) { - globPattern = this.addJsExtension(globPattern); + constructor(globPattern: string, options?: OptionsDto) { + this.options = new Options(options); + const globExt = path.extname(globPattern); + if (!globExt) { + globPattern += JS_EXTENSION; + } else if (globExt === ".") { + globPattern += JS_EXTENSION.slice(1); + } else if (!this.options.Silence) { + console.log(`Using custom '${globExt}' extension.`); } - this.globPattern = path.join(this.options.RootDir, globPattern); - - this.main(); - } - - private async main() { - - let globOptions: glob.IOptions | undefined; if (this.options.Exclude !== undefined) { - globOptions = { ignore: this.options.Exclude }; + this.globOptions = { ignore: this.options.Exclude }; } - try { + this.globPattern = path.join(this.options.RootDir, globPattern); + } - let filesList = await this.getGlobs(this.globPattern, globOptions); + private globOptions: glob.IOptions | undefined; - if (filesList.length === 0) { - console.log("No files found."); - return; - } + private globPattern: string; - let results = await this.startRecursiveUglify(filesList.slice(0)); + private options: Options; - if (this.options.RemoveSource) { - await this.deleteFiles(filesList.slice(0)); - await this.deleteEmptyDirectories(this.options.RootDir); - } + private filesList: string[] | undefined; - if (results.Failed > 0) { - console.warn(`Failed to minify ${results.Failed} file${(results.Failed > 1) ? "s" : ""}.`); - } - if (results.Succeed > 0) { - console.log(`Successfully minified ${results.Succeed} file${(results.Succeed > 1) ? "s" : ""}.`); - } + private filesDetails: Array<{ Index: number; Status: Status }> | undefined; + + private uglified: boolean = false; + public async GetFilesList(): Promise { + if (this.filesList != null) { + return this.filesList; + } + let filesList: string[]; + try { + filesList = await this.getGlobFilesList(this.globPattern, this.globOptions); } catch (error) { - if (error instanceof RejectionError) { - error.ThrowError(); - } else { - throw error; + if (this.options.Debug && !this.options.Silence) { + console.error(error); } + throw new Error(`Failed to find files by specified glob '${this.globPattern}'.`); } + if (!this.options.Silence) { + console.log(`Found ${filesList.length} files with glob pattern '${this.globPattern}'.`); + } + this.filesList = filesList; + this.filesDetails = this.filesList.map((value, index) => { + return { Index: index, Status: Status.Init }; + }); + return this.filesList; } - private async deleteFiles(fileList: Array) { - return new Promise(async (resolve, reject) => { - let rejected = false; - let file = fileList.shift(); + public async Uglify(processLimit: number = 3): Promise { + if (this.uglified && !this.options.Silence) { + console.warn("Files already uglified."); + return; + } + let filesList: string[]; + if (this.filesList == null) { + filesList = await this.GetFilesList(); + } else { + filesList = this.filesList; + } - if (file == null) { - resolve(); - return; - } + if (filesList.length === 0 && !this.options.Silence) { + console.warn(`No files found matching specified glob pattern (${this.globPattern}).`); + return; + } - await this.deleteFile(file) - .catch(error => { - rejected = true; - reject(new RejectionError(error, "deleteFile")); - }); + await this.handleStarter(processLimit); - if (!rejected) { - await this.deleteFiles(fileList); - resolve(); - } - }); - } - - private async deleteFile(filePath: string) { - return new Promise((resolve, reject) => { - fs.unlink(filePath, error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } + const results = { + Success: this.filesDetails!.filter(x => x.Status === Status.Completed).map(x => filesList[x.Index]), + Failed: this.filesDetails!.filter(x => x.Status === Status.Failed).map(x => filesList[x.Index]) + }; - private async uglifyFile(file: string, options?: uglifyjs.MinifyOptions) { - return new Promise((resolve, reject) => { + if (this.options.RemoveSource) { try { - let outputData = uglifyjs.minify(file, options); - resolve(outputData); + await this.removeSources(results.Success); } catch (error) { - reject(error); + this.handleError(error); } - }); - } + } - private async startRecursiveUglify(filesList: Array, results?: RecursiveUglifyResults) { - return new Promise(async (resolve) => { - if (results == null) { - results = new RecursiveUglifyResults(); + if (!this.options.Silence) { + if (results.Failed.length > 0) { + console.warn(`Failed to minify ${results.Failed.length} file${(results.Failed.length > 1) ? "s" : ""}.`); } - let file = filesList.shift(); - if (file != null) { - try { - await this.recursiveUglify(file); - results.OnSucceed(); - } catch (error) { - if (error instanceof RejectionError) { - error.LogError(this.options.Debug); - } else if (this.options.Debug) { - console.error(error); - } - results.OnFailed(); - } - resolve(await this.startRecursiveUglify(filesList, results)); - } else { - resolve(results); + if (results.Success.length > 0) { + console.log(`Successfully minified ${results.Success.length} file${(results.Success.length > 1) ? "s" : ""}.`); } - }); + } } - private async recursiveUglify(file: string) { - return new Promise(async (resolve, reject) => { - try { - let outputData = await this.uglifyFile(file, this.options.MinifyOptions) - .catch(error => { - throw new RejectionError(error, "uglifyFile", file); - }) as uglifyjs.MinifyOutput; + private handlerInfo: { + Resolve: () => void; + Reject: () => void; + ProcessLimit: number; + ActiveProcess: number; + }; + + private handleStarter(processLimit: number) { + return new Promise((resolve, reject) => { + this.handlerInfo = { + Reject: reject, + Resolve: resolve, + ProcessLimit: processLimit, + ActiveProcess: 0 + }; + this.tryToStartHandle(); + }); + } - let outPath = this.resolveOutFilePath(file); + private tryToStartHandle() { + if (this.handlerInfo.ActiveProcess >= this.handlerInfo.ProcessLimit) { + return; + } - await this.ensureDirectoryExistence(outPath) - .catch(error => { - throw new RejectionError(error, "ensureDirectoryExistence", file); - }); + if (this.filesDetails == null) { + return; + } - await this.writeToFile(outPath, outputData.code) - .catch(error => { - throw new RejectionError(error, "writeToFile", file); - }); + const index = this.filesDetails.findIndex(x => x.Status === Status.Init); + if (index === -1) { + return; + } - resolve(); + this.handlerInfo.ActiveProcess++; + this.startHandlingFile(index); + this.tryToStartHandle(); + } - } catch (error) { - reject(error); - } - }); + private onFileHandled() { + this.handlerInfo.ActiveProcess--; + if (this.filesDetails == null) { + return; + } + const completedList = this.filesDetails.filter(x => x.Status === Status.Completed || x.Status === Status.Failed); + if (completedList.length !== this.filesDetails.length) { + this.tryToStartHandle(); + return; + } + this.uglified = true; + this.handlerInfo.Resolve(); } - private async ensureDirectoryExistence(filePath: string) { - return new Promise(async (resolve, reject) => { - let rejected = false; - let dirname = path.dirname(filePath); - let directoryExist = await this.directoryExists(dirname); - if (directoryExist) { - resolve(); - return; - } - await this.ensureDirectoryExistence(dirname) - .catch(error => { - reject(error); - rejected = true; - }); - - if (rejected) { - return; - } + private async startHandlingFile(index: number) { + if (this.filesDetails == null || this.filesList == null) { + this.onFileHandled(); + return; + } - fs.mkdir(dirname, error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } + const file = this.filesList[this.filesDetails[index].Index]; + if (file == null) { + this.onFileHandled(); + return; + } - private async directoryExists(path: string) { - return new Promise(resolve => { - fs.stat(path, (error, stats) => { - if (error) { - resolve(false); - } else { - resolve(stats.isDirectory()); - } - }); - }); - } + try { + this.filesDetails[index].Status = Status.Pending; + await this.uglifyItem(file); + this.filesDetails[index].Status = Status.Completed; + } catch (error) { + this.filesDetails[index].Status = Status.Failed; + this.handleError(error); + } - private async deleteEmptyDirectories(directoryPath: string) { - return new Promise(async (resolve, reject) => { - let rejected = false; - let isExist = await this.directoryExists(directoryPath); - if (!isExist) { - resolve(); // or reject? - return; - } + this.onFileHandled(); + } - let files = await this.readFilesInDirectory(directoryPath); - - if (files.length > 0) { - for (let i = 0; i < files.length; i++) { - let file = files[i]; - var fullPath = path.join(directoryPath, file); - await this.deleteEmptyDirectories(fullPath) - .catch(error => { - reject(new RejectionError(error, "deleteEmptyDirectories")); - rejected = true; - }); - if (rejected) { - break; - } - } - if (rejected) { - return; - } - files = await this.readFilesInDirectory(directoryPath); + private handleError(error: any) { + if (!this.options.Silence) { + if (error instanceof RejectionError) { + error.LogError(this.options.Debug); + } else if (this.options.Debug && !this.options.Silence) { + console.error(error); } + } + } - if (files.length === 0) { - await this.removeDirectory(directoryPath) - .catch(error => { - reject(new RejectionError(error, "removeDirectory")); - rejected = true; - }); + private async removeSources(successFiles: string[]) { + for (const file of successFiles) { + try { + await fs.unlink(file); + } catch (error) { + throw new RejectionError(error, "deleteFile"); } + } + try { + await Directories.RemoveEmptyDirectories(this.options.RootDir); + } catch (error) { + throw new RejectionError(error, "deleteEmptyDirectories"); + } + } - if (!rejected) { - resolve(); - } - }); - } + private async uglifyItem(file: string): Promise { + let outputData: uglifyjs.MinifyOutput; + try { + outputData = await this.uglifyFile(file, this.options.MinifyOptions) + } catch (error) { + throw new RejectionError(error, "uglifyFile", file); + } - private async removeDirectory(directoryPath: string) { - return new Promise((resolve, reject) => { - fs.rmdir(directoryPath, error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } + const outPath = this.buildOutFilePath(file); - private async readFilesInDirectory(directoryPath: string) { - return new Promise>((resolve, reject) => { - fs.readdir(directoryPath, (error, files) => { - if (error) { - reject(error); - } else { - resolve(files); - } - }); - }); - } + try { + await Directories.MakeTree(outPath); + } catch (error) { + throw new RejectionError(error, "ensureDirectoryExistence", file); + } - /** - * Check if parsed name without extension has minified extension prefix. - * - * @private - * @param {string} nameWithoutExt Parsed file name without extension. - * @returns - * - * @memberOf GlobsUglifyJs - */ - private hasMinifiedExt(nameWithoutExt: string) { - let ext = path.extname(nameWithoutExt); - return (ext != null && ext === MINIFY_EXTENSION_PREFIX); + try { + await fs.writeFile(outPath, outputData.code, { encoding: "utf-8", flag: "w" }); + } catch (error) { + throw new RejectionError(error, "writeToFile", file); + } } - private resolveOutFilePath(filePath: string) { - let parsedPath = path.parse(filePath), - targetExt = parsedPath.ext; + private buildOutFilePath(filePath: string): string { + const parsedPath = path.parse(filePath); - if (this.options.UseMinExt && !this.hasMinifiedExt(parsedPath.name)) { + let targetExt = parsedPath.ext; + if (this.options.UseMinExt && targetExt !== MINIFY_EXTENSION_PREFIX) { targetExt = MINIFY_EXTENSION_PREFIX + targetExt; } @@ -333,40 +259,25 @@ export class GlobsUglifyJs { }); } - /** - * Asynchronously write data to file with flag "wx". - * - * @private - * @param {string} filePath File path. - * @param {string} data Data in "utf-8". - * @returns - * - * @memberOf GlobsUglifyJs - */ - private async writeToFile(filePath: string, data: string) { - return new Promise((resolve, reject) => { - fs.writeFile(filePath, data, { encoding: "utf-8", flag: "w" }, (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); + private async uglifyFile(file: string, options?: uglifyjs.MinifyOptions): Promise { + return new Promise((resolve, reject) => { + try { + let outputData = uglifyjs.minify(file, options); + resolve(outputData); + } catch (error) { + reject(error); + } }); } /** * Asynchronously return files list by pattern. * - * @private * @param {string} pattern * @param {glob.IOptions} [options={}] - * @returns - * - * @memberOf GlobsUglifyJs */ - private async getGlobs(pattern: string, options: glob.IOptions = {}) { - return new Promise>((resolve, reject) => { + private async getGlobFilesList(pattern: string, options: glob.IOptions = {}): Promise { + return new Promise((resolve, reject) => { glob(pattern, options, (err, matches) => { if (err != null) { reject(err); @@ -374,38 +285,6 @@ export class GlobsUglifyJs { resolve(matches); } }); - }); } - - /** - * Validate JS extension. - * - * @private - * @param {string} pattern - * @returns {boolean} True if extension exist. - * - * @memberOf GlobsUglifyJs - */ - private validateExtension(pattern: string): boolean { - let ext = path.extname(pattern); - if (ext.length !== 0 && ext !== JS_EXTENSION) { - console.warn("Using custom extension: ", ext); - } - return (ext != null && ext.length > 0); - } - - /** - * Add .js to glob pattern. - * - * @private - * @param {string} pattern - * @returns {string} - * - * @memberOf GlobsUglifyJs - */ - private addJsExtension(pattern: string): string { - return pattern + JS_EXTENSION; - } - } diff --git a/src/options.ts b/src/options.ts index f4faaae..2e588f7 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,7 +1,7 @@ import * as uglifyjs from "uglify-js"; import * as process from "process"; -export interface Options { +export interface OptionsDto { [key: string]: any; UseMinExt?: boolean; MinifyOptions?: uglifyjs.MinifyOptions; @@ -10,12 +10,14 @@ export interface Options { RootDir?: string; RemoveSource?: boolean; Debug?: boolean; - exclude?: Array | string; + Exclude?: Array | string; + Silence?: boolean; } -export default class OptionsConstructor implements Options { - constructor(importData?: Options) { +export class Options implements OptionsDto { + + constructor(importData?: OptionsDto) { if (importData != null) { if (importData.Cwd != null) { if (importData.Cwd.length > 0) { @@ -27,13 +29,18 @@ export default class OptionsConstructor implements Options { Object.keys(this.options).forEach(key => { if (importData[key] !== undefined) { - this.options[key] = importData[key]; + // Deprecated: now use Exclude key. + if (key === "exclude") { + this.options["Exclude"] = importData[key]; + } else { + this.options[key] = importData[key]; + } } }); } } - private options: Options = { + private options: OptionsDto = { MinifyOptions: {}, UseMinExt: true, OutDir: "", @@ -41,9 +48,14 @@ export default class OptionsConstructor implements Options { RootDir: "", RemoveSource: false, Debug: false, - exclude: undefined + Exclude: undefined, + Silence: false }; + public ToObject(): OptionsDto { + return this.options; + } + public get UseMinExt(): boolean { return this.options.UseMinExt!; } @@ -73,7 +85,11 @@ export default class OptionsConstructor implements Options { } public get Exclude(): Array | string | undefined { - return this.options.exclude; + return this.options.Exclude; + } + + public get Silence(): boolean { + return this.options.Silence!; } } diff --git a/src/rejection-error.ts b/src/rejection-error.ts index c9a3ae0..3540d43 100644 --- a/src/rejection-error.ts +++ b/src/rejection-error.ts @@ -1,5 +1,5 @@ -export default class RejectionError { +export class RejectionError { constructor(private error: NodeJS.ErrnoException | Error, private type?: string, private uniqId?: string) { } diff --git a/src/utils/directories.ts b/src/utils/directories.ts new file mode 100644 index 0000000..5123b6a --- /dev/null +++ b/src/utils/directories.ts @@ -0,0 +1,43 @@ +import * as path from "path"; +import * as fs from "mz/fs"; +import * as mkdirp from "mkdirp"; + +export async function Exists(path: string) { + try { + const stat = await fs.stat(path); + return stat.isDirectory(); + } catch (error) { + return false; + } +} + +export async function RemoveEmptyDirectories(directoryPath: string) { + const isExist = await Exists(directoryPath); + if (!isExist) { + return; + } + let files = await fs.readdir(directoryPath); + if (files.length > 0) { + for (const file of files) { + const fullPath = path.join(directoryPath, file); + await RemoveEmptyDirectories(fullPath); + } + files = await fs.readdir(directoryPath); + } + if (files.length === 0) { + await fs.rmdir(directoryPath); + } +} + +export async function MakeTree(filePath: string): Promise { + return new Promise((resolve, reject) => { + const dirname = path.dirname(filePath); + mkdirp(dirname, (error, data) => { + if (error) { + reject(error); + return; + } + resolve(data); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index f79ffea..ae88592 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,9 @@ "noImplicitUseStrict": true, "strictNullChecks": true, "skipDefaultLibCheck": true, + "skipLibCheck": true, "declaration": true, + "declarationDir": "@types", "jsx": "react", "typeRoots": [ "node_modules/@types" @@ -25,6 +27,7 @@ "exclude": [ "node_modules", "dist", - "@types" + "@types", + "src/**/_*.ts*" ] } \ No newline at end of file