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/lib/styleguide.js CHANGED
@@ -1,10 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const _ = require('lodash'),
4
- clayUtils = require('clayutils'),
5
- files = require('nymag-fs'),
3
+ const clayUtils = require('clayutils'),
4
+ files = require('./files'),
6
5
  path = require('path'),
7
- traverse = require('traverse');
6
+ { isObject, assignValue, memoize } = require('./utils');
8
7
 
9
8
  /**
10
9
  * Looks for CSS files associated with a component's variation
@@ -15,7 +14,7 @@ const _ = require('lodash'),
15
14
  function findVariationFiles(styleguide) {
16
15
  const stylePath = `styleguides/${styleguide}/components`,
17
16
  stylesheets = files.getFiles(stylePath),
18
- variations = _.filter(stylesheets, function (file) {
17
+ variations = stylesheets.filter(function (file) {
19
18
  // checks for underscores which denotes variations and make sure it's a
20
19
  // css file
21
20
  return file.includes('_') && file.includes('.css');
@@ -37,20 +36,45 @@ function findVariationFiles(styleguide) {
37
36
  * component and its variations
38
37
  */
39
38
  function getVariations(styleguide = '_default') {
40
- const foundVariations = styleguide !== '_default' ? _.uniq(findVariationFiles(styleguide).concat(findVariationFiles('_default'))) : findVariationFiles('_default'),
39
+ const foundVariations = styleguide !== '_default' ? [...new Set(findVariationFiles(styleguide).concat(findVariationFiles('_default')))] : findVariationFiles('_default'),
41
40
  componentVariations = {};
42
41
 
43
42
  if (foundVariations.length) {
44
- _.forEach(foundVariations, function (variant) {
43
+ for (const variant of foundVariations) {
45
44
  let component = variant.split('_')[0],
46
45
  variantName = path.basename(variant, '.css');
47
46
 
48
47
  (componentVariations[component] || (componentVariations[component] = [])).push(variantName);
49
- });
48
+ }
50
49
  }
51
50
  return componentVariations;
52
51
  }
53
52
 
