From 38db8e91845d1bf6c56d7fc80222cf8cf919f0ba Mon Sep 17 00:00:00 2001 From: Aleksey Date: Thu, 11 Jun 2020 10:42:59 +0300 Subject: [PATCH 1/3] Add nested dependencies parsing --- package.json | 8 ++- src/filter-nodes.js | 58 +++++++++++++++++++ src/get-dependencies.js | 90 +++++++++++++++++++++++++++++ src/loader.js | 18 ++++-- src/nodes.js | 2 + test/fixtures/include.xml | 3 + test/fixtures/nested-dependency.xml | 3 + test/fixtures/styles.css | 3 + test/fixtures/with-dependencies.xml | 4 ++ test/loader.test.js | 40 +++++++++++++ 10 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 src/filter-nodes.js create mode 100644 src/get-dependencies.js create mode 100644 src/nodes.js create mode 100644 test/fixtures/include.xml create mode 100644 test/fixtures/nested-dependency.xml create mode 100644 test/fixtures/styles.css create mode 100644 test/fixtures/with-dependencies.xml diff --git a/package.json b/package.json index 1a428a5..7d6a2e2 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ "fest": "^0.12.1", "jest": "^24.8.0", "memory-fs": "^0.4.1", - "webpack": "^4.36.1" + "webpack": "^4.36.1", + "xml2js": "^0.4.23" }, "peerDependencies": { - "fest": "^0.12.1" + "fest": "^0.12.1", + "xml2js": "^0.4.23" }, "engines": { "node": ">= 8.9.0" @@ -28,7 +30,7 @@ "scripts": { "pretest": "rm -rf test/.coverage && eslint src", "test": "jest", - "build": "babel src --out-dir=es", + "build": "rm -rf es && babel src --out-dir=es", "preversion": "npm test", "postversion": "git push && git push --tags", "prepublishOnly": "npm run build" diff --git a/src/filter-nodes.js b/src/filter-nodes.js new file mode 100644 index 0000000..4a9348a --- /dev/null +++ b/src/filter-nodes.js @@ -0,0 +1,58 @@ +import {NODE_ATTRIBUTES, NODE_TEXT} from './nodes'; + +/** + * @param {Object} object + * @param {string[]} nodeNames + * @param {string[]} ignoreNodes + * @returns {{name: string, item: {}[]}[]} + */ +export function filterNodes( + object, + nodeNames, + ignoreNodes = [NODE_ATTRIBUTES, NODE_TEXT] +) { + const stack = [object]; + const found = []; + + function isNotIgnored(key) { + return ignoreNodes.includes(key) === false; + } + + while (stack.length) { + const item = stack.shift(); + + const keys = Object.keys(item).filter(isNotIgnored); + + if (!keys.length) { + continue; + } + + for (let i = 0; i < keys.length; i++) { + const nodeName = keys[i]; + const {[nodeName]: nextItem} = item; + + if (nodeNames.includes(nodeName)) { + found.push({ + name: nodeName, + item: nextItem + }); + + continue; + } + + if (Array.isArray(nextItem)) { + stack.push(...nextItem); + + continue; + } + + if (typeof nextItem !== 'object') { + continue; + } + + stack.push(nextItem); + } + } + + return found; +} diff --git a/src/get-dependencies.js b/src/get-dependencies.js new file mode 100644 index 0000000..3b4bf34 --- /dev/null +++ b/src/get-dependencies.js @@ -0,0 +1,90 @@ +import path from 'path'; +import fs from 'fs'; +import util from 'util'; +import {parseStringPromise} from 'xml2js'; +import {NODE_ATTRIBUTES} from './nodes'; +import {filterNodes} from './filter-nodes'; + +const readFile = util.promisify(fs.readFile); + +/** + * @param {{attribute: {}[]}} node + * @return {boolean} + */ +function hasAttributes({item: [attributes]}) { + return attributes && NODE_ATTRIBUTES in attributes; +} + +function getSrcAttribute({item: [attributes]}) { + return attributes[NODE_ATTRIBUTES].src; +} + +/** + * @param {string} source + * @return {Promise>} + */ +async function getFileImports(source) { + const root = await parseStringPromise(source); + const nodes = filterNodes(root, [ + 'fest:include', + 'fest:insert' + ]); + + return new Set( + nodes.filter(hasAttributes).map(getSrcAttribute) + ); +} + +/** + * @param {string} parentFile + * @param {string} importedPath + * @return {string} + */ +function getAbsolutePath(parentFile, importedPath) { + return path.resolve(path.dirname(parentFile), importedPath); +} + +async function getAbsoluteFileImports(resourcePath, source) { + const toAbsolutePath = getAbsolutePath.bind(null, resourcePath); + const fileImports = await getFileImports(source); + + return [...fileImports].map(toAbsolutePath); +} + +export async function getDependencies(resourcePath, source) { + const dependencies = new Set(); + let stack = [ + [resourcePath, source] + ]; + + do { + const [resourcePath, source] = stack.shift(); + + let absoluteFileImports = await getAbsoluteFileImports( + resourcePath, + source + ); + absoluteFileImports = absoluteFileImports.filter((fileImport) => ( + !dependencies.has(fileImport) + )); + + for (const fileImport of absoluteFileImports) { + dependencies.add(fileImport); + + if (!fileImport.endsWith('.xml')) { + continue; + } + + let source; + try { + source = await readFile(fileImport, 'utf8'); + } catch (e) { + continue; + } + + stack.push([fileImport, source]) + } + } while (stack.length); + + return dependencies; +} diff --git a/src/loader.js b/src/loader.js index 9ee4596..6ca9682 100644 --- a/src/loader.js +++ b/src/loader.js @@ -1,8 +1,9 @@ import {getOptions} from 'loader-utils'; import validateOptions from 'schema-utils'; - import fest from 'fest'; +import {getDependencies} from './get-dependencies'; + const schema = { type: 'object', properties: { @@ -55,14 +56,21 @@ export default function festLoader(source) { baseDataPath: 'options' }); - const promise = new Promise(compile(source, Object.assign({}, options, { + const promise = new Promise(compile(source, { + ...options, resourcePath: this.resourcePath - }))); + })); const callback = this.async(); - promise.then(function(compiled) { + Promise.all([ + // Tracking dependencies is optional feature, that could fail + getDependencies(this.resourcePath, source).catch(() => []), + promise + ]).then(([deps, compiled]) => { + deps.forEach((dep) => this.addDependency(dep)); + callback(null, compiled); - }, function(exception) { + }).catch(function(exception) { callback(exception, ''); }); } diff --git a/src/nodes.js b/src/nodes.js new file mode 100644 index 0000000..4ab3a50 --- /dev/null +++ b/src/nodes.js @@ -0,0 +1,2 @@ +export const NODE_ATTRIBUTES = '$'; +export const NODE_TEXT = '_'; diff --git a/test/fixtures/include.xml b/test/fixtures/include.xml new file mode 100644 index 0000000..78204ea --- /dev/null +++ b/test/fixtures/include.xml @@ -0,0 +1,3 @@ + + Simple include + diff --git a/test/fixtures/nested-dependency.xml b/test/fixtures/nested-dependency.xml new file mode 100644 index 0000000..7db85dc --- /dev/null +++ b/test/fixtures/nested-dependency.xml @@ -0,0 +1,3 @@ + + + diff --git a/test/fixtures/styles.css b/test/fixtures/styles.css new file mode 100644 index 0000000..faa4215 --- /dev/null +++ b/test/fixtures/styles.css @@ -0,0 +1,3 @@ +.foo { + color: red; +} diff --git a/test/fixtures/with-dependencies.xml b/test/fixtures/with-dependencies.xml new file mode 100644 index 0000000..32ba471 --- /dev/null +++ b/test/fixtures/with-dependencies.xml @@ -0,0 +1,4 @@ + + +
+
diff --git a/test/loader.test.js b/test/loader.test.js index 2140248..93e3eb2 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -1,6 +1,12 @@ +import fs from 'fs'; +import path from 'path'; +import util from 'util'; +import loader from '../src/loader'; import compiler from './compiler.js'; import {getOutput} from './get-output'; +const readFile = util.promisify(fs.readFile); + describe('Default usage', function() { let stats, output; @@ -32,3 +38,37 @@ describe("'module' option", function() { expect(output).toMatch('module.exports = function'); }); }); + +describe('dependencies parse', function() { + test('should add dependencies', async function(done) { + const fixturePath = path.resolve('test/fixtures/with-dependencies.xml'); + const source = await readFile(fixturePath, 'utf8'); + const context = { + query: '', + resourcePath: fixturePath, + async() { + return function(err) { + if (err) { + return done(err); + } + + expect(context.addDependency).toHaveBeenCalledTimes(3); + expect(context.addDependency).toHaveBeenCalledWith( + expect.stringMatching('nested-dependency.xml') + ); + expect(context.addDependency).toHaveBeenCalledWith( + expect.stringMatching('styles.css') + ); + expect(context.addDependency).toHaveBeenCalledWith( + expect.stringMatching('include.xml') + ); + + done(); + } + }, + addDependency: jest.fn() + }; + + loader.call(context, source); + }); +}); From 7fc324f5c41ef1972a1c758707fd447efc7a517a Mon Sep 17 00:00:00 2001 From: Aleksey Date: Thu, 11 Jun 2020 10:51:16 +0300 Subject: [PATCH 2/3] Lint --- src/get-dependencies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/get-dependencies.js b/src/get-dependencies.js index 3b4bf34..12863e3 100644 --- a/src/get-dependencies.js +++ b/src/get-dependencies.js @@ -82,7 +82,7 @@ export async function getDependencies(resourcePath, source) { continue; } - stack.push([fileImport, source]) + stack.push([fileImport, source]); } } while (stack.length); From b704ea0f4510682b3ae7687d8b509b9fa43f0580 Mon Sep 17 00:00:00 2001 From: Aleksey Date: Thu, 11 Jun 2020 11:25:42 +0300 Subject: [PATCH 3/3] Add dependencies before template compilation --- src/loader.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/loader.js b/src/loader.js index 6ca9682..58429bc 100644 --- a/src/loader.js +++ b/src/loader.js @@ -49,26 +49,30 @@ function compile(source, { } export default function festLoader(source) { - const options = getOptions(this); + const callback = this.async(); - validateOptions(schema, options || {}, { - name: 'Fest Loader', - baseDataPath: 'options' - }); + let options; + try { + options = getOptions(this); + validateOptions(schema, options || {}, { + name: 'Fest Loader', + baseDataPath: 'options' + }); + } catch (e) { + callback(e); - const promise = new Promise(compile(source, { - ...options, - resourcePath: this.resourcePath - })); + return; + } - const callback = this.async(); - Promise.all([ - // Tracking dependencies is optional feature, that could fail - getDependencies(this.resourcePath, source).catch(() => []), - promise - ]).then(([deps, compiled]) => { + // Tracking dependencies is optional feature, that could fail + getDependencies(this.resourcePath, source).catch(() => []).then((deps) => { deps.forEach((dep) => this.addDependency(dep)); - + }).then(() => ( + new Promise(compile(source, { + ...options, + resourcePath: this.resourcePath + })) + )).then(function(compiled) { callback(null, compiled); }).catch(function(exception) { callback(exception, '');