Skip to content

Commit

Permalink
Merge 64ebe1b into c6939d8
Browse files Browse the repository at this point in the history
  • Loading branch information
ogonkov committed Jun 11, 2020
2 parents c6939d8 + 64ebe1b commit 528597a
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 15 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

This Webpack loader compiles [Fest](https://github.com/mailru/fest) templates.

Loader is trying to build dependencies tree by walking through `<fest:include/>`
and `<fest:insert/>` of template.

## Install
```bash
npm install --save-dev fest-webpack-loader
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@
"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"
},
"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"
Expand Down
58 changes: 58 additions & 0 deletions src/filter-nodes.js
Original file line number Diff line number Diff line change
@@ -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;
}
90 changes: 90 additions & 0 deletions src/get-dependencies.js
Original file line number Diff line number Diff line change
@@ -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<Set<string>>}
*/
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;
}
36 changes: 24 additions & 12 deletions src/loader.js
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -48,21 +49,32 @@ 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, Object.assign({}, options, {
resourcePath: this.resourcePath
})));
return;
}

const callback = this.async();
promise.then(function(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);
}, function(exception) {
}).catch(function(exception) {
callback(exception, '');
});
}
2 changes: 2 additions & 0 deletions src/nodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const NODE_ATTRIBUTES = '$';
export const NODE_TEXT = '_';
3 changes: 3 additions & 0 deletions test/fixtures/include.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<fest:template xmlns:fest="http://fest.mail.ru" context_name="json">
Simple include
</fest:template>
3 changes: 3 additions & 0 deletions test/fixtures/nested-dependency.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<fest:template xmlns:fest="http://fest.mail.ru" context_name="json">
<fest:include context="json.list" src="./include.xml"/>
</fest:template>
3 changes: 3 additions & 0 deletions test/fixtures/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.foo {
color: red;
}
4 changes: 4 additions & 0 deletions test/fixtures/with-dependencies.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<fest:template xmlns:fest="http://fest.mail.ru" context_name="json">
<fest:include context="json.list" src="./nested-dependency.xml"/>
<div><fest:insert src="./styles.css"/></div>
</fest:template>
40 changes: 40 additions & 0 deletions test/loader.test.js
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
});
});

0 comments on commit 528597a

Please sign in to comment.