amphora-html 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/index.js +2 -0
- package/lib/clone-deep.js +181 -0
- package/lib/files.js +66 -0
- package/lib/media.js +23 -11
- package/lib/render.js +35 -17
- package/lib/setup.js +55 -28
- package/lib/styleguide.js +40 -15
- package/lib/utils.js +127 -0
- package/package.json +19 -45
- package/.eslintrc +0 -75
- package/.github/workflows/ci.yml +0 -40
- package/.istanbul.yml +0 -51
- package/__mocks__/fileMock.js +0 -3
- package/docs/README.md +0 -31
- package/docs/integration.md +0 -93
- package/docs/intro.md +0 -21
- package/docs/style.md +0 -47
- package/lib/log.test.js +0 -42
- package/lib/media.test.js +0 -797
- package/lib/render.test.js +0 -295
- package/lib/setup.test.js +0 -153
- package/lib/styleguide.test.js +0 -268
- package/test/setupFile.js +0 -3
- package/website/README.md +0 -114
- package/website/core/Footer.js +0 -55
- package/website/package.json +0 -16
- package/website/pages/en/index.js +0 -103
- package/website/pages/en/versions.js +0 -92
- package/website/sidebars.json +0 -13
- package/website/siteConfig.js +0 -66
- package/website/static/css/custom.css +0 -16
- package/website/static/img/logo.svg +0 -13
- package/website/versioned_docs/version-4.0.0/integration.md +0 -94
- package/website/versioned_docs/version-4.0.0/intro.md +0 -22
- package/website/versioned_docs/version-4.0.0/style.md +0 -48
- package/website/versioned_sidebars/version-4.0.0-sidebars.json +0 -13
- package/website/versions.json +0 -3
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ The HTML renderer for Clay components that use [Handlebars](http://handlebarsjs.
|
|
|
5
5
|
## Install
|
|
6
6
|
`$ npm install --save amphora-html`
|
|
7
7
|
|
|
8
|
+
Requires Node 26.12.1 or later. Templates are rendered with [clayhandlebars](https://github.com/clay/handlebars) 6, which comes with amphora-html: you don't need to install it or Handlebars yourself.
|
|
9
|
+
|
|
8
10
|
## The Why
|
|
9
11
|
|
|
10
12
|
HTML rendering was controlled entirely by [Amphora](https://github.com/nymag/amphora) in v2.x, which meant that Amphora was responsible for a lot of heavy lifting. Furthermore, rendering was handled by a module called [Multiplex Templates](https://www.npmjs.com/package/multiplex-templates) which did some :crystal_ball: dark magic :sparkles: to synchronously render component templates. This required some affordances from Amphora which added to the weight of the middleware. By separating renderers out into separate modules which can be plugged into Amphora >v2.x we can create renderers for specific requirements (XML, Amp, etc.) _and_ the rendering process can be moved to separate machines to create more maintainable systems.
|
|
@@ -45,6 +47,21 @@ const helpers = {
|
|
|
45
47
|
amphoraHtml.addHelpers(helpers);
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
### The Handlebars environment
|
|
51
|
+
|
|
52
|
+
`amphoraHtml.handlebars` is the Handlebars environment amphora-html renders with: clayhandlebars' helpers and partials, the helpers given to `addHelpers`, and a partial for every component and layout (registered when amphora-html is first required). `amphoraHtml.wrapPartial(name, template)` is what each component and layout template is wrapped in before it is registered.
|
|
53
|
+
|
|
54
|
+
Use them when you compile templates outside of a page render, in a test for instance, so that they run with exactly the helpers the pages get, instead of requiring your own copy of clayhandlebars:
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
const { handlebars, wrapPartial } = require('amphora-html');
|
|
58
|
+
|
|
59
|
+
handlebars.registerPartial('my-partial', handlebars.compile(wrapPartial('my-partial', source), { preventIndent: true }));
|
|
60
|
+
const html = handlebars.compile(template, { preventIndent: true })(data);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
It is the `handlebars` module that clayhandlebars requires, so whatever you register on it is seen by the pages too.
|
|
64
|
+
|
|
48
65
|
### Amphora HTML Plugins
|
|
49
66
|
|
|
50
67
|
|
package/index.js
CHANGED
|
@@ -9,3 +9,5 @@ module.exports.addResolveMedia = setup.addResolveMedia;
|
|
|
9
9
|
module.exports.addHelpers = setup.addHelpers;
|
|
10
10
|
module.exports.addPlugins = setup.addPlugins;
|
|
11
11
|
module.exports.configureRender = setup.configureRender;
|
|
12
|
+
module.exports.handlebars = setup.handlebars;
|
|
13
|
+
module.exports.wrapPartial = setup.wrapPartial;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { types } = require('util'),
|
|
4
|
+
{ isObject, assignValue } = require('./utils'),
|
|
5
|
+
toString = Object.prototype.toString,
|
|
6
|
+
// what gets copied; anything else (functions, errors, promises, weak maps...)
|
|
7
|
+
// stays the same object, or becomes {} when it is the value given
|
|
8
|
+
CLONEABLE = new Set([
|
|
9
|
+
'[object Arguments]', '[object Array]', '[object ArrayBuffer]', '[object Boolean]',
|
|
10
|
+
'[object DataView]', '[object Date]', '[object Float32Array]', '[object Float64Array]',
|
|
11
|
+
'[object Int8Array]', '[object Int16Array]', '[object Int32Array]', '[object Map]',
|
|
12
|
+
'[object Number]', '[object Object]', '[object RegExp]', '[object Set]', '[object String]',
|
|
13
|
+
'[object Symbol]', '[object Uint8Array]', '[object Uint8ClampedArray]', '[object Uint16Array]',
|
|
14
|
+
'[object Uint32Array]'
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {ArrayBuffer} buffer
|
|
19
|
+
* @returns {ArrayBuffer}
|
|
20
|
+
*/
|
|
21
|
+
function cloneArrayBuffer(buffer) {
|
|
22
|
+
const result = new buffer.constructor(buffer.byteLength);
|
|
23
|
+
|
|
24
|
+
new Uint8Array(result).set(new Uint8Array(buffer));
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An empty object of the same kind, for the kinds that are not plain objects or arrays
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} value
|
|
32
|
+
* @param {string} tag
|
|
33
|
+
* @returns {Object}
|
|
34
|
+
*/
|
|
35
|
+
function initCloneByTag(value, tag) { // eslint-disable-line complexity
|
|
36
|
+
const Ctor = value.constructor;
|
|
37
|
+
|
|
38
|
+
switch (tag) {
|
|
39
|
+
case '[object ArrayBuffer]':
|
|
40
|
+
return cloneArrayBuffer(value);
|
|
41
|
+
case '[object Boolean]':
|
|
42
|
+
case '[object Date]':
|
|
43
|
+
return new Ctor(+value);
|
|
44
|
+
case '[object DataView]':
|
|
45
|
+
return new Ctor(cloneArrayBuffer(value.buffer), value.byteOffset, value.byteLength);
|
|
46
|
+
case '[object Number]':
|
|
47
|
+
case '[object String]':
|
|
48
|
+
return new Ctor(value);
|
|
49
|
+
case '[object RegExp]': {
|
|
50
|
+
const result = new Ctor(value.source, /\w*$/.exec(value));
|
|
51
|
+
|
|
52
|
+
result.lastIndex = value.lastIndex;
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
case '[object Map]':
|
|
56
|
+
case '[object Set]':
|
|
57
|
+
return new Ctor();
|
|
58
|
+
case '[object Symbol]':
|
|
59
|
+
return Object(Symbol.prototype.valueOf.call(value));
|
|
60
|
+
default:
|
|
61
|
+
// typed arrays
|
|
62
|
+
return new Ctor(cloneArrayBuffer(value.buffer), value.byteOffset, value.length);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An empty object with the prototype of `value`, unless `value` is a prototype
|
|
68
|
+
* itself or its constructor is not a function
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} value
|
|
71
|
+
* @returns {Object}
|
|
72
|
+
*/
|
|
73
|
+
function initCloneObject(value) {
|
|
74
|
+
const Ctor = value.constructor,
|
|
75
|
+
proto = Object.getPrototypeOf(value),
|
|
76
|
+
isPrototype = value === (typeof Ctor === 'function' && Ctor.prototype || Object.prototype);
|
|
77
|
+
|
|
78
|
+
return typeof Ctor === 'function' && !isPrototype && isObject(proto) ? Object.create(proto) : {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* An array of the same kind and length, with what RegExp#exec adds to its results
|
|
83
|
+
*
|
|
84
|
+
* @param {Array} array
|
|
85
|
+
* @returns {Array}
|
|
86
|
+
*/
|
|
87
|
+
function initCloneArray(array) {
|
|
88
|
+
const result = new array.constructor(array.length);
|
|
89
|
+
|
|
90
|
+
if (array.length && typeof array[0] === 'string' && Object.hasOwn(array, 'index')) {
|
|
91
|
+
result.index = array.index;
|
|
92
|
+
result.input = array.input;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Own enumerable keys, symbols included
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} value
|
|
102
|
+
* @returns {Array}
|
|
103
|
+
*/
|
|
104
|
+
function ownKeys(value) {
|
|
105
|
+
return Object.keys(value).concat(Object.getOwnPropertySymbols(value).filter((symbol) => Object.prototype.propertyIsEnumerable.call(value, symbol)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {*} value
|
|
110
|
+
* @param {Object} [parent] the object `value` was read from
|
|
111
|
+
* @param {Map} copies the copy of every object copied so far
|
|
112
|
+
* @returns {*}
|
|
113
|
+
*/
|
|
114
|
+
function baseClone(value, parent, copies) { // eslint-disable-line complexity
|
|
115
|
+
if (!isObject(value)) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isArray = Array.isArray(value);
|
|
120
|
+
let result;
|
|
121
|
+
|
|
122
|
+
if (isArray) {
|
|
123
|
+
result = initCloneArray(value);
|
|
124
|
+
} else {
|
|
125
|
+
const tag = toString.call(value),
|
|
126
|
+
isFunction = tag === '[object Function]' || tag === '[object GeneratorFunction]';
|
|
127
|
+
|
|
128
|
+
if (Buffer.isBuffer(value)) {
|
|
129
|
+
// a view of the same memory, as lodash's cloneDeep gave
|
|
130
|
+
return value.subarray();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (tag === '[object Object]' || tag === '[object Arguments]' || isFunction && !parent) {
|
|
134
|
+
result = isFunction ? {} : initCloneObject(value);
|
|
135
|
+
} else if (CLONEABLE.has(tag)) {
|
|
136
|
+
result = initCloneByTag(value, tag);
|
|
137
|
+
} else {
|
|
138
|
+
return parent ? value : {};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// an object met before (shared or circular) gets the same copy
|
|
143
|
+
if (copies.has(value)) {
|
|
144
|
+
return copies.get(value);
|
|
145
|
+
}
|
|
146
|
+
copies.set(value, result);
|
|
147
|
+
|
|
148
|
+
if (types.isSet(value)) {
|
|
149
|
+
value.forEach((item) => result.add(baseClone(item, value, copies)));
|
|
150
|
+
} else if (types.isMap(value)) {
|
|
151
|
+
value.forEach((item, key) => result.set(key, baseClone(item, value, copies)));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isArray) {
|
|
155
|
+
// every index, holes included
|
|
156
|
+
for (let index = 0; index < value.length; index++) {
|
|
157
|
+
assignValue(result, index, baseClone(value[index], value, copies));
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
for (const key of ownKeys(value)) {
|
|
161
|
+
assignValue(result, key, baseClone(value[key], value, copies));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A deep copy, made the way lodash's cloneDeep made it: objects keep their
|
|
170
|
+
* prototype, a shared or circular object is copied once, Dates, RegExps, Maps,
|
|
171
|
+
* Sets, typed arrays and boxed values are copied, and functions, errors and
|
|
172
|
+
* other objects that can't be copied are kept as they are.
|
|
173
|
+
*
|
|
174
|
+
* @param {*} value
|
|
175
|
+
* @returns {*}
|
|
176
|
+
*/
|
|
177
|
+
function cloneDeep(value) {
|
|
178
|
+
return baseClone(value, undefined, new Map());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = cloneDeep;
|
package/lib/files.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// What the renderer asks the file system. Every answer is remembered for the
|
|
4
|
+
// life of the process, as with nymag-fs: a file added later is not seen, and
|
|
5
|
+
// each file is read once.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs'),
|
|
8
|
+
path = require('path'),
|
|
9
|
+
{ memoize } = require('./utils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} filename
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
function fileExists(filename) {
|
|
16
|
+
try {
|
|
17
|
+
return !!fs.statSync(filename);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} file
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
function isDirectory(file) {
|
|
28
|
+
try {
|
|
29
|
+
return fs.statSync(file).isDirectory();
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The files of a folder, without folders, tests and documentation
|
|
37
|
+
*
|
|
38
|
+
* @param {string} dir
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
function getFiles(dir) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.readdirSync(dir)
|
|
44
|
+
.filter((file) => !isDirectory(path.join(dir, file)) &&
|
|
45
|
+
file.indexOf('.test.') === -1 && // ignore test files
|
|
46
|
+
file.indexOf('.md') === -1); // ignore documentation files
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The contents of a file
|
|
54
|
+
*
|
|
55
|
+
* @param {string} file
|
|
56
|
+
* @returns {Promise<string>}
|
|
57
|
+
*/
|
|
58
|
+
function readFilePromise(file) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
fs.readFile(file, 'utf8', (err, data) => err ? reject(err) : resolve(data));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports.fileExists = memoize(fileExists);
|
|
65
|
+
module.exports.getFiles = memoize(getFiles);
|
|
66
|
+
module.exports.readFilePromise = memoize(readFilePromise);
|
package/lib/media.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
files = require('nymag-fs'),
|
|
6
|
-
bluebird = require('bluebird'),
|
|
3
|
+
const path = require('path'),
|
|
4
|
+
files = require('./files'),
|
|
7
5
|
setup = require('./setup'),
|
|
8
6
|
styleguide = require('./styleguide'),
|
|
7
|
+
{ isObject, toList } = require('./utils'),
|
|
9
8
|
STYLE_TAG = 'style',
|
|
10
9
|
SCRIPT_TAG = 'scripts',
|
|
11
10
|
MODULE_SCRIPT_TAG = 'module',
|
|
@@ -164,7 +163,7 @@ function injectTags(fileArray, tag) {
|
|
|
164
163
|
// Defaults to false so existing deployments are unaffected.
|
|
165
164
|
var moduleBuster = module.exports.omitCacheBusterOnModules ? '' : buster;
|
|
166
165
|
|
|
167
|
-
return
|
|
166
|
+
return Promise.resolve(toList(fileArray).map(function (file) {
|
|
168
167
|
if (tag === STYLE_TAG) {
|
|
169
168
|
return `<link rel="stylesheet" type="text/css" href="${file}${buster}">`;
|
|
170
169
|
} else if (tag === MODULE_SCRIPT_TAG) {
|
|
@@ -180,6 +179,19 @@ function injectTags(fileArray, tag) {
|
|
|
180
179
|
}).join('\n'));
|
|
181
180
|
}
|
|
182
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Wait for the values of an object, the way Bluebird's props did
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} obj values or promises for them
|
|
186
|
+
* @returns {Promise<Object>} the same keys with the values
|
|
187
|
+
*/
|
|
188
|
+
function props(obj) {
|
|
189
|
+
const keys = Object.keys(obj);
|
|
190
|
+
|
|
191
|
+
return Promise.all(keys.map((key) => obj[key]))
|
|
192
|
+
.then((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
|
|
193
|
+
}
|
|
194
|
+
|
|
183
195
|
/**
|
|
184
196
|
* Gets a list of all css and js needed for the components listed.
|
|
185
197
|
* @param {Object} state
|
|
@@ -206,7 +218,7 @@ function getStyleFiles(state) {
|
|
|
206
218
|
assetPath = site && site.assetPath || '',
|
|
207
219
|
assetHost = site && site.assetHost || '',
|
|
208
220
|
siteStyleguide = site && site.styleguide,
|
|
209
|
-
layoutName =
|
|
221
|
+
layoutName = state._layoutData?.name;
|
|
210
222
|
let cssFilePaths = [], layoutBasePath;
|
|
211
223
|
|
|
212
224
|
// legacy behavior: load <component>.css and <component>.<site>.css
|
|
@@ -331,7 +343,7 @@ function pathJoin(assetHost, assetPath, assetDir) {
|
|
|
331
343
|
* @param {string} [cacheBuster]
|
|
332
344
|
*/
|
|
333
345
|
function configure(options, cacheBuster = '') {
|
|
334
|
-
if (options &&
|
|
346
|
+
if (options && isObject(options)) {
|
|
335
347
|
module.exports.editStylesTags = options.styles || false;
|
|
336
348
|
module.exports.editScriptsTags = options.scripts || false;
|
|
337
349
|
// modulepreload: when true, <link rel="modulepreload"> hints are injected
|
|
@@ -366,7 +378,7 @@ function injectScriptsAndStyles(state) {
|
|
|
366
378
|
const { locals } = state;
|
|
367
379
|
|
|
368
380
|
return function (html) {
|
|
369
|
-
var mediaMap
|
|
381
|
+
var mediaMap;
|
|
370
382
|
|
|
371
383
|
// Check that HTML is string
|
|
372
384
|
if (typeof html !== 'string') {
|
|
@@ -387,7 +399,7 @@ function injectScriptsAndStyles(state) {
|
|
|
387
399
|
|
|
388
400
|
mediaMap.moduleScripts = moduleScriptFiles.length
|
|
389
401
|
? injectTags(moduleScriptFiles, MODULE_SCRIPT_TAG)
|
|
390
|
-
:
|
|
402
|
+
: Promise.resolve(false);
|
|
391
403
|
|
|
392
404
|
// modulePreloads: <link rel="modulepreload"> hints for <head>.
|
|
393
405
|
// Only active when configure({ modulepreload: true }) has been called — opt-in so
|
|
@@ -398,7 +410,7 @@ function injectScriptsAndStyles(state) {
|
|
|
398
410
|
|
|
399
411
|
mediaMap.modulePreloads = modulePreloadFiles.length
|
|
400
412
|
? injectTags(modulePreloadFiles, MODULEPRELOAD_TAG)
|
|
401
|
-
:
|
|
413
|
+
: Promise.resolve(false);
|
|
402
414
|
|
|
403
415
|
if (!locals.edit) {
|
|
404
416
|
mediaMap.styles = combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG);
|
|
@@ -408,7 +420,7 @@ function injectScriptsAndStyles(state) {
|
|
|
408
420
|
mediaMap.scripts = module.exports.editScriptsTags ? injectTags(mediaMap.scripts, SCRIPT_TAG) : combineFileContents(mediaMap.scripts, 'public/js', '/js/', SCRIPT_TAG);
|
|
409
421
|
}
|
|
410
422
|
|
|
411
|
-
return
|
|
423
|
+
return props(mediaMap)
|
|
412
424
|
.then(combinedFiles => {
|
|
413
425
|
// modulepreload hints go first in <head>, before CSS, so the browser can
|
|
414
426
|
// start fetching ESM scripts at the earliest possible moment.
|
package/lib/render.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
mediaService = require('./media'),
|
|
3
|
+
const mediaService = require('./media'),
|
|
5
4
|
clayUtils = require('clayutils'),
|
|
6
|
-
bluebird = require('bluebird'),
|
|
7
5
|
styleguide = require('./styleguide'),
|
|
8
|
-
setup = require('./setup')
|
|
6
|
+
setup = require('./setup'),
|
|
7
|
+
cloneDeep = require('./clone-deep'),
|
|
8
|
+
{ assign, pick, toList } = require('./utils');
|
|
9
9
|
|
|
10
10
|
let { getIndices, getSchemaPath, getComponentPath, getLayoutPath } = require('amphora-fs'),
|
|
11
11
|
log = require('./log').setup({ file: __filename }),
|
|
@@ -18,18 +18,24 @@ let { getIndices, getSchemaPath, getComponentPath, getLayoutPath } = require('am
|
|
|
18
18
|
* @param {String} ref
|
|
19
19
|
* @param {Object} data
|
|
20
20
|
* @param {Object} locals
|
|
21
|
-
* @returns {Object} data
|
|
21
|
+
* @returns {Promise<Object>} data
|
|
22
22
|
*/
|
|
23
|
-
function applyRenderHooks(ref, data, locals) {
|
|
23
|
+
async function applyRenderHooks(ref, data, locals) {
|
|
24
24
|
// skip render hooks in edit mode
|
|
25
25
|
if (locals.edit) {
|
|
26
|
-
return
|
|
26
|
+
return data;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const plugins = setup.plugins.filter((plugin) => plugin.render);
|
|
30
|
+
// with no data, the first plugin stands in for it and its hook is skipped,
|
|
31
|
+
// as Bluebird's reduce did (a model.js `render` that returns nothing gets here)
|
|
32
|
+
let val = data === undefined ? await plugins.shift() : await data;
|
|
33
|
+
|
|
34
|
+
for (const plugin of plugins) {
|
|
35
|
+
val = await plugin.render(ref, val, locals);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return val;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -79,17 +85,29 @@ function makeHtml(state) {
|
|
|
79
85
|
|
|
80
86
|
// At the final stage of rendering we need to smoosh
|
|
81
87
|
// all the data together with the state.
|
|
82
|
-
|
|
88
|
+
assign(state, _data);
|
|
83
89
|
|
|
84
90
|
// send clone of state/data to be used by kiln so kiln doesn't load component data that was mutated by hbs templates
|
|
85
91
|
if (state.locals.edit) {
|
|
86
|
-
|
|
92
|
+
state._componentData = cloneDeep(_data);
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
return rootPartial(state);
|
|
90
96
|
};
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The styleguide of the site, `_default` when it has none
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} meta
|
|
103
|
+
* @return {String}
|
|
104
|
+
*/
|
|
105
|
+
function getStyleguideName(meta) {
|
|
106
|
+
const name = meta.locals?.site?.styleguide;
|
|
107
|
+
|
|
108
|
+
return name === undefined ? '_default' : name;
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
/**
|
|
94
112
|
* Construct the object that is needed by Kiln for rendering in
|
|
95
113
|
* edit mode. Requires reading from the file system.
|
|
@@ -120,16 +138,16 @@ function makeState(data, meta) {
|
|
|
120
138
|
|
|
121
139
|
// grab the component variations and make available via the state
|
|
122
140
|
// note: if styleguide isn't set, this gets variations from the '_default' styleguide
|
|
123
|
-
initialState._componentVariations = styleguide.getVariations(
|
|
141
|
+
initialState._componentVariations = styleguide.getVariations(getStyleguideName(meta));
|
|
124
142
|
|
|
125
143
|
if (meta.locals.edit) {
|
|
126
144
|
initialState._envVars = ENV_VARS;
|
|
127
|
-
initialState._componentSchemas =
|
|
145
|
+
initialState._componentSchemas = toList(componentList).map((name) => {
|
|
128
146
|
return { name, schema: getSchemaPath(getComponentPath(name)) };
|
|
129
147
|
});
|
|
130
148
|
}
|
|
131
149
|
|
|
132
|
-
return
|
|
150
|
+
return Object.assign(initialState, pick(meta, ['locals', '_layoutRef']));
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
/**
|
|
@@ -200,7 +218,7 @@ function configure({ editAssetTags, cacheBuster, modulepreload }) {
|
|
|
200
218
|
* @param {Array} vars
|
|
201
219
|
*/
|
|
202
220
|
function addEnvVars(vars) {
|
|
203
|
-
ENV_VARS =
|
|
221
|
+
ENV_VARS = pick(process.env, vars);
|
|
204
222
|
log('info', 'env vars registered with amphora html');
|
|
205
223
|
}
|
|
206
224
|
|
package/lib/setup.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path'),
|
|
4
|
-
_ = require('lodash'),
|
|
5
|
-
glob = require('glob'),
|
|
6
4
|
fs = require('fs'),
|
|
7
5
|
render = require('./render'),
|
|
6
|
+
{ toList } = require('./utils'),
|
|
8
7
|
nymagHbs = require('clayhandlebars'),
|
|
9
8
|
hbs = nymagHbs(),
|
|
10
9
|
TEMPLATE_NAME = 'template';
|
|
@@ -24,19 +23,19 @@ function init() {
|
|
|
24
23
|
function getRenderableItems() {
|
|
25
24
|
const items = [];
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
for (const cmpt of toList(getComponents())) {
|
|
28
27
|
items.push({
|
|
29
28
|
name: cmpt,
|
|
30
29
|
path: getComponentPath(cmpt)
|
|
31
30
|
});
|
|
32
|
-
}
|
|
31
|
+
}
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
for (const layout of toList(getLayouts())) {
|
|
35
34
|
items.push({
|
|
36
35
|
name: layout,
|
|
37
36
|
path: getLayoutPath(layout)
|
|
38
37
|
});
|
|
39
|
-
}
|
|
38
|
+
}
|
|
40
39
|
|
|
41
40
|
return items;
|
|
42
41
|
}
|
|
@@ -47,47 +46,56 @@ function getRenderableItems() {
|
|
|
47
46
|
* @param {Array} items
|
|
48
47
|
*/
|
|
49
48
|
function registerPartials(items) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
isHandlebars =
|
|
53
|
-
templateFileContents,
|
|
54
|
-
modifiedTemplateFile;
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const templateFile = getPossibleTemplates(item.path, TEMPLATE_NAME),
|
|
51
|
+
isHandlebars = typeof templateFile === 'string' && (templateFile.includes('.handlebars') || templateFile.includes('.hbs'));
|
|
55
52
|
|
|
56
53
|
if (isHandlebars) {
|
|
57
|
-
templateFileContents = fs.readFileSync(templateFile, 'utf8')
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
const templateFileContents = fs.readFileSync(templateFile, 'utf8'),
|
|
55
|
+
// this wrapper guarantees we'll never render a component in a partial if it doesn't have a _ref
|
|
56
|
+
modifiedTemplateFile = nymagHbs.wrapPartial(item.name, templateFileContents);
|
|
60
57
|
|
|
61
58
|
hbs.registerPartial(item.name, modifiedTemplateFile);
|
|
62
59
|
}
|
|
63
|
-
}
|
|
60
|
+
}
|
|
64
61
|
|
|
65
|
-
// Compile all the loaded partials
|
|
66
|
-
|
|
67
|
-
if (
|
|
62
|
+
// Compile all the loaded partials (inherited ones too, as lodash's forIn did)
|
|
63
|
+
for (const key in hbs.partials) {
|
|
64
|
+
if (typeof hbs.partials[key] === 'string') {
|
|
68
65
|
// preventIndent:true solve issue with <pre> tag/white-space: pre
|
|
69
66
|
// see http://handlebarsjs.com/reference.html compile() options
|
|
70
|
-
hbs.partials[key] = hbs.compile(
|
|
67
|
+
hbs.partials[key] = hbs.compile(hbs.partials[key], { preventIndent: true });
|
|
71
68
|
}
|
|
72
|
-
}
|
|
69
|
+
}
|
|
73
70
|
|
|
74
71
|
module.exports.hbs = hbs;
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
/**
|
|
78
75
|
* Grab the possible template files based on the name.
|
|
79
|
-
* Returns a path to the file so that it can be read
|
|
76
|
+
* Returns a path to the file so that it can be read: the first
|
|
77
|
+
* `<templateName>.*` file, in the order glob used to sort them.
|
|
80
78
|
*
|
|
81
79
|
* @param {String} filePath
|
|
82
80
|
* @param {String} templateName
|
|
83
81
|
* @return {String}
|
|
84
82
|
*/
|
|
85
83
|
function getPossibleTemplates(filePath, templateName) {
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
if (typeof filePath === 'string') {
|
|
85
|
+
const dir = path.dirname(path.join(filePath, `${templateName}.*`));
|
|
86
|
+
let names;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
names = fs.readdirSync(dir);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
return names
|
|
95
|
+
.filter((name) => name.startsWith(`${templateName}.`))
|
|
96
|
+
.map((name) => path.join(dir, name))
|
|
97
|
+
.sort((a, b) => a.localeCompare(b, 'en'))[0];
|
|
98
|
+
}
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
/**
|
|
@@ -96,10 +104,11 @@ function getPossibleTemplates(filePath, templateName) {
|
|
|
96
104
|
* @param {Object} payload
|
|
97
105
|
*/
|
|
98
106
|
function addHelpers(payload) {
|
|
99
|
-
|
|
107
|
+
// inherited helpers too, as lodash's forIn did
|
|
108
|
+
for (const key in payload) { // eslint-disable-line guard-for-in
|
|
100
109
|
// set up handlebars helpers that rely on internal services
|
|
101
|
-
hbs.registerHelper(`${key}`,
|
|
102
|
-
}
|
|
110
|
+
hbs.registerHelper(`${key}`, payload[key]);
|
|
111
|
+
}
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
/**
|
|
@@ -143,6 +152,24 @@ module.exports.renderSettings = undefined;
|
|
|
143
152
|
module.exports.resolveMedia = undefined;
|
|
144
153
|
module.exports.plugins = [];
|
|
145
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The Handlebars environment amphora-html renders with: clayhandlebars' helpers and
|
|
157
|
+
* partials, the helpers given to `addHelpers`, and a partial for each component and
|
|
158
|
+
* layout. It is the `handlebars` module clayhandlebars itself requires, so it is
|
|
159
|
+
* shared with anything else in the process that uses that module.
|
|
160
|
+
*
|
|
161
|
+
* @type {Object}
|
|
162
|
+
*/
|
|
163
|
+
module.exports.handlebars = hbs;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* What every component and layout template is wrapped in before it is registered
|
|
167
|
+
* as a partial: clayhandlebars' `wrapPartial(name, template)`.
|
|
168
|
+
*
|
|
169
|
+
* @type {Function}
|
|
170
|
+
*/
|
|
171
|
+
module.exports.wrapPartial = nymagHbs.wrapPartial;
|
|
172
|
+
|
|
146
173
|
// Setup functions
|
|
147
174
|
module.exports.init = init;
|
|
148
175
|
module.exports.addHelpers = addHelpers;
|