Skip to content

Commit

Permalink
feat: (strf-8608) update "tarjan-graph"
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxGenash committed Sep 21, 2020
1 parent 7153455 commit 4a27ee6
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 145 deletions.
2 changes: 1 addition & 1 deletion bin/stencil-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Fs = require('fs');
const Path = require('path');
const Url = require('url');

const Cycles = require('../lib/cycles');
const Cycles = require('../lib/Cycles');
const templateAssembler = require('../lib/template-assembler');
const { PACKAGE_INFO, DOT_STENCIL_FILE_PATH, THEME_PATH } = require('../constants');
const program = require('../lib/commander');
Expand Down
99 changes: 99 additions & 0 deletions lib/Cycles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const Graph = require('tarjan-graph');
const util = require('util');

class Cycles {
/**
* @param {object[]} templatePaths
*/
constructor(templatePaths) {
if (!Array.isArray(templatePaths)) {
throw new Error('templatePaths must be an Array');
}

this.templatePaths = templatePaths;
this.partialRegex = /\{\{>\s*([_|\-|a-zA-Z0-9\/]+)[^{]*?}}/g;
this.dynamicComponentRegex = /\{\{\s*?dynamicComponent\s*(?:'|")([_|\-|a-zA-Z0-9\/]+)(?:'|").*?}}/g;
}

/**
* Runs a graph based cyclical dependency check. Throws an error if circular dependencies are found
* @returns {void}
*/
detect() {
for (const templatesByPath of this.templatePaths) {
const graph = new Graph();

for (let [templatePath, templateContent] of Object.entries(templatesByPath)) {
const dependencies = [
...this.geDependantPartials(templateContent, templatePath),
...this.getDependantDynamicComponents(templateContent, templatesByPath, templatePath),
];

graph.add(templatePath, dependencies);
}

if (graph.hasCycle()) {
throw new Error('Circular dependency in template detected. \r\n' + util.inspect(graph.getCycles()));
}
}
}

/**
* @private
* @param {string} templateContent
* @param {string} pathToSkip
* @returns {string[]}
*/
geDependantPartials(templateContent, pathToSkip) {
const dependencies = [];

let match = this.partialRegex.exec(templateContent);
while (match !== null) {
const partialPath = match[1];
if (partialPath !== pathToSkip) { // skip the current templatePath
dependencies.push(partialPath);
}
match = this.partialRegex.exec(templateContent);
}

return dependencies;
}

/**
* @private
* @param {string} templateContent
* @param {object} allTemplatesByPath
* @param {string} pathToSkip
* @returns {string[]}
*/
getDependantDynamicComponents(templateContent, allTemplatesByPath, pathToSkip) {
const dependencies = [];

let match = this.dynamicComponentRegex.exec(templateContent);
while (match !== null) {
const dynamicComponents = this.getDynamicComponents(match[1], allTemplatesByPath, pathToSkip);
dependencies.push(...dynamicComponents);
match = this.dynamicComponentRegex.exec(templateContent);
}

return dependencies;
}

/**
* @private
* @param {string} componentFolder
* @param {object} possibleTemplates
* @param {string} pathToSkip
* @returns {string[]}
*/
getDynamicComponents(componentFolder, possibleTemplates, pathToSkip) {
return Object.keys(possibleTemplates).reduce((output, templatePath) => {
if (templatePath.indexOf(componentFolder) === 0 && templatePath !== pathToSkip) {
output.push(templatePath);
}
return output;
}, []);
}
}

module.exports = Cycles;
130 changes: 130 additions & 0 deletions lib/Cycles.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const Cycles = require('./Cycles');

describe('Cycles', () => {
const templatesWithCircles = [
{
"page":`---
front_matter_options:
setting_x:
value: {{theme_settings.front_matter_value}}
---
<!DOCTYPE html>
<html>
<body>
{{#if theme_settings.display_that}}
<div>{{> components/index}}</div>
{{/if}}
</body>
</html>`,
"components/index":`<h1>Oh Hai there</h1>
<p>
<h1>Test product {{dynamicComponent 'components/options'}}</h1>
</p>`,
"components/options/date":`<h1>This is a dynamic component</h1>
<h1>Test product {{> components/index}}</h1>`,
},
{
"page2":`<!DOCTYPE html>
<html>
<body>
<h1>{{theme_settings.customizable_title}}</h1>
</body>
</html>`,
},
{
"components/index":`<h1>Oh Hai there</h1>
<p>
<h1>Test product {{dynamicComponent 'components/options'}}</h1>
</p>`,
"components/options/date":`<h1>This is a dynamic component</h1>
<h1>Test product {{> components/index}}</h1>`,
},
{
"components/options/date":`<h1>This is a dynamic component</h1>
<h1>Test product {{> components/index}}</h1>`,
"components/index":`<h1>Oh Hai there</h1>
<p>
<h1>Test product {{dynamicComponent 'components/options'}}</h1>
</p>`,
},
];

const templatesWithoutCircles = [
{
"page":`---
front_matter_options:
setting_x:
value: {{theme_settings.front_matter_value}}
---
<!DOCTYPE html>
<html>
<body>
{{#if theme_settings.display_that}}
<div>{{> components/index}}</div>
{{/if}}
</body>
</html>`,
"components/index": `<h1>This is the index</h1>`,
},
{
"page2":`<!DOCTYPE html>
<html>
<body>
<h1>{{theme_settings.customizable_title}}</h1>
</body>
</html>`,
},
];

const templatesWithSelfReferences = [
{
"page":`---
front_matter_options:
setting_x:
value: {{theme_settings.front_matter_value}}
---
<!DOCTYPE html>
<html>
<body>
{{#if theme_settings.display_that}}
<div>{{> components/index}}</div>
{{/if}}
<h1>Self-reference: {{dynamicComponent 'page'}}</h1>
</body>
</html>`,
"components/index": `<h1>This is the index</h1>`,
},
];

it('should throw error when cycle is detected', () => {
const action = () => {
new Cycles(templatesWithCircles).detect();
};

expect(action).toThrow(Error, /Circular/);
});

it('should throw an error when non array passed in', () => {
const action = () => {
new Cycles('test');
};

expect(action).toThrow(Error);
});

it('should not throw an error when cycles weren\'t detected', () => {
const action = () => {
new Cycles(templatesWithoutCircles).detect();
};

expect(action).not.toThrow();
});

it('should not throw an error for self-references', () => {
const action = () => {
new Cycles(templatesWithSelfReferences).detect();
};

expect(action).not.toThrow();
});
});
83 changes: 0 additions & 83 deletions lib/cycles.js

This file was deleted.

56 changes: 0 additions & 56 deletions lib/cycles.spec.js

This file was deleted.

2 changes: 1 addition & 1 deletion lib/stencil-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const Fs = require('fs');
const Path = require('path');
const BuildConfigManager = require('./BuildConfigManager');
const BundleValidator = require('./bundle-validator');
const Cycles = require('./cycles');
const Cycles = require('./Cycles');
const CssAssembler = require('./css-assembler');
const LangAssembler = require('./lang-assembler');
const TemplateAssembler = require('./template-assembler');
Expand Down
Loading

0 comments on commit 4a27ee6

Please sign in to comment.