watch-dependency-graph
Advanced tools
Comparing version 1.1.0 to 2.0.0-beta.1
412
index.js
@@ -1,45 +0,87 @@ | ||
const { EventEmitter } = require('events') | ||
const chokidar = require('chokidar') | ||
const matched = require('matched') | ||
const uniq = require('@arr/unique') | ||
const path = require('path') | ||
const filewatcher = require('filewatcher') | ||
const debug = require('debug')('wdg') | ||
function walk ({ | ||
ids, | ||
register, | ||
entryPointer, | ||
currentPointer, | ||
childrenOfCurrent, | ||
nextChildren, | ||
visited = [] | ||
}) { | ||
for (const { id, children: childs } of nextChildren) { | ||
// push to all files | ||
if (!ids.includes(id)) ids.push(id) | ||
function clearUp (ids, tree, parentPointers) { | ||
for (const p of parentPointers) { | ||
const id = ids[p] | ||
const pointer = ids.indexOf(id) | ||
delete require.cache[id] | ||
// push to previous parent's children | ||
if (!childrenOfCurrent.includes(pointer)) childrenOfCurrent.push(pointer) | ||
clearUp(ids, tree, tree[id].parentPointers) | ||
} | ||
} | ||
// set module values | ||
if (!register[id]) | ||
register[id] = { pointer, entries: [], children: [], parents: [] } // setup | ||
if (!register[id].entries.includes(entryPointer)) | ||
register[id].entries.push(entryPointer) // set entries | ||
if (!register[id].parents.includes(currentPointer)) | ||
register[id].parents.push(currentPointer) // set entries | ||
function loadEntries (entries, { cwd }) { | ||
const files = entries.map(entry => path.resolve(cwd, entry)) | ||
// recurse, but only if we haven't walked these children yet | ||
if (childs.length && !visited.includes(id)) { | ||
visited.push(id) | ||
files.forEach(require) // load modules | ||
walk({ | ||
const mostRecentChildren = [] | ||
/** | ||
* children[] keeps growing, so we need to grab the latest | ||
* modules that match the entries | ||
* | ||
* reverse the children, pick the first that match | ||
*/ | ||
for (const c of module.children.reverse()) { | ||
if (files.includes(c.id)) mostRecentChildren.push(c) | ||
} | ||
return mostRecentChildren | ||
} | ||
function walk (modules, context) { | ||
const { ids, tree, visitedIds = [], entryPointer, parentPointer } = context | ||
for (const mod of modules) { | ||
if (!ids.includes(mod.id)) ids.push(mod.id) | ||
const selfPointer = ids.indexOf(mod.id) | ||
// setup | ||
if (!tree[mod.id]) { | ||
tree[mod.id] = { | ||
pointer: selfPointer, | ||
entryPointers: [], | ||
parentPointers: [], | ||
childrenPointers: [] | ||
} | ||
} | ||
const leaf = tree[mod.id] | ||
if (entryPointer === undefined) { | ||
// must be an entry itself | ||
leaf.entryPointers = [selfPointer] | ||
} else if ( | ||
entryPointer !== undefined && | ||
!leaf.entryPointers.includes(entryPointer) | ||
) { | ||
leaf.entryPointers.push(entryPointer) | ||
} | ||
if ( | ||
parentPointer !== undefined && | ||
!leaf.parentPointers.includes(parentPointer) | ||
) { | ||
leaf.parentPointers.push(parentPointer) | ||
} | ||
const parentLeaf = tree[ids[parentPointer]] | ||
if (parentLeaf && !parentLeaf.childrenPointers.includes(selfPointer)) { | ||
parentLeaf.childrenPointers.push(selfPointer) | ||
} | ||
if (mod.children.length && !visitedIds.includes(mod.id)) { | ||
visitedIds.push(mod.id) | ||
walk(mod.children, { | ||
ids, | ||
register, | ||
entryPointer, | ||
currentPointer: pointer, | ||
childrenOfCurrent: register[id].children, | ||
nextChildren: childs, | ||
visited | ||
tree, | ||
visitedIds, | ||
entryPointer: entryPointer === undefined ? selfPointer : entryPointer, | ||
parentPointer: selfPointer | ||
}) | ||
@@ -50,179 +92,137 @@ } | ||
function clearParentTree ({ parentPointers, ids, register }) { | ||
for (const parentPointer of parentPointers) { | ||
const parentId = ids[parentPointer] | ||
function emitter () { | ||
let events = {} | ||
delete require.cache[parentId] | ||
clearParentTree({ | ||
parentPointers: register[parentId].parents, | ||
ids, | ||
register | ||
}) | ||
return { | ||
emit (ev, ...args) { | ||
return events[ev] ? events[ev].map(fn => fn(...args)) : [] | ||
}, | ||
on (ev, fn) { | ||
events[ev] = events[ev] ? events[ev].concat(fn) : [fn] | ||
return () => events[ev].slice(events[ev].indexOf(fn), 1) | ||
}, | ||
clear () { | ||
events = {} | ||
}, | ||
listeners (ev) { | ||
return events[ev] || [] | ||
} | ||
} | ||
} | ||
function getEntries (globs) { | ||
const files = uniq(globs.map(matched.sync).flat(2)) | ||
module.exports = function graph (options) { | ||
debug('initialized with', { options }) | ||
files.map(require) // load modules | ||
const { cwd = process.cwd() } = options | ||
return module.children.filter(({ id }) => files.includes(id)) | ||
} | ||
module.exports = function graph (...globbies) { | ||
const globs = globbies.flat(2) | ||
// once instance | ||
const events = new EventEmitter() | ||
const events = emitter() | ||
// all generated from factory | ||
let ids = [] | ||
let register = {} | ||
let tree = {} | ||
let watcher | ||
let modules = [] | ||
let entries = [] | ||
let watcher | ||
// kick it off | ||
;(function init () { | ||
function bootstrap () { | ||
ids = [] | ||
register = {} | ||
entries = getEntries(globs) | ||
tree = {} | ||
for (const { id, children } of entries) { | ||
ids.push(id) | ||
try { | ||
modules = loadEntries(entries, { cwd }) | ||
} catch (e) { | ||
events.emit('error', e) | ||
if (!events.listeners('error').length) console.error(e) | ||
} | ||
const entryPointer = ids.indexOf(id) // get pointer | ||
walk(modules, { | ||
ids, | ||
tree | ||
}) | ||
} | ||
register[id] = { | ||
pointer: entryPointer, | ||
entries: [entryPointer], // self-referential | ||
parents: [], | ||
children: [] | ||
} | ||
function cleanById (id) { | ||
const { pointer, parentPointers, childrenPointers } = tree[id] | ||
if (children) { | ||
walk({ | ||
ids, | ||
register, | ||
entryPointer, | ||
currentPointer: entryPointer, | ||
childrenOfCurrent: register[id].children, | ||
nextChildren: children | ||
}) | ||
} | ||
delete tree[id] | ||
for (const p of parentPointers) { | ||
const children = tree[ids[p]].childrenPointers | ||
children.splice(children.indexOf(pointer), 1) | ||
} | ||
watcher = chokidar.watch(globs.concat(ids), { ignoreInitial: true }) | ||
for (const p of childrenPointers) { | ||
const parents = tree[ids[p]].parentPointers | ||
parents.splice(parents.indexOf(pointer), 1) | ||
} | ||
watcher.on('all', async (e, f) => { | ||
debug('chokidar', e, f) | ||
ids.splice(pointer, 1) | ||
const fullEmittedFilepath = require.resolve(f) | ||
watcher.remove(id) | ||
} | ||
debug('fullEmittedFilepath', fullEmittedFilepath) | ||
/** | ||
* Diff and update watch | ||
*/ | ||
function restart () { | ||
const prevIds = ids | ||
if (e === 'add') { | ||
await watcher.close() | ||
events.emit('add', [fullEmittedFilepath]) | ||
init() | ||
// shouldn't ever happen | ||
} else if (e === 'unlink') { | ||
const removedModule = entries.find(e => e.id === f) | ||
// an *entry* was renamed or removed | ||
if (removedModule) { | ||
await watcher.close() | ||
events.emit('remove', [removedModule.id]) | ||
init() | ||
} else { | ||
watcher.unwatch(f) | ||
} | ||
} else if (e === 'change') { | ||
const { entries, parents } = register[fullEmittedFilepath] | ||
bootstrap() | ||
const prev = | ||
require.cache[fullEmittedFilepath] || require(fullEmittedFilepath) | ||
delete require.cache[fullEmittedFilepath] | ||
require(fullEmittedFilepath) | ||
const next = require.cache[fullEmittedFilepath] | ||
const nextIds = ids | ||
const addedIds = nextIds.filter(id => !prevIds.includes(id)) | ||
const removedIds = prevIds.filter(id => !nextIds.includes(id)) | ||
// diff prev/next | ||
const removedModuleIds = (prev.children || []) | ||
.filter(c => !(next.children || []).find(_c => _c.id === c.id)) | ||
.map(c => c.id) | ||
debug('diff', { addedIds, removedIds }) | ||
// add to watch instance | ||
next.children | ||
.filter(c => !(prev.children || []).find(_c => _c.id === c.id)) | ||
.forEach(c => watcher.add(c.id)) | ||
for (const id of addedIds) { | ||
watcher.add(id) | ||
} | ||
for (const removedModuleId of removedModuleIds) { | ||
let isModuleStillInUse = false | ||
const removedModulePointer = ids.indexOf(removedModuleId) | ||
for (const id of removedIds) { | ||
watcher.remove(id) | ||
} | ||
} | ||
for (const filepath of Object.keys(register)) { | ||
if (filepath === fullEmittedFilepath) { | ||
const localChildren = register[filepath].children | ||
const localPointer = localChildren.indexOf(removedModulePointer) | ||
function handleChange (file) { | ||
const { entryPointers } = tree[file] | ||
/* | ||
* for any entries of the file that changed, remove them from childen | ||
* of this file | ||
*/ | ||
for (const entryPointer of register[filepath].entries) { | ||
for (const localChildPointer of localChildren) { | ||
const localChildFile = ids[localChildPointer] | ||
const localChildEntries = register[localChildFile].entries | ||
localChildEntries.splice( | ||
localChildEntries.indexOf(entryPointer), | ||
1 | ||
) | ||
} | ||
} | ||
// bust cache for all involved files up the tree | ||
clearUp(ids, tree, [ids.indexOf(file)]) | ||
// clean up the children of this file last | ||
register[filepath].children.splice(localPointer, 1) | ||
} else { | ||
// don't accidentally reset back to false on another iteration | ||
if (isModuleStillInUse) continue | ||
events.emit( | ||
'change', | ||
entryPointers.map(p => ids[p]) | ||
) | ||
} | ||
isModuleStillInUse = register[filepath].children.includes( | ||
removedModulePointer | ||
) | ||
} | ||
} | ||
watcher = filewatcher() | ||
if (!isModuleStillInUse) { | ||
ids.splice(removedModulePointer, 1) | ||
delete register[removedModuleId] | ||
watcher.unwatch(removedModuleId) | ||
} | ||
} | ||
watcher.on('change', async (file, stat) => { | ||
if (stat.deleted) { | ||
debug('remove', file) | ||
// clear modules that require this module | ||
clearParentTree({ parentPointers: parents, ids, register }) | ||
const { pointer, entryPointers } = tree[file] | ||
for (const entryPointer of entries) { | ||
const fileId = ids[entryPointer] | ||
// is an entry itself | ||
if (entryPointers.includes(pointer)) { | ||
events.emit('remove', [ids[pointer]]) | ||
// clear entries so users can re-require | ||
delete require.cache[fileId] | ||
entries.splice(ids[pointer], 1) | ||
walk({ | ||
ids, | ||
register, | ||
entryPointer, | ||
currentPointer: entryPointer, | ||
childrenOfCurrent: register[fileId].children, | ||
nextChildren: next.children | ||
}) | ||
} | ||
events.emit( | ||
'update', | ||
entries.map(p => ids[p]) | ||
) | ||
restart() | ||
} else { | ||
handleChange(file) | ||
cleanById(file) | ||
} | ||
}) | ||
})() | ||
} else { | ||
debug('change', file) | ||
handleChange(file) | ||
restart() | ||
} | ||
}) | ||
return { | ||
@@ -232,16 +232,60 @@ get ids () { | ||
}, | ||
get register () { | ||
return register | ||
get tree () { | ||
return tree | ||
}, | ||
on (ev, fn) { | ||
events.on(ev, fn) | ||
return () => events.removeListener(ev, fn) | ||
return events.on(ev, fn) | ||
}, | ||
async close () { | ||
events.removeAllListeners('update') | ||
events.removeAllListeners('add') | ||
events.removeAllListeners('remove') | ||
return watcher.close() | ||
close () { | ||
events.clear() | ||
watcher.removeAll() | ||
watcher.removeAllListeners() | ||
}, | ||
add (files) { | ||
files = [].concat(files).filter(entry => { | ||
// filter out any already watched files | ||
if (entries.includes(entry)) return false | ||
const isAbs = path.isAbsolute(entry) | ||
if (!isAbs) { | ||
events.emit( | ||
'error', | ||
new Error( | ||
`Watched file must be an absolute path, you passed ${entry}. Ignoring...` | ||
) | ||
) | ||
} | ||
return isAbs | ||
}) | ||
entries.push(...files) | ||
restart() | ||
}, | ||
remove (files) { | ||
files = [].concat(files).filter(entry => { | ||
const isAbs = path.isAbsolute(entry) | ||
if (!isAbs) { | ||
events.emit( | ||
'error', | ||
new Error( | ||
`Files to remove must be absolute paths, you passed ${entry}. Ignoring...` | ||
) | ||
) | ||
} | ||
return isAbs | ||
}) | ||
events.emit('remove', files) | ||
for (const file of files) { | ||
if (entries.includes(file)) entries.splice(file, 1) | ||
restart() | ||
} | ||
} | ||
} | ||
} |
{ | ||
"name": "watch-dependency-graph", | ||
"version": "1.1.0", | ||
"version": "2.0.0-beta.1", | ||
"description": "", | ||
@@ -8,5 +8,4 @@ "main": "index.js", | ||
"test": "node -r esm test", | ||
"test:watch": "npm run fixtures && nodemon -r esm test -i ./fixtures", | ||
"format": "prettier-standard --format", | ||
"lint": "prettier-standard --lint" | ||
"test:watch": "nodemon -i ./test/fixtures -r esm test", | ||
"format": "prettier-standard --format" | ||
}, | ||
@@ -38,5 +37,4 @@ "husky": { | ||
"@arr/unique": "^1.0.1", | ||
"chokidar": "^3.4.2", | ||
"debug": "^4.2.0", | ||
"matched": "^5.0.0" | ||
"filewatcher": "^3.0.1" | ||
}, | ||
@@ -43,0 +41,0 @@ "devDependencies": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
20451
3
10
710
2
1
1
+ Addedfilewatcher@^3.0.1
+ Addeddebounce@1.2.1(transitive)
+ Addedfilewatcher@3.0.1(transitive)
- Removedchokidar@^3.4.2
- Removedmatched@^5.0.0
- Removedanymatch@3.1.3(transitive)
- Removedbalanced-match@1.0.2(transitive)
- Removedbinary-extensions@2.3.0(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedbraces@3.0.3(transitive)
- Removedchokidar@3.6.0(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removedfill-range@7.1.1(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedfsevents@2.3.3(transitive)
- Removedglob@7.2.3(transitive)
- Removedglob-parent@5.1.2(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinherits@2.0.4(transitive)
- Removedis-binary-path@2.1.0(transitive)
- Removedis-extglob@2.1.1(transitive)
- Removedis-glob@4.0.3(transitive)
- Removedis-number@7.0.0(transitive)
- Removedmatched@5.0.1(transitive)
- Removedminimatch@3.1.2(transitive)
- Removednormalize-path@3.0.0(transitive)
- Removedonce@1.4.0(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedpicomatch@2.3.1(transitive)
- Removedreaddirp@3.6.0(transitive)
- Removedto-regex-range@5.0.1(transitive)
- Removedwrappy@1.0.2(transitive)