-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes to API #4
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
const htmlTag = require('html-tag') | ||
const { modifyNodes } = require('reshape-plugin-util') | ||
|
||
module.exports = function datoMetaTags () { | ||
return function datoMetaTagsPlugin (tree, ctx) { | ||
if (ctx) { | ||
Object.assign(ctx.runtime, { htmlTag }) | ||
} | ||
|
||
return modifyNodes( | ||
tree, | ||
(node) => node.name === 'dato-meta-tags', | ||
(node) => { | ||
if (!(node.attrs && node.attrs.record)) { | ||
throw new ctx.PluginError({ | ||
message: 'dato-meta-tags tag has no "record" attribute', | ||
plugin: 'spike-datocms', | ||
location: node.location | ||
}) | ||
} | ||
|
||
return { | ||
type: 'code', | ||
content: `(function() { | ||
const record = locals.${node.attrs.record[0].content}; | ||
if (!record || !record.seoMetaTags) { return null; } | ||
return record.seoMetaTags.map(tag => ( | ||
__runtime.htmlTag(tag.tagName, tag.attributes || {}, tag.content) | ||
)).join(''); | ||
})()` | ||
} | ||
} | ||
) | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,25 +7,20 @@ const fs = require('fs') | |
const reshape = require('reshape') | ||
const loader = require('reshape-loader') | ||
const {SiteClient} = require('datocms-client') | ||
const DatoLoader = require('datocms-client/lib/local/Loader') | ||
const bindAllClass = require('es6bindall') | ||
const mkdirp = require('mkdirp') | ||
|
||
module.exports = class SpikeDatoCMS { | ||
constructor (opts) { | ||
Object.assign(this, this.validate(opts)) | ||
this.client = new SiteClient(opts.token) | ||
this.client = new DatoLoader(new SiteClient(opts.token)) | ||
bindAllClass(this, ['apply', 'run']) | ||
} | ||
|
||
apply (compiler) { | ||
this.util = new SpikeUtil(compiler.options) | ||
this.util.runAll(compiler, this.run) | ||
|
||
// set cache to full path for use in emit + run functions | ||
if (this.cache) { | ||
this.cache = path.join(compiler.options.context, this.cache) | ||
} | ||
|
||
// TODO: this pulls the incorrect loader context | ||
compiler.plugin('compilation', (compilation) => { | ||
compilation.plugin('normal-module-loader', (loaderContext) => { | ||
|
@@ -34,61 +29,21 @@ module.exports = class SpikeDatoCMS { | |
}) | ||
|
||
compiler.plugin('emit', (compilation, done) => { | ||
if (this.json) { | ||
writeJson(compilation, this.json, this.addDataTo.dato) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also removing the writeJson feature? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that was just temporary, I can re-enable it, but I thought that was redundant, as you can use the the |
||
|
||
// if cache is set and cache file doesn't exist; write it | ||
if (this.cache && this.rewrite) { | ||
writeCache(this.cache, this.addDataTo.dato) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why remove the cache feature? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, DatoCMS pushes some websocket notifications everytime the content changes: const SiteChangeWatcher = require('datocms-client/dump/SiteChangeWatcher');
const watcher = new SiteChangeWatcher(site.id);
watcher.connect(function() {
console.log('Data changed!');
}); So I think we should definitely get rid of the |
||
if (this.singlePages) { | ||
W.map(this.singlePages(this.client.itemsRepo), (page) => { | ||
return writeTemplate.call(this, compiler, compilation, page) | ||
}).done(() => done(), done) | ||
} | ||
|
||
this.models.filter((m) => m.json).map((m) => { | ||
return writeJson(compilation, m.json, this.addDataTo.dato[m.type]) | ||
}) | ||
|
||
W.map(this.models.filter((m) => m.template), (contentType) => { | ||
return writeTemplate.call(this, compiler, compilation, contentType) | ||
}).done(() => done(), done) | ||
done() | ||
}) | ||
} | ||
|
||
run (compilation, done) { | ||
return W.all([ | ||
fetchData.call(this), | ||
W.reduce(this.models, (memo, model) => { | ||
// format options | ||
const options = {} | ||
if (model.ids) { options['filter[ids]'] = model.ids } | ||
if (model.query) { options['filter[query]'] = model.query } | ||
if (model.offset) { options['page[offset]'] = model.offset } | ||
if (model.limit) { options['page[limit]'] = model.limit } | ||
const transformFn = model.transform ? model.transform : (x) => x | ||
|
||
// fetch items and itemTypes | ||
return W.all([ | ||
W(this.client.items.all(options)), | ||
W(this.client.itemTypes.all()) | ||
]) | ||
// make sure linked entries are resolved | ||
.then(resolveLinks) | ||
// filter to the model type, if necessary | ||
.then(([records, itemTypes]) => { | ||
if (!model.type) return records | ||
const t = itemTypes.find((it) => it.apiKey === model.type) | ||
return records.filter((r) => r.itemType === t.id) | ||
}) | ||
// transform if necessary | ||
.then((res) => W.map(res, (entry) => transformFn(entry))) | ||
// add resolved item to the response | ||
.tap((res) => { memo[model.type || 'all'] = res }) | ||
.yield(memo) | ||
}, {}) | ||
]).done(([site, models]) => { | ||
// add to the locals | ||
Object.assign(this.addDataTo, { dato: Object.assign(models, { _meta: site }) }) | ||
return this.client.load().then(() => { | ||
Object.assign(this.addDataTo, { dato: this.client.itemsRepo }) | ||
done() | ||
}, done) | ||
}) | ||
} | ||
|
||
/** | ||
|
@@ -99,24 +54,7 @@ module.exports = class SpikeDatoCMS { | |
const schema = Joi.object().keys({ | ||
token: Joi.string().required(), | ||
addDataTo: Joi.object().required(), | ||
json: Joi.string(), | ||
cache: Joi.string(), | ||
models: Joi.array().items( | ||
Joi.object().keys({ | ||
type: Joi.string().default(Joi.ref('name')), | ||
name: Joi.string(), // this is an alias for type | ||
ids: Joi.array().single(), | ||
query: Joi.string(), | ||
offset: Joi.number(), | ||
limit: Joi.number(), | ||
transform: Joi.func(), | ||
json: Joi.string(), | ||
template: Joi.object().keys({ | ||
path: Joi.string(), | ||
output: Joi.func() | ||
}) | ||
}) | ||
) | ||
singlePages: Joi.func() | ||
}) | ||
|
||
const res = Joi.validate(opts, schema, { | ||
|
@@ -125,92 +63,33 @@ module.exports = class SpikeDatoCMS { | |
object: { child: '!![spike-datocms constructor] option {{reason}}' } | ||
} | ||
}) | ||
|
||
if (res.error) { throw new Error(res.error) } | ||
return res.value | ||
} | ||
} | ||
|
||
function fetchData () { | ||
// if cache is set | ||
if (this.cache) { | ||
// if cache is set and cache file doesn't exist; write it | ||
if (!fs.existsSync(this.cache)) { | ||
this.rewrite = true | ||
} else { // if cache is set and cache file exists; parse it | ||
return W.resolve(JSON.parse(fs.readFileSync(this.cache, 'utf8'))) | ||
} | ||
} else { // if cache isn't set, hit the API | ||
return this.client.site.find() | ||
} | ||
} | ||
|
||
// TODO: use proxies so there can be no infinite loopz | ||
function resolveLinks ([records, itemTypes]) { | ||
// find all model ids | ||
const recordIds = records.map((r) => r.id) | ||
// scan all model values | ||
records.map((r) => { | ||
for (let k in r) { | ||
if (k === 'id') continue | ||
// check to see if it is a model id, which means it's a link | ||
// if so, replace the id with the actual item | ||
if (Array.isArray(r[k])) { | ||
r[k] = r[k].map(resolveLink.bind(null, recordIds, records)) | ||
} else { | ||
r[k] = resolveLink(recordIds, records, r[k]) | ||
} | ||
} | ||
}) | ||
return [records, itemTypes] | ||
} | ||
|
||
function resolveLink (ids, records, record) { | ||
if (ids.indexOf(record) > -1) { | ||
return records.find((r2) => r2.id === record) | ||
} else { | ||
return record | ||
} | ||
} | ||
|
||
function writeJson (compilation, filename, data) { | ||
const src = JSON.stringify(data, null, 2) | ||
compilation.assets[filename] = { | ||
source: () => src, | ||
size: () => src.length | ||
} | ||
} | ||
|
||
function writeCache (filename, data) { | ||
const src = JSON.stringify(data, null, 2) | ||
return mkdirp(path.dirname(filename), function () { | ||
fs.writeFileSync(filename, src) | ||
}) | ||
} | ||
|
||
// TODO: get rid of this, put the templates through webpack | ||
function writeTemplate (compiler, compilation, model) { | ||
const data = this.addDataTo.dato[model.type] | ||
const filePath = path.join(compiler.options.context, model.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, {}) | ||
|
||
// for any plugins that pull locals from the options | ||
options.locals = newLocals | ||
options.filename = filePath | ||
|
||
return reshape(options) | ||
.process(template) | ||
.then((res) => { | ||
const html = res.output(newLocals) | ||
compilation.assets[model.template.output(item)] = { | ||
source: () => html, | ||
size: () => html.length | ||
} | ||
}) | ||
}) | ||
function writeTemplate (compiler, compilation, page) { | ||
const filePath = path.join(compiler.options.context, page.template) | ||
|
||
return node.call(fs.readFile.bind(fs), filePath, 'utf8') | ||
.then((template) => { | ||
const newLocals = Object.assign({}, this.addDataTo, page.locals) | ||
const options = loader.parseOptions.call(this.loaderContext, this.util.getSpikeOptions().reshape, {}) | ||
|
||
// for any plugins that pull locals from the options | ||
options.locals = newLocals | ||
options.filename = filePath | ||
|
||
return reshape(options) | ||
.process(template) | ||
.then((res) => { | ||
const html = res.output(newLocals) | ||
compilation.assets[page.output] = { | ||
source: () => html, | ||
size: () => html.length | ||
} | ||
}) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very clever way of making this happen! Curious if you had any other thoughts working with reshape and its API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was an hack actually 😛 I'm not using reshape to generate the HTML, resorting to some external helper. I don't think that's very advisable... it would be better to return some reshape AST nodes instead, so that they could then be further processed by other reshape plugins down the chain (ie.
reshape/minify
). I could not find a way to do that though... 🆘