53
+ /**
54
+ * Call `visit` with every value in `node`, depth first, as traverse's forEach did:
55
+ * an object's keys are read before it is visited (so keys `visit` adds are not
56
+ * walked), functions are not walked, and an object found inside itself is
57
+ * visited but not walked again.
58
+ *
59
+ * @param {*} node
60
+ * @param {Function} visit
61
+ * @param {Object[]} [parents] the objects being walked
62
+ */
63
+ function walk(node, visit, parents = []) {
64
+ const isWalked = typeof node === 'object' && node !== null,
65
+ keys = isWalked ? Object.keys(node) : [];
66
+
67
+ visit(node);
68
+
69
+ if (isWalked && !parents.includes(node)) {
70
+ parents.push(node);
71
+ for (const key of keys) {
72
+ walk(node[key], visit, parents);
73
+ }
74
+ parents.pop();
75
+ }
76
+ }
77
+
54
78
  /**
55
79
  * Goes through page data and sets a default for components
56
80
  *
@@ -71,25 +95,26 @@ function setDefaultVariations(_data, state) {
71
95
  [topLevelVariation || layoutOrSelf]: true
72
96
  };
73
97
 
74
- traverse(_data).forEach(function (val) {
98
+ walk(_data, function (val) {
75
99
  let componentName, componentVariations;
76
100
 
77
- if (!_.isObject(val) || !val._ref) {
101
+ if (!isObject(val) || !val._ref) {
78
102
  return; // we only care about components' root data
79
103
  }
80
104
 
81
105
  componentName = clayUtils.getComponentName(val._ref);
82
- componentVariations = variations[componentName] || [];
106
+ // a name like `constructor` finds what the object inherits, which is not a list
107
+ componentVariations = Array.isArray(variations[componentName]) ? variations[componentName] : [];
83
108
 
84
- if (val.componentVariation && !_.includes(componentVariations, val.componentVariation)) {
109
+ if (val.componentVariation && !componentVariations.includes(val.componentVariation)) {
85
110
  // component has a variation set, but it doesn't exist in the site's styleguide or the default styleguide!
86
111
  // render the component with the default variation
87
- _.set(val, 'componentVariation', componentName);
112
+ assignValue(val, 'componentVariation', componentName);
88
113
  usedVariations[`${componentName}`] = true;
89
114
  } else if (!val.componentVariation) {
90
115
  // component has no variation set!
91
116
  // render the component with the default variation
92
- _.set(val, 'componentVariation', componentName);
117
+ assignValue(val, 'componentVariation', componentName);
93
118
  usedVariations[`${componentName}`] = true;
94
119
  } else {
95
120
  usedVariations[`${val.componentVariation}`] = true;
@@ -98,5 +123,5 @@ function setDefaultVariations(_data, state) {
98
123
  state._usedVariations = Object.keys(usedVariations);
99
124
  }
100
125
 
101
- exports.getVariations = _.memoize(getVariations);
126
+ exports.getVariations = memoize(getVariations);
102
127
  exports.setDefaultVariations = setDefaultVariations;
package/lib/utils.js ADDED
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ // The few behaviors of lodash that amphora-html depends on, in native code.
4
+
5
+ /**
6
+ * Whether a value is an object or a function, as lodash's isObject
7
+ *
8
+ * @param {*} value
9
+ * @returns {boolean}
10
+ */
11
+ function isObject(value) {
12
+ return value !== null && (typeof value === 'object' || typeof value === 'function');
13
+ }
14
+
15
+ /**
16
+ * Set a property the way lodash's assign and set did: nothing is written when
17
+ * the object already has that value, a `__proto__` key becomes a property of
18
+ * its own instead of replacing the prototype, and a property that can't be
19
+ * written is left as it is instead of throwing.
20
+ *
21
+ * @param {Object} object
22
+ * @param {string|symbol} key
23
+ * @param {*} value
24
+ */
25
+ function assignValue(object, key, value) {
26
+ const current = object[key];
27
+
28
+ // SameValueZero, like lodash's eq
29
+ if (Object.hasOwn(object, key) && (current === value || Number.isNaN(current) && Number.isNaN(value))) {
30
+ return;
31
+ }
32
+
33
+ if (key === '__proto__') {
34
+ Object.defineProperty(object, key, { configurable: true, enumerable: true, value, writable: true });
35
+ } else {
36
+ Reflect.set(object, key, value);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Copy the own enumerable properties of `source` onto `target`, as lodash's assign
42
+ *
43
+ * @param {Object} target
44
+ * @param {*} source
45
+ * @returns {Object} target
46
+ */
47
+ function assign(target, source) {
48
+ for (const key of Object.keys(Object(source))) {
49
+ assignValue(target, key, source[key]);
50
+ }
51
+
52
+ return target;
53
+ }
54
+
55
+ /**
56
+ * The properties of `object` with these names, as lodash's pick: a name is
57
+ * picked when it is `in` the object, inherited or not, except the ones that
58
+ * would reach a prototype.
59
+ *
60
+ * @param {Object} object
61
+ * @param {string[]|string} keys
62
+ * @returns {Object}
63
+ */
64
+ function pick(object, keys) {
65
+ const picked = {};
66
+
67
+ for (const key of [].concat(keys)) {
68
+ if (object != null && key in Object(object) && key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
69
+ picked[key] = object[key];
70
+ }
71
+ }
72
+
73
+ return picked;
74
+ }
75
+
76
+ /**
77
+ * The items of a collection, as lodash's map and each see them: nothing for
78
+ * null or undefined, every index of an array or anything with a length
79
+ * (holes included), the own values of anything else.
80
+ *
81
+ * @param {*} collection
82
+ * @returns {Array}
83
+ */
84
+ function toList(collection) {
85
+ if (collection == null) {
86
+ return [];
87
+ }
88
+
89
+ if (typeof collection !== 'function' && Number.isSafeInteger(collection.length) && collection.length >= 0) {
90
+ return Array.from({ length: collection.length }, (item, index) => collection[index]);
91
+ }
92
+
93
+ return Object.values(Object(collection));
94
+ }
95
+
96
+ /**
97
+ * Remember what `fn` returns for each first argument, as lodash's memoize:
98
+ * the cache is read from `memoized.cache` on every call, so it can be cleared
99
+ * or replaced with anything that has `get`, `set` and `has`.
100
+ *
101
+ * @param {Function} fn
102
+ * @returns {Function}
103
+ */
104
+ function memoize(fn) {
105
+ function memoized(key) {
106
+ const cache = memoized.cache;
107
+
108
+ if (cache.has(key)) {
109
+ return cache.get(key);
110
+ }
111
+
112
+ const result = fn.apply(this, arguments);
113
+
114
+ memoized.cache = cache.set(key, result) || cache;
115
+ return result;
116
+ }
117
+
118
+ memoized.cache = new Map();
119
+ return memoized;
120
+ }
121
+
122
+ module.exports.isObject = isObject;
123
+ module.exports.assignValue = assignValue;
124
+ module.exports.assign = assign;
125
+ module.exports.pick = pick;
126
+ module.exports.toList = toList;
127
+ module.exports.memoize = memoize;
package/package.json CHANGED
@@ -1,65 +1,39 @@
1
1
  {
2
2
  "name": "amphora-html",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "An HTML renderer for component data",
5
5
  "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "lib",
9
+ "!**/*.test.js"
10
+ ],
6
11
  "engines": {
7
- "node": ">=20.14.0"
12
+ "node": ">=26.12.1"
8
13
  },
9
14
  "scripts": {
10
- "test": "npm run eslint && npm run jest -- --coverage --no-cache",
11
- "eslint": "eslint --max-warnings 0 lib test",
12
- "jest": "jest"
15
+ "lint": "eslint --max-warnings 0",
16
+ "test-local": "node --test \"lib/*.test.js\" \"test/*.test.js\"",
17
+ "test": "npm run lint && node --test --experimental-test-coverage --test-coverage-exclude=\"**/*.test.js\" --test-coverage-lines=100 --test-coverage-branches=100 --test-coverage-functions=100 \"lib/*.test.js\" \"test/*.test.js\""
13
18
  },
14
19
  "dependencies": {
15
- "amphora-fs": "^2.0.0",
16
- "bluebird": "^3.5.0",
17
- "clay-log": "^1.2.0",
18
- "clayutils": "^3.0.0",
19
- "glob": "^7.1.1",
20
- "js-yaml": "^3.8.2",
21
- "lodash": "^4.17.5",
22
- "nymag-fs": "^1.0.1",
23
- "traverse": "^0.6.6"
20
+ "amphora-fs": "^3.0.0",
21
+ "clay-log": "^1.6.0",
22
+ "clayhandlebars": "^6.0.0",
23
+ "clayutils": "^4.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "clayhandlebars": "^5.0.1",
27
- "coveralls": "^3.0.2",
28
- "eslint": "^3.19.0",
29
- "jest": "23.6.0"
30
- },
31
- "peerDependencies": {
32
- "handlebars": "4",
33
- "clayhandlebars": "5"
26
+ "@eslint/js": "^10.0.1",
27
+ "eslint": "^10.14.0"
34
28
  },
35
29
  "repository": {
36
30
  "type": "git",
37
- "url": "git+https://github.com/nymag/amphora-html.git"
38
- },
39
- "jest": {
40
- "clearMocks": true,
41
- "restoreMocks": true,
42
- "collectCoverageFrom": [
43
- "<rootDir>/lib/**/*.js"
44
- ],
45
- "coverageReporters": [
46
- "lcov",
47
- "text-summary"
48
- ],
49
- "moduleFileExtensions": [
50
- "js"
51
- ],
52
- "setupFiles": [
53
- "<rootDir>/test/setupFile.js"
54
- ],
55
- "moduleNameMapper": {
56
- "\\.(hbs)$": "<rootDir>/__mocks__/fileMock.js"
57
- }
31
+ "url": "git+https://github.com/clay/amphora-html.git"
58
32
  },
59
33
  "author": "",
60
34
  "license": "MIT",
61
35
  "bugs": {
62
- "url": "https://github.com/nymag/amphora-html/issues"
36
+ "url": "https://github.com/clay/amphora-html/issues"
63
37
  },
64
- "homepage": "https://github.com/nymag/amphora-html#readme"
38
+ "homepage": "https://github.com/clay/amphora-html#readme"
65
39
  }
package/.eslintrc DELETED
@@ -1,75 +0,0 @@
1
- {
2
- "env": {
3
- "browser": false,
4
- "node": true,
5
- "mocha": true,
6
- "es6": true
7
- },
8
- // enable the es6 features supported by our browsers / iojs
9
- "ecmaFeatures": {
10
- "arrowFunctions": true,
11
- "blockBindings": true,
12
- "forOf": true,
13
- "objectLiteralDuplicateProperties": true,
14
- "objectLiteralShorthandProperties": true,
15
- "objectLiteralShorthandMethods": true,
16
- "octalLiterals": true,
17
- "binaryLiterals": true,
18
- "templateStrings": true,
19
- "generators": false
20
- },
21
- "rules": {
22
- // possible errors
23
- "no-extra-parens": 1,
24
- "valid-jsdoc": [1, {
25
- "requireReturn": false,
26
- "requireParamDescription": false,
27
- "requireReturnDescription": false
28
- }],
29
- // best practices
30
- "complexity": [2, 8],
31
- "default-case": 2,
32
- "guard-for-in": 2,
33
- "no-alert": 1,
34
- "no-floating-decimal": 1,
35
- "no-self-compare": 2,
36
- "no-throw-literal": 2,
37
- "no-void": 2,
38
- "quote-props": [2, "as-needed"],
39
- "vars-on-top": 2,
40
- "wrap-iife": 2,
41
- // strict mode
42
- "strict": [2, "global"],
43
- // variables
44
- "no-unused-vars": 2,
45
- // node.js
46
- "handle-callback-err": [2, "^.*(e|E)rr"],
47
- "no-mixed-requires": 0,
48
- "no-new-require": 2,
49
- "no-path-concat": 2,
50
- // stylistic issues
51
- "brace-style": [2, "1tbs", { "allowSingleLine": true }],
52
- "comma-style": [2, "last"],
53
- "indent": [2, 2, { "SwitchCase": 1 }],
54
- "max-nested-callbacks": [2, 4],
55
- "newline-after-var": [2, "always"],
56
- "no-nested-ternary": 2,
57
- "no-spaced-func": 0,
58
- "no-trailing-spaces": 2,
59
- "no-underscore-dangle": 0,
60
- "no-unneeded-ternary": 1,
61
- "one-var": 0,
62
- "quotes": [2, "single", "avoid-escape"],
63
- "semi": [2, "always"],
64
- "keyword-spacing": 2,
65
- "space-before-blocks": [2, "always"],
66
- "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
67
- "space-infix-ops": [1, {"int32Hint": false}],
68
- "spaced-comment": [2, "always"],
69
- // es6
70
- "generator-star-spacing": [2, "before"],
71
- // legacy jshint rules
72
- "max-depth": [2, 4],
73
- "max-params": [2, 4]
74
- }
75
- }
@@ -1,40 +0,0 @@
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: [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
-
package/.istanbul.yml DELETED
@@ -1,51 +0,0 @@
1
- verbose: false
2
- instrumentation:
3
- root: .
4
- default-excludes: true
5
- excludes: []
6
- embed-source: false
7
- variable: __coverage__
8
- compact: true
9
- preserve-comments: false
10
- complete-copy: false
11
- save-baseline: false
12
- baseline-file: ./coverage/coverage-baseline.json
13
- include-all-sources: true
14
- include-pid: false
15
- reporting:
16
- print: summary
17
- reports:
18
- - lcov
19
- dir: ./coverage
20
- watermarks:
21
- statements: [98, 100]
22
- lines: [98, 100]
23
- functions: [98, 100]
24
- branches: [98, 100]
25
- report-config:
26
- clover: {file: clover.xml}
27
- cobertura: {file: cobertura-coverage.xml}
28
- json: {file: coverage-final.json}
29
- json-summary: {file: coverage-summary.json}
30
- lcovonly: {file: lcov.info}
31
- teamcity: {file: null}
32
- text: {file: null, maxCols: 0}
33
- text-lcov: {file: lcov.info}
34
- text-summary: {file: null}
35
- hooks:
36
- hook-run-in-context: false
37
- post-require-hook: null
38
- handle-sigint: false
39
- check:
40
- global:
41
- statements: 100
42
- lines: 100
43
- branches: 100
44
- functions: 100
45
- excludes: []
46
- each:
47
- statements: 0
48
- lines: 0
49
- branches: 0
50
- functions: 0
51
- excludes: []
@@ -1,3 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = 'test-export';
package/docs/README.md DELETED
@@ -1,31 +0,0 @@
1
- # Clay Documentation
2
-
3
- On this directory, we have the pre-release documentation written using markdown. This file has the `.md` extension and as we mentioned before, these are pre-release if you haven't added a documentation version yet.
4
-
5
- ## Markdown options
6
-
7
- These files have the following structure at the beginning of the file:
8
-
9
- ```
10
- ---
11
- id: idOftheFile
12
- title: Title of your file
13
- sidebar_label: Title of the sidebar
14
- ---
15
-
16
- My new content here..
17
- ```
18
-
19
- The previous example has a couple of the most important part to identify the content that you wish to add to your documentation. Here is a more detailed explanation on the [Markdown feature](https://docusaurus.io/docs/en/doc-markdown) that you can use.
20
-
21
- ## Test documentation page
22
-
23
- We have a local server that we use to test your current docs content. Here are the steps on how to set the [local server](https://github.com/clay/amphora-html/tree/master/website#run-the-local-server). Normally you will have access on your browser with a url like this:
24
-
25
- `http://localhost:3000/amphora-html/docs/idOfTheDocs`
26
-
27
- But if you already have a [version](https://github.com/clay/amphora-html/tree/master/website#versioning-the-code) on your docs you will need to add `next` on your url
28
-
29
- `http://localhost:3000/amphora-html/docs/next/idOfTheDocs`
30
-
31
- [Here](https://docusaurus.io/docs/en/versioning) are more insights about what to do when you already have documentation with version.
@@ -1,93 +0,0 @@
1
- ---
2
- id: integration
3
- title: Integration
4
- sidebar_label: Integration
5
- ---
6
-
7
- ---
8
- ## Basic Configuration
9
-
10
- First, ensure that you have a compatible version of Amphora installed (v3.x or greater) and require `amphora-html` from wherever you are running Amphora.
11
-
12
- ```javascript
13
- const amphoraHtml = require('amphora-html');
14
- ```
15
-
16
- Second, register a `rootPath` with the renderer. This will allow the renderer to reference your components directory and static assets directory properly. Usually this is the root of your project, but that may not be the case for your implementation.
17
-
18
- ```javascript
19
- // Register a root path for Amphora HTML
20
- amphoraHtml.addRootPath(path.dirname(path.resolve('./package.json')));
21
- ```
22
- ---
23
- ## Handlebars Helpers
24
-
25
- If your templates require any custom [Handlebars Helpers](http://handlebarsjs.com/block_helpers.html) you can register them with the renderer's Handlebars instance. Simply pass in an object whose keys are the names of your helpers and whose values are the helper themselves. Like so:
26
-
27
- ```javascript
28
- // My helpers
29
- const helpers = {
30
- // set up handlebars helpers that rely on internal services
31
- 'nameOfHelper': () => {
32
- // helper that does something you need.
33
- return 'foobar';
34
- }
35
- };
36
-
37
- // Register helpers
38
- amphoraHtml.addHelpers(helpers);
39
- ```
40
- ---
41
- ## Amphora HTML Plugins
42
-
43
- Amphora HTML plugins let you read and modify both the data sent to Handlebars and the HTML returned by Handlebars. An Amphora HTML plugin is an object with a `render` function that returns a modified `data` object and/or a `postRender` function that returns the rendered HTML string:
44
-
45
- ```javascript
46
-
47
- module.exports.render = (ref, data, locals) => {
48
- // you have the option to mutate `data` here
49
- // do **not** attempt to mutate `locals`
50
-
51
- // return `data` or a promise for `data`
52
- return data;
53
- };
54
-
55
- module.exports.postRender = (ref, html, locals) => {
56
- // you have the option to mutate `html` here
57
- // do **not** attempt to mutate `locals`
58
-
59
- // return `html` or a promise for `html`
60
- return html;
61
- };
62
- ```
63
-
64
- You can add Amphora HTML plugins like this:
65
-
66
- ```javascript
67
- amphoraHtml.addPlugins([
68
- { render: plugin1 },
69
- { postRender: plugin2 }
70
- ]);
71
- ```
72
-
73
- One use case for Amphora HTML plugins is to skip rendering of certain components depending on query parameters, which are available in `locals.query`. Amphora HTML plugins do not run when rendering for edit mode.
74
-
75
- ---
76
- ## Register Amphora HTML with your Amphora Instance
77
-
78
- Now that you have registered any helpers and plugins and have provided a root path which Amphora HTML can work from, you can register your renderer with Amphora. Registering consists of providing a `renderers` object whose keys are the extension of an HTTP request and whose values are the renderer. You can also specify a `default` property whose value is the extension that Amphora should default to when rendering. This is handy for rendering routes who don't end in extensions, such as `mycoolsite.com/about/`.
79
-
80
- ```javascript
81
- return amphora({
82
- app: app,
83
- renderers: {
84
- html: amphoraHtml,
85
- default: 'html'
86
- },
87
- providers: ['apikey', amphoraProvider],
88
- sessionStore: redisStore,
89
- plugins: [
90
- amphoraSearch
91
- ]
92
- });
93
- ```
package/docs/intro.md DELETED
@@ -1,21 +0,0 @@
1
- ---
2
- id: intro
3
- title: Amphora HTML
4
- sidebar_label: Amphora HTML
5
- ---
6
-
7
- ---
8
- The HTML renderer for Clay components that use [Handlebars](http://handlebarsjs.com/) templates.
9
-
10
- ---
11
- ## Why?
12
-
13
- 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.
14
-
15
- ---
16
-
17
- ## Installation
18
-
19
- ---
20
-
21
- `$ npm install --save amphora-html`