From 4a27ee6823563cfccb4f377d5db84647605e85c9 Mon Sep 17 00:00:00 2001 From: MaxGenash Date: Mon, 21 Sep 2020 21:16:37 +0300 Subject: [PATCH] feat: (strf-8608) update "tarjan-graph" --- bin/stencil-start.js | 2 +- lib/Cycles.js | 99 ++++++++++++++++++++++++++++++++ lib/Cycles.spec.js | 130 ++++++++++++++++++++++++++++++++++++++++++ lib/cycles.js | 83 --------------------------- lib/cycles.spec.js | 56 ------------------ lib/stencil-bundle.js | 2 +- package-lock.json | 6 +- package.json | 2 +- 8 files changed, 235 insertions(+), 145 deletions(-) create mode 100644 lib/Cycles.js create mode 100644 lib/Cycles.spec.js delete mode 100644 lib/cycles.js delete mode 100644 lib/cycles.spec.js 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

\n

Test 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

\n

Test product {{> components/index}}

\n", - }, - { - "components/options/date":"

This is a dynamic component

\n

Test 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",