amphora-html 5.0.0 → 5.2.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/.eslintrc CHANGED
@@ -58,7 +58,7 @@
58
58
  "no-trailing-spaces": 2,
59
59
  "no-underscore-dangle": 0,
60
60
  "no-unneeded-ternary": 1,
61
- "one-var": 2,
61
+ "one-var": 0,
62
62
  "quotes": [2, "single", "avoid-escape"],
63
63
  "semi": [2, "always"],
64
64
  "keyword-spacing": 2,
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ['**']
6
+ tags: ['**']
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ test:
19
+ name: Test (Node ${{ matrix.node-version }})
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ node-version: [18, 20]
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - name: Use Node.js ${{ matrix.node-version }}
29
+ uses: actions/setup-node@v4
30
+ with:
31
+ node-version: ${{ matrix.node-version }}
32
+ cache: npm
33
+ cache-dependency-path: package-lock.json
34
+
35
+ - name: Install dependencies
36
+ run: npm ci
37
+
38
+ - name: Run tests
39
+ run: npm test
40
+
@@ -1,26 +1,39 @@
1
- on: push
2
1
  name: Deploy to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+
3
7
  jobs:
4
- filterBranch:
5
- name: Filter branch
8
+ deploy-docs:
9
+ name: Deploy docs
6
10
  runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
7
13
  steps:
8
- - uses: actions/checkout@master
9
- - name: Filter branch
10
- uses: actions/bin/filter@master
11
- with:
12
- args: branch master
13
- - name: Install
14
- uses: actions/npm@master
15
- with:
16
- args: install --prefix ./website
17
- - name: Update version
18
- uses: clay/docusaurus-github-action@master
19
- with:
20
- args: version
21
- - name: Build and push docs
22
- uses: clay/docusaurus-github-action@master
23
- env:
24
- DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
25
- with:
26
- args: deploy
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - name: Use Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '16'
22
+
23
+ - name: Install website dependencies
24
+ run: npm install
25
+ working-directory: website
26
+
27
+ - name: Update Docusaurus version
28
+ run: npm run version
29
+ working-directory: website
30
+
31
+ - name: Build Docusaurus site
32
+ run: npm run build
33
+ working-directory: website
34
+
35
+ - name: Publish docs to gh-pages
36
+ uses: peaceiris/actions-gh-pages@v4
37
+ with:
38
+ github_token: ${{ secrets.GITHUB_TOKEN }}
39
+ publish_dir: ./website/build/amphora-html
package/lib/media.js CHANGED
@@ -8,6 +8,8 @@ const _ = require('lodash'),
8
8
  styleguide = require('./styleguide'),
9
9
  STYLE_TAG = 'style',
10
10
  SCRIPT_TAG = 'scripts',
11
+ MODULE_SCRIPT_TAG = 'module',
12
+ MODULEPRELOAD_TAG = 'modulepreload',
11
13
  MEDIA_DIRECTORY = path.join(process.cwd(), 'public');
12
14
 
