From 4d0a9517bb015304045c9a551523abcfd9a873f5 Mon Sep 17 00:00:00 2001 From: Lukasz Gornicki Date: Wed, 29 Apr 2020 17:06:15 +0200 Subject: [PATCH] feat: support filters from external library (#298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Fran Méndez --- docs/authoring.md | 41 +++++-------------- lib/filtersRegistry.js | 90 ++++++++++++++++++++++++++++++++++++++++++ lib/generator.js | 44 ++------------------- lib/utils.js | 2 +- package.json | 2 +- 5 files changed, 105 insertions(+), 74 deletions(-) create mode 100644 lib/filtersRegistry.js diff --git a/docs/authoring.md b/docs/authoring.md index fe43a0884..c0aca779a 100644 --- a/docs/authoring.md +++ b/docs/authoring.md @@ -8,7 +8,7 @@ The AsyncAPI generator has been built with extensibility in mind. The package us 1. The template can have own dependencies. Just create `package.json` for the template. The generator makes sure to trigger the installation of dependencies. 1. Templates may contain multiple files. Unless stated otherwise, all files will be rendered. 1. The template engine is [Nunjucks](https://mozilla.github.io/nunjucks). -1. Templates may contain `filters` or helper functions. They must be stored in the `.filters` directory under the template directory. [Read more about filters](#filters). +1. Templates may contain [Nunjucks filters or helper functions](https://mozilla.github.io/nunjucks/templating.html#builtin-filters). [Read more about filters](#filters). 1. Templates may contain `hooks` that are functions invoked after the generation. They must be stored in the `.hooks` directory under the template directory. [Read more about hooks](#hooks). 1. Templates may contain `partials` (reusable chunks). They must be stored in the `.partials` directory under the template directory. [Read more about partials](#partials). 1. Templates may have a configuration file. It must be stored in the template directory and its name must be `.tp-config.json`. [Read more about the configuration file](#configuration-file). @@ -64,41 +64,16 @@ Schema name is 'people' and properties are: ``` ## Filters -A filter is a helper function that you can create to perform complex tasks. They are JavaScript files that register one or many [Nunjuck filters](https://mozilla.github.io/nunjucks/api.html#custom-filters). The generator will parse all the files in the `.filters` directory. +A filter is a helper function that you can create to perform complex tasks. They are JavaScript files that register one or many [Nunjuck filters](https://mozilla.github.io/nunjucks/api.html#custom-filters). The generator parses all the files in the `filters` directory. Functions exported from these files are registered as filters. -Each file must export a function that will receive the following parameters: - -* `Nunjucks`: a reference to the [Nunjucks](https://mozilla.github.io/nunjucks) template engine used internally by the generator. -* `_`: a convenient [Lodash](https://www.lodash.com) reference. -* `Markdown`: a reference to the [markdown-it](https://github.com/markdown-it/markdown-it) package. Use it to convert Markdown to HTML. -* `OpenAPISampler`: a reference to the [openapi-sampler](https://github.com/Redocly/openapi-sampler) package. It generates examples from OpenAPI/AsyncAPI schemas. - -The common structure for one of these files is the following: - -```js -module.exports = ({ Nunjucks, _, Markdown, OpenAPISampler }) => { - Nunjucks.addFilter('yourFilterName', (yourFilterParams) => { - return 'doSomething'; - }); -}; -``` - -An example filter called `camelCase` which uses the Lodash function `_.camelCase(String)` to convert a template string as camel case: - -```js -module.exports = ({ Nunjucks, _, Markdown, OpenAPISampler }) => { - Nunjucks.addFilter('camelCase', (str) => { - return _.camelCase(str); - }); -}; -``` - -And then you can use the filter in your template as follows: +You can use the filter function in your template as in the following example: ```js const {{ channelName | camelCase }} = '{{ channelName }}'; ``` +In case you have more than one template and want to reuse filters, you can put them in a single library. You can configure such a library in the template configuration under `filters` property. You can also use the official AsyncAPI [filters library](https://github.com/asyncapi/generator-filters). To learn how to add such filters to configuration [read more about the configuration file](#configuration-file). + ## Hooks Hooks are functions called by the generator on a specific moment in the generation process. For now there is one hook supported called `generate:after` that is called at the very end of the generation. The generator will parse all the files in the `.hooks` directory. @@ -160,6 +135,7 @@ The `.tp-config.json` file contains a JSON object that may have the following in |`conditionalFiles[filePath].validation`| Object | The `validation` is a JSON Schema Draft 07 object. This JSON Schema definition will be applied to the JSON value resulting from the `subject` query. If validation doesn't have errors, the condition is met, and therefore the given file will be rendered. Otherwise, the file is ignored. |`nonRenderableFiles`| [String] | A list of file paths or [globs](https://en.wikipedia.org/wiki/Glob_(programming)) that must be copied "as-is" to the target directory, i.e., without performing any rendering process. This is useful when you want to copy binary files. |`generator`| [String] | A string representing the Generator version-range the template is compatible with. This value must follow the [semver](https://docs.npmjs.com/misc/semver) syntax. E.g., `>=1.0.0`, `>=1.0.0 <=2.0.0`, `~1.0.0`, `^1.0.0`, `1.0.0`, etc. +|`filters`| [String] | A list of modules containing functions that can be uses as Nunjucks filters. In case of external modules, remember they need to be added as a dependency in `package.json` of your template. ### Example @@ -190,7 +166,10 @@ The `.tp-config.json` file contains a JSON object that may have the following in "src/api/middlewares/*.*", "lib/lib/config.js" ], - "generator": "<2.0.0" + "generator": "<2.0.0", + "filters": [ + "@asyncapi/generator-filters" + ] } ``` diff --git a/lib/filtersRegistry.js b/lib/filtersRegistry.js new file mode 100644 index 000000000..aa7bb0c6e --- /dev/null +++ b/lib/filtersRegistry.js @@ -0,0 +1,90 @@ +const path = require('path'); +const fs = require('fs'); +const xfs = require('fs.extra'); +const { isLocalTemplate } = require('./utils'); + +/** + * Registers all template filters. + * @param {Object} nunjucks Nunjucks environment. + * @param {Object} templateConfig Template configuration. + * @param {String} templateDir Directory where template is located. + * @param {String} filtersDir Directory where local filters are located. + */ +module.exports.registerFilters = async (nunjucks, templateConfig, templateDir, filtersDir) => { + await registerLocalFilters(nunjucks, templateDir, filtersDir); + await registerConfigFilters(nunjucks, templateDir, templateConfig); +}; + +/** + * Registers the local template filters. + * @private + * @param {Object} nunjucks Nunjucks environment. + * @param {String} templateDir Directory where template is located. + * @param {String} filtersDir Directory where local filters are located. + */ +function registerLocalFilters(nunjucks, templateDir, filtersDir) { + return new Promise((resolve, reject) => { + const localFilters = path.resolve(templateDir, filtersDir); + + if (!fs.existsSync(localFilters)) return resolve(); + + const walker = xfs.walk(localFilters, { + followLinks: false + }); + + walker.on('file', async (root, stats, next) => { + try { + const filePath = path.resolve(templateDir, path.resolve(root, stats.name)); + // If it's a module constructor, inject dependencies to ensure consistent usage in remote templates in other projects or plain directories. + const mod = require(filePath); + addFilters(nunjucks, mod); + next(); + } catch (e) { + reject(e); + } + }); + + walker.on('errors', (root, nodeStatsArray) => { + reject(nodeStatsArray); + }); + + walker.on('end', async () => { + resolve(); + }); + }); +} + +/** +* Registers the additionally configured filters. +* @private +* @param {Object} nunjucks Nunjucks environment. +* @param {String} templateDir Directory where template is located. +* @param {Object} templateConfig Template configuration. +*/ +async function registerConfigFilters(nunjucks, templateDir, templateConfig) { + const confFilters = templateConfig.filters; + const DEFAULT_MODULES_DIR = 'node_modules'; + if (!Array.isArray(confFilters)) return; + + confFilters.forEach(async el => { + const modulePath = await isLocalTemplate(templateDir) ? path.resolve(templateDir, DEFAULT_MODULES_DIR, el) : el; + const mod = require(modulePath); + + addFilters(nunjucks, mod); + }); +} + +/** + * Add filter functions to Nunjucks environment. Only owned functions from the module are added. + * @private + * @param {Object} nunjucks Nunjucks environment. + * @param {Object} filters Module with functions. + */ +function addFilters(nunjucks, filters) { + Object.getOwnPropertyNames(filters).forEach((key) => { + const value = filters[key]; + if (!(value instanceof Function)) return; + + nunjucks.addFilter(key, value); + }); +} diff --git a/lib/generator.js b/lib/generator.js index 096ff15a6..45ef3ddfd 100644 --- a/lib/generator.js +++ b/lib/generator.js @@ -25,8 +25,9 @@ const { readDir, writeFile, copyFile, - exists, + exists } = require('./utils'); +const { registerFilters } = require('./filtersRegistry'); const ajv = new Ajv({ allErrors: true }); @@ -145,7 +146,7 @@ class Generator { this.configNunjucks(); await this.loadTemplateConfig(); this.registerHooks(); - await this.registerFilters(); + await registerFilters(this.nunjucks, this.templateConfig, this.templateDir, FILTERS_DIRNAME); if (this.entrypoint) { const entrypointPath = path.resolve(this.templateContentDir, this.entrypoint); @@ -329,43 +330,6 @@ class Generator { }); } - /** - * Registers the template filters. - * @private - */ - registerFilters() { - return new Promise((resolve, reject) => { - this.helpersDir = path.resolve(this.templateDir, FILTERS_DIRNAME); - if (!fs.existsSync(this.helpersDir)) return resolve(); - - const walker = xfs.walk(this.helpersDir, { - followLinks: false - }); - - walker.on('file', async (root, stats, next) => { - try { - const filePath = path.resolve(this.templateDir, path.resolve(root, stats.name)); - // If it's a module constructor, inject dependencies to ensure consistent usage in remote templates in other projects or plain directories. - const mod = require(filePath); - if (typeof mod === 'function') { - mod({ Nunjucks: this.nunjucks }); - } - next(); - } catch (e) { - reject(e); - } - }); - - walker.on('errors', (root, nodeStatsArray) => { - reject(nodeStatsArray); - }); - - walker.on('end', async () => { - resolve(); - }); - }); - } - /** * Returns all the parameters on the AsyncAPI document. * @@ -647,7 +611,6 @@ class Generator { */ configNunjucks() { this.nunjucks = new Nunjucks.Environment(new Nunjucks.FileSystemLoader(this.templateDir)); - this.nunjucks.addFilter('log', console.log); } /** @@ -667,7 +630,6 @@ class Generator { } catch (e) { this.templateConfig = {}; } - await this.validateTemplateConfig(); } diff --git a/lib/utils.js b/lib/utils.js index d4ef4a962..332ed59fd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -80,4 +80,4 @@ utils.getLocalTemplateDetails = async (templatePath) => { link: linkTarget, resolvedLink: path.resolve(path.dirname(templatePath), linkTarget), }; -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index ec468c14f..9820cdb26 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "release": "semantic-release", "docker-build": "docker build -t asyncapi/generator:latest .", "get-version": "echo $npm_package_version", - "eslint": "eslint --config .eslintrc ." + "lint": "eslint --config .eslintrc ." }, "preferGlobal": true, "bugs": {