diff --git a/docs/contributing/tasks.md b/docs/contributing/tasks.md index 5f8a6df991..41a8bb80c1 100644 --- a/docs/contributing/tasks.md +++ b/docs/contributing/tasks.md @@ -35,6 +35,7 @@ npm scripts are defined in `package.json`. These trigger a number of Gulp tasks. - clean the `./package` folder - copy Sass files, applying Autoprefixer via PostCSS - copy Nunjucks component template/macro files, including JSON configs +- copy GOV.UK Prototype Kit config files - copy JavaScript ESM source files - compile JavaScript ESM to CommonJS (`gulp js:compile`) - runs `npm run test:build:package` (which will test the output is correct) diff --git a/gulpfile.js b/gulpfile.js index 656bb4ce1c..da2b03f721 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -12,6 +12,7 @@ require('./tasks/gulp/watch.js') // Node tasks const { updateDistAssetsVersion } = require('./tasks/asset-version.js') +const { updatePrototypeKitConfig } = require('./tasks/prototype-kit-config.js') const { clean } = require('./tasks/clean.js') const { npmScriptTask } = require('./tasks/run.js') @@ -73,7 +74,8 @@ gulp.task('dev', gulp.series( gulp.task('build:package', gulp.series( clean, 'copy:files', - 'js:compile' + 'js:compile', + updatePrototypeKitConfig )) /** diff --git a/package/govuk-prototype-kit.config.json b/package/govuk-prototype-kit.config.json index a514066fba..39ca7a384f 100644 --- a/package/govuk-prototype-kit.config.json +++ b/package/govuk-prototype-kit.config.json @@ -3,12 +3,127 @@ "/" ], "scripts": [ - "/govuk/all.js" + "/govuk/all.js", + "/govuk/_govuk-prototype-kit-init.js" ], "assets": [ "/govuk/assets" ], "sass": [ - "/govuk/all.scss" + "/govuk/_govuk-prototype-kit.scss" + ], + "nunjucksMacros": [ + { + "importFrom": "govuk/components/accordion/macro.njk", + "macroName": "govukAccordion" + }, + { + "importFrom": "govuk/components/back-link/macro.njk", + "macroName": "govukBackLink" + }, + { + "importFrom": "govuk/components/breadcrumbs/macro.njk", + "macroName": "govukBreadcrumbs" + }, + { + "importFrom": "govuk/components/button/macro.njk", + "macroName": "govukButton" + }, + { + "importFrom": "govuk/components/character-count/macro.njk", + "macroName": "govukCharacterCount" + }, + { + "importFrom": "govuk/components/checkboxes/macro.njk", + "macroName": "govukCheckboxes" + }, + { + "importFrom": "govuk/components/cookie-banner/macro.njk", + "macroName": "govukCookieBanner" + }, + { + "importFrom": "govuk/components/date-input/macro.njk", + "macroName": "govukDateInput" + }, + { + "importFrom": "govuk/components/details/macro.njk", + "macroName": "govukDetails" + }, + { + "importFrom": "govuk/components/error-message/macro.njk", + "macroName": "govukErrorMessage" + }, + { + "importFrom": "govuk/components/error-summary/macro.njk", + "macroName": "govukErrorSummary" + }, + { + "importFrom": "govuk/components/fieldset/macro.njk", + "macroName": "govukFieldset" + }, + { + "importFrom": "govuk/components/file-upload/macro.njk", + "macroName": "govukFileUpload" + }, + { + "importFrom": "govuk/components/input/macro.njk", + "macroName": "govukInput" + }, + { + "importFrom": "govuk/components/inset-text/macro.njk", + "macroName": "govukInsetText" + }, + { + "importFrom": "govuk/components/notification-banner/macro.njk", + "macroName": "govukNotificationBanner" + }, + { + "importFrom": "govuk/components/pagination/macro.njk", + "macroName": "govukPagination" + }, + { + "importFrom": "govuk/components/panel/macro.njk", + "macroName": "govukPanel" + }, + { + "importFrom": "govuk/components/phase-banner/macro.njk", + "macroName": "govukPhaseBanner" + }, + { + "importFrom": "govuk/components/radios/macro.njk", + "macroName": "govukRadios" + }, + { + "importFrom": "govuk/components/select/macro.njk", + "macroName": "govukSelect" + }, + { + "importFrom": "govuk/components/skip-link/macro.njk", + "macroName": "govukSkipLink" + }, + { + "importFrom": "govuk/components/summary-list/macro.njk", + "macroName": "govukSummaryList" + }, + { + "importFrom": "govuk/components/table/macro.njk", + "macroName": "govukTable" + }, + { + "importFrom": "govuk/components/tabs/macro.njk", + "macroName": "govukTabs" + }, + { + "importFrom": "govuk/components/tag/macro.njk", + "macroName": "govukTag" + }, + { + "importFrom": "govuk/components/textarea/macro.njk", + "macroName": "govukTextarea" + }, + { + "importFrom": "govuk/components/warning-text/macro.njk", + "macroName": "govukWarningText" + } ] } diff --git a/src/govuk-prototype-kit/govuk-prototype-kit.config.mjs b/src/govuk-prototype-kit/govuk-prototype-kit.config.mjs new file mode 100644 index 0000000000..d5add35c12 --- /dev/null +++ b/src/govuk-prototype-kit/govuk-prototype-kit.config.mjs @@ -0,0 +1,44 @@ +import { join, relative } from 'path' +import slash from 'slash' + +import { filterPath, getDirectories, getListing } from '../../lib/file-helper.js' +import { componentNameToMacroName } from '../../lib/helper-functions.js' +import configPaths from '../../config/paths.js' + +/** + * GOV.UK Prototype Kit config builder + */ +export default async () => { + const componentsFiles = await getListing(configPaths.components) + const componentNames = await getDirectories(configPaths.components) + + // Build array of macros + const nunjucksMacros = componentNames + .map((componentName) => { + const [macroPath] = componentsFiles + .filter(filterPath([`${componentName}/macro.njk`])) + + // Relative path to src without 'govuk' + const rootPath = join(configPaths.src, '../') + + return { + importFrom: slash(relative(rootPath, join(configPaths.components, macroPath))), + macroName: componentNameToMacroName(componentName) + } + }) + + return { + assets: [ + '/govuk/assets' + ], + sass: [ + '/govuk-prototype-kit/init.scss' + ], + scripts: [ + '/govuk/all.js', + '/govuk-prototype-kit/init.js' + ], + nunjucksMacros, + nunjucksPaths: ['/'] + } +} diff --git a/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs b/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs new file mode 100644 index 0000000000..19710f59d2 --- /dev/null +++ b/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs @@ -0,0 +1,163 @@ +import configFn from './govuk-prototype-kit.config.mjs' + +describe('GOV.UK Prototype Kit config', () => { + let config + + beforeAll(async () => { + config = await configFn() + }) + + it('includes paths for assets, scripts, sass', () => { + expect(config.assets).toEqual([ + '/govuk/assets' + ]) + + expect(config.sass).toEqual([ + '/govuk-prototype-kit/init.scss' + ]) + + expect(config.scripts).toEqual([ + '/govuk/all.js', + '/govuk-prototype-kit/init.js' + ]) + }) + + describe('Nunjucks', () => { + it('includes macros list', () => { + expect(config.nunjucksMacros).toEqual([ + { + importFrom: 'govuk/components/accordion/macro.njk', + macroName: 'govukAccordion' + }, + { + importFrom: 'govuk/components/back-link/macro.njk', + macroName: 'govukBackLink' + }, + { + importFrom: 'govuk/components/breadcrumbs/macro.njk', + macroName: 'govukBreadcrumbs' + }, + { + importFrom: 'govuk/components/button/macro.njk', + macroName: 'govukButton' + }, + { + importFrom: 'govuk/components/character-count/macro.njk', + macroName: 'govukCharacterCount' + }, + { + importFrom: 'govuk/components/checkboxes/macro.njk', + macroName: 'govukCheckboxes' + }, + { + importFrom: 'govuk/components/cookie-banner/macro.njk', + macroName: 'govukCookieBanner' + }, + { + importFrom: 'govuk/components/date-input/macro.njk', + macroName: 'govukDateInput' + }, + { + importFrom: 'govuk/components/details/macro.njk', + macroName: 'govukDetails' + }, + { + importFrom: 'govuk/components/error-message/macro.njk', + macroName: 'govukErrorMessage' + }, + { + importFrom: 'govuk/components/error-summary/macro.njk', + macroName: 'govukErrorSummary' + }, + { + importFrom: 'govuk/components/fieldset/macro.njk', + macroName: 'govukFieldset' + }, + { + importFrom: 'govuk/components/file-upload/macro.njk', + macroName: 'govukFileUpload' + }, + { + importFrom: 'govuk/components/footer/macro.njk', + macroName: 'govukFooter' + }, + { + importFrom: 'govuk/components/header/macro.njk', + macroName: 'govukHeader' + }, + { + importFrom: 'govuk/components/hint/macro.njk', + macroName: 'govukHint' + }, + { + importFrom: 'govuk/components/input/macro.njk', + macroName: 'govukInput' + }, + { + importFrom: 'govuk/components/inset-text/macro.njk', + macroName: 'govukInsetText' + }, + { + importFrom: 'govuk/components/label/macro.njk', + macroName: 'govukLabel' + }, + { + importFrom: 'govuk/components/notification-banner/macro.njk', + macroName: 'govukNotificationBanner' + }, + { + importFrom: 'govuk/components/pagination/macro.njk', + macroName: 'govukPagination' + }, + { + importFrom: 'govuk/components/panel/macro.njk', + macroName: 'govukPanel' + }, + { + importFrom: 'govuk/components/phase-banner/macro.njk', + macroName: 'govukPhaseBanner' + }, + { + importFrom: 'govuk/components/radios/macro.njk', + macroName: 'govukRadios' + }, + { + importFrom: 'govuk/components/select/macro.njk', + macroName: 'govukSelect' + }, + { + importFrom: 'govuk/components/skip-link/macro.njk', + macroName: 'govukSkipLink' + }, + { + importFrom: 'govuk/components/summary-list/macro.njk', + macroName: 'govukSummaryList' + }, + { + importFrom: 'govuk/components/table/macro.njk', + macroName: 'govukTable' + }, + { + importFrom: 'govuk/components/tabs/macro.njk', + macroName: 'govukTabs' + }, + { + importFrom: 'govuk/components/tag/macro.njk', + macroName: 'govukTag' + }, + { + importFrom: 'govuk/components/textarea/macro.njk', + macroName: 'govukTextarea' + }, + { + importFrom: 'govuk/components/warning-text/macro.njk', + macroName: 'govukWarningText' + } + ]) + }) + + it('includes paths', () => { + expect(config.nunjucksPaths).toEqual(['/']) + }) + }) +}) diff --git a/src/govuk-prototype-kit/init.js b/src/govuk-prototype-kit/init.js new file mode 100644 index 0000000000..d9876d09b4 --- /dev/null +++ b/src/govuk-prototype-kit/init.js @@ -0,0 +1,7 @@ +if (window.GOVUKPrototypeKit && + window.GOVUKPrototypeKit.documentReady && + window.GOVUKPrototypeKit.majorVersion >= 13) { + window.GOVUKPrototypeKit.documentReady(function () { + window.GOVUKFrontend.initAll() + }) +} diff --git a/src/govuk-prototype-kit/init.scss b/src/govuk-prototype-kit/init.scss new file mode 100644 index 0000000000..3da1b76a45 --- /dev/null +++ b/src/govuk-prototype-kit/init.scss @@ -0,0 +1,12 @@ +$govuk-prototype-kit-major-version: 0 !default; + +$govuk-assets-path: if( + $govuk-prototype-kit-major-version > 12, + $govuk-extensions-url-context + "/govuk-frontend/govuk/assets/", + "/govuk/assets/" +) !default; + +$govuk-global-styles: true !default; +$govuk-new-link-styles: true !default; + +@import "../govuk/all"; diff --git a/tasks/gulp/__tests__/after-build-package.test.js b/tasks/gulp/__tests__/after-build-package.test.js index 2fb7e5a582..c63f7bc481 100644 --- a/tasks/gulp/__tests__/after-build-package.test.js +++ b/tasks/gulp/__tests__/after-build-package.test.js @@ -42,29 +42,36 @@ describe('package/', () => { const listingExpected = listingSource .filter(filterPath(filterPatterns)) - // Replaces source component '*.mjs' with: - // - `*.mjs` file copied to `govuk-esm` - // - `*.js` file compiled to `govuk` + // Replaces GOV.UK Prototype kit config with JSON + .flatMap(mapPathTo(['**/govuk-prototype-kit.config.mjs'], ({ dir: requirePath, name }) => [ + join(requirePath, '../', `${name}.json`) + ])) + + // Replaces all source '*.mjs' files .flatMap(mapPathTo(['**/*.mjs'], ({ dir: requirePath, name }) => { - const importPath = requirePath.replace(/^govuk/, 'govuk-esm') + const importFilter = /^govuk(?!-)/ + + // All source `**/*.mjs` files compiled to `**/*.js` + const output = [join(requirePath, `${name}.js`)] + + // Only source `./govuk/**/*.mjs` files copied to `./govuk-esm/**/*.mjs` + if (importFilter.test(requirePath)) { + output.push(join(requirePath.replace(importFilter, 'govuk-esm'), `${name}.mjs`)) + } - return [ - join(importPath, `${name}.mjs`), - join(requirePath, `${name}.js`) - ] + return output })) // Replaces source component '*.yaml' with: // - `fixtures.json` fixtures for tests // - `macro-options.json` component options - .flatMap(mapPathTo(['**/*.yaml'], ({ dir }) => [ - join(dir, 'fixtures.json'), - join(dir, 'macro-options.json') + .flatMap(mapPathTo(['**/*.yaml'], ({ dir: requirePath }) => [ + join(requirePath, 'fixtures.json'), + join(requirePath, 'macro-options.json') ])) // Files already present in 'package' .concat(...[ - 'govuk-prototype-kit.config.json', 'package.json', 'README.md' ]) diff --git a/tasks/prototype-kit-config.js b/tasks/prototype-kit-config.js new file mode 100644 index 0000000000..0e8329a1c6 --- /dev/null +++ b/tasks/prototype-kit-config.js @@ -0,0 +1,49 @@ +const { copyFile, mkdir, writeFile } = require('fs/promises') +const { EOL } = require('os') +const { dirname, join } = require('path') +const { cwd } = require('process') + +const { destination } = require('./task-arguments.js') + +/** + * Write GOV.UK Prototype Kit config + * + * @returns {Promise} + */ +async function updatePrototypeKitConfig () { + const { default: configFn } = await import('../src/govuk-prototype-kit/govuk-prototype-kit.config.mjs') + + // Files to copy + const copyFiles = [ + join('govuk-prototype-kit', 'init.js'), + join('govuk-prototype-kit', 'init.scss') + ] + + // Copy files to destination + const configTasks = copyFiles.map(async (file) => { + const fileSource = join(cwd(), 'src', file) + const fileTarget = join(cwd(), destination, file) + + // Create destination directory + await mkdir(dirname(fileTarget), { recursive: true }) + + // Copy file to destination + return copyFile(fileSource, fileTarget) + }) + + // JSON config file path + contents + const configPath = join(destination, 'govuk-prototype-kit.config.json') + const configJSON = JSON.stringify(await configFn(), null, 2) + EOL + + // Write JSON config file + configTasks.push(writeFile(configPath, configJSON)) + + // Resolve on completion + return Promise.all(configTasks) +} + +updatePrototypeKitConfig.displayName = 'update-prototype-kit-config' + +module.exports = { + updatePrototypeKitConfig +}