13
15
  /**
@@ -146,15 +148,35 @@ function appendMediaToBottom(scripts, html) {
146
148
  return splice(html, index, scripts);
147
149
  }
148
150
 
149
- function injectTags(fileArray, site, tag) {
151
+ /**
152
+ * Build inline <link>/<script> tags for an array of asset paths. Used by edit
153
+ * mode (where assets are loaded individually rather than bundled) and by the
154
+ * Vite ESM pipeline (module / modulepreload).
155
+ *
156
+ * @param {string[]} fileArray asset paths
157
+ * @param {string} tag one of STYLE_TAG, SCRIPT_TAG, MODULE_SCRIPT_TAG, MODULEPRELOAD_TAG
158
+ * @returns {Promise<string>} newline-joined tag string
159
+ */
160
+ function injectTags(fileArray, tag) {
150
161
  var buster = module.exports.cacheBuster ? `?version=${module.exports.cacheBuster}` : '';
162
+ // When omitCacheBusterOnModules is enabled, content-hashed ESM files omit the
163
+ // ?version= query string — the hash in the filename already provides cache busting.
164
+ // Defaults to false so existing deployments are unaffected.
165
+ var moduleBuster = module.exports.omitCacheBusterOnModules ? '' : buster;
151
166
 
152
167
  return bluebird.resolve(_.map(fileArray, function (file) {
153
168
  if (tag === STYLE_TAG) {
154
169
  return `<link rel="stylesheet" type="text/css" href="${file}${buster}">`;
155
- } else {
156
- return `<script type="text/javascript" src="${file}${buster}"></script>`;
170
+ } else if (tag === MODULE_SCRIPT_TAG) {
171
+ return `<script type="module" src="${file}${moduleBuster}"></script>`;
172
+ } else if (tag === MODULEPRELOAD_TAG) {
173
+ // <link rel="modulepreload"> tells the browser to fetch ESM scripts early,
174
+ // during HTML parsing, before reaching the <script> tags at </body>.
175
+ return `<link rel="modulepreload" href="${file}${moduleBuster}">`;
157
176
  }
177
+
178
+ // Default: classic <script src=...> for SCRIPT_TAG and any other value.
179
+ return `<script type="text/javascript" src="${file}${buster}"></script>`;
158
180
  }).join('\n'));
159
181
  }
160
182
 
@@ -166,7 +188,7 @@ function injectTags(fileArray, site, tag) {
166
188
  function getMediaMap(state) {
167
189
  return {
168
190
  styles: module.exports.getStyleFiles(state),
169
- scripts: module.exports.getScriptFiles(state)
191
+ scripts: module.exports.getScriptFiles(state),
170
192
  };
171
193
  }
172
194
 
@@ -238,6 +260,14 @@ function getStyleFiles(state) {
238
260
  }
239
261
  }
240
262
 
263
+ // check for clay compile linked? clay compile font files?
264
+ const defaultFontPath = path.join(assetDir, 'css', '_linked-fonts._default.css');
265
+ const siteFontPath = path.join(assetDir, 'css', `_linked-fonts.${siteStyleguide}.css`);
266
+
267
+ // add any default and site font css
268
+ cssFilePaths.push(defaultFontPath);
269
+ cssFilePaths.push(siteFontPath);
270
+
241
271
  return cssFilePaths
242
272
  .filter(files.fileExists)
243
273
  .map(pathJoin(assetHost, assetPath, assetDir));
