amphora-html 6.0.1-dev.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 +83 -79
- package/lib/render.js +44 -19
- package/lib/setup.js +55 -28
- package/lib/styleguide.js +40 -15
- package/lib/utils.js +127 -0
- package/package.json +21 -44
- package/.circleci/config.yml +0 -44
- package/.eslintrc +0 -75
- package/.github/workflows/push.yml +0 -26
- 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 -908
- 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,17 +1,14 @@
|
|
|
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'),
|
|
9
|
-
|
|
7
|
+
{ isObject, toList } = require('./utils'),
|
|
10
8
|
STYLE_TAG = 'style',
|
|
11
9
|
SCRIPT_TAG = 'scripts',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
ASYNC_DEFER_SCRIPT_TAG = 'async-defer',
|
|
10
|
+
MODULE_SCRIPT_TAG = 'module',
|
|
11
|
+
MODULEPRELOAD_TAG = 'modulepreload',
|
|
15
12
|
MEDIA_DIRECTORY = path.join(process.cwd(), 'public');
|
|
16
13
|
|
|
17
14
|
/**
|
|
@@ -150,47 +147,49 @@ function appendMediaToBottom(scripts, html) {
|
|
|
150
147
|
return splice(html, index, scripts);
|
|
151
148
|
}
|
|
152
149
|
|
|
153
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Build inline <link>/<script> tags for an array of asset paths. Used by edit
|
|
152
|
+
* mode (where assets are loaded individually rather than bundled) and by the
|
|
153
|
+
* Vite ESM pipeline (module / modulepreload).
|
|
154
|
+
*
|
|
155
|
+
* @param {string[]} fileArray asset paths
|
|
156
|
+
* @param {string} tag one of STYLE_TAG, SCRIPT_TAG, MODULE_SCRIPT_TAG, MODULEPRELOAD_TAG
|
|
157
|
+
* @returns {Promise<string>} newline-joined tag string
|
|
158
|
+
*/
|
|
159
|
+
function injectTags(fileArray, tag) {
|
|
154
160
|
var buster = module.exports.cacheBuster ? `?version=${module.exports.cacheBuster}` : '';
|
|
161
|
+
// When omitCacheBusterOnModules is enabled, content-hashed ESM files omit the
|
|
162
|
+
// ?version= query string — the hash in the filename already provides cache busting.
|
|
163
|
+
// Defaults to false so existing deployments are unaffected.
|
|
164
|
+
var moduleBuster = module.exports.omitCacheBusterOnModules ? '' : buster;
|
|
155
165
|
|
|
156
|
-
return
|
|
166
|
+
return Promise.resolve(toList(fileArray).map(function (file) {
|
|
157
167
|
if (tag === STYLE_TAG) {
|
|
158
168
|
return `<link rel="stylesheet" type="text/css" href="${file}${buster}">`;
|
|
159
|
-
} else if (tag ===
|
|
160
|
-
return `<script
|
|
161
|
-
} else if (tag ===
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return `<
|
|
165
|
-
} else {
|
|
166
|
-
return `<script type="text/javascript" src="${file}${buster}"></script>`;
|
|
169
|
+
} else if (tag === MODULE_SCRIPT_TAG) {
|
|
170
|
+
return `<script type="module" src="${file}${moduleBuster}"></script>`;
|
|
171
|
+
} else if (tag === MODULEPRELOAD_TAG) {
|
|
172
|
+
// <link rel="modulepreload"> tells the browser to fetch ESM scripts early,
|
|
173
|
+
// during HTML parsing, before reaching the <script> tags at </body>.
|
|
174
|
+
return `<link rel="modulepreload" href="${file}${moduleBuster}">`;
|
|
167
175
|
}
|
|
176
|
+
|
|
177
|
+
// Default: classic <script src=...> for SCRIPT_TAG and any other value.
|
|
178
|
+
return `<script type="text/javascript" src="${file}${buster}"></script>`;
|
|
168
179
|
}).join('\n'));
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
/**
|
|
172
|
-
*
|
|
173
|
-
* assets so that we can fall back on Browserify.
|
|
183
|
+
* Wait for the values of an object, the way Bluebird's props did
|
|
174
184
|
*
|
|
175
|
-
* @param {Object}
|
|
176
|
-
* @returns {
|
|
185
|
+
* @param {Object} obj values or promises for them
|
|
186
|
+
* @returns {Promise<Object>} the same keys with the values
|
|
177
187
|
*/
|
|
178
|
-
function
|
|
179
|
-
const
|
|
180
|
-
assetDir = site && site.assetDir || MEDIA_DIRECTORY,
|
|
181
|
-
manifestName = site && site.manifestName || 'assets-manifest.json',
|
|
182
|
-
manifestFile = path.resolve(path.join(assetDir, manifestName));
|
|
183
|
-
let manifest;
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
manifest = files.tryRequire(manifestFile);
|
|
187
|
-
} catch (err) {
|
|
188
|
-
return state._components;
|
|
189
|
-
}
|
|
190
|
-
|
|
188
|
+
function props(obj) {
|
|
189
|
+
const keys = Object.keys(obj);
|
|
191
190
|
|
|
192
|
-
return
|
|
193
|
-
.
|
|
191
|
+
return Promise.all(keys.map((key) => obj[key]))
|
|
192
|
+
.then((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
/**
|
|
@@ -202,7 +201,6 @@ function getMediaMap(state) {
|
|
|
202
201
|
return {
|
|
203
202
|
styles: module.exports.getStyleFiles(state),
|
|
204
203
|
scripts: module.exports.getScriptFiles(state),
|
|
205
|
-
manifestAssets: module.exports.getManifestAssets(state)
|
|
206
204
|
};
|
|
207
205
|
}
|
|
208
206
|
|
|
@@ -220,7 +218,7 @@ function getStyleFiles(state) {
|
|
|
220
218
|
assetPath = site && site.assetPath || '',
|
|
221
219
|
assetHost = site && site.assetHost || '',
|
|
222
220
|
siteStyleguide = site && site.styleguide,
|
|
223
|
-
layoutName =
|
|
221
|
+
layoutName = state._layoutData?.name;
|
|
224
222
|
let cssFilePaths = [], layoutBasePath;
|
|
225
223
|
|
|
226
224
|
// legacy behavior: load <component>.css and <component>.<site>.css
|
|
@@ -303,7 +301,7 @@ function getScriptFiles(state) {
|
|
|
303
301
|
assetPath = site && site.assetPath || '',
|
|
304
302
|
assetHost = site && site.assetHost || '';
|
|
305
303
|
|
|
306
|
-
return
|
|
304
|
+
return state._components
|
|
307
305
|
.reduce((prev, name) => {
|
|
308
306
|
prev.push(
|
|
309
307
|
path.join(assetDir, 'js', `${name}.js`),
|
|
@@ -316,36 +314,6 @@ function getScriptFiles(state) {
|
|
|
316
314
|
.map(pathJoin(assetHost, assetPath, assetDir));
|
|
317
315
|
}
|
|
318
316
|
|
|
319
|
-
/**
|
|
320
|
-
* Read the asset manifest generated by the `clay pack` bundling command and
|
|
321
|
-
* check for scripts which match the rendered component names. The manifest for
|
|
322
|
-
* each asset lists its complete set of dependencies, so we dedupe them.
|
|
323
|
-
* Webpack's runtime is added to the beginning of the list, downstream legacy
|
|
324
|
-
* globals and Clay CLI's init script to the end.
|
|
325
|
-
*
|
|
326
|
-
* @param {Object} state - Amphora's render state.
|
|
327
|
-
* @return {string[]} A list of component assets.
|
|
328
|
-
*/
|
|
329
|
-
function getManifestAssets(state) {
|
|
330
|
-
const site = state.locals.site,
|
|
331
|
-
assetDir = site && site.assetDir || MEDIA_DIRECTORY,
|
|
332
|
-
assetHost = site && site.assetHost || '/',
|
|
333
|
-
manifestName = site && site.manifestName || 'assets-manifest.json',
|
|
334
|
-
manifestFile = path.resolve(path.join(assetDir, manifestName));
|
|
335
|
-
let manifest;
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
manifest = files.tryRequire(manifestFile);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
return [];
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const entrypoints = _.get(manifest, 'entrypoints', []);
|
|
344
|
-
const scriptFiles = _.flatMap(entrypoints, entrypoint => _.get(entrypoint, 'assets.js', []));
|
|
345
|
-
|
|
346
|
-
return scriptFiles.map(script => url.resolve(assetHost, script));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
317
|
/**
|
|
350
318
|
* Trim the path on the file system bythe link of the
|
|
351
319
|
* asset directory and then join it with either the assetHost
|
|
@@ -375,9 +343,17 @@ function pathJoin(assetHost, assetPath, assetDir) {
|
|
|
375
343
|
* @param {string} [cacheBuster]
|
|
376
344
|
*/
|
|
377
345
|
function configure(options, cacheBuster = '') {
|
|
378
|
-
if (options &&
|
|
346
|
+
if (options && isObject(options)) {
|
|
379
347
|
module.exports.editStylesTags = options.styles || false;
|
|
380
348
|
module.exports.editScriptsTags = options.scripts || false;
|
|
349
|
+
// modulepreload: when true, <link rel="modulepreload"> hints are injected
|
|
350
|
+
// into <head> for ESM scripts, and ?version= is omitted from module URLs
|
|
351
|
+
// since content-hashed filenames already provide cache busting.
|
|
352
|
+
// Opt-in only — defaults to false for backwards compatibility.
|
|
353
|
+
if (options.modulepreload !== undefined) {
|
|
354
|
+
module.exports.modulepreload = !!options.modulepreload;
|
|
355
|
+
module.exports.omitCacheBusterOnModules = !!options.modulepreload;
|
|
356
|
+
}
|
|
381
357
|
} else {
|
|
382
358
|
module.exports.editStylesTags = options;
|
|
383
359
|
module.exports.editScriptsTags = options;
|
|
@@ -397,11 +373,12 @@ function configure(options, cacheBuster = '') {
|
|
|
397
373
|
* @param {Object} state
|
|
398
374
|
* @return {Function}
|
|
399
375
|
*/
|
|
376
|
+
/* eslint-disable complexity */
|
|
400
377
|
function injectScriptsAndStyles(state) {
|
|
401
378
|
const { locals } = state;
|
|
402
379
|
|
|
403
380
|
return function (html) {
|
|
404
|
-
var mediaMap
|
|
381
|
+
var mediaMap;
|
|
405
382
|
|
|
406
383
|
// Check that HTML is string
|
|
407
384
|
if (typeof html !== 'string') {
|
|
@@ -412,26 +389,50 @@ function injectScriptsAndStyles(state) {
|
|
|
412
389
|
mediaMap = module.exports.getMediaMap(state);
|
|
413
390
|
|
|
414
391
|
// allow site to change the media map before applying it
|
|
392
|
+
// Expose rendered component names so resolveMedia can do per-component script resolution
|
|
393
|
+
// (e.g. pack-next manifest lookup) without needing a reference to the full state object.
|
|
394
|
+
locals._components = state._components;
|
|
415
395
|
if (setup.resolveMedia) mediaMap = setup.resolveMedia(mediaMap, locals) || mediaMap;
|
|
416
396
|
|
|
397
|
+
// moduleScripts: ESM scripts (e.g. from esbuild pack-next) that need type="module"
|
|
398
|
+
const moduleScriptFiles = mediaMap.moduleScripts || [];
|
|
399
|
+
|
|
400
|
+
mediaMap.moduleScripts = moduleScriptFiles.length
|
|
401
|
+
? injectTags(moduleScriptFiles, MODULE_SCRIPT_TAG)
|
|
402
|
+
: Promise.resolve(false);
|
|
403
|
+
|
|
404
|
+
// modulePreloads: <link rel="modulepreload"> hints for <head>.
|
|
405
|
+
// Only active when configure({ modulepreload: true }) has been called — opt-in so
|
|
406
|
+
// sites not using the clay build pipeline are completely unaffected.
|
|
407
|
+
const modulePreloadFiles = module.exports.modulepreload
|
|
408
|
+
? mediaMap.modulePreloads || []
|
|
409
|
+
: [];
|
|
410
|
+
|
|
411
|
+
mediaMap.modulePreloads = modulePreloadFiles.length
|
|
412
|
+
? injectTags(modulePreloadFiles, MODULEPRELOAD_TAG)
|
|
413
|
+
: Promise.resolve(false);
|
|
414
|
+
|
|
417
415
|
if (!locals.edit) {
|
|
418
416
|
mediaMap.styles = combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG);
|
|
419
|
-
mediaMap.scripts =
|
|
420
|
-
? injectTags(mediaMap.manifestAssets, locals.site, SCRIPT_TAG)
|
|
421
|
-
: combineFileContents(mediaMap.scripts, 'public/js', '/js/', SCRIPT_TAG);
|
|
417
|
+
mediaMap.scripts = combineFileContents(mediaMap.scripts, 'public/js', '/js/', SCRIPT_TAG);
|
|
422
418
|
} else {
|
|
423
|
-
mediaMap.styles = module.exports.editStylesTags ? injectTags(mediaMap.styles,
|
|
424
|
-
mediaMap.scripts = module.exports.editScriptsTags ? injectTags(mediaMap.scripts,
|
|
419
|
+
mediaMap.styles = module.exports.editStylesTags ? injectTags(mediaMap.styles, STYLE_TAG) : combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG);
|
|
420
|
+
mediaMap.scripts = module.exports.editScriptsTags ? injectTags(mediaMap.scripts, SCRIPT_TAG) : combineFileContents(mediaMap.scripts, 'public/js', '/js/', SCRIPT_TAG);
|
|
425
421
|
}
|
|
426
422
|
|
|
427
|
-
return
|
|
423
|
+
return props(mediaMap)
|
|
428
424
|
.then(combinedFiles => {
|
|
425
|
+
// modulepreload hints go first in <head>, before CSS, so the browser can
|
|
426
|
+
// start fetching ESM scripts at the earliest possible moment.
|
|
427
|
+
html = combinedFiles.modulePreloads ? appendMediaToTop(combinedFiles.modulePreloads, html) : html;
|
|
429
428
|
html = combinedFiles.styles ? appendMediaToTop(combinedFiles.styles, html) : html; // If there are styles, append them
|
|
429
|
+
html = combinedFiles.moduleScripts ? appendMediaToBottom(combinedFiles.moduleScripts, html) : html; // ESM module scripts (type="module")
|
|
430
430
|
html = combinedFiles.scripts ? appendMediaToBottom(combinedFiles.scripts, html) : html; // If there are scripts, append them
|
|
431
431
|
return html; // Return the compiled HTML
|
|
432
432
|
});
|
|
433
433
|
};
|
|
434
434
|
}
|
|
435
|
+
/* eslint-enable complexity */
|
|
435
436
|
|
|
436
437
|
module.exports.injectScriptsAndStyles = injectScriptsAndStyles;
|
|
437
438
|
module.exports.getMediaMap = getMediaMap;
|
|
@@ -439,8 +440,11 @@ module.exports.cacheBuster = '';
|
|
|
439
440
|
module.exports.configure = configure;
|
|
440
441
|
module.exports.editStylesTags = false;
|
|
441
442
|
module.exports.editScriptsTags = false;
|
|
443
|
+
// Opt-in flags for the clay build (esbuild) pipeline.
|
|
444
|
+
// Enable via configure({ modulepreload: true }).
|
|
445
|
+
module.exports.modulepreload = false;
|
|
446
|
+
module.exports.omitCacheBusterOnModules = false;
|
|
442
447
|
|
|
443
448
|
// For testing
|
|
444
|
-
module.exports.getManifestAssets = getManifestAssets;
|
|
445
449
|
module.exports.getScriptFiles = getScriptFiles;
|
|
446
450
|
module.exports.getStyleFiles = getStyleFiles;
|
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
|
/**
|
|
@@ -38,6 +44,9 @@ function applyRenderHooks(ref, data, locals) {
|
|
|
38
44
|
*
|
|
39
45
|
* @param {String} ref
|
|
40
46
|
* @param {Object} locals
|
|
47
|
+
* @param {Object} res
|
|
48
|
+
* @param {String} self Clay ref of the component being rendered, e.g.
|
|
49
|
+
* "site.com/_components/article/instances/foo"
|
|
41
50
|
* @returns {Function}
|
|
42
51
|
*/
|
|
43
52
|
function applyPostRenderHooks(ref, locals, res, self) {
|
|
@@ -76,17 +85,29 @@ function makeHtml(state) {
|
|
|
76
85
|
|
|
77
86
|
// At the final stage of rendering we need to smoosh
|
|
78
87
|
// all the data together with the state.
|
|
79
|
-
|
|
88
|
+
assign(state, _data);
|
|
80
89
|
|
|
81
90
|
// send clone of state/data to be used by kiln so kiln doesn't load component data that was mutated by hbs templates
|
|
82
91
|
if (state.locals.edit) {
|
|
83
|
-
|
|
92
|
+
state._componentData = cloneDeep(_data);
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
return rootPartial(state);
|
|
87
96
|
};
|
|
88
97
|
}
|
|
89
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
|
+
|
|
90
111
|
/**
|
|
91
112
|
* Construct the object that is needed by Kiln for rendering in
|
|
92
113
|
* edit mode. Requires reading from the file system.
|
|
@@ -117,16 +138,16 @@ function makeState(data, meta) {
|
|
|
117
138
|
|
|
118
139
|
// grab the component variations and make available via the state
|
|
119
140
|
// note: if styleguide isn't set, this gets variations from the '_default' styleguide
|
|
120
|
-
initialState._componentVariations = styleguide.getVariations(
|
|
141
|
+
initialState._componentVariations = styleguide.getVariations(getStyleguideName(meta));
|
|
121
142
|
|
|
122
143
|
if (meta.locals.edit) {
|
|
123
144
|
initialState._envVars = ENV_VARS;
|
|
124
|
-
initialState._componentSchemas =
|
|
145
|
+
initialState._componentSchemas = toList(componentList).map((name) => {
|
|
125
146
|
return { name, schema: getSchemaPath(getComponentPath(name)) };
|
|
126
147
|
});
|
|
127
148
|
}
|
|
128
149
|
|
|
129
|
-
return
|
|
150
|
+
return Object.assign(initialState, pick(meta, ['locals', '_layoutRef']));
|
|
130
151
|
}
|
|
131
152
|
|
|
132
153
|
/**
|
|
@@ -182,8 +203,12 @@ function logTime(hrStart, msg, route) {
|
|
|
182
203
|
};
|
|
183
204
|
}
|
|
184
205
|
|
|
185
|
-
function configure({ editAssetTags, cacheBuster }) {
|
|
186
|
-
|
|
206
|
+
function configure({ editAssetTags, cacheBuster, modulepreload }) {
|
|
207
|
+
const mediaOptions = editAssetTags && typeof editAssetTags === 'object'
|
|
208
|
+
? Object.assign({}, editAssetTags, modulepreload !== undefined ? { modulepreload } : {})
|
|
209
|
+
: editAssetTags;
|
|
210
|
+
|
|
211
|
+
mediaService.configure(mediaOptions, cacheBuster);
|
|
187
212
|
}
|
|
188
213
|
|
|
189
214
|
/**
|
|
@@ -193,7 +218,7 @@ function configure({ editAssetTags, cacheBuster }) {
|
|
|
193
218
|
* @param {Array} vars
|
|
194
219
|
*/
|
|
195
220
|
function addEnvVars(vars) {
|
|
196
|
-
ENV_VARS =
|
|
221
|
+
ENV_VARS = pick(process.env, vars);
|
|
197
222
|
log('info', 'env vars registered with amphora html');
|
|
198
223
|
}
|
|
199
224
|
|