├── .eslintignore ├── templates ├── .npmignore ├── __.gitignore ├── README.md ├── src │ ├── api │ │ ├── middlewares │ │ │ ├── buffer2string.js │ │ │ ├── string2json.js │ │ │ └── logger.js │ │ ├── routes │ │ │ └── ___route.js │ │ ├── index.js │ │ ├── adapters │ │ │ └── ___adapter.js │ │ └── services │ │ │ └── ___service.js │ └── lib │ │ └── config.js ├── .editorconfig ├── Dockerfile ├── package.json ├── config │ └── common.yml └── .eslintrc ├── .npmignore ├── logo.ai ├── logo.png ├── .travis.yml ├── .gitignore ├── tests ├── test.js └── sample.yml ├── .editorconfig ├── lib ├── codegen.js ├── parser.js ├── beautifier.js ├── helpers │ └── handlebars.js └── generator.js ├── package.json ├── cli.js ├── .eslintrc └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | .DS_Store 3 | *.swp 4 | -------------------------------------------------------------------------------- /templates/__.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asyncapi-archived-repos/node-codegen/HEAD/logo.ai -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asyncapi-archived-repos/node-codegen/HEAD/logo.png -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | # {{asyncapi.info.title}} 2 | 3 | {{asyncapi.info.description}} 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: 5 | - npm install 6 | - npm run test 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | tests/generated 5 | tests/generated-yaml 6 | tests/generated-json 7 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const process = require('../lib/codegen').process; 3 | 4 | process(path.resolve(__dirname, 'sample.yml'), path.resolve(__dirname, './output/')); 5 | -------------------------------------------------------------------------------- /templates/src/api/middlewares/buffer2string.js: -------------------------------------------------------------------------------- 1 | module.exports = (message, next) => { 2 | if (message.payload instanceof Buffer) { 3 | message.payload = message.payload.toString(); 4 | } 5 | 6 | next(); 7 | }; 8 | -------------------------------------------------------------------------------- /templates/src/lib/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const yamlConfig = require('node-yaml-config'); 3 | 4 | const config = yamlConfig.load(path.join(__dirname, '../../config/common.yml')); 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /templates/src/api/middlewares/string2json.js: -------------------------------------------------------------------------------- 1 | module.exports = (message, next) => { 2 | try { 3 | message.payload = JSON.parse(message.payload); 4 | } catch (e) { 5 | // We did our best... 6 | } 7 | 8 | next(); 9 | }; 10 | -------------------------------------------------------------------------------- /templates/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /templates/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7-alpine 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | # Install app dependencies 8 | COPY package.json /usr/src/app/ 9 | RUN npm install 10 | 11 | # Copy app source 12 | COPY . /usr/src/app 13 | 14 | CMD [ "npm", "start" ] 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /templates/src/api/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | function yellow (text) { 4 | return `\x1b[33m${text}\x1b[0m`; 5 | } 6 | 7 | module.exports = (message, next) => { 8 | const action = message.from.broker ? 'received from' : 'sent to'; 9 | console.log(`${yellow(message.topic)} was ${action} broker:`); 10 | console.log(util.inspect(message.payload, { depth: null, colors: true })); 11 | next(); 12 | }; 13 | -------------------------------------------------------------------------------- /templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{snakecase asyncapi.info.title}}", 3 | "description": "{{asyncapi.__descriptionInOneLine}}", 4 | "version": "{{asyncapi.info.version}}", 5 | "scripts": { 6 | "start": "node src/api/index.js" 7 | }, 8 | "dependencies": { 9 | "hermesjs": "1.x", 10 | "hermesjs-router": "1.x", 11 | {{#each asyncapi.__schemes as |scheme|}} 12 | "hermesjs-{{scheme}}": "1.x", 13 | {{/each}} 14 | "node-yaml-config": "0.0.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/codegen.js: -------------------------------------------------------------------------------- 1 | const parse = require('./parser'); 2 | const generate = require('./generator'); 3 | const beautify = require('./beautifier'); 4 | const codegen = module.exports = {}; 5 | 6 | codegen.process = async (filePath, targetDir, customTemplateDir) => { 7 | const json = await parse(filePath); 8 | const asyncapi = beautify(json); 9 | const templatesDir = customTemplateDir ? '../' + customTemplateDir : '../templates'; 10 | 11 | await generate.package(asyncapi, targetDir, templatesDir); 12 | await generate.APIindex(asyncapi, targetDir, templatesDir); 13 | await generate.adapters(asyncapi, targetDir, templatesDir); 14 | await generate.topics(asyncapi, targetDir, templatesDir); 15 | await generate.config(asyncapi, targetDir, templatesDir); 16 | await generate.readme(asyncapi, targetDir, templatesDir); 17 | await generate.static(targetDir, templatesDir); 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /templates/src/api/routes/___route.js: -------------------------------------------------------------------------------- 1 | const router = require('hermesjs-router')(); 2 | const {{service}} = require('../services/{{service}}'); 3 | 4 | {{#each asyncapi.topics as |topic topicName|}} 5 | {{#if topic.publish}} 6 | {{#if topic.publish.descriptionLines}} 7 | /** 8 | {{#each topic.publish.descriptionLines}} 9 | * {{this}} 10 | {{/each}} 11 | */ 12 | {{/if}} 13 | router.use('{{dotsToSlashes topicName}}', async (message, next) => { 14 | await {{topic.serviceName}}.{{topic.publish.operationId}}({message}); 15 | next(); 16 | }); 17 | 18 | {{/if}} 19 | {{#if topic.subscribe}} 20 | {{#if topic.subscribe.descriptionLines}} 21 | /** 22 | {{#each topic.subscribe.descriptionLines}} 23 | * {{this}} 24 | {{/each}} 25 | */ 26 | {{/if}} 27 | router.use('{{dotsToSlashes topicName}}', async (message, next) => { 28 | await {{topic.serviceName}}.{{topic.subscribe.operationId}}({message}); 29 | next(); 30 | }); 31 | 32 | {{/if}} 33 | {{/each}} 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /templates/src/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const hermes = require('hermesjs')(); 4 | const buffer2string = require('./middlewares/buffer2string'); 5 | const string2json = require('./middlewares/string2json'); 6 | const logger = require('./middlewares/logger'); 7 | {{#each asyncapi.__schemes as |scheme|}} 8 | const {{capitalize scheme}}Adapter = require('./adapters/{{scheme}}'); 9 | {{/each}} 10 | {{#each asyncapi.__services as |service|}} 11 | const {{service}} = require('./routes/{{service}}.js'); 12 | {{/each}} 13 | 14 | {{#each asyncapi.__schemes as |scheme|}} 15 | hermes.add('broker', {{capitalize scheme}}Adapter); 16 | {{/each}} 17 | 18 | hermes.on('broker:ready', ({name}) => { 19 | console.log(`${name} is listening...`); 20 | }); 21 | 22 | hermes.use(buffer2string); 23 | hermes.use(string2json); 24 | hermes.use(logger); 25 | 26 | {{#each asyncapi.__services as |service|}} 27 | hermes.use({{service}}); 28 | {{/each}} 29 | 30 | hermes.use((err, message, next) => { 31 | console.error(err); 32 | next(); 33 | }); 34 | 35 | hermes.listen(); 36 | -------------------------------------------------------------------------------- /templates/config/common.yml: -------------------------------------------------------------------------------- 1 | default: 2 | broker: 3 | {{#each asyncapi.__schemes as |scheme|}} 4 | {{#compare scheme '===' 'amqp'}} 5 | amqp: 6 | exchange: 7 | username: 8 | password: 9 | host: localhost 10 | port: 11 | topic: {{../../asyncapi.__commonTopic}} 12 | queue: {{queueName ../../asyncapi.info.title ../../asyncapi.info.version}} 13 | queue_options: 14 | exclusive: false 15 | durable: true 16 | autoDelete: true 17 | {{/compare}} 18 | {{#compare scheme '===' 'mqtt'}} 19 | mqtt: 20 | host_url: mqtt://localhost 21 | topics: {{toMQTT ../../asyncapi.__commonTopic}} 22 | qos: 23 | protocol: mqtt 24 | retain: 25 | {{/compare}} 26 | {{/each}} 27 | 28 | 29 | development: 30 | 31 | test: 32 | 33 | staging: 34 | 35 | production: 36 | broker: 37 | {{#each asyncapi.__schemes as |scheme|}} 38 | {{#compare scheme '===' 'amqp'}} 39 | amqp: 40 | host: {{../../asyncapi.host}} 41 | {{/compare}} 42 | {{#compare scheme '===' 'mqtt'}} 43 | mqtt: 44 | host_url: mqtt://{{../../asyncapi.host}} 45 | {{/compare}} 46 | {{/each}} 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asyncapi-node-codegen", 3 | "version": "1.3.2", 4 | "description": "An AsyncAPI codegen for Node.js", 5 | "scripts": { 6 | "test": "node tests/test" 7 | }, 8 | "bin": { 9 | "asyncapi-node-codegen": "./cli.js", 10 | "anc": "./cli.js" 11 | }, 12 | "preferGlobal": true, 13 | "bugs": { 14 | "url": "https://github.com/asyncapi/node-codegen/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/asyncapi/node-codegen.git" 19 | }, 20 | "keywords": [ 21 | "asyncapi", 22 | "codegen", 23 | "generator" 24 | ], 25 | "author": { 26 | "name": "Fran Mendez", 27 | "email": "fmvilas@gmail.com" 28 | }, 29 | "homepage": "https://github.com/asyncapi/node-codegen", 30 | "dependencies": { 31 | "asyncapi": "1.0.0", 32 | "asyncapi-topic-parser": "^0.2.0", 33 | "commander": "^2.12.2", 34 | "handlebars": "^4.0.6", 35 | "js-yaml": "^3.8.3", 36 | "json-schema-ref-parser": "^3.1.2", 37 | "lodash": "^4.17.4", 38 | "mkdirp": "^0.5.1", 39 | "word-wrap": "^1.2.1", 40 | "z-schema": "^3.18.2" 41 | }, 42 | "devDependencies": {} 43 | } 44 | -------------------------------------------------------------------------------- /templates/src/api/adapters/___adapter.js: -------------------------------------------------------------------------------- 1 | const config = require('../../lib/config'); 2 | {{#compare adapter '===' 'amqp' }} 3 | const hermesAMQP = require('hermesjs-amqp'); 4 | 5 | const adapter = hermesAMQP({ 6 | exchange: config.broker.amqp.exchange, 7 | username: config.broker.amqp.username, 8 | password: config.broker.amqp.password, 9 | host: config.broker.amqp.host, 10 | port: config.broker.amqp.port, 11 | topic: config.broker.amqp.topic, 12 | queue: config.broker.amqp.queue, 13 | queue_options: config.broker.amqp.queue_options, 14 | subscribe: {{shouldSubscribe ../asyncapi}} // ATTENTION: If subscribe is true you might receive the messages you send. 15 | }); 16 | {{/compare}} 17 | {{#compare adapter '===' 'mqtt' }} 18 | const hermesMQTT = require('hermesjs-mqtt'); 19 | 20 | const adapter = hermesMQTT({ 21 | host_url: config.broker.mqtt.host_url, 22 | topics: config.broker.mqtt.topics, 23 | qos: config.broker.mqtt.qos, 24 | protocol: config.broker.mqtt.protocol, 25 | retain: config.broker.mqtt.retain, 26 | subscribe: {{shouldSubscribe ../asyncapi}} // ATTENTION: If subscribe is true you might receive the messages you send. 27 | }); 28 | {{/compare}} 29 | 30 | module.exports = adapter; 31 | -------------------------------------------------------------------------------- /templates/src/api/services/___service.js: -------------------------------------------------------------------------------- 1 | const {{service}} = module.exports = {}; 2 | 3 | {{#each asyncapi.topics as |topic topicName|}} 4 | {{#if topic.publish}} 5 | /** 6 | {{#if topic.publish.descriptionLines}} 7 | {{#each topic.publish.descriptionLines}} 8 | * {{this}} 9 | {{/each}} 10 | * 11 | {{/if}} 12 | * @param {Object} options 13 | * @param {Object} options.message 14 | {{#if topic.publish.headers}} 15 | {{#each topic.publish.headers.properties as |field fieldName|}} 16 | {{{docline field fieldName 'options.message.headers'}}} 17 | {{/each}} 18 | {{/if}} 19 | {{#each topic.publish.payload.properties as |field fieldName|}} 20 | {{{docline field fieldName 'options.message.payload'}}} 21 | {{/each}} 22 | */ 23 | {{topic.serviceName}}.{{topic.publish.operationId}} = async ({message}) => { 24 | // Implement your business logic here... 25 | }; 26 | 27 | {{/if}} 28 | {{#if topic.subscribe}} 29 | /** 30 | {{#if topic.subscribe.descriptionLines}} 31 | {{#each topic.subscribe.descriptionLines}} 32 | * {{this}} 33 | {{/each}} 34 | * 35 | {{/if}} 36 | * @param {Object} options 37 | * @param {Object} options.message 38 | {{#if topic.subscribe.headers}} 39 | {{#each topic.subscribe.headers.properties as |field fieldName|}} 40 | {{{docline field fieldName 'options.message.headers'}}} 41 | {{/each}} 42 | {{/if}} 43 | {{#each topic.subscribe.payload.properties as |field fieldName|}} 44 | {{{docline field fieldName 'options.message.payload'}}} 45 | {{/each}} 46 | */ 47 | {{topic.serviceName}}.{{topic.subscribe.operationId}} = async ({message}) => { 48 | // Implement your business logic here... 49 | }; 50 | 51 | {{/if}} 52 | {{/each}} 53 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const program = require('commander'); 5 | const mkdirp = require('mkdirp'); 6 | const packageInfo = require('./package.json'); 7 | const generate = require('./lib/codegen').process; 8 | 9 | const red = text => `\x1b[31m${text}\x1b[0m`; 10 | const magenta = text => `\x1b[35m${text}\x1b[0m`; 11 | const yellow = text => `\x1b[33m${text}\x1b[0m`; 12 | const green = text => `\x1b[32m${text}\x1b[0m`; 13 | 14 | let asyncAPIFile; 15 | 16 | const parseOutput = dir => path.resolve(dir); 17 | 18 | const showError = err => { 19 | console.error(red('Something went wrong:')); 20 | console.error(red(err.stack || err.message)); 21 | }; 22 | 23 | program 24 | .version(packageInfo.version) 25 | .arguments('') 26 | .action((asyncAPIFilePath) => { 27 | asyncAPIFile = path.resolve(asyncAPIFilePath); 28 | }) 29 | .option('-o, --output ', 'directory where to put the generated files (defaults to current directory)', parseOutput, process.cwd()) 30 | .option('-t, --templates ', 'directory where templates are located (defaults to internal nodejs templates)') 31 | .parse(process.argv); 32 | 33 | if (!asyncAPIFile) { 34 | console.error(red('> Path to AsyncAPI file not provided.')); 35 | program.help(); // This exits the process 36 | } 37 | 38 | mkdirp(program.output, err => { 39 | if (err) return showError(err); 40 | 41 | generate(asyncAPIFile, program.output, program.templates).then(() => { 42 | console.log(green('Done! ✨')); 43 | console.log(yellow('Check out your shiny new API at ') + magenta(program.output) + yellow('.')); 44 | }).catch(showError); 45 | }); 46 | 47 | process.on('unhandledRejection', showError); 48 | -------------------------------------------------------------------------------- /templates/.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 'eslint:recommended' 2 | 3 | parserOptions: 4 | ecmaVersion: 8 5 | 6 | env: 7 | node: true 8 | es6: true 9 | 10 | ecmaFeatures: 11 | forOf: true 12 | modules: true 13 | 14 | rules: 15 | # Possible Errors 16 | no-console: 0 17 | valid-jsdoc: [0, {requireReturn: false, requireParamDescription: false, requireReturnDescription: false}] 18 | 19 | # Best Practices 20 | consistent-return: 0 21 | curly: 0 22 | block-scoped-var: 2 23 | no-else-return: 2 24 | no-process-env: 2 25 | no-self-compare: 2 26 | no-throw-literal: 2 27 | no-void: 2 28 | radix: 2 29 | wrap-iife: [2, outside] 30 | 31 | # Variables 32 | no-shadow: 0 33 | no-use-before-define: [2, nofunc] 34 | 35 | # Node.js 36 | no-process-exit: 0 37 | handle-callback-err: [2, err] 38 | no-new-require: 2 39 | no-path-concat: 2 40 | 41 | # Stylistic Issues 42 | quotes: [2, single] 43 | camelcase: 0 44 | indent: [2, 2] 45 | no-lonely-if: 2 46 | no-floating-decimal: 2 47 | brace-style: [2, 1tbs, { "allowSingleLine": true }] 48 | comma-style: [2, last] 49 | consistent-this: [0, self] 50 | func-style: 0 51 | max-nested-callbacks: 0 52 | new-cap: 2 53 | no-multiple-empty-lines: [2, {max: 1}] 54 | no-nested-ternary: 2 55 | semi-spacing: [2, {before: false, after: true}] 56 | operator-assignment: [2, always] 57 | padded-blocks: [2, never] 58 | quote-props: [2, as-needed] 59 | space-before-function-paren: [2, always] 60 | keyword-spacing: [2, {"before": true, "after": true}] 61 | space-before-blocks: [2, always] 62 | array-bracket-spacing: [2, never] 63 | computed-property-spacing: [2, never] 64 | space-in-parens: [2, never] 65 | space-unary-ops: [2, {words: true, nonwords: false}] 66 | #spaced-line-comment: [2, always] 67 | wrap-regex: 2 68 | linebreak-style: [2, unix] 69 | semi: [2, always] 70 | 71 | # ECMAScript 6 72 | arrow-spacing: [2, {before: true, after: true}] 73 | no-class-assign: 2 74 | no-const-assign: 2 75 | no-dupe-class-members: 2 76 | no-this-before-super: 2 77 | no-var: 2 78 | object-shorthand: [2, always] 79 | prefer-arrow-callback: 2 80 | prefer-const: 2 81 | prefer-spread: 2 82 | prefer-template: 2 83 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 2017 3 | 4 | env: 5 | node: true 6 | es6: true 7 | 8 | ecmaFeatures: 9 | forOf: true 10 | 11 | rules: 12 | # Ignore Rules 13 | strict: 0 14 | no-underscore-dangle: 0 15 | no-mixed-requires: 0 16 | no-process-exit: 0 17 | no-warning-comments: 0 18 | curly: 0 19 | no-multi-spaces: 0 20 | no-alert: 0 21 | consistent-return: 0 22 | consistent-this: [0, self] 23 | func-style: 0 24 | max-nested-callbacks: 0 25 | 26 | # Warnings 27 | no-debugger: 1 28 | no-empty: 1 29 | no-invalid-regexp: 1 30 | no-unused-expressions: 1 31 | no-native-reassign: 1 32 | no-fallthrough: 1 33 | camelcase: 0 34 | 35 | # Errors 36 | eqeqeq: 2 37 | no-undef: 2 38 | no-dupe-keys: 2 39 | no-empty-character-class: 2 40 | no-self-compare: 2 41 | valid-typeof: 2 42 | no-unused-vars: [2, { "args": "none" }] 43 | handle-callback-err: 2 44 | no-shadow-restricted-names: 2 45 | no-new-require: 2 46 | no-mixed-spaces-and-tabs: 2 47 | block-scoped-var: 2 48 | no-else-return: 2 49 | no-throw-literal: 2 50 | no-void: 2 51 | radix: 2 52 | wrap-iife: [2, outside] 53 | no-shadow: 0 54 | no-use-before-define: [2, nofunc] 55 | no-path-concat: 2 56 | valid-jsdoc: [0, {requireReturn: false, requireParamDescription: false, requireReturnDescription: false}] 57 | 58 | # stylistic errors 59 | no-spaced-func: 2 60 | semi-spacing: 2 61 | quotes: [2, 'single'] 62 | key-spacing: [2, { beforeColon: false, afterColon: true }] 63 | indent: [2, 2] 64 | no-lonely-if: 2 65 | no-floating-decimal: 2 66 | brace-style: [2, 1tbs, { allowSingleLine: true }] 67 | comma-style: [2, last] 68 | new-cap: [2, {capIsNewExceptions: ["express.Router"]}] 69 | no-multiple-empty-lines: [2, {max: 1}] 70 | no-nested-ternary: 2 71 | operator-assignment: [2, always] 72 | padded-blocks: [2, never] 73 | quote-props: [2, as-needed] 74 | space-before-function-paren: [2, always] 75 | keyword-spacing: [2, {'before': true, 'after': true, 'overrides': {}}] 76 | space-before-blocks: [2, always] 77 | array-bracket-spacing: [2, never] 78 | computed-property-spacing: [2, never] 79 | space-in-parens: [2, never] 80 | space-unary-ops: [2, {words: true, nonwords: false}] 81 | wrap-regex: 2 82 | linebreak-style: [2, unix] 83 | semi: [2, always] 84 | arrow-spacing: [2, {before: true, after: true}] 85 | no-class-assign: 2 86 | no-const-assign: 2 87 | no-dupe-class-members: 2 88 | no-this-before-super: 2 89 | no-var: 2 90 | object-shorthand: [2, always] 91 | prefer-arrow-callback: 2 92 | prefer-const: 2 93 | prefer-spread: 2 94 | prefer-template: 2 95 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const ZSchema = require('z-schema'); 4 | const YAML = require('js-yaml'); 5 | const RefParser = require('json-schema-ref-parser'); 6 | const asyncAPIschema = require('asyncapi'); 7 | 8 | const validator = new ZSchema(); 9 | 10 | async function getFileContent (filePath) { 11 | return new Promise((resolve, reject) => { 12 | fs.readFile(path.resolve(__dirname, filePath), (err, content) => { 13 | if (err) return reject(err); 14 | resolve(content); 15 | }); 16 | }); 17 | } 18 | 19 | function parseContent (content) { 20 | content = content.toString('utf8'); 21 | try { 22 | return JSON.parse(content); 23 | } catch (e) { 24 | return YAML.safeLoad(content); 25 | } 26 | } 27 | 28 | async function dereference (json) { 29 | return RefParser.dereference(json, { 30 | dereference: { 31 | circular: 'ignore' 32 | } 33 | }); 34 | } 35 | 36 | async function bundle (json) { 37 | return RefParser.bundle(json, { 38 | dereference: { 39 | circular: 'ignore' 40 | } 41 | }); 42 | } 43 | 44 | async function validate (json, schema) { 45 | return new Promise((resolve, reject) => { 46 | validator.validate(json, schema, (err, valid) => { 47 | if (err) return reject(err); 48 | return resolve(json); 49 | }); 50 | }); 51 | } 52 | 53 | async function parse (filePath) { 54 | let content, parsedContent, dereferencedJSON, bundledJSON, parsed; 55 | 56 | try { 57 | content = await getFileContent(filePath); 58 | } catch (e) { 59 | console.error('Can not load the content of the AsyncAPI specification file'); 60 | console.error(e); 61 | return; 62 | } 63 | 64 | try { 65 | parsedContent = parseContent(content); 66 | } catch (e) { 67 | console.error('Can not parse the content of the AsyncAPI specification file'); 68 | console.error(e); 69 | return; 70 | } 71 | 72 | try { 73 | dereferencedJSON = await dereference(parsedContent); 74 | } catch (e) { 75 | console.error('Can not dereference the JSON obtained from the content of the AsyncAPI specification file'); 76 | console.error(e); 77 | return; 78 | } 79 | 80 | try { 81 | bundledJSON = await bundle(dereferencedJSON); 82 | } catch (e) { 83 | console.error('Can not bundle the JSON obtained from the content of the AsyncAPI specification file'); 84 | console.error(e); 85 | return; 86 | } 87 | 88 | try { 89 | parsed = await validate(bundledJSON, asyncAPIschema); 90 | } catch (e) { 91 | console.error('Invalid JSON obtained from the content of the AsyncAPI specification file'); 92 | console.error(e); 93 | return; 94 | } 95 | 96 | return JSON.parse(JSON.stringify(parsed)); 97 | }; 98 | 99 | module.exports = parse; 100 | -------------------------------------------------------------------------------- /lib/beautifier.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const wrap = require('word-wrap'); 3 | const topicParser = require('asyncapi-topic-parser'); 4 | 5 | const getOperationId = (parsedTopic) => { 6 | const name = `${parsedTopic.resources.join('-')}-${parsedTopic.operation}`; 7 | const id = parsedTopic.status ? `${name}-${parsedTopic.status}` : name; 8 | return _.camelCase(parsedTopic.type === 'event' ? `on-${id}` : id); 9 | }; 10 | 11 | const sharedStart = (array) => { 12 | const A = array.concat().sort(); 13 | const a1 = A[0], a2= A[A.length-1], L= a1.length; 14 | let i = 0; 15 | while (i { 20 | return `\x1b[33m${text}\x1b[0m`; 21 | }; 22 | 23 | module.exports = (asyncapi) => { 24 | const services = []; 25 | const usedOperations = []; 26 | asyncapi.baseTopic = asyncapi.baseTopic || ''; 27 | asyncapi.__descriptionInOneLine = (asyncapi.info.description || '').replace(/\n/g, ' '); 28 | 29 | asyncapi.__schemes = {}; 30 | 31 | _.each(asyncapi.servers || [], server => { 32 | asyncapi.__schemes[server.scheme.toLowerCase()] = 1; 33 | }); 34 | 35 | asyncapi.__schemes = Object.keys(asyncapi.__schemes); 36 | 37 | _.each(asyncapi.topics, (topic, topicName) => { 38 | const baseTopic = asyncapi.baseTopic.trim(); 39 | 40 | let newTopicName = `${baseTopic}.${topicName}`; 41 | newTopicName = newTopicName.replace(/\{(.*)\}/, ':$1'); 42 | 43 | if (newTopicName !== topicName) { 44 | asyncapi.topics[newTopicName] = topic; 45 | delete asyncapi.topics[topicName]; 46 | topicName = newTopicName; 47 | } 48 | 49 | const parsedTopic = topicParser.parse(topicName); 50 | 51 | _.each(topic, (operation, operationName) => { 52 | if (!usedOperations.includes(operationName)) usedOperations.push(operationName); 53 | operation['operationId'] = operation['operationId'] || getOperationId(parsedTopic); 54 | const description = operation['summary'] || operation['description'] || ''; 55 | const descriptionLines = description ? wrap(description, { width: 60, indent: '' }).split(/\n/) : []; 56 | operation['descriptionLines'] = descriptionLines; 57 | }); 58 | 59 | topic['serviceName'] = parsedTopic.service; 60 | if (!services.includes(parsedTopic.service)) services.push(parsedTopic.service); 61 | 62 | if (topic.publish && topic.subscribe) { 63 | console.log(yellow('WARNING:'), `Topic ${topicName} is using publish and subscribe operations. It means that you'll receive the messages you publish. Please double-check this is the behaviour you want achieve.`); 64 | } 65 | }); 66 | 67 | asyncapi.__usedOperations = usedOperations; 68 | asyncapi.__services = services; 69 | 70 | const commonPrefix = sharedStart(Object.keys(asyncapi.topics)); 71 | const levels = commonPrefix.split('.').length - 1; 72 | asyncapi.__commonPrefix = commonPrefix.split('.').slice(0, levels).join('.'); 73 | asyncapi.__commonTopic = `${asyncapi.__commonPrefix}.#`; 74 | 75 | return asyncapi; 76 | }; 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | 3 | This package is deprecated and maintanance has been stopped in favor of the Node.js template at [github.com/asyncapi/generator](https://github.com/asyncapi/generator). 4 | 5 | --- 6 | 7 |