@@ -304,6 +334,14 @@ function configure(options, cacheBuster = '') {
304
334
  if (options && _.isObject(options)) {
305
335
  module.exports.editStylesTags = options.styles || false;
306
336
  module.exports.editScriptsTags = options.scripts || false;
337
+ // modulepreload: when true, <link rel="modulepreload"> hints are injected
338
+ // into <head> for ESM scripts, and ?version= is omitted from module URLs
339
+ // since content-hashed filenames already provide cache busting.
340
+ // Opt-in only — defaults to false for backwards compatibility.
341
+ if (options.modulepreload !== undefined) {
342
+ module.exports.modulepreload = !!options.modulepreload;
343
+ module.exports.omitCacheBusterOnModules = !!options.modulepreload;
344
+ }
307
345
  } else {
308
346
  module.exports.editStylesTags = options;
309
347
  module.exports.editScriptsTags = options;
@@ -323,6 +361,7 @@ function configure(options, cacheBuster = '') {
323
361
  * @param {Object} state
324
362
  * @return {Function}
325
363
  */
364
+ /* eslint-disable complexity */
326
365
  function injectScriptsAndStyles(state) {
327
366
  const { locals } = state;
328
367
 
@@ -338,24 +377,50 @@ function injectScriptsAndStyles(state) {
338
377
  mediaMap = module.exports.getMediaMap(state);
339
378
 
340
379
  // allow site to change the media map before applying it
380
+ // Expose rendered component names so resolveMedia can do per-component script resolution
381
+ // (e.g. pack-next manifest lookup) without needing a reference to the full state object.
382
+ locals._components = state._components;
341
383
  if (setup.resolveMedia) mediaMap = setup.resolveMedia(mediaMap, locals) || mediaMap;
342
384
 
385
+ // moduleScripts: ESM scripts (e.g. from esbuild pack-next) that need type="module"
386
+ const moduleScriptFiles = mediaMap.moduleScripts || [];
387
+
388
+ mediaMap.moduleScripts = moduleScriptFiles.length
389
+ ? injectTags(moduleScriptFiles, MODULE_SCRIPT_TAG)
390
+ : bluebird.resolve(false);
391
+
392
+ // modulePreloads: <link rel="modulepreload"> hints for <head>.
393
+ // Only active when configure({ modulepreload: true }) has been called — opt-in so
394
+ // sites not using the clay build pipeline are completely unaffected.
395
+ const modulePreloadFiles = module.exports.modulepreload
396
+ ? mediaMap.modulePreloads || []
397
+ : [];
398
+
399
+ mediaMap.modulePreloads = modulePreloadFiles.length
400
+ ? injectTags(modulePreloadFiles, MODULEPRELOAD_TAG)
401
+ : bluebird.resolve(false);
402
+
343
403
  if (!locals.edit) {
344
404
  mediaMap.styles = combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG);
345
405
  mediaMap.scripts = combineFileContents(mediaMap.scripts, 'public/js', '/js/', SCRIPT_TAG);
346
406
  } else {
347
- mediaMap.styles = module.exports.editStylesTags ? injectTags(mediaMap.styles, locals.site, STYLE_TAG) : combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG);
348
- mediaMap.scripts = module.exports.editScriptsTags ? injectTags(mediaMap.scripts, locals.site, SCRIPT_TAG) : combineFileContents(mediaMap.scripts, 'public/js', '/js/', SCRIPT_TAG);
407
+ mediaMap.styles = module.exports.editStylesTags ? injectTags(mediaMap.styles, STYLE_TAG) : combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG);
408
+ mediaMap.scripts = module.exports.editScriptsTags ? injectTags(mediaMap.scripts, SCRIPT_TAG) : combineFileContents(mediaMap.scripts, 'public/js', '/js/', SCRIPT_TAG);
349
409
  }
350
410
 
351
411
  return bluebird.props(mediaMap)
352
412
  .then(combinedFiles => {
413
+ // modulepreload hints go first in <head>, before CSS, so the browser can
414
+ // start fetching ESM scripts at the earliest possible moment.
415
+ html = combinedFiles.modulePreloads ? appendMediaToTop(combinedFiles.modulePreloads, html) : html;
353
416
  html = combinedFiles.styles ? appendMediaToTop(combinedFiles.styles, html) : html; // If there are styles, append them
417
+ html = combinedFiles.moduleScripts ? appendMediaToBottom(combinedFiles.moduleScripts, html) : html; // ESM module scripts (type="module")
354
418
  html = combinedFiles.scripts ? appendMediaToBottom(combinedFiles.scripts, html) : html; // If there are scripts, append them
355
419
  return html; // Return the compiled HTML
356
420
  });
357
421
  };
358
422
  }
423
+ /* eslint-enable complexity */
359
424
 
360
425
  module.exports.injectScriptsAndStyles = injectScriptsAndStyles;
361
426
  module.exports.getMediaMap = getMediaMap;
@@ -363,6 +428,10 @@ module.exports.cacheBuster = '';
363
428
  module.exports.configure = configure;
364
429
  module.exports.editStylesTags = false;
365
430
  module.exports.editScriptsTags = false;
431
+ // Opt-in flags for the clay build (esbuild) pipeline.
432
+ // Enable via configure({ modulepreload: true }).
433
+ module.exports.modulepreload = false;
434
+ module.exports.omitCacheBusterOnModules = false;
366
435
 
367
436
  // For testing
368
437
  module.exports.getScriptFiles = getScriptFiles;
package/lib/media.test.js CHANGED
@@ -10,7 +10,8 @@ const _ = require('lodash'),
10
10
 
11
11
  jest.mock('nymag-fs', () => ({
12
12
  fileExists: () => false,
13
- readFilePromise: () => Promise.resolve('')
13
+ readFilePromise: () => Promise.resolve(''),
14
+ tryRequire: () => ({})
14
15
  }));
