diff --git a/bin/stencil-start.js b/bin/stencil-start.js
index cff25ce7..9c9b6c4e 100755
--- a/bin/stencil-start.js
+++ b/bin/stencil-start.js
@@ -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');
diff --git a/lib/Cycles.js b/lib/Cycles.js
new file mode 100644
index 00000000..09274506
--- /dev/null
+++ b/lib/Cycles.js
@@ -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;
diff --git a/lib/Cycles.spec.js b/lib/Cycles.spec.js
new file mode 100644
index 00000000..72d4d542
--- /dev/null
+++ b/lib/Cycles.spec.js
@@ -0,0 +1,130 @@
+const Cycles = require('./Cycles');
+
+describe('Cycles', () => {
+ const templatesWithCircles = [
+ {
+ "page":`---
+ front_matter_options:
+ setting_x:
+ value: {{theme_settings.front_matter_value}}
+ ---
+
+
+
+ {{#if theme_settings.display_that}}
+ {{> components/index}}
+ {{/if}}
+
+ `,
+ "components/index":`Oh Hai there
+
+
Test product {{dynamicComponent 'components/options'}}
+ `,
+ "components/options/date":`This is a dynamic component
+ Test product {{> components/index}}
`,
+ },
+ {
+ "page2":`
+
+
+ {{theme_settings.customizable_title}}
+
+ `,
+ },
+ {
+ "components/index":`Oh Hai there
+
+
Test product {{dynamicComponent 'components/options'}}
+ `,
+ "components/options/date":`This is a dynamic component
+ Test product {{> components/index}}
`,
+ },
+ {
+ "components/options/date":`This is a dynamic component
+ Test product {{> components/index}}
`,
+ "components/index":`Oh Hai there
+
+
Test product {{dynamicComponent 'components/options'}}
+ `,
+ },
+ ];
+
+ const templatesWithoutCircles = [
+ {
+ "page":`---
+ front_matter_options:
+ setting_x:
+ value: {{theme_settings.front_matter_value}}
+ ---
+
+
+
+ {{#if theme_settings.display_that}}
+ {{> components/index}}
+ {{/if}}
+
+ `,
+ "components/index": `This is the index
`,
+ },
+ {
+ "page2":`
+
+
+ {{theme_settings.customizable_title}}
+
+ `,
+ },
+ ];
+
+ const templatesWithSelfReferences = [
+ {
+ "page":`---
+ front_matter_options:
+ setting_x:
+ value: {{theme_settings.front_matter_value}}
+ ---
+
+
+
+ {{#if theme_settings.display_that}}
+ {{> components/index}}
+ {{/if}}
+ Self-reference: {{dynamicComponent 'page'}}
+
+ `,
+ "components/index": `This is the index
`,
+ },
+ ];
+
+ 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();
+ });
+});
diff --git a/lib/cycles.js b/lib/cycles.js
deleted file mode 100644
index 5ca350bc..00000000
--- a/lib/cycles.js
+++ /dev/null
@@ -1,83 +0,0 @@
-var Graph = require('tarjan-graph');
-var util = require('util');
-
-/**
- *
- * @param {array} templatePaths
- * @constructor
- */
-function Cycles(templatePaths) {
- if (! Array.isArray(templatePaths)) {
- throw new Error('templatePaths Must be Array');
- }
-
- this.templatePaths = templatePaths;
-}
-
-/**
- * Runs a graph based cyclical dependency check.
- */
-Cycles.prototype.detect = function () {
- detectCycles.call(this);
-};
-
-function detectCycles() {
- var partialRegex = /\{\{>\s*([_|\-|a-zA-Z0-9\/]+)[^{]*?}}/g;
- var dynamicComponentRegex = /\{\{\s*?dynamicComponent\s*(?:'|")([_|\-|a-zA-Z0-9\/]+)(?:'|").*?}}/g;
-
- this.templatePaths.forEach(function (fileName) {
- var graph = new Graph();
- var dynamicComponents;
- var match;
- var matches;
- var partial;
- var partialPath;
- var prop;
-
- for (prop in fileName) {
- if (fileName.hasOwnProperty(prop)) {
- matches = [];
- partial = fileName[prop];
- match = partialRegex.exec(partial);
- while (match !== null) {
- partialPath = match[1];
- matches.push(partialPath);
- match = partialRegex.exec(partial);
- }
-
- match = dynamicComponentRegex.exec(partial);
-
- while (match !== null) {
- dynamicComponents = getDynamicComponents(match[1], fileName);
- matches.push.apply(matches, dynamicComponents);
- match = dynamicComponentRegex.exec(partial);
-
- }
-
- graph.add(prop, matches);
- }
- }
-
- if (graph.hasCycle()) {
- throw new Error('Circular dependency in template detected. \r\n' + util.inspect(graph.getCycles()));
- }
-
- });
-}
-
-function getDynamicComponents(componentFolder, possibleTemplates) {
- var output = [];
- var prop;
-
- for (prop in possibleTemplates) {
- if (possibleTemplates.hasOwnProperty(prop)) {
- if (prop.indexOf(componentFolder) === 0) {
- output.push(prop);
- }
- }
- }
-
- return output;
-}
-
-module.exports = Cycles;
diff --git a/lib/cycles.spec.js b/lib/cycles.spec.js
deleted file mode 100644
index 1bc84ec6..00000000
--- a/lib/cycles.spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const Cycles = require('./cycles');
-
-describe('Cycles', () => {
- const invaldResults = [
- {
- "page":"---\nfront_matter_options:\n setting_x:\n value: {{theme_settings.front_matter_value}}\n---\n\n\n\n{{#if theme_settings.display_that}}\n {{> components/index}}
\n{{/if}}\n\n\n",
- "components/index":"Oh Hai there
\n\n
Test product {{dynamicComponent 'components/options'}}
\n\n",
- "components/options/date":"This is a dynamic component
\nTest product {{> components/index}}
\n",
- },
- {
- "page2":"\n\n\n {{theme_settings.customizable_title}}
\n\n\n",
- },
- {
- "components/index":"Oh Hai there
\n\n
Test product {{dynamicComponent 'components/options'}}
\n\n",
- "components/options/date":"This is a dynamic component
\nTest product {{> components/index}}
\n",
- },
- {
- "components/options/date":"This is a dynamic component
\nTest product {{> components/index}}
\n",
- "components/index":"Oh Hai there
\n\n
Test product {{dynamicComponent 'components/options'}}
\n\n",
- },
- ];
-
- const validResults = [
- {
- "page":"---\nfront_matter_options:\n setting_x:\n value: {{theme_settings.front_matter_value}}\n---\n\n\n\n{{#if theme_settings.display_that}}\n {{> components/index}}
\n{{/if}}\n\n\n",
- "components/index":"This is the index
\n",
- },
- {
- "page2":"\n\n\n {{theme_settings.customizable_title}}
\n\n\n",
- },
- ];
-
- it('should throw error when cycle is detected', () => {
- const action = () => {
- new Cycles(invaldResults).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(validResults).detect();
- };
-
- expect(action).not.toThrow();
- });
-});
diff --git a/lib/stencil-bundle.js b/lib/stencil-bundle.js
index 3763d452..4fe2b41f 100644
--- a/lib/stencil-bundle.js
+++ b/lib/stencil-bundle.js
@@ -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');
diff --git a/package-lock.json b/package-lock.json
index 3016a868..2a9ece93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18319,9 +18319,9 @@
}
},
"tarjan-graph": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/tarjan-graph/-/tarjan-graph-0.3.0.tgz",
- "integrity": "sha1-ztt8EDUAck7Ebm4i9Kujn9N5uyA="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tarjan-graph/-/tarjan-graph-2.0.0.tgz",
+ "integrity": "sha512-fDe57nO2Ukw2A/jHwVeiEgERGrGHukf3aHmR/YZ9BrveOtHVlFs289AnVeb1wD2aj9g01ZZ6f7VyMJ2QxI2NBQ=="
},
"taskgroup": {
"version": "4.3.1",
diff --git a/package.json b/package.json
index 19043e6c..d5e149b2 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
"recursive-readdir": "^2.2.2",
"semver": "^7.3.2",
"simple-git": "^2.20.1",
- "tarjan-graph": "^0.3.0",
+ "tarjan-graph": "^2.0.0",
"tmp": "0.0.26",
"upath": "^1.2.0",
"uuid4": "^2.0.2",