diff --git a/README.md b/README.md index 549deae..9afcf1a 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,10 @@ new Contentful({ }) ``` +### Aggressive Refresh + +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. + ### Testing To run the tests locally, you'll need to add a `test/.env` with your name and token values: diff --git a/lib/index.js b/lib/index.js index 1873bd4..95e8c0f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,12 +1,8 @@ const contentful = require('contentful') const Joi = require('joi') const W = require('when') -const fs = require('fs') -const path = require('path') -const node = require('when/node') -const reshape = require('reshape') -const loader = require('reshape-loader') const SpikeUtil = require('spike-util') +const bindAllClass = require('es6bindall') // This plugin works in almost exactly the same way as spike-records, but has // been customized specifically for contentful. For a more thoroughly annotated @@ -25,17 +21,44 @@ class Contentful { accessToken: this.accessToken, space: this.spaceId }) + bindAllClass(this, ['apply', 'run']) } apply (compiler) { this.util = new SpikeUtil(compiler.options) + this.util.runAll(compiler, this.run) + let templatePairs + + // if there are single template pages, configure them here + compiler.plugin('before-loader-process', (ctx, options) => { + // map each template path to its config position + if (!templatePairs) { + templatePairs = this.contentTypes.reduce((m, model, idx) => { + if (!model.template) return m + if (!model.template.path) { + throw new Error(`${model.name}.template must have a "path" property`) + } + if (!model.template.output) { + throw new Error(`${model.name}.template must have an "output" function`) + } + m[model.template.path] = idx + return m + }, {}) + } - this.util.runAll(compiler, this.run.bind(this, compiler)) + // get the relative path of the file currently being compiled + const p = ctx.resourcePath.replace(`${compiler.options.context}/`, '') - compiler.plugin('compilation', (compilation) => { - compilation.plugin('normal-module-loader', (loaderContext) => { - this.loaderContext = loaderContext + // match this path to the template pairs to get the model's full config + if (typeof templatePairs[p] === 'undefined') return options + const conf = this.contentTypes[templatePairs[p]] + const data = this.addDataTo.contentful[conf.name] + + // add a reshape multi option to compile each template separately + options.multi = data.map((d) => { + return { locals: { item: d }, name: conf.template.output(d) } }) + return options }) compiler.plugin('emit', (compilation, done) => { @@ -47,17 +70,14 @@ class Contentful { return writeJson(compilation, ct.json, this.addDataTo.contentful[ct.name]) }) - const templateContent = this.contentTypes.filter((ct) => { - return ct.template - }) - - W.map(templateContent, (contentType) => { - return writeTemplate.call(this, compiler, compilation, contentType) - }).done(() => done(), done) + done() }) } - run (compiler, compilation, done) { + run (compilation, done) { + // only pull data on the initial compile in watch mode + if (this.addDataTo.contentful && !this.aggressiveRefresh) return done() + return W.reduce(this.contentTypes, (m, ct) => { let transformFn = ct.transform let options = Object.assign({ @@ -175,28 +195,5 @@ function writeJson (compilation, filename, data) { } } -function writeTemplate (compiler, compilation, contentType) { - const data = this.addDataTo.contentful[contentType.name] - const filePath = path.join(compiler.options.context, contentType.template.path) - - return node.call(fs.readFile.bind(fs), filePath, 'utf8').then((template) => { - return W.map(data, (item) => { - const newLocals = Object.assign({}, this.addDataTo, { item }) - - const options = loader.parseOptions.call(this.loaderContext, this.util.getSpikeOptions().reshape, {}) - - return reshape(options) - .process(template) - .then((res) => { - const html = res.output(newLocals) - compilation.assets[contentType.template.output(item)] = { - source: () => html, - size: () => html.length - } - }) - }) - }) -} - module.exports = Contentful module.exports.transform = transform diff --git a/package.json b/package.json index d2abc8a..e9e81a9 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,8 @@ "bugs": "https://github.com/static-dev/spike-contentful/issues", "dependencies": { "contentful": "^4.5.0", + "es6bindall": "^0.0.9", "joi": "^10.6.0", - "reshape": "^0.4.1", - "reshape-loader": "^1.1.0", "when": "^3.7.8" }, "devDependencies": { @@ -20,7 +19,7 @@ "coveralls": "^2.13.1", "dotenv": "^4.0.0", "nyc": "^11.0.3", - "reshape-standard": "^3.0.0", + "reshape-standard": "^3.0.1", "rimraf": "^2.6.0", "spike-core": "^2.2.0", "standard": "^10.0.2" diff --git a/test/fixtures/default/app.js b/test/fixtures/default/app.js index 2ec8a9e..6c9c3f2 100644 --- a/test/fixtures/default/app.js +++ b/test/fixtures/default/app.js @@ -3,7 +3,6 @@ const standard = require('reshape-standard') const locals = {} module.exports = { - matchers: { html: '*(**/)*.sgr' }, reshape: standard({ locals }), plugins: [new Contentful({ accessToken: process.env.accessToken, diff --git a/test/fixtures/default/index.html b/test/fixtures/default/index.html new file mode 100644 index 0000000..1fe2801 --- /dev/null +++ b/test/fixtures/default/index.html @@ -0,0 +1,3 @@ + +

{{ cat.fields.name }}

+
diff --git a/test/fixtures/default/index.sgr b/test/fixtures/default/index.sgr deleted file mode 100644 index 766bcef..0000000 --- a/test/fixtures/default/index.sgr +++ /dev/null @@ -1,2 +0,0 @@ -each(loop='cat of contentful.cats') - p {{ cat.fields.name }} diff --git a/test/fixtures/error/error.html b/test/fixtures/error/error.html new file mode 100644 index 0000000..4d307b5 --- /dev/null +++ b/test/fixtures/error/error.html @@ -0,0 +1 @@ +

{{ notItem.fields.name }}

diff --git a/test/fixtures/json/app.js b/test/fixtures/json/app.js index 6f414cc..679ce85 100644 --- a/test/fixtures/json/app.js +++ b/test/fixtures/json/app.js @@ -3,7 +3,6 @@ const htmlStandards = require('reshape-standard') const locals = {} module.exports = { - matchers: { html: '*(**/)*.sgr' }, reshape: htmlStandards({ locals }), plugins: [new Contentful({ accessToken: process.env.accessToken, diff --git a/test/fixtures/json/index.html b/test/fixtures/json/index.html new file mode 100644 index 0000000..f4a161c --- /dev/null +++ b/test/fixtures/json/index.html @@ -0,0 +1,3 @@ + +

{{ dog.fields.name }}

+
diff --git a/test/fixtures/json/index.sgr b/test/fixtures/json/index.sgr deleted file mode 100644 index 7966c54..0000000 --- a/test/fixtures/json/index.sgr +++ /dev/null @@ -1,2 +0,0 @@ -each(loop='dog of contentful.dogs') - p {{ dog.fields.name }} diff --git a/test/fixtures/template/error.sgr b/test/fixtures/template/error.sgr deleted file mode 100644 index c3a4da3..0000000 --- a/test/fixtures/template/error.sgr +++ /dev/null @@ -1 +0,0 @@ -p {{ notItem.fields.name }} diff --git a/test/fixtures/template/template.html b/test/fixtures/template/template.html new file mode 100644 index 0000000..93ffc24 --- /dev/null +++ b/test/fixtures/template/template.html @@ -0,0 +1 @@ +

{{ item.fields.name }}

diff --git a/test/fixtures/template/template.sgr b/test/fixtures/template/template.sgr deleted file mode 100644 index 01669dc..0000000 --- a/test/fixtures/template/template.sgr +++ /dev/null @@ -1 +0,0 @@ -p {{ item.fields.name }} diff --git a/test/index.js b/test/index.js index 1d6f384..c699a72 100644 --- a/test/index.js +++ b/test/index.js @@ -8,8 +8,6 @@ const fs = require('fs') const rimraf = require('rimraf') const standard = require('reshape-standard') -const compilerMock = { options: { spike: { locals: {} } } } - test('errors without an "accessToken"', (t) => { t.throws( () => { new Contentful({ spaceId: 'xxx' }) }, // eslint-disable-line @@ -98,7 +96,7 @@ test.cb('returns valid content', (t) => { ] }) - api.run(compilerMock, undefined, () => { + api.run(undefined, () => { t.is(locals.contentful.dogs.length, 2) t.is(locals.contentful.cats.length, 3) t.end() @@ -114,7 +112,7 @@ test.cb('defaults id to name if not present', (t) => { contentTypes: [{ name: 'cat' }] }) - api.run(compilerMock, undefined, () => { + api.run(undefined, () => { t.is(locals.contentful.cat.length, 3) t.end() }) @@ -135,7 +133,7 @@ test.cb('implements request options', (t) => { ] }) - api.run(compilerMock, undefined, () => { + api.run(undefined, () => { t.is(locals.contentful.cats.length, 1) t.is(locals.contentful.cats[0].fields.name, 'Garfield') t.end() @@ -164,7 +162,7 @@ test.cb('works with custom transform function', (t) => { ] }) - api.run(compilerMock, undefined, () => { + api.run(undefined, () => { t.is(locals.contentful.cats[0].doge, 'wow') t.end() }) @@ -188,7 +186,7 @@ test.cb('can implement default transform function', (t) => { ] }) - api.run(compilerMock, undefined, () => { + api.run(undefined, () => { t.truthy(typeof locals.contentful.cats[0].name === 'string') t.end() }) @@ -205,7 +203,7 @@ test.cb('works as a plugin to spike', (t) => { project.on('warning', t.end) project.on('compile', () => { const src = fs.readFileSync(path.join(projectPath, 'public/index.html'), 'utf8') - t.truthy(src === '

Nyan Cat

') + t.truthy(src.trim() === '

Nyan Cat

') rimraf.sync(path.join(projectPath, 'public')) t.end() }) @@ -253,24 +251,22 @@ test.cb('accepts template object and generates html', (t) => { order: 'sys.createdAt' }, template: { - path: '../template/template.sgr', + path: 'template.html', output: (item) => `cats/${item.fields.name}.html` } } ] }) - const projectPath = path.join(__dirname, 'fixtures/default') + const projectPath = path.join(__dirname, 'fixtures/template') const project = new Spike({ root: projectPath, - matchers: { html: '**/*.sgr' }, reshape: standard({ locals }), entry: { main: [path.join(projectPath, 'main.js')] }, plugins: [contentful] }) project.on('error', t.end) - project.on('warning', t.end) project.on('compile', () => { const file1 = fs.readFileSync(path.join(projectPath, 'public/cats/Happy Cat.html'), 'utf8') const file2 = fs.readFileSync(path.join(projectPath, 'public/cats/Nyan Cat.html'), 'utf8') @@ -297,17 +293,16 @@ test.cb('generates error if template has an error', (t) => { limit: 1 }, template: { - path: '../template/error.sgr', + path: 'error.html', output: (item) => `cats/${item.fields.name}.html` } } ] }) - const projectPath = path.join(__dirname, 'fixtures/default') + const projectPath = path.join(__dirname, 'fixtures/error') const project = new Spike({ root: projectPath, - matchers: { html: '**/*.sgr' }, reshape: standard({ locals }), entry: { main: [path.join(projectPath, 'main.js')] }, plugins: [contentful] @@ -316,7 +311,7 @@ test.cb('generates error if template has an error', (t) => { project.on('warning', t.end) project.on('compile', () => t.end('no error')) project.on('error', (error) => { - t.is(error.toString(), "Error: Cannot read property 'fields' of undefined") + t.regex(error.toString(), /Error: Cannot read property 'fields' of undefined/) rimraf.sync(path.join(projectPath, 'public')) t.end() }) diff --git a/yarn.lock b/yarn.lock index 9361e46..54d77df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2216,6 +2216,10 @@ es6-weak-map@^2.0.1: es6-iterator "^2.0.1" es6-symbol "^3.1.1" +es6bindall@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/es6bindall/-/es6bindall-0.0.9.tgz#71e00afa69f8dd59ac5ac898a0d31c978df817d5" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -5426,17 +5430,17 @@ reshape-plugin-util@^0.2.0, reshape-plugin-util@^0.2.1: dependencies: when "^3.7.7" -reshape-retext@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/reshape-retext/-/reshape-retext-1.0.0.tgz#879730f6e5b5d108913e28ee210fc42fb8848cda" +reshape-retext@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/reshape-retext/-/reshape-retext-1.0.1.tgz#015f7ca4162d4ae69359c26e122737c46262a793" dependencies: reshape-plugin-util "^0.2.0" retext "^5.0.0" when "^3.7.8" -reshape-standard@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/reshape-standard/-/reshape-standard-3.0.0.tgz#8aff2cfbbe336e5737d68cde19bde72387d76378" +reshape-standard@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reshape-standard/-/reshape-standard-3.0.1.tgz#dbaef3c67839ebdd7a316e1b3862a554b946e09f" dependencies: markdown-it "^8.2.2" reshape-beautify "^0.1.2" @@ -5446,7 +5450,7 @@ reshape-standard@^3.0.0: reshape-include "^1.0.0" reshape-layouts "^1.0.0" reshape-minify "^1.1.0" - reshape-retext "^1.0.0" + reshape-retext "^1.0.1" retext-smartypants "^3.0.0" reshape@^0.4.1: