UNPKG

watch-dependency-graph

Version:

A Node file watcher, but instead of scanning the filesystem for files to be watched, it monitors only specified entry files and their dependency trees.

946 lines (770 loc) 18.3 kB
const fs = require('fs-extra') const path = require('path') const assert = require('assert') const test = require('baretest')('wdg') const fixtures = require('./fixtures') const graph = require('../') fixtures.setRoot(path.join(__dirname, 'fixtures')) fs.ensureDirSync(fixtures.getRoot()) fs.emptyDirSync(fixtures.getRoot()) process.chdir(fixtures.getRoot()) const DELAY = 500 const wait = t => new Promise(resolve => setTimeout(resolve, t)) function subscribe (event, instance) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject('timeout'), DELAY * 4) const close = instance.on(event, ids => { clearTimeout(timeout) close() resolve(ids) }) }) } /** * It's very important that each set of fixtures is written to separate directories */ /** * Note on the wait periods. * * Basically, after initializing fixtures and wdg, wait 1s, then run the test. * * @see https://github.com/fgnass/filewatcher/blob/master/test/index.js#L85 */ test('ignores non-absolute paths', async () => { const files = { a: { url: './isAbs/a.js', content: `export default ''` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) const event = subscribe('error', w) await w.add('./isAbs/a.js') const error = await event assert(/isAbs/.test(error)) w.close() fsx.cleanup() }) test('constructs valid tree', async () => { const files = { a: { url: './valid/a.js', content: ` import a_a from './a_a' import a_b from './a_b' export default '' ` }, a_a: { url: './valid/a_a.js', content: ` import a_a_a from './a_a_a' export default '' ` }, a_a_a: { url: './valid/a_a_a.js', content: ` export default '' ` }, a_b: { url: './valid/a_b.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) await wait(DELAY) const tree = w.tree // entry assert(tree[fsx.files.a].pointer === 0) // children assert( tree[fsx.files.a].childrenPointers.includes(tree[fsx.files.a_a].pointer) ) assert( tree[fsx.files.a].childrenPointers.includes(tree[fsx.files.a_b].pointer) ) assert( tree[fsx.files.a_a].childrenPointers.includes(tree[fsx.files.a_a_a].pointer) ) // parents assert(tree[fsx.files.a_a].parentPointers.includes(tree[fsx.files.a].pointer)) assert( tree[fsx.files.a_a_a].parentPointers.includes(tree[fsx.files.a_a].pointer) ) assert(tree[fsx.files.a_b].parentPointers.includes(tree[fsx.files.a].pointer)) // pointers assert(tree[fsx.files.a_a].entryPointers.includes(tree[fsx.files.a].pointer)) assert( tree[fsx.files.a_a_a].entryPointers.includes(tree[fsx.files.a].pointer) ) assert(tree[fsx.files.a_b].entryPointers.includes(tree[fsx.files.a].pointer)) w.close() fsx.cleanup() }) test('constructs valid tree in inverse alpha/write order', async () => { const files = { b: { url: './reverse-valid/b.js', content: ` import a from './a.js' export default '' ` }, a: { url: './reverse-valid/a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.b) await wait(DELAY) const tree = w.tree assert(tree[fsx.files.b].childrenPointers.includes(tree[fsx.files.a].pointer)) assert(tree[fsx.files.a].parentPointers.includes(tree[fsx.files.b].pointer)) w.close() fsx.cleanup() }) test('supports jsx', async () => { const files = { a: { url: './jsx/a.js', content: ` import b from './b.js' export default () => <b /> ` }, b: { url: './jsx/b.js', content: ` export default () => <h1>hello</h1> ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) await wait(DELAY) const tree = w.tree assert(tree[fsx.files.a]) assert(tree[fsx.files.b]) w.close() fsx.cleanup() }) test('handles shared deps', async () => { const files = { a: { url: './shared-deps/a.js', content: ` import c from './c.js' export default '' ` }, b: { url: './shared-deps/b.js', content: ` import c from './c.js' export default '' ` }, c: { url: './shared-deps/c.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a, fsx.files.b]) await wait(DELAY) const tree = w.tree assert(tree[fsx.files.a].childrenPointers.includes(tree[fsx.files.c].pointer)) assert(tree[fsx.files.b].childrenPointers.includes(tree[fsx.files.c].pointer)) assert(tree[fsx.files.c].entryPointers.includes(tree[fsx.files.a].pointer)) assert(tree[fsx.files.c].entryPointers.includes(tree[fsx.files.b].pointer)) w.close() fsx.cleanup() }) test('handles circular deps', async () => { const files = { a: { url: './circular/a.js', content: ` import b from './b.js' export default '' ` }, b: { url: './circular/b.js', content: ` import a from './a.js' export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a, fsx.files.b]) await wait(DELAY) const tree = w.tree assert(tree[fsx.files.a].childrenPointers.includes(tree[fsx.files.b].pointer)) assert(tree[fsx.files.b].childrenPointers.includes(tree[fsx.files.a].pointer)) w.close() fsx.cleanup() }) test('handles inverse tree', async () => { const files = { a: { url: './inverse-tree/a.js', content: ` import c from './c.js' export default '' ` }, b: { url: './inverse-tree/b.js', content: ` import c from './c.js' export default '' ` }, c: { url: './inverse-tree/c.js', content: ` import d from './d.js' export default '' ` }, d: { url: './inverse-tree/d.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a, fsx.files.b]) await wait(DELAY) const tree = w.tree assert(tree[fsx.files.d].entryPointers.includes(tree[fsx.files.a].pointer)) assert(tree[fsx.files.d].entryPointers.includes(tree[fsx.files.b].pointer)) w.close() fsx.cleanup() }) test('correctly generates filepaths', async () => { const files = { a: { url: './filepaths/a.js', content: ` import a_b from './lib/a_b.js' export default '' ` }, a_b: { url: './filepaths/lib/a_b.js', content: ` import a_b_a from '../util/a_b_a.js' export default '' ` }, a_b_a: { url: './filepaths/util/a_b_a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a]) await wait(DELAY) const tree = w.tree assert(!!tree[fsx.files.a_b]) assert(!!tree[fsx.files.a_b_a]) w.close() fsx.cleanup() }) test('support aliases', async () => { const files = { a: { url: './aliases/a.js', content: ` import a_b from '@/aliases/a_b.js' export default '' ` }, a_b: { url: './aliases/a_b.js', content: ` export default () => {} ` } } const fsx = fixtures.create(files) const w = graph({ alias: { '@': fixtures.getRoot() } }) await w.add([fsx.files.a]) await wait(DELAY) const tree = w.tree assert(!!tree[fsx.files.a_b]) w.close() fsx.cleanup() }) test('handles syntax error', async () => { const files = { a: { url: './syntax/a.js', content: ` import a_b from './a_b.js' export default '' ` }, a_b: { url: './syntax/a_b.js', content: ` export const fn () => {} ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) // silence error w.on('error', () => {}) await w.add([fsx.files.a]) await wait(DELAY) const tree = w.tree assert(!!tree[fsx.files.a_b]) w.close() fsx.cleanup() }) test('accepts but does not traverse non-js files', async () => { const files = { a: { url: './non-js/a.js', content: ` import pkg from './package.json' export default '' ` }, pkg: { url: './non-js/package.json', content: ` { "version": "1" } ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a]) await wait(DELAY) const tree = w.tree assert(!!tree[fsx.files.pkg]) w.close() fsx.cleanup() }) test('supports node_modules', async () => { const files = { a: { url: './node_modules/a.js', content: ` import assert from 'barecolor' import baretest from 'baretest' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a]) await wait(DELAY) const tree = w.tree assert(!!tree[fsx.files.a]) assert(!!tree[require.resolve('baretest')]) assert(!!tree[require.resolve('barecolor')]) w.close() fsx.cleanup() }) test('emits change when entry file is updated', async () => { const files = { a: { url: './change/a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) const event = subscribe('change', w) await wait(DELAY) fs.outputFileSync( fsx.files.a, ` export default 'foo' ` ) const [file] = await event assert(file === fsx.files.a) w.close() fsx.cleanup() }) test('emits change when nested children are updated', async () => { const files = { a: { url: './nested-change/a.js', content: ` import a_a from './a_a.js' export default '' ` }, a_a: { url: './nested-change/a_a.js', content: ` import a_a_a from './a_a_a' export default '' ` }, a_a_a: { url: './nested-change/a_a_a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) const event = subscribe('change', w) fs.outputFileSync( fsx.files.a_a, ` import a_a_a from './a_a_a' export default 'foo' ` ) const [file] = await event assert(file === fsx.files.a) const event2 = subscribe('change', w) await wait(DELAY) fs.outputFileSync( fsx.files.a_a_a, ` export default 'foo' ` ) const [file2] = await event2 assert(file2 === fsx.files.a) w.close() fsx.cleanup() }) test('de-referenced nested child is ignored, then re-added', async () => { const files = { a: { url: './deref/a.js', content: ` import a_a from './a_a.js' export default '' ` }, a_a: { url: './deref/a_a.js', content: ` import a_a_a from './a_a_a' export default '' ` }, a_a_a: { url: './deref/a_a_a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) await wait(DELAY) // de-reference, would trigger change fs.outputFileSync(fsx.files.a_a, `export default 'de-referenced'`) // wait for change event to pass await wait(DELAY) const changeOnDereferencedFile = subscribe('change', w) fs.outputFileSync(fsx.files.a_a_a, `export default 'bar'`) try { console.log(await changeOnDereferencedFile) assert(false) } catch (e) { assert(e === 'timeout') } // re-reference fs.outputFileSync( fsx.files.a_a, ` import a_a_a from './a_a_a.js' export default 'foo' ` ) // await re-init await wait(DELAY) const changeOnRereferencedFile = subscribe('change', w) fs.outputFileSync( fsx.files.a_a_a, ` export default 'referenced again' ` ) const [entryAgain] = await changeOnRereferencedFile assert(entryAgain === fsx.files.a) w.close() fsx.cleanup() }) test('emits remove event when entry file is removed', async () => { const files = { a: { url: './remove/a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) const event = subscribe('remove', w) await wait(DELAY) fs.removeSync(fsx.files.a) const [file] = await event assert(file === fsx.files.a) w.close() fsx.cleanup() }) test(`when entry file is removed, its children are removed too and don't trigger an update`, async () => { const files = { a: { url: './removed-entry/a.js', content: ` import a_a from './a_a.js' export default '' ` }, a_a: { url: './removed-entry/a_a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) const event = subscribe('change', w) await wait(DELAY) fs.removeSync(fsx.files.a) // await re-start await wait(DELAY) fs.outputFileSync(fsx.files.a_a, `export default 'updated'`) try { await event assert(false) } catch (e) { assert(e === 'timeout') } assert(!w.tree[fsx.files.a]) assert(!w.tree[fsx.files.a_a]) w.close() fsx.cleanup() }) test(`when child file is removed, triggers update and references are removed`, async () => { const files = { a: { url: './removed-children/a.js', content: ` import a_a from './a_a.js' export default '' ` }, a_a: { url: './removed-children/a_a.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add(fsx.files.a) const event = subscribe('change', w) await wait(DELAY) fs.removeSync(fsx.files.a_a) const [file] = await event assert(file === fsx.files.a) assert(!w.tree[fsx.files.a].childrenPointers.length) assert(!w.tree[fsx.files.a_a]) w.close() fsx.cleanup() }) test(`files removed from watching aren't watched`, async () => { const files = { a: { url: './remove-from-watch/a.js', content: ` export default '' ` }, b: { url: './remove-from-watch/b.js', content: ` export default '' ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) const event = subscribe('change', w) await w.add(fsx.files.a) await w.add(fsx.files.b) await wait(DELAY) fs.outputFileSync( fsx.files.a, ` export default 'updated' ` ) const [file] = await event assert(file === fsx.files.a) w.remove(fsx.files.a) await wait(DELAY) const noChangeEvent = subscribe('change', w) fs.outputFileSync( fsx.files.a, ` export default 'updated again' ` ) try { await noChangeEvent assert(false) } catch (e) { assert(e === 'timeout') } w.close() fsx.cleanup() }) test('kitchen sink', async () => { const files = { a: { url: './all/a.js', content: ` import * as a_b from './a_b.js' import { a_c } from './a_c.js' import a_d from './a_d.js' let lazy = null if (true) lazy = import('./a_e.js') export default '' ` }, a_b: { url: './all/a_b.js', content: ` export const a_b = () => {} ` }, a_c: { url: './all/a_c.js', content: ` export const a_c = () => {} ` }, a_d: { url: './all/a_d.js', content: ` export default { a_d() {} } ` }, a_e: { url: './all/a_e.js', content: ` export const a_e = () => {} ` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a]) await wait(DELAY) const tree = w.tree assert(!!tree[fsx.files.a_b]) assert(!!tree[fsx.files.a_c]) assert(!!tree[fsx.files.a_d]) assert(!!tree[fsx.files.a_e]) w.close() fsx.cleanup() }) test.only('rename file', async () => { const files = { a: { url: './rename/a.js', content: `export default ''` }, b: { url: './rename/b.js', content: `export default ''` } } const fsx = fixtures.create(files) const w = graph({ cwd: fixtures.getRoot() }) await w.add([fsx.files.a, fsx.files.b]) w.on('error', e => { console.log(e) }) await wait(DELAY) const noChangeEvent = subscribe('change', w) const newFileName = path.join(fsx.root, '/rename/c.js') // rename fs.moveSync(fsx.files.b, newFileName) // change it fs.outputFileSync(newFileName, `export default ''`, 'utf8') // no event try { await noChangeEvent assert(false) } catch (e) { assert(e === 'timeout') } // renamed file was removed assert(w.tree[fsx.files.b] === undefined) assert(w.ids[0] === fsx.files.a) assert(w.ids[1] === undefined) // add renamed file await w.add([newFileName]) const changeEvent = subscribe('change', w) // change it fs.outputFileSync(newFileName, `export default ''`, 'utf8') // event emitted this time const [file] = await changeEvent assert(file === newFileName) // added file is in tree now assert(w.tree[newFileName]) w.close() fsx.cleanup() }) !(async function () { console.time('test') await test.run() console.timeEnd('test') process.exit() })()