Skip to content

Commit 448355f

Browse files
Merge pull request #2987 from alphagov/fix-types
Various JSDoc + type checking fixes
2 parents 01a3a33 + 0718c67 commit 448355f

33 files changed

+557
-309
lines changed

app/app.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ module.exports = async (options) => {
155155
})
156156

157157
// Component example preview
158-
app.get('/components/:componentName/:exampleName*?/preview', function (req, res, next) {
158+
app.get('/components/:componentName/:exampleName?/preview', function (req, res, next) {
159159
// Find the data for the specified example (or the default example)
160160
const componentName = req.params.componentName
161161
const exampleName = req.params.exampleName || 'default'
162162

163-
const previewLayout = res.locals.componentData.previewLayout || 'layout'
163+
const previewLayout = res.locals.componentData?.previewLayout || 'layout'
164164

165-
const exampleConfig = res.locals.componentData.examples.find(
165+
const exampleConfig = res.locals.componentData?.examples.find(
166166
example => example.name.replace(/ /g, '-') === exampleName
167167
)
168168

config/jest/environment/jsdom.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class BrowserVirtualEnvironment extends TestEnvironment {
1414
const { virtualConsole } = this.dom
1515

1616
// Ensure test fails for browser exceptions
17-
virtualConsole.on('jsdomError', (error) => process.emit('error', error))
17+
virtualConsole.on('jsdomError', (error) => process.emit('uncaughtException', error))
1818

1919
// Add shared test globals
2020
// componentsData, componentsDirectory, examplesDirectory

config/jest/environment/puppeteer.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class BrowserAutomationEnvironment extends PuppeteerEnvironment {
1919
delete error.stack
2020

2121
// Ensure test fails
22-
process.emit('error', error)
22+
process.emit('uncaughtException', error)
2323
})
2424

2525
// Add shared test globals

docs/contributing/coding-standards/js.md

+59-37
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,45 @@
55
JavaScript files have the same name as the component's folder name. Test files have a `.test` suffix placed before the file extension.
66

77
```
8-
checkboxes
9-
├── checkboxes.mjs
10-
└── checkboxes.test.js
8+
component
9+
├── component.mjs
10+
└── component.test.js
1111
```
1212

1313
## Skeleton
1414

1515
```js
16-
import { nodeListForEach } from '../vendor/common.mjs'
16+
import '../../vendor/polyfills/Element.mjs'
1717

18-
function Checkboxes ($module) {
19-
// code goes here
18+
/**
19+
* Component name
20+
*
21+
* @class
22+
* @param {Element} $module - HTML element to use for component
23+
*/
24+
function Example ($module) {
25+
if (!$module) {
26+
return this
27+
}
28+
29+
this.$module = $module
30+
31+
// Code goes here
2032
}
2133

22-
Checkboxes.prototype.init = function () {
23-
// code goes here
34+
/**
35+
* Initialise component
36+
*/
37+
Example.prototype.init = function () {
38+
// Check that required elements are present
39+
if (!this.$module) {
40+
return
41+
}
42+
43+
// Code goes here
2444
}
2545

26-
export default Checkboxes
46+
export default Example
2747
```
2848

2949
## Use data attributes to initialise component JavaScript
@@ -48,15 +68,15 @@ Use `/** ... */` for multi-line comments. Include a description, and specify typ
4868

4969
```js
5070
/**
51-
* Get the nearest ancestor element of a node that matches a given tag name
52-
* @param {object} node element
53-
* @param {string} match tag name (e.g. div)
54-
* @return {object} ancestor element
55-
*/
56-
57-
function (node, match) {
58-
// code goes here
59-
return ancestor
71+
* Get the first descendent (child) of an HTML element that matches a given tag name
72+
*
73+
* @param {Element} $element - HTML element
74+
* @param {string} tagName - Tag name (for example 'div')
75+
* @returns {Element} Ancestor element
76+
*/
77+
function ($element, tagName) {
78+
// Code goes here
79+
return $element.querySelector(tagName)
6080
}
6181
```
6282

@@ -73,52 +93,54 @@ Use the prototype design pattern to structure your code.
7393
Create a constructor and define any variables that the object needs.
7494

7595
```js
76-
function Checkboxes ($module) {
77-
// code goes here
96+
function Example ($module) {
97+
// Code goes here
7898
}
7999
```
80100

81101
Assign methods to the prototype object. Do not overwrite the prototype with a new object as this makes inheritance impossible.
82102

83103
```js
84-
// bad
85-
Checkboxes.prototype = {
104+
// Bad
105+
Example.prototype = {
86106
init: function () {
87-
// code goes here
107+
// Code goes here
88108
}
89109
}
90110

91-
// good
92-
Checkboxes.prototype.init = function () {
93-
// code goes here
111+
// Good
112+
Example.prototype.init = function () {
113+
// Code goes here
94114
}
95115
```
96116

97117
When initialising an object, use the `new` keyword.
98118

99119
```js
100-
// bad
101-
var myCheckbox = Checkbox().init()
120+
// Bad
121+
var myExample = Example()
102122

103-
// good
104-
var myCheckbox = new Checkbox().init()
123+
// Good
124+
var myExample = new Example()
105125
```
106126

107127
## Modules
108128

109-
Use ES6 modules (`import`/`export`) over a non-standard module system. You can always transpile to your preferred module system.
129+
Use ECMAScript modules (`import`/`export`) over CommonJS and other formats. You can always transpile to your preferred module system.
110130

111131
```js
112-
import { nodeListForEach } from '../vendor/common.mjs'
113-
// code goes here
114-
export default Checkboxes
132+
import { closestAttributeValue } from '../common/index.mjs'
133+
134+
// Code goes here
135+
export function exampleHelper1 () {}
136+
export function exampleHelper2 () {}
115137
```
116138

117-
Avoid using wildcard (`import * as nodeListForEach`) imports.
139+
You must specify the file extension when using the import keyword.
118140

119-
You must specify the file extension for a file when importing it.
141+
Avoid using namespace imports (`import * as namespace`) in code transpiled to CommonJS (or AMD) bundled code as this can prevent "tree shaking" optimisations.
120142

121-
Use default export over named export.
143+
Prefer named exports over default exports to avoid compatibility issues with transpiler "synthetic default" as discussed in: https://github.com/alphagov/govuk-frontend/issues/2829
122144

123145
## Polyfilling
124146

lib/file-helper.js

+41-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const { readFile } = require('fs/promises')
2-
const { join, parse, ParsedPath, relative } = require('path')
2+
const { join, parse, relative } = require('path')
33
const { promisify } = require('util')
44

55
const glob = promisify(require('glob'))
@@ -14,6 +14,11 @@ const configPaths = require('../config/paths')
1414
* Used to cache slow operations
1515
*
1616
* See `config/jest/globals.mjs`
17+
*
18+
* @type {{
19+
* directories?: Map<string, string[]>,
20+
* componentsData?: ComponentData[]
21+
* }}
1722
*/
1823
const cache = global.cache || {}
1924

@@ -27,7 +32,6 @@ const cache = global.cache || {}
2732
*/
2833
const getListing = async (directoryPath, pattern = '**/*', options = {}) => {
2934
const listing = await glob(pattern, {
30-
allowEmpty: true,
3135
cwd: directoryPath,
3236
matchBase: true,
3337
nodir: true,
@@ -51,7 +55,7 @@ const getDirectories = (directoryPath) => {
5155

5256
// Retrieve from cache
5357
if (directories) {
54-
return directories
58+
return Promise.resolve(directories)
5559
}
5660

5761
// Read from disk
@@ -66,12 +70,11 @@ const getDirectories = (directoryPath) => {
6670
* @returns {function(string): boolean} Returns true for files matching every pattern
6771
*/
6872
const filterPath = (patterns) => (entryPath) => {
69-
const isMatch = (pattern) => minimatch(entryPath, pattern, {
70-
matchBase: true
71-
})
72-
73-
// Return true for files matching every pattern
74-
return patterns.every(isMatch)
73+
return patterns.every(
74+
(pattern) => minimatch(entryPath, pattern, {
75+
matchBase: true
76+
})
77+
)
7578
}
7679

7780
/**
@@ -107,16 +110,12 @@ const getComponentData = async (componentName) => {
107110
}
108111

109112
// Read from disk
110-
try {
111-
const yamlPath = join(configPaths.components, componentName, `${componentName}.yaml`)
112-
const yamlData = yaml.load(await readFile(yamlPath, 'utf8'), { json: true })
113+
const yamlPath = join(configPaths.components, componentName, `${componentName}.yaml`)
114+
const yamlData = yaml.load(await readFile(yamlPath, 'utf8'), { json: true })
113115

114-
return {
115-
name: componentName,
116-
...yamlData
117-
}
118-
} catch (error) {
119-
throw new Error(error)
116+
return {
117+
name: componentName,
118+
...yamlData
120119
}
121120
}
122121

@@ -177,7 +176,7 @@ module.exports = {
177176
* Directory entry path mapper callback
178177
*
179178
* @callback mapPathToCallback
180-
* @param {ParsedPath} file - Parsed file
179+
* @param {import('path').ParsedPath} file - Parsed file
181180
* @returns {string[]} Returns path (or array of paths)
182181
*/
183182

@@ -186,12 +185,33 @@ module.exports = {
186185
*
187186
* @typedef {object} ComponentData
188187
* @property {string} name - Component name
189-
* @property {unknown[]} [params] - Nunjucks macro options
190-
* @property {unknown[]} [examples] - Example Nunjucks macro options
188+
* @property {ComponentOption[]} [params] - Nunjucks macro options
189+
* @property {ComponentExample[]} [examples] - Example Nunjucks macro options
191190
* @property {string} [previewLayout] - Nunjucks layout for component preview
192191
* @property {string} [accessibilityCriteria] - Accessibility criteria
193192
*/
194193

194+
/**
195+
* Component option from YAML
196+
*
197+
* @typedef {object} ComponentOption
198+
* @property {string} name - Option name
199+
* @property {string} type - Option type
200+
* @property {boolean} required - Option required
201+
* @property {string} description - Option description
202+
* @property {boolean} [isComponent] - Option is another component
203+
* @property {ComponentOption[]} [params] - Nested Nunjucks macro options
204+
*/
205+
206+
/**
207+
* Component example from YAML
208+
*
209+
* @typedef {object} ComponentExample
210+
* @property {string} name - Example name
211+
* @property {object} data - Example data
212+
* @property {boolean} [hidden] - Example hidden from review app
213+
*/
214+
195215
/**
196216
* Full page example from front matter
197217
*

lib/file-helper.unit.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ describe('getComponentData', () => {
44
it('rejects if unable to load component data', async () => {
55
await expect(fileHelper.getComponentData('not-a-real-component'))
66
.rejects
7-
.toThrow('Error: ENOENT: no such file or directory')
7+
.toThrow(/ENOENT: no such file or directory/)
88
})
99

1010
it('outputs objects with an array of params and examples', async () => {

lib/helper-functions.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,11 @@ function componentPathToModuleName (componentPath) {
6767
* the project root node_modules
6868
*
6969
* @param {string} packageName - Installed npm package name
70-
* @param {...string} [paths] - Optional child directory paths
70+
* @param {string} [childPath] - Optional child directory path
7171
* @returns {string} Path to installed npm package
7272
*/
73-
function packageNameToPath (packageName, ...paths) {
74-
const packagePath = dirname(require.resolve(`${packageName}/package.json`))
75-
76-
return paths?.length
77-
? join(packagePath, ...paths)
78-
: packagePath
73+
function packageNameToPath (packageName, childPath = '') {
74+
return join(dirname(require.resolve(`${packageName}/package.json`)), childPath)
7975
}
8076

8177
module.exports = {

0 commit comments

Comments
 (0)