15
16
 
16
17
  describe(_.startCase(filename), () => {
package/lib/render.js CHANGED
@@ -38,9 +38,12 @@ function applyRenderHooks(ref, data, locals) {
38
38
  *
39
39
  * @param {String} ref
40
40
  * @param {Object} locals
41
+ * @param {Object} res
42
+ * @param {String} self Clay ref of the component being rendered, e.g.
43
+ * "site.com/_components/article/instances/foo"
41
44
  * @returns {Function}
42
45
  */
43
- function applyPostRenderHooks(ref, locals) {
46
+ function applyPostRenderHooks(ref, locals, res, self) {
44
47
  return (html) => {
45
48
 
46
49
  // skip postRender hooks in edit mode
@@ -50,7 +53,7 @@ function applyPostRenderHooks(ref, locals) {
50
53
 
51
54
  return setup.plugins.filter((plugin) => plugin.postRender)
52
55
  .reduce((val, plugin) => {
53
- return plugin.postRender(ref, html, locals);
56
+ return plugin.postRender(ref, html, locals, res, self);
54
57
  }, html);
55
58
  };
56
59
  }
@@ -148,7 +151,7 @@ function render(data, meta, res) {
148
151
  return applyRenderHooks(state._layoutRef || state.self, data, state.locals)
149
152
  .then(makeHtml(state))
150
153
  .then(mediaService.injectScriptsAndStyles(state))
151
- .then(applyPostRenderHooks(state._layoutRef || state.self, state.locals))
154
+ .then(applyPostRenderHooks(state._layoutRef || state.self, state.locals, res, meta._ref))
152
155
  .then(result => {
153
156
  res.type('text/html');
154
157
  res.send(result);
@@ -182,8 +185,12 @@ function logTime(hrStart, msg, route) {
182
185
  };
183
186
  }
184
187
 
185
- function configure({ editAssetTags, cacheBuster }) {
186
- mediaService.configure(editAssetTags, cacheBuster);
188
+ function configure({ editAssetTags, cacheBuster, modulepreload }) {
189
+ const mediaOptions = editAssetTags && typeof editAssetTags === 'object'
190
+ ? Object.assign({}, editAssetTags, modulepreload !== undefined ? { modulepreload } : {})
191
+ : editAssetTags;
192
+
193
+ mediaService.configure(mediaOptions, cacheBuster);
187
194
  }
188
195
 
189
196
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amphora-html",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "An HTML renderer for component data",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -20,6 +20,7 @@
20
20
  "traverse": "^0.6.6"
21
21
  },
22
22
  "devDependencies": {
23
+ "clayhandlebars": "^5.0.1",
23
24
  "coveralls": "^3.0.2",
24
25
  "eslint": "^3.19.0",
25
26
  "jest": "23.6.0"
@@ -1,44 +0,0 @@
1
- version: 2
2
-
3
- # Set defaults to reuse
4
- defaults: &defaults
5
- working_directory: ~/repo
6
- docker:
7
- - image: circleci/node:8.10.0
8
-
9
- jobs:
10
- test:
11
- <<: *defaults
12
- steps:
13
- - checkout
14
-
15
- - restore_cache:
16
- keys:
17
- - v2-dependencies-{{ checksum "package.json" }}
18
- # fallback to using the latest cache if no exact match is found
19
- - v2-dependencies-
20
-
21
- - run: npm install
22
- - run:
23
- name: Run tests
24
- command: npm test
25
- - run:
26
- name: Send to Coveralls
27
- command: cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
28
- - save_cache:
29
- paths:
30
- - node_modules
31
- key: v2-dependencies-{{ checksum "package.json" }}
32
-
33
- - persist_to_workspace:
34
- root: ~/repo
35
- paths: .
36
-
37
- workflows:
38
- version: 2
39
- test-deploy:
40
- jobs:
41
- - test:
42
- filters:
43
- tags:
44
- only: /^v[0-9]+\.[0-9]+\.[0-9]+/