UNPKG

alm

Version:

The best IDE for TypeScript

337 lines (293 loc) 12.1 kB
import * as sw from "../../utils/simpleWorker"; import * as contract from "./fileListingContract"; import * as fs from "fs"; import * as fsu from "../../utils/fsu"; import * as utils from "../../../common/utils"; import * as glob from "glob"; import chokidar = require('chokidar'); import { throttle } from "../../../common/utils"; import path = require('path'); import { TypedEvent } from "../../../common/events"; import * as types from "../../../common/types"; import * as chalk from "chalk"; const maxFileCount = 100000; /** A Map for faster live calculations */ type LiveList = { [filePath: string]: types.FilePathType }; /** The directory to watch */ let directoryUnderWatch: string; namespace Worker { export const echo: typeof contract.worker.echo = (q) => { return master.increment(q).then((res) => { return { text: q.text, num: res.num }; }); } export const setupWatch: typeof contract.worker.setupWatch = (q) => { directoryUnderWatch = q.directory; let completed = false; let liveList: LiveList = {}; // Effectively a list of the mutations that `liveList` is going through after initial sync let bufferedAdded: types.FilePath[] = []; let bufferedRemoved: types.FilePath[] = []; const filterName = (filePath: string) => { return ( // Remove .git we have no use for that here !filePath.endsWith('.git') && !filePath.includes('/.git/') // MAC && !filePath.endsWith('.DS_Store') ); } // Utility to send new file list const sendNewFileList = () => { let filePaths = Object.keys(liveList) .filter(filterName) // sort .sort((a, b) => { // sub dir wins! if (b.startsWith(a)) { return -1; } if (a.startsWith(b)) { return 1; } // The next sorts are slow and only done after initial listing! if (!completed) { return a.length - b.length; } // sort by name return a.toLowerCase().localeCompare(b.toLowerCase()); }) // Convert ot file path type .map(filePath => { let type = liveList[filePath]; return { filePath, type }; }); master.fileListUpdated({ filePaths, completed }); // Send out the delta as well // Unless of course this is the *initial* sending of file listing if (bufferedAdded.length || bufferedRemoved.length) { master.fileListingDelta({ addedFilePaths: bufferedAdded.filter(x => filterName(x.filePath)), removedFilePaths: bufferedRemoved.filter(x => filterName(x.filePath)) }); bufferedAdded = []; bufferedRemoved = []; } }; /** * Slower version for * - initial partial serach * - later updates which might be called a lot because of some directory of files removed */ let sendNewFileListThrottled = throttle(sendNewFileList, 1500); /** * Utility function to get the listing from a directory * No side effects in this function */ const getListing = (dirPath: string): Promise<types.FilePath[]> => { return new Promise<types.FilePath[]>((resolve) => { let mg = new glob.Glob('**', { cwd: dirPath, dot: true }, (e, globResult) => { if (e) { console.error('Globbing error:', e); } let list = globResult.map(nl => { let p = fsu.resolve(dirPath, nl); let type = mg.cache[p] && mg.cache[p] == 'FILE' ? types.FilePathType.File : types.FilePathType.Dir; return { filePath: fsu.consistentPath(p), type, } }); resolve(list); }); }); } // create initial list using 10x faster glob.Glob! (function() { /** These things are coming on a mac for some reason */ const ignoreThisPathThatGlobGivesForUnknownReasons = (filePath: string) => { return filePath.includes('0.0.0.0') || (filePath.includes('[object Object]')) } const cwd = q.directory; const mg = new glob.Glob('**', { cwd, dot: true }, (e, newList) => { if (e) { checkGlobbingError(e); if (abortGlobbing) { mg.abort(); // if we don't exit then glob keeps globbing + erroring despite mg.abort() process.exit(); return; } console.error('Globbing error:', e); } let list = newList.map(nl => { let p = fsu.resolve(cwd, nl); // NOTE: the glob cache also uses consistent path even on windows, hence `fsu.resolve` ^ :) let type = mg.cache[p] && mg.cache[p] == 'FILE' ? types.FilePathType.File : types.FilePathType.Dir; if (ignoreThisPathThatGlobGivesForUnknownReasons(nl)) { // console.log(nl, mg.cache[p]); /// DEBUG return null; } return { filePath: fsu.consistentPath(p), type, } }).filter(x => !!x); // Initial search complete! completed = true; list.forEach(entry => liveList[entry.filePath] = entry.type); sendNewFileList(); }); let matchLength = 0 /** Still send the listing while globbing so user gets immediate feedback */ mg.on('match', (match) => { matchLength++; if (matchLength > maxFileCount) { abortDueToTooManyFiles(); return; } let p = fsu.resolve(cwd, match); if (mg.cache[p]) { if (ignoreThisPathThatGlobGivesForUnknownReasons(match)) { return; } liveList[fsu.consistentPath(p)] = mg.cache[p] == 'FILE' ? types.FilePathType.File : types.FilePathType.Dir; sendNewFileListThrottled(); } }); })(); function fileAdded(filePath: string) { filePath = fsu.consistentPath(filePath); // Only send if we don't know about this already (because of faster initial scan) if (!liveList[filePath]) { let type = types.FilePathType.File; liveList[filePath] = type; bufferedAdded.push({ filePath, type }); sendNewFileListThrottled(); } } function dirAdded(dirPath: string) { dirPath = fsu.consistentPath(dirPath); liveList[dirPath] = types.FilePathType.Dir; bufferedAdded.push({ filePath: dirPath, type: types.FilePathType.Dir }); /** * - glob the folder * - send the folder throttled */ getListing(dirPath).then(res => { res.forEach(fpDetails => { if (!liveList[fpDetails.filePath]) { let type = fpDetails.type liveList[fpDetails.filePath] = type; bufferedAdded.push({ filePath: fpDetails.filePath, type }); } }); sendNewFileListThrottled(); }).catch(res => { console.error('[FLW] DirPath listing failed:', dirPath, res); }); } function fileDeleted(filePath: string) { filePath = fsu.consistentPath(filePath); delete liveList[filePath]; bufferedRemoved.push({ filePath, type: types.FilePathType.File }); sendNewFileListThrottled(); } function dirDeleted(dirPath: string) { dirPath = fsu.consistentPath(dirPath); Object.keys(liveList).forEach(filePath => { if (filePath.startsWith(dirPath)) { bufferedRemoved.push({ filePath, type: liveList[filePath] }); delete liveList[filePath]; } }); sendNewFileListThrottled(); } /** Create watcher */ let watcher = chokidar.watch(directoryUnderWatch, { /** Don't care about initial as we did that using glob as its faster */ ignoreInitial: true, // For fixing file permission errors on windows. // Recommended here : https://github.com/paulmillr/chokidar/issues/446 // Someday. // Not enabled because the CPU useage goes *way* up. // /** // * Use polling, otherwise other files get locked // * e.g. `npm install foo` will fail sadly on windows // */ // usePolling: true, // /** // * Because we have `usePolling` the external process will most likely work // * However *we* might not be able to stat a file temporarily when its open // * e.g. *immediately* after an external `npm install` on windows we get a perm error. // */ // ignorePermissionErrors: true, }); // Just the ones that impact file listing // https://github.com/paulmillr/chokidar#methods--events watcher.on('add', fileAdded); watcher.on('addDir', dirAdded); watcher.on('unlink', fileDeleted); watcher.on('unlinkDir', dirDeleted); // Just for changes watcher.on('change', (filePath) => { // We have no use for this right now }); return Promise.resolve({}); } } // Ensure that the namespace follows the contract const _checkTypes: typeof contract.worker = Worker; // run worker export const { master } = sw.runWorker({ workerImplementation: Worker, masterContract: contract.master }); function debug(...args) { console.error.apply(console, args); } /** * If its a permission error warn the user that they should start in some project directory */ let abortGlobbing = false; function checkGlobbingError(err: any) { if (err.path && (err.code == 'EPERM' /*win*/ || err.code == 'EACCES' /*mac*/) ) { abortGlobbing = true; const errorMessage = `Exiting: Permission error when trying to list ${err.path}. - Start the IDE in a project folder (e.g. '/your/project') - Check access to the path` master.abort({ errorMessage }); } } function abortDueToTooManyFiles() { abortGlobbing = true; const errorMessage = `Exiting: Too many files in folder. Currently we limit to ${maxFileCount}. - Start the IDE in a project folder (e.g. '/your/project')`; master.abort({ errorMessage }); } process.on('error', () => { console.log('here'); process.exit(); })