diff --git a/README.md b/README.md index 77055259..206f6b49 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ npm init ember-app super-blog cd super-blog # you can replace the template with the one you want to use -npx ember-cli install empress-blog empress-blog-casper-template +npx ember install empress-blog empress-blog-casper-template ``` It will ask you if you want to update the `index.html` file and you should say yes 👍 @@ -62,7 +62,7 @@ This addon comes with helpful blueprints to generate posts and authors for you i expects. The first thing you should do is generate an author as follows: ```sh -ember g author your-name +npx ember g author your-name ``` Then you should be able to edit the file `author/your-name.md` to update the details. @@ -71,7 +71,7 @@ Next you will want to generate some posts. If you only have one author generated blog) you can generate a post as simply as running: ```sh -ember g post "this is a post I want to write" +npx ember g post "this is a post I want to write" ``` ### Configuring @@ -150,6 +150,29 @@ and I'd be happy to help you. You can check out the documentation for If you do end up writing an empress-blog template please let us know so I can include your template in a list of existing templates. +Importing content +------------------------------------------------------------------------------ + +If you would like to import your blog content from another platform into +empress-blog we have a beta import function that we would love for people to try +out and give feedback. It requires that you create an export file from your +respective blog platform and then run the following command from the command +line: + +```sh +npx ember empress-blog:import --type=[import_type] [dump_file] +``` + +Currently available `import_type`s are: + - [Ghost](https://ghost.org/faq/the-importer/) + - [WordPress](https://en.support.wordpress.com/export/) + - [Tumblr](https://tumblr.zendesk.com/hc/en-us/articles/360005118894-Export-your-blog) + +**Note:** When exporting a Tumblr blog you will be downloading a zip file which +you need to unpack. Once you unpack your export zip file you will find a +`posts.zip` file which also needs to be unpacked. Your `dump_file` will be the +resulting `posts.xml` file. + Upgrade notes ------------------------------------------------------------------------------ diff --git a/index.js b/index.js index 31773940..c8511fba 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,12 @@ const AutomaticNewTag = require('./lib/automatic-new-tag'); module.exports = { name: require('./package').name, + includedCommands: function() { + return { + 'empress-blog:import': require('./lib/import.js'), + } + }, + config() { return { blog: {}, diff --git a/lib/import.js b/lib/import.js new file mode 100644 index 00000000..93f43ba4 --- /dev/null +++ b/lib/import.js @@ -0,0 +1,88 @@ +const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('fs'); +const { safeDump } = require('js-yaml'); + +const importers = require('./importers'); + +const supportedTypes = ['wordpress', 'ghost', 'tumblr']; + +function ensureFolder(dir) { + if (!existsSync(dir)){ + mkdirSync(dir); + } +} + +function dump(data) { + return safeDump(data, { skipInvalid: true }); +} + +module.exports = { + name: 'empress-blog:import', + description: 'Imports data from other blog systems', + works: 'insideProject', + + async run(commandOptions, rawArgs) { + if (!commandOptions.type || !supportedTypes.includes(commandOptions.type)) { + throw new Error(`You must run this command with the '--type=' parameter. Supported types are: ${supportedTypes}`) + } + + const fileName = rawArgs[0]; + + if (!fileName) { + throw new Error('You must pass in the export file from your blog as the first argument'); + } + + if (!existsSync(fileName)) { + throw new Error(`File "${fileName}" does not exits`); + } + + let data = await importers[commandOptions.type](readFileSync(fileName)); + + if(data.tags) { + ensureFolder('tag'); + + data.tags.forEach((tag) => { + writeFileSync(`tag/${tag.id}.md`, `--- +${dump({ + name: tag.name, + image: '', +})}--- +${tag.description}`) + }) + } + + if(data.authors) { + ensureFolder('author'); + + data.authors.forEach((author) => { + writeFileSync(`author/${author.id}.md`, `--- +${dump({ + name: author.name, + id: author.id, + image: author.image, + coverImage: author.coverImage, + website: author.website, + twitter: author.twitter, + facebook: author.facebook, + location: author.location, +})}--- +${author.bio}`) + }) + } + + if(data.content) { + ensureFolder('content'); + + data.content.forEach((content) => { + writeFileSync(`content/${content.id}.md`, `--- +${dump({ + title: content.title, + image: content.image, + authors: content.authors, + date: content.date, + tags: content.tags, +})}--- +${content.content}`) + }) + } + }, +}; diff --git a/lib/importers/ghost.js b/lib/importers/ghost.js new file mode 100644 index 00000000..889beb0c --- /dev/null +++ b/lib/importers/ghost.js @@ -0,0 +1,73 @@ +const _ = require('lodash'); + +module.exports = async function ghostImport(fileContents) { + + const data = await JSON.parse(fileContents).db[0].data; + + let authorMap = {}; + + let authors = data.users.map((author) => { + authorMap[author.id] = author.slug; + + return { + id: author.slug, + name: author.name || '', + image: author.profile_image || '', + coverImage: author.cover_image || '', + website: author.website || '', + twitter: author.twitter || '', + facebook: author.facebook || '', + location: author.location || '', + bio: author.bio || '', + } + }) + + let tagMap = {}; + + let tags = data.tags.map((tag) => { + tagMap[tag.id] = tag.slug; + + return { + id: tag.slug, + name: tag.name, + image: tag.feature_image || '', + description: tag.description || '' + } + }); + + let content = _.chain(data.posts) + .map((post) => { + // console.log(post); + + // don't import draft posts + if(post.status !== 'published') { + return; + } + + let result = { + id: post.slug, + title: post.title || '', + image: post.feature_image || '', + date: post.created_at, + content: post.plaintext, + tags: _.chain(data.posts_tags) + .filter(item => item.post_id === post.id) + .map(item => tagMap[item.tag_id]) + .value(), + authors: _.chain(data.posts_authors) + .filter(item => item.post_id === post.id) + .map(item => authorMap[item.author_id]) + .value(), + } + + return result; + }) + .compact() + .value(); + + return { + tags, + authors, + content, + } +} diff --git a/lib/importers/index.js b/lib/importers/index.js new file mode 100644 index 00000000..97dca49f --- /dev/null +++ b/lib/importers/index.js @@ -0,0 +1,2 @@ +var requireDirectory = require('require-directory'); +module.exports = requireDirectory(module); diff --git a/lib/importers/tumblr.js b/lib/importers/tumblr.js new file mode 100644 index 00000000..e15d3374 --- /dev/null +++ b/lib/importers/tumblr.js @@ -0,0 +1,72 @@ +const { promisify } = require("util"); +const { parseString } = require("xml2js"); +const _ = require("lodash"); + +const parse = promisify(parseString); + +module.exports = async function tumblrImport(fileContents) { + const data = await parse(fileContents); + + let channel = data.tumblr.posts[0]; + + let allAuthors = []; + let allTags = []; + + let allContent = _.chain(channel.post) + .map(post => { + let postType = post.$.type; + let id = post.$.slug; + let date = new Date(post.$.date); + let author = post.$.tumblelog; + let tag = post.$.type; + let title, content; + + switch (postType) { + case "regular": + title = post["regular-title"][0]; + content = post["regular-body"][0]; + break; + case "quote": + title = post["quote-text"][0]; + content = post["quote-text"][0]; + break; + case "conversation": + title = post["conversation-title"][0]; + content = post["conversation-text"][0]; + } + + if (!allAuthors.find(existingAuthor => existingAuthor.id === author)) { + allAuthors.push({ + id: author, + name: author + }); + } + + if (!allTags.find(existingTag => existingTag.id === tag)) { + allTags.push({ + id: tag, + name: tag + }); + } + + + let result = { + id, + title, + authors: [author], + date, + tags: [tag], + content + }; + + return result; + }) + .compact() + .value(); + + return { + tags: allTags, + authors: allAuthors, + content: allContent + }; +}; diff --git a/lib/importers/wordpress.js b/lib/importers/wordpress.js new file mode 100644 index 00000000..1a615635 --- /dev/null +++ b/lib/importers/wordpress.js @@ -0,0 +1,64 @@ +const { promisify } = require('util'); +const { parseString } = require('xml2js'); +const _ = require('lodash'); + +const parse = promisify(parseString); + +module.exports = async function worpressImport(fileContents) { + + const data = await parse(fileContents); + + let channel = data.rss.channel[0]; + + let authors = channel['wp:author'].map((author) => { + return { + name: author['wp:author_display_name'][0].trim(), + id: author['wp:author_login'][0].trim(), + } + }) + + let postTags = {}; + + let content = _.chain(channel.item) + .map((post) => { + if(post['wp:post_type'][0].trim() !== 'post') { + return; + } + + // don't import draft posts + if(post['wp:status'][0].trim() !== 'publish') { + return; + } + + let result = { + id: post['wp:post_name'][0].trim(), + title: post.title[0], + authors: [post['dc:creator'][0].trim()], + date: (new Date(post['wp:post_date_gmt'][0])), + tags: (post.category || []).map((category) => { + // collect tags from the posts because wordpress exports don't quite + // include tags correctly + postTags[category.$.nicename] = category._.trim(); + return category.$.nicename + }), + content: post['content:encoded'][0].trim(), + } + + return result; + }) + .compact() + .value(); + + let tags = _.map(postTags, (name, id) => { + return { + id, + name, + } + }) + + return { + tags, + authors, + content, + } +} diff --git a/package-lock.json b/package-lock.json index b135b280..c82221a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,8 +28,11 @@ "empress-blueprint-helpers": "^1.2.1", "js-yaml": "^3.13.1", "lodash": "^4.17.20", + "recast": "^0.17.6", + "require-directory": "^2.1.1", "resolve": "^1.10.1", "walk-sync": "^2.2.0", + "xml2js": "^0.4.19", "yaml-front-matter": "^4.0.0" }, "devDependencies": { @@ -3902,9 +3905,9 @@ } }, "node_modules/ast-types": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", - "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==", + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.12.4.tgz", + "integrity": "sha512-ky/YVYCbtVAS8TdMIaTiPFHwEpRB5z1hctepJplTr3UW5q8TDrpIMCILyk8pmLxGtn2KCtC/lSn7zOsaI7nzDw==", "engines": { "node": ">=4" } @@ -14113,6 +14116,39 @@ "node": "8.* || 10.* || >= 12" } }, + "node_modules/ember-router-generator/node_modules/ast-types": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", + "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ember-router-generator/node_modules/recast": { + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.10.tgz", + "integrity": "sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==", + "dev": true, + "dependencies": { + "ast-types": "0.13.3", + "esprima": "~4.0.0", + "private": "^0.1.8", + "source-map": "~0.6.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ember-router-generator/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ember-scroll": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/ember-scroll/-/ember-scroll-1.0.2.tgz", @@ -15873,6 +15909,36 @@ "node": "8.* || >= 10.*" } }, + "node_modules/empress-blueprint-helpers/node_modules/ast-types": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", + "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/empress-blueprint-helpers/node_modules/recast": { + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.10.tgz", + "integrity": "sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==", + "dependencies": { + "ast-types": "0.13.3", + "esprima": "~4.0.0", + "private": "^0.1.8", + "source-map": "~0.6.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/empress-blueprint-helpers/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -24748,11 +24814,11 @@ } }, "node_modules/recast": { - "version": "0.18.10", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.10.tgz", - "integrity": "sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.17.6.tgz", + "integrity": "sha512-yoQRMRrK1lszNtbkGyM4kN45AwylV5hMiuEveUBlxytUViWevjvX6w+tzJt1LH4cfUhWt4NZvy3ThIhu6+m5wQ==", "dependencies": { - "ast-types": "0.13.3", + "ast-types": "0.12.4", "esprima": "~4.0.0", "private": "^0.1.8", "source-map": "~0.6.1" @@ -25406,8 +25472,7 @@ "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/saxes": { "version": "3.1.11", @@ -28948,6 +29013,26 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -32305,9 +32390,9 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, "ast-types": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", - "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==" + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.12.4.tgz", + "integrity": "sha512-ky/YVYCbtVAS8TdMIaTiPFHwEpRB5z1hctepJplTr3UW5q8TDrpIMCILyk8pmLxGtn2KCtC/lSn7zOsaI7nzDw==" }, "astral-regex": { "version": "1.0.0", @@ -41139,6 +41224,32 @@ "@babel/parser": "^7.4.5", "@babel/traverse": "^7.4.5", "recast": "^0.18.1" + }, + "dependencies": { + "ast-types": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", + "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==", + "dev": true + }, + "recast": { + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.10.tgz", + "integrity": "sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==", + "dev": true, + "requires": { + "ast-types": "0.13.3", + "esprima": "~4.0.0", + "private": "^0.1.8", + "source-map": "~0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "ember-scroll": { @@ -42554,6 +42665,29 @@ "integrity": "sha512-4MCS9uOoo6iOr1IdJ3I6AYCKVhV6V9eK8MeU50kIEm2+SA7qPti4+gQ9iAlNc0JIewAbUw+vSsv3sIsdl0elbQ==", "requires": { "recast": "^0.18.5" + }, + "dependencies": { + "ast-types": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", + "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==" + }, + "recast": { + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.10.tgz", + "integrity": "sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==", + "requires": { + "ast-types": "0.13.3", + "esprima": "~4.0.0", + "private": "^0.1.8", + "source-map": "~0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "encodeurl": { @@ -49892,11 +50026,11 @@ } }, "recast": { - "version": "0.18.10", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.10.tgz", - "integrity": "sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.17.6.tgz", + "integrity": "sha512-yoQRMRrK1lszNtbkGyM4kN45AwylV5hMiuEveUBlxytUViWevjvX6w+tzJt1LH4cfUhWt4NZvy3ThIhu6+m5wQ==", "requires": { - "ast-types": "0.13.3", + "ast-types": "0.12.4", "esprima": "~4.0.0", "private": "^0.1.8", "source-map": "~0.6.1" @@ -50416,8 +50550,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "saxes": { "version": "3.1.11", @@ -53373,6 +53506,20 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index 6f0b72c1..18329d24 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,11 @@ "empress-blueprint-helpers": "^1.2.1", "js-yaml": "^3.13.1", "lodash": "^4.17.20", + "recast": "^0.17.6", + "require-directory": "^2.1.1", "resolve": "^1.10.1", "walk-sync": "^2.2.0", + "xml2js": "^0.4.19", "yaml-front-matter": "^4.0.0" }, "devDependencies": {