123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- 'use strict'
- const { promisify } = require('util')
- const mm = require('minimatch')
- const Glob = require('glob').Glob
- const fs = require('graceful-fs')
- const statAsync = promisify(fs.stat.bind(fs))
- const pathLib = require('path')
- const _ = require('lodash')
- const File = require('./file')
- const Url = require('./url')
- const helper = require('./helper')
- const log = require('./logger').create('filelist')
- const createPatternObject = require('./config').createPatternObject
- class FileList {
- constructor (patterns, excludes, emitter, preprocess, autoWatchBatchDelay) {
- this._patterns = patterns || []
- this._excludes = excludes || []
- this._emitter = emitter
- this._preprocess = preprocess
- this.buckets = new Map()
- // A promise that is pending if and only if we are active in this.refresh_()
- this._refreshing = null
- const emit = () => {
- this._emitter.emit('file_list_modified', this.files)
- }
- const debouncedEmit = _.debounce(emit, autoWatchBatchDelay)
- this._emitModified = (immediate) => {
- immediate ? emit() : debouncedEmit()
- }
- }
- _findExcluded (path) {
- return this._excludes.find((pattern) => mm(path, pattern))
- }
- _findIncluded (path) {
- return this._patterns.find((pattern) => mm(path, pattern.pattern))
- }
- _findFile (path, pattern) {
- if (!path || !pattern) return
- return this._getFilesByPattern(pattern.pattern).find((file) => file.originalPath === path)
- }
- _exists (path) {
- return !!this._patterns.find((pattern) => mm(path, pattern.pattern) && this._findFile(path, pattern))
- }
- _getFilesByPattern (pattern) {
- return this.buckets.get(pattern) || []
- }
- _refresh () {
- const matchedFiles = new Set()
- let lastCompletedRefresh = this._refreshing
- lastCompletedRefresh = Promise.all(
- this._patterns.map(async ({ pattern, type, nocache, isBinary }) => {
- if (helper.isUrlAbsolute(pattern)) {
- this.buckets.set(pattern, [new Url(pattern, type)])
- return
- }
- const mg = new Glob(pathLib.normalize(pattern), { cwd: '/', follow: true, nodir: true, sync: true })
- const files = mg.found
- .filter((path) => {
- if (this._findExcluded(path)) {
- log.debug(`Excluded file "${path}"`)
- return false
- } else if (matchedFiles.has(path)) {
- return false
- } else {
- matchedFiles.add(path)
- return true
- }
- })
- .map((path) => new File(path, mg.statCache[path].mtime, nocache, type, isBinary))
- if (nocache) {
- log.debug(`Not preprocessing "${pattern}" due to nocache`)
- } else {
- await Promise.all(files.map((file) => this._preprocess(file)))
- }
- this.buckets.set(pattern, files)
- if (_.isEmpty(mg.found)) {
- log.warn(`Pattern "${pattern}" does not match any file.`)
- } else if (_.isEmpty(files)) {
- log.warn(`All files matched by "${pattern}" were excluded or matched by prior matchers.`)
- }
- })
- )
- .then(() => {
- // When we return from this function the file processing chain will be
- // complete. In the case of two fast refresh() calls, the second call
- // will overwrite this._refreshing, and we want the status to reflect
- // the second call and skip the modification event from the first call.
- if (this._refreshing !== lastCompletedRefresh) {
- return this._refreshing
- }
- this._emitModified(true)
- return this.files
- })
- return lastCompletedRefresh
- }
- get files () {
- const served = []
- const included = {}
- const lookup = {}
- this._patterns.forEach((p) => {
- // This needs to be here sadly, as plugins are modifiying
- // the _patterns directly resulting in elements not being
- // instantiated properly
- if (p.constructor.name !== 'Pattern') {
- p = createPatternObject(p)
- }
- const files = this._getFilesByPattern(p.pattern)
- files.sort((a, b) => {
- if (a.path > b.path) return 1
- if (a.path < b.path) return -1
- return 0
- })
- if (p.served) {
- served.push(...files)
- }
- files.forEach((file) => {
- if (lookup[file.path] && lookup[file.path].compare(p) < 0) return
- lookup[file.path] = p
- if (p.included) {
- included[file.path] = file
- } else {
- delete included[file.path]
- }
- })
- })
- return {
- served: _.uniq(served, 'path'),
- included: _.values(included)
- }
- }
- refresh () {
- this._refreshing = this._refresh()
- return this._refreshing
- }
- reload (patterns, excludes) {
- this._patterns = patterns || []
- this._excludes = excludes || []
- return this.refresh()
- }
- async addFile (path) {
- const excluded = this._findExcluded(path)
- if (excluded) {
- log.debug(`Add file "${path}" ignored. Excluded by "${excluded}".`)
- return this.files
- }
- const pattern = this._findIncluded(path)
- if (!pattern) {
- log.debug(`Add file "${path}" ignored. Does not match any pattern.`)
- return this.files
- }
- if (this._exists(path)) {
- log.debug(`Add file "${path}" ignored. Already in the list.`)
- return this.files
- }
- const file = new File(path)
- this._getFilesByPattern(pattern.pattern).push(file)
- const [stat] = await Promise.all([statAsync(path), this._refreshing])
- file.mtime = stat.mtime
- await this._preprocess(file)
- log.info(`Added file "${path}".`)
- this._emitModified()
- return this.files
- }
- async changeFile (path, force) {
- const pattern = this._findIncluded(path)
- const file = this._findFile(path, pattern)
- if (!file) {
- log.debug(`Changed file "${path}" ignored. Does not match any file in the list.`)
- return this.files
- }
- const [stat] = await Promise.all([statAsync(path), this._refreshing])
- if (force || stat.mtime > file.mtime) {
- file.mtime = stat.mtime
- await this._preprocess(file)
- log.info(`Changed file "${path}".`)
- this._emitModified(force)
- }
- return this.files
- }
- async removeFile (path) {
- const pattern = this._findIncluded(path)
- const file = this._findFile(path, pattern)
- if (file) {
- helper.arrayRemove(this._getFilesByPattern(pattern.pattern), file)
- log.info(`Removed file "${path}".`)
- this._emitModified()
- } else {
- log.debug(`Removed file "${path}" ignored. Does not match any file in the list.`)
- }
- return this.files
- }
- }
- FileList.factory = function (config, emitter, preprocess) {
- return new FileList(config.files, config.exclude, emitter, preprocess, config.autoWatchBatchDelay)
- }
- FileList.factory.$inject = ['config', 'emitter', 'preprocess']
- module.exports = FileList
|