diff --git a/__tests__/utils/index.js b/__tests__/utils/index.js index 7c0c46befb..6b6bc931b1 100644 --- a/__tests__/utils/index.js +++ b/__tests__/utils/index.js @@ -41,10 +41,13 @@ function mkdtempSync () { * @param {string} prototypePath * @param {Object} [options] * @param {string} [options.kitPath] - Path to the kit to use when creating prototype, if not provided uses mkReleaseArchive - * @param {bool} [options.allowTracking] - If undefined no usage-data-config.json is created, + * @param {boolean} [options.overwrite] - Allow existing prototype to be overwritten (optional) + * @param {boolean} [options.allowTracking] - If undefined no usage-data-config.json is created (optional), * if true a usage-data-config.json is created allowing tracking, * if false a usage-data-config.json is crated disallowing tracking - * @returns {void} + * @param {boolean} [options.npmInstallLinks] - Set value for npm config install-links (optional) + * @param {string} [options.commandLineParameters] - Command line parameters (optional) + * @returns {Promise} */ async function mkPrototype (prototypePath, { kitPath, diff --git a/bin/utils/argv-parser.js b/bin/utils/argv-parser.js index 00f8a04497..e764ae4127 100644 --- a/bin/utils/argv-parser.js +++ b/bin/utils/argv-parser.js @@ -1,11 +1,23 @@ +/** + * + * @param {NodeJS.Process["argv"]} argvInput + * @param {{ booleans?: string[] }} config + */ function parse (argvInput, config = {}) { const args = [...argvInput].splice(2) - const options = {} - const paths = [] + const options = /** @type {Object} */ ({}) + const paths = /** @type {string[]} */ ([]) const booleanOptions = config.booleans || [] + + /** @type {string | undefined} */ let command + + /** @type {{ option: string } | undefined} */ let contextFromPrevious + /** + * @param {string} unprocessed + */ const processOptionName = (unprocessed) => { if (unprocessed.startsWith('--')) { return unprocessed.substring(2) @@ -15,6 +27,9 @@ function parse (argvInput, config = {}) { } } + /** + * @param {string} arg + */ const prepareArg = (arg) => { if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith('\'') && arg.endsWith('\''))) { return arg.substring(1, arg.length - 1) @@ -64,3 +79,7 @@ function parse (argvInput, config = {}) { module.exports = { parse } + +/** + * @typedef {ReturnType} ArgvParsed + */ diff --git a/lib/govukFrontendPaths.js b/lib/govukFrontendPaths.js index 000ba2f51e..34cb546d31 100644 --- a/lib/govukFrontendPaths.js +++ b/lib/govukFrontendPaths.js @@ -47,5 +47,5 @@ module.exports = { * @property {string} baseDir - GOV.UK Frontend directory path * @property {URL["pathname"]} includePath - URL path to GOV.UK Frontend includes * @property {URL["pathname"]} assetPath - URL path to GOV.UK Frontend assets - * @property {{ [key: string]: unknown }} config - GOV.UK Frontend plugin config + * @property {import('./plugins/plugins').ConfigManifest} config - GOV.UK Frontend plugin config */ diff --git a/lib/plugins/plugin-validator.js b/lib/plugins/plugin-validator.js index 2a392608d0..1564180b60 100644 --- a/lib/plugins/plugin-validator.js +++ b/lib/plugins/plugin-validator.js @@ -21,18 +21,32 @@ const knownKeys = [ 'meta' ] +/** + * @param {string} executionPath + * @param {string} pathToValidate + * @param {string} key + */ function checkPathExists (executionPath, pathToValidate, key) { const absolutePathToValidate = path.join(executionPath, pathToValidate) if (!fse.existsSync(absolutePathToValidate)) errors.push(`In section ${key}, the path '${pathToValidate}' does not exist`) } +/** + * @param {string} executionPath + * @param {string} nunjucksFileName + * @param {string | string[]} nunjucksPaths + */ function checkNunjucksMacroExists (executionPath, nunjucksFileName, nunjucksPaths) { // set up a flag for the existance of a nunjucks path let nunjucksPathExists = false - if (nunjucksPaths === undefined) { - // Check if the nunjucksMacros are at the root level of the project - if (fse.existsSync(path.join(executionPath, nunjucksFileName))) nunjucksPathExists = true + if (!nunjucksPaths || typeof nunjucksPaths === 'string') { + const pathToCheck = typeof nunjucksPaths === 'string' + ? path.join(executionPath, nunjucksPaths, nunjucksFileName) + : path.join(executionPath, nunjucksFileName) // root level + + // Check if the nunjucksMacros are at a single path in the project + if (fse.existsSync(pathToCheck)) nunjucksPathExists = true } else { // Combine the file path name for each nunjucks path and check if any one of them exists for (const nunjucksPath of nunjucksPaths) { @@ -47,14 +61,22 @@ function checkNunjucksMacroExists (executionPath, nunjucksFileName, nunjucksPath if (!nunjucksPathExists) errors.push(`The nunjucks file '${nunjucksFileName}' does not exist`) } +/** + * @param {string[]} invalidKeys + */ function reportInvalidKeys (invalidKeys) { errors.push(`The following invalid keys exist in your config: ${invalidKeys}`) } +/** + * @param {ConfigManifest} pluginConfig + * @param {ArgvParsed} argv + * @returns {ConfigManifest} + */ function validateConfigKeys (pluginConfig, argv) { console.log('Config file exists, validating contents.') - const keysToAllowThrough = argv?.options?.keysToIgnoreIfUnknown || '' - const allowedKeys = knownKeys.concat(keysToAllowThrough.split(',').filter(key => !!key)) + const keysToAllowThrough = `${argv?.options?.keysToIgnoreIfUnknown || ''}` + const allowedKeys = [...knownKeys, ...keysToAllowThrough.split(',').filter(key => !!key)] const invalidKeys = [] const validKeysPluginConfig = Object.fromEntries(Object.entries(pluginConfig).filter(([key]) => { @@ -73,6 +95,12 @@ function validateConfigKeys (pluginConfig, argv) { return validKeysPluginConfig } +/** + * @template {ConfigEntry | ConfigURLs} ObjectType + * @param {ObjectType} objectToEvaluate + * @param {string[]} allowedKeys + * @param {string} [keyPath] + */ function reportUnknownKeys (objectToEvaluate, allowedKeys, keyPath) { const invalidMetaUrlKeys = Object.keys(objectToEvaluate).filter(key => !allowedKeys.includes(key)).map(key => `${keyPath || ''}${key}`) if (invalidMetaUrlKeys.length > 0) { @@ -80,6 +108,9 @@ function reportUnknownKeys (objectToEvaluate, allowedKeys, keyPath) { } } +/** + * @param {string} url + */ function isValidUrl (url) { if (!url.startsWith('https://') && !url.startsWith('http://')) { return false @@ -90,6 +121,9 @@ function isValidUrl (url) { return true } +/** + * @param {ConfigURLs} [metaUrls] + */ function validateMetaUrls (metaUrls) { if (typeof metaUrls === 'undefined') { return @@ -121,6 +155,9 @@ function validateMetaUrls (metaUrls) { }) } +/** + * @param {ConfigEntry} meta + */ function validateMeta (meta) { const metaKeys = ['urls', 'description'] @@ -142,6 +179,10 @@ function validateMeta (meta) { validateMetaUrls(meta.urls) } +/** + * @param {string} key + * @param {ConfigEntry} configEntry + */ function validatePluginDependency (key, configEntry) { if (typeof configEntry !== 'object' || Array.isArray(configEntry)) { return @@ -165,16 +206,20 @@ function validatePluginDependency (key, configEntry) { } } +/** + * @param {ConfigManifest} pluginConfig + * @param {string} executionPath + */ function validateConfigurationValues (pluginConfig, executionPath) { console.log('Validating whether config paths meet criteria.') const keysToValidate = Object.keys(pluginConfig) keysToValidate.forEach(key => { - // Convert any strings to an array so that they can be processed - let criteriaConfig = pluginConfig[key] - if (!Array.isArray(criteriaConfig)) { - criteriaConfig = [criteriaConfig] - } + /** + * Convert any strings to an array so that they can be processed + * @type {ConfigEntry[]} + */ + const criteriaConfig = [pluginConfig[key]].flat() criteriaConfig.forEach((configEntry) => { try { @@ -203,11 +248,16 @@ function validateConfigurationValues (pluginConfig, executionPath) { }) } +/** + * @param {string} executionPath + * @param {ArgvParsed} argv + */ async function validatePlugin (executionPath, argv) { console.log() const configPath = path.join(executionPath, 'govuk-prototype-kit.config.json') await fse.exists(configPath).then(exists => { if (exists) { + /** @type {ConfigManifest | undefined} */ let pluginConfig try { pluginConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')) @@ -267,3 +317,10 @@ module.exports = { validateMeta, validatePluginDependency } + +/** + * @typedef {import('./plugins').ConfigManifest} ConfigManifest + * @typedef {import('./plugins').ConfigURLs} ConfigURLs + * @typedef {import('./plugins').ConfigEntry} ConfigEntry + * @typedef {import('../../bin/utils/argv-parser').ArgvParsed} ArgvParsed + */ diff --git a/lib/plugins/plugins.js b/lib/plugins/plugins.js index f8523286cc..b94d69641d 100644 --- a/lib/plugins/plugins.js +++ b/lib/plugins/plugins.js @@ -9,37 +9,8 @@ * The kit code retrieves the paths as and when needed; this module just * contains the code to find and list paths defined by plugins. * - * A schema for an example manifest file follows: - * - * // govuk-prototype-kit.config.json - * { - * "assets": string | string[], - * "importNunjucksMacrosInto": string | string[], - * "meta": { - * "description": string, - * "urls": { - * "documentation": string, - * "versionHistory": string, - * "releaseNotes": string - * } - * }, - * "nunjucksMacros": {"importFrom": string, "macroName": string} | {"importFrom": string, "macroName": string}[], - * "nunjucksPaths": string | string[], - * "nunjucksFilters": string | string[], - * "nunjucksFunctions": string | string[], - * "pluginDependencies": [{"packageName": string, "minVersion": string, "maxVersion": string}], - * "sass": string | string[], - * "scripts": string | string[] | {"path": string, "type": string} | {"path": string, "type": string}[], - * "stylesheets": string | string[], - * "templates": { - * "name": string, - * "path": string, - * "type": string - * }[] - * } - * - * Note that all the top-level keys are optional. - * + * See JSDoc `ConfigManifest` for example govuk-prototype-kit.json manifiest + * {@link ConfigManifest} */ // core dependencies @@ -313,8 +284,9 @@ const getByType = type => getList(type) /** * Gets public urls for all plugins of type - * @param {string} listType - (scripts, stylesheets, nunjucks etc) - * @return {string[]} A list of urls + * @template {'assets' | 'scripts' | 'stylesheets'} ListType + * @param {ListType} listType - (scripts, stylesheets, nunjucks etc) + * @return {ListType extends 'scripts' ? AppScript[] | string[] : string[]} A list of urls or script objects */ const getPublicUrls = listType => getList(listType).map(({ packageName, item }) => { // item will either be the plugin path or will be an object containing the plugin path within the src property @@ -343,8 +315,8 @@ const getPublicUrlAndFileSystemPaths = type => getList(type).map(getPublicUrlAnd /** * This is used in the views to output links and scripts for each file - * @param {{scripts: string[], stylesheets: string[]}} additionalConfig - * @return {{scripts: {src: string, type: string}[], stylesheets: string[]}} Returns an object containing two keys(scripts & stylesheets), + * @param {Partial<{ scripts: (AppScript | string)[], stylesheets: string[] }>} [additionalConfig] + * @return {{ scripts: AppScript[], stylesheets: string[] }} Returns an object containing two keys(scripts & stylesheets), * each item contains an array of full paths to specific files. */ function getAppConfig (additionalConfig) { @@ -385,3 +357,91 @@ const self = module.exports = { setPluginsByType, watchPlugins } + +/** + * Prototype Kit plugin config + * + * Schema for govuk-prototype-kit.json manifiest + * Note: All top-level keys are optional + * + * @typedef {object} ConfigManifest + * @property {string | string[]} [assets] - Static asset paths + * @property {string | string[]} [importNunjucksMacrosInto] - Templates to import Nunjucks macros into + * @property {string | string[]} [nunjucksPaths] - Nunjucks paths + * @property {ConfigNunjucksMacro[]} [nunjucksMacros] - Nunjucks macros to include + * @property {string | string[]} [nunjucksFilters] - Nunjucks filters to include + * @property {string | string[]} [nunjucksFunctions] - Nunjucks functions to include + * @property {string | string[]} [sass] - Sass stylesheets to compile + * @property {ConfigScript[] | string[]} [scripts] - JavaScripts to serve + * @property {string | string[]} [stylesheets] - Stylesheets to serve + * @property {ConfigTemplate[]} [templates] - Templates available + * @property {ConfigDependency[]} [pluginDependencies] - Plugin dependencies + * @property {ConfigMeta} [meta] - Plugin metadata + */ + +/** + * Prototype Kit plugin Nunjucks macro + * + * @typedef {object} ConfigNunjucksMacro + * @property {string} macroName - Nunjucks macro name + * @property {string} importFrom - Path to import Nunjucks macro from + */ + +/** + * Prototype Kit plugin script + * + * @typedef {object} ConfigScript + * @property {string} path - Path to script + * @property {string} [type] - Type attribute for script + */ + +/** + * Prototype Kit plugin template + * + * @typedef {object} ConfigTemplate + * @property {string} name - Template name + * @property {string} path - Path to template + * @property {string} type - Template type + */ + +/** + * Prototype Kit plugin dependency + * + * @typedef {object} ConfigDependency + * @property {string} packageName - Package name + * @property {string} minVersion - Package minimum version + * @property {string} maxVersion - Package maximum version + */ + +/** + * Prototype Kit plugin metadata + * + * @typedef {object} ConfigMeta + * @property {string} description - Plugin description + * @property {ConfigURLs} urls - Plugin URLs + */ + +/** + * Prototype Kit plugin URLs + * + * @typedef {object} ConfigURLs + * @property {string} documentation - Documentation URL + * @property {string} releaseNotes - Release notes URL + * @property {string} versionHistory - Version history URL + */ + +/** + * Prototype Kit application script + * + * Plugin {@link ConfigScript} objects use `path` keys but + * these keys are renamed to `src` once imported into the + * application by `plugins.getAppConfig()` + * + * @typedef {object} AppScript + * @property {string} src - Path to script + * @property {string} [type] - Type attribute for script + */ + +/** + * @typedef {string | string[] | ConfigNunjucksMacro | ConfigScript | ConfigTemplate | ConfigDependency | ConfigMeta} ConfigEntry + */ diff --git a/lib/utils/index.js b/lib/utils/index.js index 353b3bea58..313a9a248e 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -35,6 +35,12 @@ marked.use({ } }) +/** + * Application scripts passed into `plugins.getAppConfig()` where + * plugin {@link ConfigScript} is converted to {@link AppScript} + * + * @type {(AppScript | string)[]} + */ const scripts = [] if (existsSync(path.join(projectDir, 'app', 'assets', 'javascripts', 'application.js'))) { scripts.push({ @@ -312,3 +318,8 @@ module.exports = { sortByObjectKey, hasNewVersion } + +/** + * @typedef {import('../plugins/plugins').AppScript} AppScript + * @typedef {import('../plugins/plugins').ConfigScript} ConfigScript + */