├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── contributing.md ├── lib └── index.js ├── package.json ├── test ├── fixtures │ ├── basic │ │ └── index.html │ ├── readCache │ │ ├── cache │ │ │ └── dato.json │ │ └── index.html │ ├── template │ │ ├── index.html │ │ └── template.html │ └── writeCache │ │ └── index.html └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | contributing.md 3 | .editorconfig 4 | coverage 5 | .nyc_output 6 | yarn.lock 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 8 5 | - 9 6 | after_script: 7 | - npm run codecov 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License (MIT) 2 | ------------- 3 | 4 | Copyright (c) 2016 static-dev 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spike DatoCMS Plugin 2 | 3 | [![npm](https://img.shields.io/npm/v/spike-datocms.svg?style=flat-square)](https://npmjs.com/package/spike-datocms) 4 | [![tests](https://img.shields.io/travis/static-dev/spike-datocms.svg?style=flat-square)](https://travis-ci.org/static-dev/spike-datocms?branch=master) 5 | [![dependencies](https://img.shields.io/david/static-dev/spike-datocms.svg?style=flat-square)](https://david-dm.org/static-dev/spike-datocms) 6 | [![coverage](https://img.shields.io/codecov/c/github/static-dev/spike-datocms.svg?style=flat-square)](https://codecov.io/gh/static-dev/spike-datocms) 7 | 8 | A quick and easy interface to DatoCMS content 9 | 10 | > **Note:** This project is in early development, and versioning is a little different. [Read this](http://markup.im/#q4_cRZ1Q) for more details. 11 | 12 | ### Installation 13 | 14 | `npm install spike-datocms -S` 15 | 16 | ### Usage 17 | 18 | Sometimes the best way to see how to use something is an example! 19 | 20 | ```js 21 | const htmlStandards = require('reshape-standard') 22 | const SpikeDatoCMS = require('spike-datocms') 23 | const locals = {} 24 | 25 | module.exports = { 26 | reshape: htmlStandards({ locals }), 27 | plugins: [ 28 | new SpikeDatoCMS({ 29 | addDataTo: locals, 30 | token: 'xxx', 31 | models: [ 32 | { 33 | type: 'post', // if you leave this off, it will pull content from all models 34 | ids: [10, 13], // (optional) only return specific records 35 | query: 'foo', // (optional) text query for records 36 | offset: 3, // (optional) offset results 37 | limit: 10, // (optional) limit number of results returned 38 | transform: record => { 39 | // each record is passed through this function, if provided 40 | // change it however you want and return the modified result! 41 | return record 42 | } 43 | }, 44 | { 45 | type: 'author' 46 | } 47 | ] 48 | }) 49 | ] 50 | } 51 | ``` 52 | 53 | Now, in your views, you can see your records as such: 54 | 55 | ``` 56 | p {{{ JSON.stringify(dato) }}} 57 | ``` 58 | 59 | Or, for example, loop through one of your models: 60 | 61 | ``` 62 | ul 63 | each(loop='post in dato.post') 64 | li post.title 65 | ``` 66 | 67 | This plugin will also automatically pull the meta information for the site, including the site title, SEO fields, etc. that you control via the "Settings" menu, and assign it as `dato._meta`. This makes it super easy to reflect CMS-controlled SEO fields in your layouts. 68 | 69 | ### Single Template Render 70 | 71 | Using the template option allows you to write records returned from Dato to single page templates. For example, if you are trying to render a blog as static, you might want each post returned from the API to be rendered as a single page by itself. 72 | 73 | The `template` option is an object with `path` and `output` keys. The `path` is an absolute or relative path to a template to be used to render each item, and output is a function with the currently iterated item as a parameter, which should return a string representing a path relative to the project root where the single view should be rendered. For example: 74 | 75 | ```js 76 | new SpikeDatoCMS({ 77 | addDataTo: locals, 78 | token: 'xxx', 79 | models: [ 80 | { 81 | name: 'posts', 82 | template: { 83 | path: 'templates/post.html', 84 | output: post => { 85 | return `posts/${post.slug}.html` 86 | } 87 | } 88 | } 89 | ] 90 | }) 91 | ``` 92 | 93 | Your template must use the `item` variable as seen below. 94 | 95 | > **Note:** Make sure your template is _not ignored_ by spike, or this functionality will not work 96 | 97 | ```html 98 |

{{ item.title }}

99 | ``` 100 | 101 | ### JSON Output 102 | 103 | Finally, if you'd like to have the output written locally to a JSON file so that it's cached locally, you can pass the name of the file, resolved relative to your project's output, as a `json` option to the plugin. For example: 104 | 105 | ```js 106 | new SpikeDatoCMS({ 107 | addDataTo: locals, 108 | token: 'xxx', 109 | models: [{ name: 'posts' }], 110 | json: 'data.json' 111 | }) 112 | ``` 113 | 114 | You may also choose to have the output written specifically for any content type: 115 | 116 | ```js 117 | new SpikeDatoCMS({ 118 | addDataTo: locals, 119 | token: 'xxx', 120 | models: [ 121 | { 122 | name: 'posts', 123 | 124 | json: 'posts.json' 125 | }, 126 | { 127 | name: 'press', 128 | id: '4Em9bQeIQxxxxxxxxx' 129 | // No JSON output needed for this content type 130 | } 131 | ], 132 | // Save all content types data in one file 133 | json: 'alldata.json' 134 | }) 135 | ``` 136 | 137 | ### Aggressive Refresh 138 | 139 | By default, this plugin will only fetch data once when you start your watcher, for development speed purposes. This means that if you change your data, you will have to restart the watcher to pick up the changes. If you are in a phase where you are making frequent data changes and would like a more aggressive updating strategy, you can set the `aggressiveRefresh` option to `true`, and your dreams will come true. However, note that this will slow down your local development, as it will fetch and link all entires every time you save a file, so it's only recommended for temporary use. 140 | 141 | ### Drafts 142 | 143 | Passing `drafts: true` as an option to the plugin will make dato return the latest version of each post, rather than the published version. This is recommended in staging or development. 144 | 145 | ### License & Contributing 146 | 147 | * Details on the license [can be found here](LICENSE.md) 148 | * Details on running tests and contributing [can be found here](contributing.md) 149 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to spike-datocms 2 | 3 | Hello there! First of all, thanks for being interested in spike-datocms and helping out. We all think you are awesome, and by contributing to open source projects, you are making the world a better place. That being said, there are a few ways to make the process of contributing code to spike-datocms smoother, detailed below: 4 | 5 | ### Filing Issues 6 | 7 | If you are opening an issue about a bug, make sure that you include clear steps for how we can reproduce the problem. _If we can't reproduce it, we can't fix it_. If you are suggesting a feature, make sure your explanation is clear and detailed. 8 | 9 | ### Getting Set Up 10 | 11 | - Clone the project down 12 | - Make sure [nodejs](http://nodejs.org) has been installed and is above version `6.x` 13 | - Run `npm install` 14 | - Put in work 15 | 16 | ### Testing 17 | 18 | This project is constantly evolving, and to ensure that things are secure and working for everyone, we need to have tests. If you are adding a new feature, please make sure to add a test for it. The test suite for this project uses [ava](https://github.com/sindresorhus/ava). 19 | 20 | To run the test suite just use `npm test` or install ava globally and use the `ava` command to run the tests. 21 | 22 | ### Code Style 23 | 24 | This project uses ES6, interpreted directly by node.js. To keep a consistent coding style in the project, we are using [standard js](http://standardjs.com/). In order for tests to pass, all code must pass standard js linting. This project also uses an [editorconfig](http://editorconfig.org/). It will make life much easier if you have the [editorconfig plugin](http://editorconfig.org/#download) for your text editor. For any inline documentation in the code, we're using [JSDoc](http://usejsdoc.org/). 25 | 26 | ### Commit Cleanliness 27 | 28 | It's ok if you start out with a bunch of experimentation and your commit log isn't totally clean, but before any pull requests are accepted, we like to have a nice clean commit log. That means [well-written and clear commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) and commits that each do something significant, rather than being typo or bug fixes. 29 | 30 | If you submit a pull request that doesn't have a clean commit log, we will ask you to clean it up before we accept. This means being familiar with rebasing - if you are not, [this guide](https://help.github.com/articles/interactive-rebase) by github should help you to get started. And if you are still confused, feel free to ask! 31 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const SpikeUtil = require('spike-util') 2 | const W = require('when') 3 | const Joi = require('joi') 4 | const { SiteClient } = require('datocms-client') 5 | const bindAllClass = require('es6bindall') 6 | 7 | module.exports = class SpikeDatoCMS { 8 | constructor(opts) { 9 | Object.assign(this, this.validate(opts)) 10 | this.client = new SiteClient(opts.token, { 'X-Api-Version': '1' }) 11 | bindAllClass(this, ['apply', 'run']) 12 | } 13 | 14 | apply(compiler) { 15 | this.util = new SpikeUtil(compiler.options) 16 | this.util.runAll(compiler, this.run) 17 | let templatePairs 18 | 19 | // if there are single template pages, configure them here 20 | compiler.plugin('before-loader-process', (ctx, options) => { 21 | // map each template path to its config position 22 | if (!templatePairs) { 23 | templatePairs = this.models.reduce((m, model, idx) => { 24 | if (!model.template) return m 25 | if (!model.template.path) { 26 | throw new Error( 27 | `${model.name}.template must have a "path" property` 28 | ) 29 | } 30 | if (!model.template.output) { 31 | throw new Error( 32 | `${model.name}.template must have an "output" function` 33 | ) 34 | } 35 | m[model.template.path] = idx 36 | return m 37 | }, {}) 38 | } 39 | 40 | // get the relative path of the file currently being compiled 41 | const p = ctx.resourcePath.replace(`${compiler.options.context}/`, '') 42 | 43 | // match this path to the template pairs to get the model's full config 44 | if (typeof templatePairs[p] === 'undefined') return 45 | const conf = this.models[templatePairs[p]] 46 | const data = 47 | this.addDataTo.dato[conf.name] || this.addDataTo.dato[conf.type] 48 | 49 | // add a reshape multi option to compile each template separately 50 | options.multi = data.map(d => { 51 | return { 52 | locals: Object.assign({}, this.addDataTo, { item: d }), 53 | name: conf.template.output(d) 54 | } 55 | }) 56 | return options 57 | }) 58 | 59 | compiler.plugin('emit', (compilation, done) => { 60 | if (this.json) { 61 | writeJson(compilation, this.json, this.addDataTo.dato) 62 | } 63 | 64 | this.models.filter(m => m.json).map(m => { 65 | return writeJson(compilation, m.json, this.addDataTo.dato[m.type]) 66 | }) 67 | 68 | done() 69 | }) 70 | } 71 | 72 | run(compilation, done) { 73 | if (this.addDataTo.dato && !this.aggressiveRefresh) return done() 74 | return W.all([ 75 | this.client.site.find(), 76 | W.reduce( 77 | this.models, 78 | (memo, model) => { 79 | // format options 80 | const options = { allPages: true } 81 | const params = { version: this.drafts ? 'latest' : 'published' } 82 | if (model.ids) { 83 | params['filter[ids]'] = model.ids 84 | } 85 | if (model.query) { 86 | params['filter[query]'] = model.query 87 | } 88 | if (model.offset) { 89 | params['page[offset]'] = model.offset 90 | } 91 | if (model.limit) { 92 | params['page[limit]'] = model.limit 93 | } 94 | const transformFn = model.transform ? model.transform : x => x 95 | 96 | // fetch items and itemTypes 97 | return ( 98 | W.all([ 99 | W(this.client.items.all(params, options)), 100 | W(this.client.itemTypes.all()) 101 | ]) 102 | // make sure linked entries are resolved 103 | .then(resolveLinks) 104 | // filter to the model type, if necessary 105 | .then(([records, itemTypes]) => { 106 | if (!model.type) return records 107 | const t = itemTypes.find(it => it.apiKey === model.type) 108 | let modelRecords = records.filter(r => r.itemType === t.id) 109 | 110 | // if model has position key to sort by, use it to sort 111 | if (modelRecords.length && modelRecords[0].position) { 112 | modelRecords = modelRecords.sort( 113 | (a, b) => a.position - b.position 114 | ) 115 | } 116 | 117 | return modelRecords 118 | }) 119 | // transform if necessary 120 | .then(res => W.map(res, entry => transformFn(entry))) 121 | // add resolved item to the response 122 | .tap(res => { 123 | memo[model.type || 'all'] = res 124 | }) 125 | .yield(memo) 126 | ) 127 | }, 128 | {} 129 | ) 130 | ]).done(([site, models]) => { 131 | // clear existing locals, add new data 132 | if (this.addDataTo.dato) { 133 | delete this.addDataTo.dato 134 | } 135 | Object.assign(this.addDataTo, { 136 | dato: Object.assign(models, { _meta: site }) 137 | }) 138 | done() 139 | }, done) 140 | } 141 | 142 | /** 143 | * Validate options 144 | * @private 145 | */ 146 | validate(opts = {}) { 147 | const schema = Joi.object().keys({ 148 | token: Joi.string().required(), 149 | addDataTo: Joi.object().required(), 150 | json: Joi.string(), 151 | drafts: Joi.boolean(), 152 | aggressiveRefresh: Joi.boolean(), 153 | models: Joi.array().items( 154 | Joi.object().keys({ 155 | type: Joi.string().default(Joi.ref('name')), 156 | name: Joi.string(), // this is an alias for type 157 | ids: Joi.array().single(), 158 | query: Joi.string(), 159 | offset: Joi.number(), 160 | limit: Joi.number(), 161 | transform: Joi.func(), 162 | json: Joi.string(), 163 | template: Joi.object().keys({ 164 | path: Joi.string(), 165 | output: Joi.func() 166 | }) 167 | }) 168 | ) 169 | }) 170 | 171 | const res = Joi.validate(opts, schema, { 172 | language: { 173 | messages: { wrapArrays: false }, 174 | object: { child: '!![spike-datocms constructor] option {{reason}}' } 175 | } 176 | }) 177 | if (res.error) { 178 | throw new Error(res.error) 179 | } 180 | return res.value 181 | } 182 | } 183 | 184 | // TODO: use proxies so there can be no infinite loopz 185 | function resolveLinks([records, itemTypes]) { 186 | // find all model ids 187 | const recordIds = records.map(r => r.id) 188 | // scan all model values 189 | records.map(r => { 190 | for (let k in r) { 191 | if (k === 'id') continue 192 | // check to see if it is a model id, which means it's a link 193 | // if so, replace the id with the actual item 194 | if (Array.isArray(r[k])) { 195 | r[k] = r[k].map(resolveLink.bind(null, recordIds, records)) 196 | } else { 197 | r[k] = resolveLink(recordIds, records, r[k]) 198 | } 199 | } 200 | }) 201 | return [records, itemTypes] 202 | } 203 | 204 | function resolveLink(ids, records, record) { 205 | if (ids.indexOf(record) > -1) { 206 | return records.find(r2 => r2.id === record) 207 | } else { 208 | return record 209 | } 210 | } 211 | 212 | function writeJson(compilation, filename, data) { 213 | const src = JSON.stringify(data, null, 2) 214 | compilation.assets[filename] = { 215 | source: () => src, 216 | size: () => src.length 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spike-datocms", 3 | "description": "quick and easy interface to datocms content", 4 | "version": "2.4.1", 5 | "author": "Jeff Escalante", 6 | "ava": { 7 | "verbose": "true", 8 | "serial": "true" 9 | }, 10 | "bugs": "https://github.com/static-dev/spike-datocms/issues", 11 | "dependencies": { 12 | "datocms-client": "^0.5.2", 13 | "es6bindall": "^0.0.9", 14 | "joi": "^13.2.0", 15 | "spike-util": "^1.3.0", 16 | "when": "^3.7.8" 17 | }, 18 | "devDependencies": { 19 | "ava": "^0.25.0", 20 | "codecov": "^3.0.4", 21 | "husky": "^0.14.3", 22 | "nyc": "^12.0.2", 23 | "prettier": "^1.13.7", 24 | "reshape-standard": "^3.3.0", 25 | "rimraf": "^2.6.2", 26 | "spike-core": "^2.3.0" 27 | }, 28 | "engines": { 29 | "node": ">= 6" 30 | }, 31 | "homepage": "https://github.com/static-dev/spike-datocms", 32 | "keywords": [ 33 | "spikeplugin" 34 | ], 35 | "license": "MIT", 36 | "main": "lib", 37 | "repository": "static-dev/spike-datocms", 38 | "scripts": { 39 | "codecov": "nyc report -r lcovonly && codecov", 40 | "coverage": "nyc ava && nyc report --reporter=html && open ./coverage/index.html", 41 | "lint": "prettier --no-semi --single-quote --write '**/*.js'", 42 | "precommit": "npm run lint", 43 | "test": "nyc ava" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures/basic/index.html: -------------------------------------------------------------------------------- 1 | {{{ JSON.stringify(dato) }}} 2 | -------------------------------------------------------------------------------- /test/fixtures/readCache/cache/dato.json: -------------------------------------------------------------------------------- 1 | { 2 | "article": [ 3 | { 4 | "id": "48624", 5 | "updatedAt": "2017-04-13T18:54:25.346Z", 6 | "isValid": true, 7 | "image": { 8 | "alt": null, 9 | "path": "/1598/1492109658-absurd-png", 10 | "size": 476633, 11 | "title": null, 12 | "width": 1228, 13 | "format": "png", 14 | "height": 764 15 | }, 16 | "slug": "testing-2-post", 17 | "body": "What a great post this is", 18 | "title": "Testing #2 Post", 19 | "itemType": "6793", 20 | "lastEditor": "1347" 21 | }, 22 | { 23 | "id": "42067", 24 | "updatedAt": "2017-04-13T18:53:48.664Z", 25 | "isValid": true, 26 | "image": { 27 | "alt": null, 28 | "path": "/1598/1489204301-logo-text-png", 29 | "size": 10781, 30 | "title": null, 31 | "width": 636, 32 | "format": "png", 33 | "height": 212 34 | }, 35 | "slug": "testing-post", 36 | "body": "Wow, what a great post this is!", 37 | "title": "Testing Post", 38 | "itemType": "6793", 39 | "lastEditor": "1347" 40 | } 41 | ], 42 | "_meta": { 43 | "id": "1598", 44 | "name": "Test", 45 | "locales": [ 46 | "en" 47 | ], 48 | "themeHue": 200, 49 | "domain": null, 50 | "internalDomain": "test-72.admin.datocms.com", 51 | "globalSeo": { 52 | "siteName": "Testing", 53 | "titleSuffix": null, 54 | "twitterAccount": "@jescalan", 55 | "facebookPageUrl": "https://facebook.com/carrot", 56 | "fallbackSeo": { 57 | "title": "Testing", 58 | "description": "Nulla vitae elit libero, a pharetra augue. Maecenas sed diam eget risus varius blandit sit amet non magna.", 59 | "image": { 60 | "path": "/1598/1492702970-2-jpg", 61 | "width": 2560, 62 | "height": 1440, 63 | "format": "jpg", 64 | "size": 779218, 65 | "alt": null, 66 | "title": null 67 | } 68 | } 69 | }, 70 | "favicon": null, 71 | "noIndex": false, 72 | "frontendUrl": null, 73 | "ssg": "other", 74 | "account": "1347", 75 | "users": [], 76 | "menuItems": [ 77 | "8645", 78 | "8644", 79 | "8643", 80 | "8642", 81 | "8641", 82 | "8047", 83 | "7322" 84 | ], 85 | "itemTypes": [ 86 | "6793", 87 | "8166", 88 | "8163", 89 | "8164", 90 | "8165", 91 | "7491", 92 | "8162" 93 | ] 94 | } 95 | } -------------------------------------------------------------------------------- /test/fixtures/readCache/index.html: -------------------------------------------------------------------------------- 1 | {{{ JSON.stringify(dato) }}} 2 | -------------------------------------------------------------------------------- /test/fixtures/template/index.html: -------------------------------------------------------------------------------- 1 | {{{ JSON.stringify(dato) }}} 2 | -------------------------------------------------------------------------------- /test/fixtures/template/template.html: -------------------------------------------------------------------------------- 1 | {{{ foo }}} 2 | {{{ JSON.stringify(item) }}} 3 | -------------------------------------------------------------------------------- /test/fixtures/writeCache/index.html: -------------------------------------------------------------------------------- 1 | {{{ JSON.stringify(dato) }}} 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const SpikeDatoCMS = require('..') 2 | const test = require('ava') 3 | const Spike = require('spike-core') 4 | const htmlStandards = require('reshape-standard') 5 | const path = require('path') 6 | const fs = require('fs') 7 | const rimraf = require('rimraf') 8 | 9 | const fixturesPath = path.join(__dirname, 'fixtures') 10 | const datoToken = 'cb1f960dfb4f14a7ae93' 11 | 12 | test.cb('basic', t => { 13 | const locals = {} 14 | const plugin = new SpikeDatoCMS({ 15 | token: datoToken, 16 | addDataTo: locals, 17 | models: [{ name: 'article' }] 18 | }) 19 | return plugin.run({}, () => { 20 | t.truthy(locals.dato._meta.id) 21 | t.truthy(locals.dato.article.length > 0) 22 | t.truthy(locals.dato.article.length < 3) 23 | const testPost = locals.dato.article.find(a => a.slug === 'testing-post') 24 | t.truthy(testPost.category.id) 25 | t.truthy(testPost.categories[0].id) 26 | t.end() 27 | }) 28 | }) 29 | 30 | test.cb('sorts models by position', t => { 31 | const locals = {} 32 | const plugin = new SpikeDatoCMS({ 33 | token: datoToken, 34 | addDataTo: locals, 35 | models: [{ name: 'product' }] 36 | }) 37 | return plugin.run({}, () => { 38 | t.truthy( 39 | locals.dato.product === 40 | locals.dato.product.sort((a, b) => a.position - b.position) 41 | ) 42 | t.end() 43 | }) 44 | }) 45 | 46 | test.cb('works with spike', t => { 47 | const locals = {} 48 | const projPath = path.join(fixturesPath, 'basic') 49 | const project = new Spike({ 50 | root: projPath, 51 | // so if the locals are not passed as a function, the reference doesn't 52 | // update. i'm not 100% sure why this is, in theory it should 53 | reshape: htmlStandards({ parser: false, locals: () => locals, retext: [] }), 54 | plugins: [ 55 | new SpikeDatoCMS({ 56 | token: datoToken, 57 | addDataTo: locals, 58 | aggressiveRefresh: true, 59 | models: [{ name: 'article' }] 60 | }) 61 | ] 62 | }) 63 | 64 | project.on('error', t.end) 65 | project.on('compile', () => { 66 | const output = JSON.parse( 67 | fs.readFileSync(path.join(projPath, 'public/index.html'), 'utf8') 68 | ) 69 | t.truthy(output.article.length > 0) 70 | rimraf.sync(path.join(projPath, 'public')) 71 | t.end() 72 | }) 73 | 74 | project.compile() 75 | }) 76 | 77 | test.todo('generates json correctly') 78 | test.todo('agressively refreshes data') 79 | 80 | test.cb('generates single page templates correctly', t => { 81 | const locals = { foo: 'bar' } 82 | const projPath = path.join(fixturesPath, 'template') 83 | const project = new Spike({ 84 | root: projPath, 85 | reshape: htmlStandards({ parser: false, locals: () => locals, retext: [] }), 86 | plugins: [ 87 | new SpikeDatoCMS({ 88 | token: datoToken, 89 | addDataTo: locals, 90 | models: [ 91 | { 92 | name: 'article', 93 | template: { 94 | path: 'template.html', 95 | output: obj => `articles/${obj.slug}.html` 96 | } 97 | } 98 | ] 99 | }) 100 | ] 101 | }) 102 | 103 | project.on('error', t.end) 104 | project.on('compile', () => { 105 | const file1 = fs.readFileSync( 106 | path.join(projPath, 'public/articles/testing-post.html'), 107 | 'utf8' 108 | ) 109 | const file2 = fs.readFileSync( 110 | path.join(projPath, 'public/articles/testing-2-post.html'), 111 | 'utf8' 112 | ) 113 | 114 | t.is(file1.match(/(.*)<\/global>/)[1], 'bar') 115 | t.is(JSON.parse(file1.match(/(.*)<\/item>/)[1]).title, 'Testing Post') 116 | t.is(file2.match(/(.*)<\/global>/)[1], 'bar') 117 | t.is( 118 | JSON.parse(file2.match(/(.*)<\/item>/)[1]).title, 119 | 'Testing #2 Post' 120 | ) 121 | rimraf.sync(path.join(projPath, 'public')) 122 | t.end() 123 | }) 124 | 125 | project.compile() 126 | }) 127 | 128 | test.cb('errors when there is no template.path', t => { 129 | const locals = {} 130 | const projPath = path.join(fixturesPath, 'template') 131 | const project = new Spike({ 132 | root: projPath, 133 | reshape: htmlStandards({ parser: false, locals: () => locals, retext: [] }), 134 | plugins: [ 135 | new SpikeDatoCMS({ 136 | token: datoToken, 137 | addDataTo: locals, 138 | models: [ 139 | { 140 | name: 'article', 141 | template: { 142 | output: obj => `articles/${obj.slug}.html` 143 | } 144 | } 145 | ] 146 | }) 147 | ] 148 | }) 149 | 150 | project.on('error', err => { 151 | t.regex(err.toString(), /article\.template must have a "path" property/) 152 | rimraf.sync(path.join(projPath, 'public')) 153 | t.end() 154 | }) 155 | 156 | project.compile() 157 | }) 158 | 159 | test.cb('errors when there is no template.output', t => { 160 | const locals = {} 161 | const projPath = path.join(fixturesPath, 'template') 162 | const project = new Spike({ 163 | root: projPath, 164 | reshape: htmlStandards({ parser: false, locals: () => locals, retext: [] }), 165 | plugins: [ 166 | new SpikeDatoCMS({ 167 | token: datoToken, 168 | addDataTo: locals, 169 | models: [ 170 | { 171 | name: 'article', 172 | template: { 173 | path: 'template.html' 174 | } 175 | } 176 | ] 177 | }) 178 | ] 179 | }) 180 | 181 | project.on('error', err => { 182 | t.regex(err.toString(), /article\.template must have an "output" function/) 183 | rimraf.sync(path.join(projPath, 'public')) 184 | t.end() 185 | }) 186 | 187 | project.compile() 188 | }) 189 | 190 | test.cb('writes json', t => { 191 | const locals = {} 192 | const projPath = path.join(fixturesPath, 'basic') 193 | const project = new Spike({ 194 | root: projPath, 195 | reshape: htmlStandards({ parser: false, locals: () => locals, retext: [] }), 196 | ignore: ['template.html'], 197 | plugins: [ 198 | new SpikeDatoCMS({ 199 | token: datoToken, 200 | addDataTo: locals, 201 | json: 'all.json', 202 | models: [ 203 | { 204 | name: 'article', 205 | json: 'articles.json' 206 | } 207 | ] 208 | }) 209 | ] 210 | }) 211 | 212 | project.on('error', t.end) 213 | project.on('compile', () => { 214 | const all = JSON.parse( 215 | fs.readFileSync(path.join(projPath, 'public/all.json'), 'utf8') 216 | ) 217 | const articles = JSON.parse( 218 | fs.readFileSync(path.join(projPath, 'public/articles.json'), 'utf8') 219 | ) 220 | t.truthy(all.article.length > 0) 221 | t.truthy(articles.length > 0) 222 | rimraf.sync(path.join(projPath, 'public')) 223 | t.end() 224 | }) 225 | 226 | project.compile() 227 | }) 228 | 229 | test.todo('pulls draft posts') 230 | --------------------------------------------------------------------------------