8 |

9 | AsyncAPI Node.js
Code Generator
10 |

11 |

12 | Use your AsyncAPI definition to
generate the code for your API.
13 |

14 |

15 | 16 |

17 |

18 | 19 | 20 | The generated code features: 21 | 22 | - Default Node.js template, featuring: 23 | * ES7 24 | * ESLint 25 | * YAML config file 26 | * Hermes 27 | * No transpiling 28 | - Custom templates. Check `--templates` option in the [Usage section](#usage). Kudos to [@jantoniucci](https://github.com/jantoniucci). 29 | 30 | ## Install 31 | 32 | To use it from the CLI: 33 | 34 | ```bash 35 | npm install -g asyncapi-node-codegen 36 | ``` 37 | 38 | To use it as a module in your project: 39 | 40 | ```bash 41 | npm install --save asyncapi-node-codegen 42 | ``` 43 | 44 | ## Usage 45 | 46 | ### From the command-line interface (CLI) 47 | 48 | ```bash 49 | Usage: anc [options] 50 | 51 | 52 | Options: 53 | 54 | -V, --version output the version number 55 | -o, --output directory where to put the generated files (defaults to current directory) 56 | -t, --templates directory where templates are located (defaults to internal nodejs templates) 57 | -h, --help output usage information 58 | ``` 59 | 60 | #### Examples 61 | 62 | The shortest possible syntax: 63 | ```bash 64 | anc asyncapi.yaml 65 | ``` 66 | 67 | Specify where to put the generated code: 68 | ```bash 69 | anc asyncapi.yaml -o ./my-api 70 | ``` 71 | 72 | Specify where to find the code templates: 73 | ```bash 74 | anc asyncapi.yaml -t ../my-specific-templates-dir -o ./my-api 75 | ``` 76 | 77 | ### As a module in your project 78 | 79 | ```js 80 | const path = require('path'); 81 | const codegen = require('asyncapi-node-codegen'); 82 | const asyncapi = '/path/to/asyncapi.yaml'; // Or a path to a JSON file 83 | 84 | codegen.process(asyncapi, path.resolve(__dirname, './my-api')).then(() => { 85 | console.log('Done!'); 86 | }).catch(err => { 87 | console.error(`Something went wrong: ${err.message}`); 88 | }); 89 | ``` 90 | 91 | #### Using async/await 92 | 93 | The function `codegen.process` returns a Promise, so it means you can use async/await: 94 | 95 | ```js 96 | const path = require('path'); 97 | const codegen = require('asyncapi-node-codegen'); 98 | const asyncapi = '/path/to/asyncapi.yaml'; // Or a path to a JSON file 99 | 100 | try { 101 | await codegen.process(asyncapi, path.resolve(__dirname, './my-api')); 102 | console.log('Done!'); 103 | } catch (err) { 104 | console.error(`Something went wrong: ${err.message}`); 105 | } 106 | ``` 107 | 108 | ## Author 109 | 110 | Fran Méndez ([@fmvilas](http://twitter.com/fmvilas)) 111 | -------------------------------------------------------------------------------- /lib/helpers/handlebars.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Handlebars = require('handlebars'); 3 | 4 | Handlebars.registerHelper('equal', (lvalue, rvalue, options) => { 5 | if (arguments.length < 3) 6 | throw new Error('Handlebars Helper equal needs 2 parameters'); 7 | if (lvalue!==rvalue) { 8 | return options.inverse(this); 9 | } 10 | 11 | return options.fn(this); 12 | }); 13 | 14 | Handlebars.registerHelper('validOperation', (operation, options) => { 15 | const authorized_operations = ['PUBLISH', 'SUBSCRIBE']; 16 | 17 | if (arguments.length < 3) 18 | throw new Error('Handlebars Helper validOperation needs 1 parameter'); 19 | if (!authorized_operations.includes(operation.toUpperCase())) { 20 | return options.inverse(this); 21 | } 22 | 23 | return options.fn(this); 24 | }); 25 | 26 | Handlebars.registerHelper('match', (lvalue, rvalue, options) => { 27 | if (arguments.length < 3) 28 | throw new Error('Handlebars Helper match needs 2 parameters'); 29 | if (!lvalue.match(rvalue)) { 30 | return options.inverse(this); 31 | } 32 | 33 | return options.fn(this); 34 | }); 35 | 36 | Handlebars.registerHelper('compare', (lvalue, operator, rvalue, options) => { 37 | if (arguments.length < 4) throw new Error('Handlerbars Helper "compare" needs 3 parameters'); 38 | 39 | const operators = { 40 | '==': (l,r) => { return l == r; }, 41 | '===': (l,r) => { return l === r; }, 42 | '!=': (l,r) => { return l != r; }, 43 | '<': (l,r) => { return l < r; }, 44 | '>': (l,r) => { return l > r; }, 45 | '<=': (l,r) => { return l <= r; }, 46 | '>=': (l,r) => { return l >= r; }, 47 | typeof: (l,r) => { return typeof l === r; } 48 | }; 49 | 50 | if (!operators[operator]) throw new Error(`Handlerbars Helper 'compare' doesn't know the operator ${operator}`); 51 | const result = operators[operator](lvalue,rvalue); 52 | 53 | if (result) return options.fn(this); 54 | 55 | return options.inverse(this); 56 | }); 57 | 58 | Handlebars.registerHelper('capitalize', (str) => { 59 | return _.capitalize(str); 60 | }); 61 | 62 | Handlebars.registerHelper('toMQTT', (str) => { 63 | return String(str).split('.').join('/'); 64 | }); 65 | 66 | Handlebars.registerHelper('snakecase', (str) => { 67 | return _.snakeCase(String(str).toLowerCase()); 68 | }); 69 | 70 | Handlebars.registerHelper('kebabcase', (str) => { 71 | return _.kebabCase(String(str).toLowerCase()); 72 | }); 73 | 74 | Handlebars.registerHelper('queueName', (title, version) => { 75 | return _.kebabCase(`${title}-${version}`.toLowerCase()).split('-').join('.'); 76 | }); 77 | 78 | Handlebars.registerHelper('shouldSubscribe', (asyncapi) => { 79 | return asyncapi.__usedOperations.includes('subscribe'); 80 | }); 81 | 82 | Handlebars.registerHelper('docline', (field, fieldName, scopePropName) => { 83 | const buildLine = (f, fName, pName) => { 84 | const type = f.type ? _.capitalize(f.type) : 'String'; 85 | const description = f.description ? ` - ${f.description.replace(/\r?\n|\r/g, '')}` : ''; 86 | let def = f.default; 87 | 88 | if (def && type === 'String') def = `'${def}'`; 89 | 90 | let line; 91 | if (def !== undefined) { 92 | line = ` * @param {${type}} [${pName ? `${pName}.` : ''}${fName}=${def}]`; 93 | } else { 94 | line = ` * @param {${type}} ${pName ? `${pName}.` : ''}${fName}`; 95 | } 96 | 97 | if (type === 'Object') { 98 | let lines = `${line}\n`; 99 | let first = true; 100 | for (const propName in f.properties) { 101 | lines = `${lines}${first ? '' : '\n'}${buildLine(f.properties[propName], propName, `${pName ? `${pName}.` : ''}${fName}`)}`; 102 | first = false; 103 | } 104 | return lines; 105 | } 106 | 107 | return `${line}${description}`; 108 | }; 109 | 110 | return buildLine(field, fieldName, scopePropName); 111 | }); 112 | 113 | Handlebars.registerHelper('dotsToSlashes', (topicName) => { 114 | return topicName.replace(/\./g, '/'); 115 | }); 116 | -------------------------------------------------------------------------------- /tests/sample.yml: -------------------------------------------------------------------------------- 1 | asyncapi: "1.0.0" 2 | info: 3 | title: AsyncAPI Sample 4 | version: "1.0.0" 5 | description: | 6 | This is a simple example of an _AsyncAPI_ document. 7 | termsOfService: https://api.company.com/terms 8 | baseTopic: 'hitch' 9 | 10 | servers: 11 | - url: api.company.com:{port}/{app-id} 12 | description: Allows you to connect using the MQTT protocol. 13 | scheme: mqtt 14 | variables: 15 | app-id: 16 | default: demo 17 | description: You can find your `app-id` in our control panel, under the auth tab. 18 | port: 19 | enum: 20 | - '5676' 21 | - '5677' 22 | default: '5676' 23 | - url: api.company.com:{port}/{app-id} 24 | description: Allows you to connect using the AMQP protocol. 25 | scheme: amqp 26 | variables: 27 | app-id: 28 | default: demo 29 | description: You can find your `app-id` in our control panel, under the auth tab. 30 | port: 31 | enum: 32 | - '5676' 33 | - '5677' 34 | default: '5676' 35 | 36 | topics: 37 | accounts.1.0.action.user.signup: 38 | publish: 39 | $ref: "#/components/messages/userSignUp" 40 | accounts.1.0.event.user.signup: 41 | subscribe: 42 | $ref: "#/components/messages/userSignedUp" 43 | 44 | components: 45 | messages: 46 | userSignUp: 47 | deprecated: true 48 | summary: Action to sign a user up. 49 | description: | 50 | Multiline description of what this action does. **It allows Markdown.** 51 | tags: 52 | - name: user 53 | - name: signup 54 | headers: 55 | type: object 56 | properties: 57 | qos: 58 | $ref: "#/components/schemas/MQTTQoSHeader" 59 | retainFlag: 60 | $ref: "#/components/schemas/MQTTRetainHeader" 61 | payload: 62 | type: object 63 | properties: 64 | user: 65 | $ref: "#/components/schemas/userCreate" 66 | signup: 67 | $ref: "#/components/schemas/signup" 68 | 69 | 70 | userSignedUp: 71 | payload: 72 | type: object 73 | properties: 74 | test: 75 | type: array 76 | items: 77 | type: object 78 | properties: 79 | key1: 80 | type: string 81 | key2: 82 | type: integer 83 | user: 84 | $ref: "#/components/schemas/user" 85 | signup: 86 | $ref: "#/components/schemas/signup" 87 | schemas: 88 | id: 89 | title: id 90 | description: Resource identifier 91 | type: string 92 | username: 93 | title: username 94 | description: User handle 95 | type: string 96 | datetime: 97 | title: datetime 98 | description: Date and Time of the message 99 | type: string 100 | format: date-time 101 | MQTTQoSHeader: 102 | title: qos 103 | description: Quality of Service 104 | type: integer 105 | format: int32 106 | default: 1 107 | enum: 108 | - 0 109 | - 2 110 | MQTTRetainHeader: 111 | title: retainFlag 112 | description: | 113 | This flag determines if the message will be saved by the broker for the specified 114 | topic as last known good value. New clients that subscribe to that topic will receive 115 | the last retained message on that topic instantly after subscribing. More on retained messages 116 | and best practices in one of the next posts. 117 | type: boolean 118 | default: false 119 | user: 120 | type: object 121 | required: 122 | - id 123 | - username 124 | properties: 125 | id: 126 | description: User Id 127 | $ref: "#/components/schemas/id" 128 | full_name: 129 | description: User full name 130 | type: string 131 | username: 132 | $ref: "#/components/schemas/username" 133 | userCreate: 134 | type: object 135 | required: 136 | - username 137 | properties: 138 | full_name: 139 | description: User full name 140 | type: string 141 | username: 142 | $ref: "#/components/schemas/username" 143 | 144 | signup: 145 | type: object 146 | required: 147 | - method 148 | - datetime 149 | properties: 150 | method: 151 | description: Signup method 152 | type: string 153 | enum: 154 | - email 155 | - facebook 156 | - twitter 157 | - github 158 | - google 159 | datetime: 160 | $ref: "#/components/schemas/datetime" 161 | -------------------------------------------------------------------------------- /lib/generator.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const util = require('util'); 4 | const Handlebars = require('handlebars'); 5 | const helpers = require('./helpers/handlebars'); 6 | const _ = require('lodash'); 7 | const mkdirp = require('mkdirp'); 8 | const generate = module.exports = {}; 9 | 10 | generate.package = async (asyncapi, targetDir, templatesDir) => { 11 | fs.readFile(path.resolve(__dirname, `${templatesDir}/package.json`), 'utf8', (err, data) => { 12 | if (err) throw err; 13 | 14 | const targetFile = path.resolve(targetDir, 'package.json'); 15 | const template = Handlebars.compile(data.toString()); 16 | const content = template({ asyncapi }); 17 | 18 | mkdirp(path.dirname(targetFile), err => { 19 | if (err) return console.error(err); 20 | 21 | fs.writeFile(targetFile, content, { encoding: 'utf8', flag: 'w' }, (err) => { 22 | if (err) throw err; 23 | }); 24 | }); 25 | }); 26 | }; 27 | 28 | generate.APIindex = async (asyncapi, targetDir, templatesDir) => { 29 | fs.readFile(path.resolve(__dirname, `${templatesDir}/src/api/index.js`), 'utf8', (err, data) => { 30 | if (err) throw err; 31 | 32 | const targetFile = path.resolve(targetDir, 'src/api/', 'index.js'); 33 | const template = Handlebars.compile(data.toString()); 34 | const content = template({ asyncapi }); 35 | 36 | mkdirp(path.dirname(targetFile), err => { 37 | if (err) return console.error(err); 38 | 39 | fs.writeFile(targetFile, content, { encoding: 'utf8', flag: 'w' }, (err) => { 40 | if (err) throw err; 41 | }); 42 | }); 43 | }); 44 | }; 45 | 46 | generate.adapters = async (asyncapi, targetDir, templatesDir) => { 47 | fs.readFile(path.resolve(__dirname, `${templatesDir}/src/api/adapters/___adapter.js`), 'utf8', (err, data) => { 48 | if (err) throw err; 49 | 50 | asyncapi.__schemes.map(adapter => { 51 | const targetFile = path.resolve(targetDir, 'src/api/adapters/', `${adapter}.js`); 52 | const template = Handlebars.compile(data.toString()); 53 | const content = template({ asyncapi, adapter }); 54 | 55 | mkdirp(path.dirname(targetFile), err => { 56 | if (err) return console.error(err); 57 | 58 | fs.writeFile(targetFile, content, { encoding: 'utf8', flag: 'w' }, (err) => { 59 | if (err) throw err; 60 | }); 61 | }); 62 | }); 63 | }); 64 | }; 65 | 66 | generate.topics = async (asyncapi, targetDir, templatesDir) => { 67 | await generate.routes({asyncapi, targetDir, templatesDir}); 68 | await generate.services({asyncapi, targetDir, templatesDir}); 69 | }; 70 | 71 | generate.routes = async ({asyncapi, targetDir, templatesDir}) => { 72 | fs.readFile(path.resolve(__dirname, `${templatesDir}/src/api/routes/___route.js`), 'utf8', (err, data) => { 73 | if (err) throw err; 74 | 75 | asyncapi.__services.map(service => { 76 | const targetFile = path.resolve(targetDir, 'src/api/routes/', `${service}.js`); 77 | const template = Handlebars.compile(data.toString()); 78 | const content = template({ asyncapi, service }); 79 | 80 | mkdirp(path.dirname(targetFile), err => { 81 | if (err) return console.error(err); 82 | 83 | fs.writeFile(targetFile, content, { encoding: 'utf8', flag: 'w' }, (err) => { 84 | if (err) throw err; 85 | }); 86 | }); 87 | }); 88 | }); 89 | }; 90 | 91 | generate.services = async ({asyncapi, targetDir, templatesDir}) => { 92 | fs.readFile(path.resolve(__dirname, `${templatesDir}/src/api/services/___service.js`), 'utf8', (err, data) => { 93 | if (err) throw err; 94 | 95 | asyncapi.__services.map(service => { 96 | const targetFile = path.resolve(targetDir, 'src/api/services/', `${service}.js`); 97 | const template = Handlebars.compile(data.toString()); 98 | const content = template({ asyncapi, service }); 99 | 100 | mkdirp(path.dirname(targetFile), err => { 101 | if (err) return console.error(err); 102 | 103 | fs.writeFile(targetFile, content, { encoding: 'utf8', flag: 'w' }, (err) => { 104 | if (err) throw err; 105 | }); 106 | }); 107 | }); 108 | }); 109 | }; 110 | 111 | generate.config = async (asyncapi, targetDir, templatesDir) => { 112 | fs.readFile(path.resolve(__dirname, `${templatesDir}/config/common.yml`), 'utf8', (err, data) => { 113 | if (err) throw err; 114 | 115 | const targetFile = path.resolve(targetDir, 'config/common.yml'); 116 | const template = Handlebars.compile(data.toString()); 117 | const content = template({ asyncapi }); 118 | 119 | mkdirp(path.dirname(targetFile), err => { 120 | if (err) return console.error(err); 121 | 122 | fs.writeFile(targetFile, content, { encoding: 'utf8', flag: 'w' }, (err) => { 123 | if (err) throw err; 124 | }); 125 | }); 126 | }); 127 | }; 128 | 129 | generate.readme = async (asyncapi, targetDir, templatesDir) => { 130 | fs.readFile(path.resolve(__dirname, `${templatesDir}/README.md`), 'utf8', (err, data) => { 131 | if (err) throw err; 132 | 133 | const targetFile = path.resolve(targetDir, 'README.md'); 134 | const template = Handlebars.compile(data.toString()); 135 | const content = template({ asyncapi }); 136 | 137 | mkdirp(path.dirname(targetFile), err => { 138 | if (err) return console.error(err); 139 | 140 | fs.writeFile(targetFile, content, { encoding: 'utf8', flag: 'w' }, (err) => { 141 | if (err) throw err; 142 | }); 143 | }); 144 | }); 145 | }; 146 | 147 | generate.static = async (targetDir, templatesDir) => { 148 | const files = [ 149 | '.editorconfig', 150 | '.eslintrc', 151 | '__.gitignore', 152 | 'Dockerfile', 153 | 'src/lib/config.js', 154 | 'src/api/middlewares/string2json.js', 155 | 'src/api/middlewares/logger.js', 156 | 'src/api/middlewares/buffer2string.js' 157 | ]; 158 | 159 | files.map(file => { 160 | const targetFile = path.resolve(targetDir, file.substr(0, 2) === '__' ? file.substr(2) : file); 161 | 162 | mkdirp(path.dirname(targetFile), err => { 163 | if (err) return console.error(err); 164 | 165 | fs.createReadStream(path.resolve(__dirname, `${templatesDir}/`, file)) 166 | .pipe(fs.createWriteStream(targetFile)); 167 | }); 168 | }); 169 | }; 170 | --------------------------------------------------------------------------------