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 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 _ = require('lodash'),
4
- path = require('path'),
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 bluebird.resolve(_.map(fileArray, function (file) {
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 = _.get(state, '_layoutData.name');
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 && _.isObject(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
- : bluebird.resolve(false);
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
- : bluebird.resolve(false);
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 bluebird.props(mediaMap)
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 _ = require('lodash'),
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 bluebird.resolve(data);
26
+ return data;
27
27
  }
28
28
 
29
- return bluebird.filter(setup.plugins, (plugin) => plugin.render)
30
- .reduce((val, plugin) => {
31
- return plugin.render(ref, val, locals);
32
- }, data);
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
- _.assign(state, _data);
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
- _.assign(state, { _componentData: _.cloneDeep(_data) });
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(_.get(meta, 'locals.site.styleguide', '_default'));
141
+ initialState._componentVariations = styleguide.getVariations(getStyleguideName(meta));
124
142
 
125
143
  if (meta.locals.edit) {
126
144
  initialState._envVars = ENV_VARS;
127
- initialState._componentSchemas = _.map(componentList, (name) => {
145
+ initialState._componentSchemas = toList(componentList).map((name) => {
128
146
  return { name, schema: getSchemaPath(getComponentPath(name)) };
129
147
  });
130
148
  }
131
149
 
132
- return _.assign(initialState, _.pick(meta, ['locals', '_layoutRef']));
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 = _.pick(process.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
- _.each(getComponents(), function (cmpt) {
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
- _.each(getLayouts(), function (layout) {
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
- _.each(items, function (item) {
51
- var templateFile = getPossibleTemplates(item.path, TEMPLATE_NAME),
52
- isHandlebars = _.includes(templateFile, '.handlebars') || _.includes(templateFile, '.hbs'),
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
- // this wrapper guarantees we'll never render a component in a partial if it doesn't have a _ref
59
- modifiedTemplateFile = nymagHbs.wrapPartial(item.name, templateFileContents);
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
- _.forIn(hbs.partials, function (value, key) {
67
- if (_.isString(value)) {
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(value, { preventIndent: true });
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 (_.isString(filePath)) {
87
- return glob.sync(path.join(filePath, `${templateName}.*`))[0];
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
- return [];
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
- _.forIn(payload, function (value, key) {
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}`, value);
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;