Skip to content

Commit

Permalink
feat: recursive dependency walk implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
learosema committed Oct 17, 2024
1 parent 6f23cdb commit 1dc53a1
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 72 deletions.
30 changes: 28 additions & 2 deletions src/dependency-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import path from 'node:path';
* Gets a Record of dependencies per file
* @param {*} inputFilePaths
* @param {*} resolver
* @returns {Promise<Record<string, string[]>>} record of paths. As soon as the path in key changes, the array of files in the value need to be rebuilt.
* @returns {Promise<Record<string, string[]>>} a map documenting: the file key is included by the array of files in the value
*/
export async function getDependencyGraph(inputRootDir, inputFilePaths, resolver) {
export async function getDependencyMap(inputRootDir, inputFilePaths, resolver) {
const deps = {};
for (const inputFilePath of inputFilePaths) {
if (/\.(md|css|html?)$/.test(inputFilePath)) {
Expand All @@ -28,3 +28,29 @@ export async function getDependencyGraph(inputRootDir, inputFilePaths, resolver)
}
return deps;
}

/**
* Recursively walk through the dependency map for a specific file
* @param {*} map the dependency map
* @param {*} file the file in question
* @param {number} depth number of iterations
* @returns {string[]} array of files
*/
export function walkDependencyMap(map, file, depth = 10) {
const deps = map[file];
const childDeps = [];
if (! deps) {
return [];
}
let result = new Map(Array.from(deps.map(x => [x, true])));
for (const dep of deps) {
result.set(dep, true);
if (depth > 0) {
childDeps.push(...walkDependencyMap(map, dep, depth - 1));
}
}
for (const childDep of childDeps) {
result.set(childDep, true);
}
return Array.from(result.keys());
}
12 changes: 8 additions & 4 deletions src/sissi.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { serve } from './httpd.js';
import EventEmitter from 'node:stream';
import { readDataDir } from './data.js';
import { handleTemplateFile } from './transforms/template-data.js';
import { getDependencyGraph } from './dependency-graph.js';
import { getDependencyMap, walkDependencyMap } from './dependency-graph.js';
import { resolve } from './resolver.js';

export class Sissi {
Expand Down Expand Up @@ -78,9 +78,13 @@ export class Sissi {
}
lastExec.set(event.filename, performance.now());
console.log(`[${event.eventType}] ${event.filename}`);
const deps = await getDependencyGraph(this.config.dir.input, await readdir(path.normalize(this.config.dir.input), {recursive: true}),
this.config.resolve || resolve);
await this.build([event.filename, ...(deps[event.filename]??[])], eventEmitter);
const deps = await getDependencyMap(
this.config.dir.input,
await readdir(path.normalize(this.config.dir.input), {recursive: true}),
this.config.resolve || resolve
);
const allDependants = walkDependencyMap(deps, event.filename);
await this.build([event.filename, ...allDependants], eventEmitter);
}
} catch (err) {
if (err.name === 'AbortError') return;
Expand Down
165 changes: 100 additions & 65 deletions tests/dependency-graph.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import path from 'node:path';
import { getDependencyGraph } from '../src/dependency-graph.js';
import { getDependencyMap, walkDependencyMap } from '../src/dependency-graph.js';

describe('Dependency Graph', () => {
describe('Dependency Map', () => {

const withFrontmatter = (str, data) => `---json\n${JSON.stringify(data)}\n---\n${str}`

Expand All @@ -18,81 +18,116 @@ describe('Dependency Graph', () => {
}
}

it('should return a records, mapping each dependants per file', async () => {
const files = {
'index.html': withFrontmatter('# {{ title }}', {title: 'Hello', layout: 'base.html'}),
'_layouts/base.html': '<body>{{ content | safe }}</body>',
};

const resolve = setupVFS(files);

const dependencies = await getDependencyGraph('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_layouts/base.html': ['index.html'],
describe('getDependencyMap', () => {
it('should return a records, mapping each dependants per file', async () => {
const files = {
'index.html': withFrontmatter('# {{ title }}', {title: 'Hello', layout: 'base.html'}),
'_layouts/base.html': '<body>{{ content | safe }}</body>',
};

const resolve = setupVFS(files);

const dependencies = await getDependencyMap('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_layouts/base.html': ['index.html'],
});
});
});

it('should handle layouts depending on other layouts correctly', async () => {
const files = {
'index.html': withFrontmatter('# {{ title }}', {title: 'Hello', layout: 'base.html'}),
'_layouts/base.html': '<body>{{ content | safe }}</body>',
'_layouts/article.html': withFrontmatter('<article><h1>{{ title }}</h1>{{ content | safe }}</article>', {layout: 'base.html'})
};

const resolve = setupVFS(files);

const dependencies = await getDependencyGraph('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_layouts/base.html': ['index.html', '_layouts/article.html'],

it('should handle layouts depending on other layouts correctly', async () => {
const files = {
'index.html': withFrontmatter('# {{ title }}', {title: 'Hello', layout: 'base.html'}),
'_layouts/base.html': '<body>{{ content | safe }}</body>',
'_layouts/article.html': withFrontmatter(
'<article><h1>{{ title }}</h1>{{ content | safe }}</article>',
{ layout: 'base.html' }
)
};

const resolve = setupVFS(files);

const dependencies = await getDependencyMap('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_layouts/base.html': ['index.html', '_layouts/article.html'],
});
});
});

it('should handle css dependencies correctly', async () => {
const files = {
'styles.css': 'import "./_reset.css";',
'_reset.css': '*{box-sizing:border-box;margin:0}\n'
};

const resolve = setupVFS(files);

const dependencies = await getDependencyGraph('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_reset.css': ['styles.css'],

it('should handle css dependencies correctly', async () => {
const files = {
'styles.css': 'import "./_reset.css";',
'_reset.css': '*{box-sizing:border-box;margin:0}\n'
};

const resolve = setupVFS(files);

const dependencies = await getDependencyMap('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_reset.css': ['styles.css'],
});
});

it('should handle html dependencies correctly', async () => {
const files = {
'index.html': '<html-include src="top.html">',
'_includes/top.html': '<header>header</header>\n'
};

const resolve = setupVFS(files);

const dependencies = await getDependencyMap('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_includes/top.html': ['index.html'],
});
});

it('should handle html includes in markdown correctly', async () => {
const files = {
'index.md': '<html-include src="top.html">',
'_includes/top.html': '<header>\nheader\n<html-include src="nav.html">\n</header>\n',
'_includes/nav.html': '<nav></nav>'
};

const resolve = setupVFS(files);

const dependencies = await getDependencyMap('', Object.keys(files), resolve);

assert.deepEqual(dependencies, {
'_includes/top.html': ['index.md'],
'_includes/nav.html': ['_includes/top.html']
});
});
});

it('should handle html dependencies correctly', async () => {
const files = {
'index.html': '<html-include src="top.html">',
'_includes/top.html': '<header>header</header>\n'
};

const resolve = setupVFS(files);

const dependencies = await getDependencyGraph('', Object.keys(files), resolve);
describe('walkDependencyMap', () => {

assert.deepEqual(dependencies, {
'_includes/top.html': ['index.html'],
it('should handle waterfall includes', () => {
const dependencyMap = {
'_includes/top.html': ['index.md'],
'_includes/nav.html': ['_includes/top.html']
};

const result = walkDependencyMap(dependencyMap, '_includes/nav.html');
result.sort(); // order doesn't matter, we sort it for a deterministic assertion

assert.deepEqual(result, ['_includes/top.html', 'index.md']);
});
});

it('should handle html includes in markdown correctly', async () => {
const files = {
'index.md': '<html-include src="top.html">',
'_includes/top.html': '<header>header</header>\n'
};
it('should handle circular dependencies', () => {
const dependencyMap = {
'b.html': ['a.html', 'c.html'],
'c.html': ['b.html'],
};

const resolve = setupVFS(files);

const dependencies = await getDependencyGraph('', Object.keys(files), resolve);
const result = walkDependencyMap(dependencyMap, 'c.html');
result.sort(); // order doesn't matter, we sort it for a deterministic assertion

assert.deepEqual(dependencies, {
'_includes/top.html': ['index.md'],
assert.deepEqual(result, ['a.html', 'b.html', 'c.html']);
});
});

});

});
1 change: 0 additions & 1 deletion tests/sissi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ describe('sissi', () => {

it('should successfully build a smallsite', async () => {


const config = new SissiConfig({
dir: {
input: 'tests/fixtures/smallsite',
Expand Down

0 comments on commit 1dc53a1

Please sign in to comment.